Skip to content

Instantly share code, notes, and snippets.

@mitay-walle
Last active April 30, 2024 20:29
Show Gist options
  • Save mitay-walle/dd2e52e5e883878b91e549a9d40bde11 to your computer and use it in GitHub Desktop.
Save mitay-walle/dd2e52e5e883878b91e549a9d40bde11 to your computer and use it in GitHub Desktop.
Unity3d uGUI Sector Custom Graphic
// Why giant monoscript - drop and use file, copy text code
using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Unity.Profiling;
using UnityEditor;
using UnityEditor.UI;
using UnityEngine;
using UnityEngine.U2D;
using UnityEngine.UI;
using T = UnityEngine.TooltipAttribute;
namespace Mitaywalle.UI.Sector
{
/// <summary>
///9-slice graphic that draws an oval sector inside:<br />
///- 1 script<br />
///- no custom shaders<br />
///- input is caught correctly by sector<br />
///- SpriteBorder support<br />
///- SpriteAtlas support<br />
///- pixelPerUnitMultiplier support<br />
///- there are anchors for other transforms, including resizing to squeeze content into a sector<br />
///- UV-by radius and tile by RectTransform<br />
///- free pivot, or generated in the center of the sector<br />
///- gradients<br />
///- nested sectors can stick to the parent<br />
///- offset by angle<br />
///- pixel offset<br />
///- performance-wise it seems fast<br /><br />
///The only negative is that when the number of sectors in the circle changes, it is not immediately rebuilt. I haven't found the reason yet<br />
/// </summary>
[RequireComponent(typeof(CanvasRenderer)), ExecuteAlways]
public class Sector : MaskableGraphic, ICanvasRaycastFilter, ILayoutSelfController
{
static ProfilerMarker MarkerEnable = new("Sector.OnEnable");
static ProfilerMarker MarkerDisable = new("Sector.OnDisable");
static ProfilerMarker MarkerRender = new("Sector.OnPopulateMesh");
static ProfilerMarker MarkerLayoutSelf = new("Sector.LayoutSelf");
static ProfilerMarker MarkerLayout = new("Sector.Layout");
[SerializeField] public bool Logs;
[field: SerializeField] public Settings Settings { get; private set; } = new();
[field: NonSerialized] public Cache Cache { get; private set; } = new();
[NonSerialized] Raycasting _raycasting = new();
[NonSerialized] Rendering _rendering = new();
public Layout Layout = new();
float m_CachedReferencePixelsPerUnit;
public override Material material
{
get
{
if (m_Material != null)
return m_Material;
#if UNITY_EDITOR
if (Application.isPlaying && Settings.Sprite.activeSprite && Settings.Sprite.activeSprite.associatedAlphaSplitTexture != null)
return Image.defaultETC1GraphicMaterial;
#else
if (Settings.Sprite.activeSprite && Settings.Sprite.activeSprite.associatedAlphaSplitTexture != null)
return Image.defaultETC1GraphicMaterial;
#endif
return defaultMaterial;
}
set
{
base.material = value;
}
}
public override Texture mainTexture
{
get
{
if (Settings.Sprite.activeSprite == null)
{
if (material != null && material.mainTexture != null)
{
return material.mainTexture;
}
return s_WhiteTexture;
}
return Settings.Sprite.activeSprite.texture;
}
}
public float pixelsPerUnit
{
get
{
float spritePixelsPerUnit = 100;
if (Settings.Sprite.activeSprite)
spritePixelsPerUnit = Settings.Sprite.activeSprite.pixelsPerUnit;
if (canvas)
m_CachedReferencePixelsPerUnit = canvas.referencePixelsPerUnit;
return spritePixelsPerUnit / m_CachedReferencePixelsPerUnit;
}
}
public float multipliedPixelsPerUnit => pixelsPerUnit * Settings.PixelsPerUnitMultiplier;
protected Sector() => useLegacyMeshGeneration = false;
protected override void OnEnable()
{
MarkerEnable.Begin(this);
Settings.Sprite.OnEnable(this);
base.OnEnable();
Layout.SetLayoutHorizontal(this);
MarkerEnable.End();
}
protected override void OnDisable()
{
MarkerDisable.Begin(this);
Settings.Sprite.OnDisable();
base.OnDisable();
MarkerDisable.End();
}
void Update()
{
_rendering.Update(this);
MarkerLayout.Begin(this);
Layout.Update(this);
MarkerLayout.End();
}
protected override void OnPopulateMesh(VertexHelper vh)
{
MarkerRender.Begin(this);
_rendering.OnPopulateMesh(vh, this, _raycasting);
MarkerRender.End();
}
void ILayoutController.SetLayoutHorizontal()
{
MarkerLayoutSelf.Begin(this);
Layout.SetLayoutHorizontal(this);
MarkerLayoutSelf.End();
}
void ILayoutController.SetLayoutVertical()
{
MarkerLayoutSelf.Begin(this);
Layout.SetLayoutHorizontal(this);
MarkerLayoutSelf.End();
}
protected override void OnTransformParentChanged()
{
Layout.SetLayoutHorizontal(this);
base.OnTransformParentChanged();
}
protected override void OnRectTransformDimensionsChange()
{
Layout.SetLayoutHorizontal(this);
base.OnRectTransformDimensionsChange();
}
bool ICanvasRaycastFilter.IsRaycastLocationValid(Vector2 screenPoint, Camera eventCamera)
{
return _raycasting.IsRaycastLocationValid(this, screenPoint, eventCamera);
}
public Transform GetRootSector()
{
if (Settings.ShapeSource == eShapeSource.CircleAlways) return null;
if (Settings.ShapeSource == eShapeSource.CurrentTransform) return transform;
Transform current = transform;
Transform previous = current.parent;
for (int i = 0; i < Settings.ParentOffsetTransform; i++)
{
Transform parent = current.parent;
if (!parent) return previous;
previous = current;
current = parent;
}
return current;
}
#region Editor
#if UNITY_EDITOR
protected override void OnValidate()
{
Settings.ParentOffsetTransform = Mathf.Clamp(Settings.ParentOffsetTransform, 1, GetTransformDepth());
Settings.PixelsPerUnitMultiplier = Mathf.Max(Settings.PixelsPerUnitMultiplier, .001f);
if (Settings.LocalRescaleDelta != 0)
{
//Settings.PivotToSector = true;
}
base.OnValidate();
}
int GetTransformDepth()
{
Transform parent = transform.parent;
int transformDepth = 0;
while (parent != null)
{
parent = parent.parent;
transformDepth++;
}
return transformDepth;
}
#endif
#endregion
}
/// <summary>
/// Image fill type controls how to display the image.
/// </summary>
public enum eType
{
/// <summary>
/// Displays the full Image
/// </summary>
/// <remarks>
/// This setting shows the entire image stretched across the Image's RectTransform
/// </remarks>
Simple = 0,
/// <summary>
/// Displays the Image as a 9-sliced graphic.
/// </summary>
/// <remarks>
/// A 9-sliced image displays a central area stretched across the image surrounded by a border comprising of 4 corners and 4 stretched edges.
///
/// This has the effect of creating a resizable skinned rectangular element suitable for dialog boxes, windows, and general UI elements.
///
/// Note: For this method to work properly the Sprite assigned to Image.sprite needs to have Sprite.border defined.
/// </remarks>
Sliced = 1,
/// <summary>
/// Displays a sliced Sprite with its resizable sections tiled instead of stretched.
/// </summary>
/// <remarks>
/// A Tiled image behaves similarly to a UI.Image.Type.Sliced|Sliced image, except that the resizable sections of the image are repeated instead of being stretched. This can be useful for detailed UI graphics that do not look good when stretched.
///
/// It uses the Sprite.border value to determine how each part (border and center) should be tiled.
///
/// The Image sections will repeat the corresponding section in the Sprite until the whole section is filled. The corner sections will be unaffected and will draw in the same way as a Sliced Image. The edges will repeat along their lengths. The center section will repeat across the whole central part of the Image.
///
/// The Image section will repeat the corresponding section in the Sprite until the whole section is filled.
///
/// Be aware that if you are tiling a Sprite with borders or a packed sprite, a mesh will be generated to create the tiles. The size of the mesh will be limited to 16250 quads; if your tiling would require more tiles, the size of the tiles will be enlarged to ensure that the number of generated quads stays below this limit.
///
/// For optimum efficiency, use a Sprite with no borders and with no packing, and make sure the Sprite.texture wrap mode is set to TextureWrapMode.Repeat.These settings will prevent the generation of additional geometry.If this is not possible, limit the number of tiles in your Image.
/// </remarks>
Tiled = 2,
/// <summary>
/// Displays only a portion of the Image.
/// </summary>
/// <remarks>
/// A Filled Image will display a section of the Sprite, with the rest of the RectTransform left transparent. The Image.fillAmount determines how much of the Image to show, and Image.fillMethod controls the shape in which the Image will be cut.
///
/// This can be used for example to display circular or linear status information such as timers, health bars, and loading bars.
/// </remarks>
Filled = 3
}
public enum eColorOperation
{
Skip = -1,
Multiply = 0,
Add = 1,
Override = 2,
}
/// <summary>
/// The possible fill method types for a Filled Image.
/// </summary>
public enum eFillMethod
{
DegreeSector,
Degree,
Radius,
Rect,
}
public enum eXY
{
X,
Y
}
public enum eClockwise
{
Clockwise = -1,
CounterClockwise = 1,
}
public enum eShapeSource
{
CurrentTransform,
CircleAlways,
ParentOffsetTransform,
}
[Serializable]
public class Settings
{
const string T0 = "<b>Current Transform</b> = trasnform.parent.childCount used to calculate Sector angle\n<b>Circle Always</b> = full circle always\n<b>Parent Offset Transform</b> get number in ParentOffsetTransform to use it's parent.childCount to calculate sector angle";
const string T1 = "<b>degrees</b>. Total outer angle to include all sectors with sibling Transforms";
const string T2 = "<b>degrees</b>. Rotate Angle";
const string T3 = "<b>degrees</b>. Create expanding triangle Margin between neibhour Sectors with sibling Transforms";
const string T4 = "Required for <b>LocalRescaleDelta</b>\nRecalculate pivot of this RectTransform, to fit in Sector center";
const string T5 = "Require and force <b>PivotToSector</b>!\nscales down or up Sector graphic, to it's local center. Create straight line Margin in abstract 'pixels' between neibhour Sectors with sibling Transforms";
const string T6 = "<b>Affects Performance!</b> % Percents % Geometry Resolution, how many quads-per-degree would be generated";
const string T7 = "Work only if <b>Type = Sliced</b> and Sprite has border in import settings\n% Percents % \n-- more radius border \n++ more degrees border";
const string T8 = "% Percents % from RectTransform.size";
const string T9 = "Work only if <b>Type = Sliced</b> or Tiled. Adjust Sprite scale";
const string TA = "Work only if <b>Type = Sliced<b>. Skip center vertices";
const string TB = "Work Same way as UI.Image.type\nSimple - stretch sprite to Sector along outer sides\nSliced - 9-slice sprite according to its Border in import settings\nTiled - Tiles sprite at RectTransform-spaced UV";
const string TC = "Required for <b>CloneRootSectorOuter</b>. -1 = full circle always\n0 = use this.trasnform.parent.childCount to calculate Sector angle to draw\n1 and higher = this.transform.parent.parent.childCount used";
const string TD = "Require ParentOffsetTransform > 0\nClone some settings from Sector, found at chosen parent";
const string TE = "Sprite used to draw";
const string TF = "<b>Affects Performance!</b>\nUnity's Gradient is slow. Remap vertex colors by radius, total degree or degree inside Sector";
[T(T0)] public eShapeSource ShapeSource;
[T(TD)] public bool CloneParentSectorSettings;
[T(TC)] public int ParentOffsetTransform;
//[Header("Cloned from root")]
public eClockwise Clockwise = eClockwise.Clockwise;
[T(T1), Range(0f, 360f)] public float DegreesTotal = 360;
[T(T2), Range(-360f, 360f)] public float DegreesOffset;
[T(T3), Range(-360f, 360f)] public float DegreesGap;
[T(T4)] public bool PivotToSector;
[T(T5)] public float LocalRescaleDelta;
[Header("Other")]
[T(T6), Range(1, 100)] public float GeometryResolution = 20;
[T(T7), Range(-98f, 98f)] public float SpriteBorderBalance = 85;
[T(T8)] public float Radius1 = 50;
[T(T8)] public float Radius2 = 100;
[T(T9)] public float PixelsPerUnitMultiplier = 1;
[T(TA)] public bool FillCenter = true;
[T(TB)] public eType Type = eType.Sliced;
[T(TE)] public TrackedSprite Sprite = new();
[T(TF)] public SectorGradient[] Gradients = Array.Empty<SectorGradient>();
// public eMinMax FillOrigin;
// public eFillMethod FillMethod;
}
//[Serializable]
public struct SharedSettings : IEquatable<SharedSettings>
{
[Range(0, 360)] public float AngleMax;
[Range(-360, 360)] public float AngleOffset;
[Range(-360, 360)] public float AngleMargin;
public float PixelsMargin;
public bool PivotToSector;
public eClockwise Clockwise;
public bool Equals(SharedSettings other)
{
return Mathf.Abs(AngleMax - other.AngleMax) > .01f
&& Mathf.Abs(AngleOffset - other.AngleOffset) > .01f
&& Mathf.Abs(AngleMargin - other.AngleMargin) > .01f
&& Mathf.Abs(PixelsMargin - other.PixelsMargin) > .01f
&& PivotToSector == other.PivotToSector
&& Clockwise == other.Clockwise;
}
public override int GetHashCode() => HashCode.Combine(AngleMax, AngleOffset, AngleMargin, PixelsMargin, PivotToSector, (int)Clockwise);
public override bool Equals(object obj) => obj is SharedSettings other && Equals(other);
public static bool operator ==(SharedSettings c1, SharedSettings c2) => c1.Equals(c2);
public static bool operator !=(SharedSettings c1, SharedSettings c2) => !c1.Equals(c2);
}
//[Serializable]
public class Cache
{
public Sector RootSector;
public Vector3 CircleCenter;
public Vector3 SectorCenter;
public Vector2 StartVector;
public Vector2 MiddleVector;
public Vector2 EndVector;
public Color Color;
public Color32 Color32;
public Rect TransformRect;
public RectTransform RootSectorTransform;
public RectTransform Root;
public Matrix4x4 Matrix4x4;
public int childCount;
public int SiblingIndex;
public float InnerRadius;
public float OuterRadius;
public float HalfRadius;
public float MinAngle;
public float MiddleAngle;
public float MaxAngle;
public float DeltaAngle;
public float DeltaAngleFinalAbs;
public float DeltaAngleAbs;
public float RectMinSize;
public float ContentSize;
public float DegreesOffset;
}
//[Serializable]
public class Rendering
{
const float SEGMENT_MIN_ANGLE = 3;
const float DEG2_RAD = Mathf.Deg2Rad;
static readonly Vector4 DEFAULT_UV = new Vector4(0, 0, 1, 1);
static readonly float SQRT_2 = Mathf.Sqrt(2);
static readonly UIVertexPool _pool = new(100);
/// left,center horizontal, right
static readonly int[] s_segmentsPerBorder = new int[3];
static readonly float[] s_radius = new float[2];
[NonSerialized] ArcRenderValues values;
/// normalized uv outside all Sprite
[NonSerialized] Vector4 outerUV;
/// normalized uv inside Sprite-border
[NonSerialized] Vector4 innerUV;
/// X=left, Y=bottom, Z=right, W=top | pixel-metric printed in Sprite Editor
[NonSerialized] Vector4 spriteBorder;
[NonSerialized] float multipliedPixelsPerUnit;
[NonSerialized] VertexHelper _vh;
[NonSerialized] Raycasting _raycasting;
[NonSerialized] Sector _sector;
[NonSerialized] Settings Settings;
[NonSerialized] Cache Cache;
[NonSerialized] Vector4 _spriteUV;
[NonSerialized] bool hasGradients;
[NonSerialized] SharedSettings _sharedSettings;
public void OnPopulateMesh(VertexHelper vh, Sector sector, Raycasting raycasting)
{
vh.Clear();
_vh = vh;
_sector = sector;
_raycasting = raycasting;
Settings = _sector.Settings;
FillCache();
if (_sector.Cache.Color32.a != 0)
{
Render();
}
}
void FillCache()
{
Cache = _sector.Cache;
Cache.RootSectorTransform = (RectTransform)_sector.GetRootSector();
CloneRootSector();
Cache.Root = (RectTransform)Cache.RootSectorTransform?.parent;
Cache.childCount = 1;
Cache.SiblingIndex = 0;
if (Settings.ShapeSource != eShapeSource.CircleAlways)
{
if (Cache.Root)
{
Cache.childCount = Cache.Root.childCount;
}
if (Cache.RootSectorTransform)
{
Cache.SiblingIndex = Cache.RootSectorTransform.GetSiblingIndex();
}
}
Cache.InnerRadius = Mathf.Min(Settings.Radius1, Settings.Radius2) / 100f;
Cache.OuterRadius = Mathf.Max(Settings.Radius1, Settings.Radius2) / 100f;
Cache.HalfRadius = Mathf.LerpUnclamped(Cache.OuterRadius, Cache.InnerRadius, .5f);
Cache.TransformRect = _sector.rectTransform.rect;
// if (Settings.PivotToSector)
// {
// Cache.TransformRect.size *= 2;
// Cache.TransformRect.min -= Cache.TransformRect.size / 2;
// }
Rect rect = Cache.TransformRect;
Cache.CircleCenter = rect.center;
float centerRadius = Mathf.Lerp(Cache.OuterRadius, Cache.InnerRadius, .5f);
float radiusX = rect.width / 2 * centerRadius;
float radiusY = rect.height / 2 * centerRadius;
int clockwise = (int)Settings.Clockwise;
Cache.DegreesOffset = Settings.DegreesOffset * clockwise;
float offset = Cache.DegreesOffset;
float degreeStep = Cache.DeltaAngle = Settings.DegreesTotal / Cache.childCount * clockwise;
Cache.DeltaAngleAbs = Mathf.Abs(degreeStep);
float currentDegree = offset + degreeStep * Cache.SiblingIndex;
Cache.MinAngle = currentDegree + Settings.DegreesGap * clockwise;
Cache.MaxAngle = currentDegree + degreeStep - Settings.DegreesGap * clockwise;
Cache.MiddleAngle = Mathf.LerpUnclamped(Cache.MinAngle, Cache.MaxAngle, .5f);
Cache.DeltaAngleFinalAbs = Mathf.Abs(Cache.MaxAngle - Cache.MinAngle);
Vector3 vector = Cache.MiddleVector = new Vector2(Math.Cos(Cache.MiddleAngle * DEG2_RAD) * radiusX, Math.Sin(Cache.MiddleAngle * DEG2_RAD) * radiusY);
Cache.SectorCenter = Cache.CircleCenter + vector;
Cache.Matrix4x4 = Matrix4x4.TRS(default, Quaternion.identity, Vector3.one * (Cache.OuterRadius - Settings.LocalRescaleDelta));
Cache.Color = _sector.color;
Cache.Color32 = _sector.color;
Cache.StartVector = new Vector2(Math.Cos(Cache.MinAngle * DEG2_RAD) * radiusX, Math.Sin(Cache.MinAngle * DEG2_RAD) * radiusY);
Cache.EndVector = new Vector2(Math.Cos(Cache.MaxAngle * DEG2_RAD) * radiusX, Math.Sin(Cache.MaxAngle * DEG2_RAD) * radiusY);
hasGradients = Settings.Gradients.Length > 0;
Cache.RectMinSize = Mathf.Min(Cache.TransformRect.width, Cache.TransformRect.height);
float size = (_sector.Cache.OuterRadius - _sector.Cache.InnerRadius) * Cache.RectMinSize / 2;
float size2 = Mathf.Abs(Cache.DeltaAngleFinalAbs * DEG2_RAD * Cache.HalfRadius * Cache.RectMinSize * Mathf.PI / 8);
Cache.ContentSize = Mathf.Min(size, size2) - Settings.LocalRescaleDelta * Cache.OuterRadius * Cache.RectMinSize / 2 * SQRT_2;
}
public void Update(Sector sector)
{
Settings = sector.Settings;
if (!Settings.CloneParentSectorSettings) return;
_sector = sector;
Cache = sector.Cache;
CloneRootSector();
}
void CloneRootSector()
{
if (!Settings.CloneParentSectorSettings) return;
if (!Cache.RootSectorTransform) return;
Cache.RootSector = Cache.RootSectorTransform.GetComponent<Sector>();
Sector root = Cache.RootSector;
if (!root) return;
SharedSettings newSettings = _sharedSettings;
_sharedSettings.PivotToSector = Settings.PivotToSector;
_sharedSettings.AngleMargin = Settings.DegreesGap;
_sharedSettings.PixelsMargin = Settings.LocalRescaleDelta;
_sharedSettings.AngleMax = Settings.DegreesTotal;
_sharedSettings.AngleOffset = Settings.DegreesOffset;
_sharedSettings.Clockwise = Settings.Clockwise;
newSettings.PivotToSector = root.Settings.PivotToSector;
newSettings.AngleMargin = root.Settings.DegreesGap;
newSettings.PixelsMargin = root.Settings.LocalRescaleDelta;
newSettings.AngleMax = root.Settings.DegreesTotal;
newSettings.AngleOffset = root.Settings.DegreesOffset;
newSettings.Clockwise = root.Settings.Clockwise;
if (newSettings != _sharedSettings)
{
Settings.PivotToSector = root.Settings.PivotToSector;
Settings.DegreesGap = root.Settings.DegreesGap;
Settings.LocalRescaleDelta = root.Settings.LocalRescaleDelta;
Settings.DegreesTotal = root.Settings.DegreesTotal;
Settings.DegreesOffset = root.Settings.DegreesOffset;
Settings.Clockwise = root.Settings.Clockwise;
SetDirty();
}
}
void Render()
{
switch (Settings.Type)
{
case eType.Sliced:
{
bool isSliceAvailable = Settings.Sprite.sprite && Settings.Sprite.sprite.border != default || !Settings.FillCenter;
if (isSliceAvailable)
{
RenderSliced();
}
else
{
RenderSimple();
}
break;
}
case eType.Filled:
{
RenderFilled();
break;
}
case eType.Simple:
{
RenderSimple();
break;
}
case eType.Tiled:
{
RenderTiled();
break;
}
}
_pool.ReleaseAll();
}
void RenderSimple()
{
_spriteUV = Settings.Sprite.activeSprite != null ? UnityEngine.Sprites.DataUtility.GetOuterUV(Settings.Sprite.activeSprite) : DEFAULT_UV;
multipliedPixelsPerUnit = 1;
values = new ArcRenderValues
{
uvMin = new Vector2(_spriteUV.x, _spriteUV.y),
uvMax = new Vector2(_spriteUV.z, _spriteUV.w),
segmentCount = Mathf.CeilToInt(360 / SEGMENT_MIN_ANGLE * (Settings.GeometryResolution / 100f) * (Cache.DeltaAngleAbs / 360)),
angle1 = Cache.MinAngle,
angle2 = Cache.MaxAngle,
radius1 = Cache.InnerRadius,
radius2 = Cache.OuterRadius,
};
values.uvDelta = (values.uvMax - values.uvMin) / values.segmentCount;
RenderArc(ref values);
}
void RenderTiled()
{
_spriteUV = (Settings.Sprite.activeSprite != null) ? UnityEngine.Sprites.DataUtility.GetOuterUV(Settings.Sprite.activeSprite) : DEFAULT_UV;
multipliedPixelsPerUnit = _sector.multipliedPixelsPerUnit;
values = new ArcRenderValues
{
uvMin = new Vector2(_spriteUV.x, _spriteUV.y),
uvMax = new Vector2(_spriteUV.z, _spriteUV.w),
segmentCount = Mathf.CeilToInt(360 / SEGMENT_MIN_ANGLE * (Settings.GeometryResolution / 100f) * (Cache.DeltaAngleAbs / Settings.DegreesTotal)),
angle1 = Cache.MinAngle,
angle2 = Cache.MaxAngle,
radius1 = Cache.InnerRadius,
radius2 = Cache.OuterRadius,
};
values.uvDelta = (values.uvMax - values.uvMin) / values.segmentCount;
RenderArc(ref values);
}
void RenderFilled()
{
}
void RenderSliced()
{
if (!_sector.Settings.Sprite.hasBorder)
{
RenderSimple();
return;
}
multipliedPixelsPerUnit = _sector.multipliedPixelsPerUnit;
Sprite activeSprite = _sector.Settings.Sprite.activeSprite;
outerUV = UnityEngine.Sprites.DataUtility.GetOuterUV(activeSprite);
innerUV = UnityEngine.Sprites.DataUtility.GetInnerUV(activeSprite);
/// X=left, Y=bottom, Z=right, W=top | pixel-metric printed in Sprite Editor
spriteBorder = activeSprite.border;
float widthFactor = activeSprite.rect.width * multipliedPixelsPerUnit * (1 + Settings.SpriteBorderBalance / 100f) * Cache.DeltaAngleAbs / 360;
float heightFactor = activeSprite.rect.height * multipliedPixelsPerUnit * (1 - Settings.SpriteBorderBalance / 100f) * (Cache.OuterRadius - Cache.InnerRadius);
spriteBorder.x /= widthFactor;
spriteBorder.z /= widthFactor;
spriteBorder.y /= heightFactor;
spriteBorder.w /= heightFactor;
// sprite border applied per-quad, to simplify math
/// left,center horizontal, right
int segmentCount = Mathf.CeilToInt(360 / SEGMENT_MIN_ANGLE * (Settings.GeometryResolution / 100f) * (Cache.DeltaAngleAbs / 360));
s_segmentsPerBorder[0] = Mathf.Clamp(Mathf.CeilToInt(spriteBorder.x * segmentCount), 0, segmentCount);
s_segmentsPerBorder[2] = Mathf.Clamp(Mathf.CeilToInt(spriteBorder.z * segmentCount), 0, segmentCount);
s_segmentsPerBorder[1] = Mathf.Clamp(segmentCount - s_segmentsPerBorder[0] - s_segmentsPerBorder[2], 1, segmentCount);
s_radius[0] = Mathf.Lerp(Cache.InnerRadius, Cache.OuterRadius, spriteBorder.y);
s_radius[1] = Mathf.Lerp(Cache.InnerRadius, Cache.OuterRadius, 1 - spriteBorder.w);
// bot left
//values = new ArcRenderValues();
values.uvMin = new Vector2(outerUV.x, outerUV.y);
values.uvMax = new Vector2(innerUV.x, innerUV.y);
values.segmentCount = s_segmentsPerBorder[0];
values.angle1 = Cache.MinAngle;
values.angle2 = Cache.MinAngle + Cache.DeltaAngle * spriteBorder.x;
values.radius1 = Cache.InnerRadius;
values.radius2 = s_radius[0];
values.uvDelta = (values.uvMax - values.uvMin) / values.segmentCount;
RenderArc(ref values);
// left
//values = new ArcRenderValues();
values.uvMin = new Vector2(outerUV.x, innerUV.y);
values.uvMax = new Vector2(innerUV.x, innerUV.w);
values.segmentCount = s_segmentsPerBorder[0];
values.angle1 = Cache.MinAngle;
values.angle2 = Cache.MinAngle + Cache.DeltaAngle * spriteBorder.x;
values.radius1 = s_radius[0];
values.radius2 = s_radius[1];
values.uvDelta = (values.uvMax - values.uvMin) / values.segmentCount;
RenderArc(ref values);
// left top
//values = new ArcRenderValues();
values.uvMin = new Vector2(outerUV.x, innerUV.w);
values.uvMax = new Vector2(innerUV.x, outerUV.w);
values.segmentCount = s_segmentsPerBorder[0];
values.angle1 = Cache.MinAngle;
values.angle2 = Cache.MinAngle + Cache.DeltaAngle * spriteBorder.x;
values.radius1 = s_radius[1];
values.radius2 = Cache.OuterRadius;
values.uvDelta = (values.uvMax - values.uvMin) / values.segmentCount;
RenderArc(ref values);
// bot
//values = new ArcRenderValues();
values.uvMin = new Vector2(innerUV.x, outerUV.y);
values.uvMax = new Vector2(innerUV.z, innerUV.y);
values.segmentCount = s_segmentsPerBorder[1];
values.angle1 = Cache.MinAngle + Cache.DeltaAngle * spriteBorder.x;
values.angle2 = Cache.MaxAngle - Cache.DeltaAngle * spriteBorder.z;
values.radius1 = Cache.InnerRadius;
values.radius2 = s_radius[0];
values.uvDelta = (values.uvMax - values.uvMin) / values.segmentCount;
RenderArc(ref values);
// center
if (Settings.FillCenter)
{
//values = new ArcRenderValues();
values.uvMin = new Vector2(innerUV.x, innerUV.y);
values.uvMax = new Vector2(innerUV.z, innerUV.w);
values.segmentCount = s_segmentsPerBorder[1];
values.angle1 = Cache.MinAngle + Cache.DeltaAngle * spriteBorder.x;
values.angle2 = Cache.MaxAngle - Cache.DeltaAngle * spriteBorder.z;
values.radius1 = s_radius[0];
values.radius2 = s_radius[1];
values.uvDelta = (values.uvMax - values.uvMin) / values.segmentCount;
RenderArc(ref values);
}
// top
//values = new ArcRenderValues();
values.uvMin = new Vector2(innerUV.x, innerUV.w);
values.uvMax = new Vector2(innerUV.z, outerUV.w);
values.segmentCount = s_segmentsPerBorder[1];
values.angle1 = Cache.MinAngle + Cache.DeltaAngle * spriteBorder.x;
values.angle2 = Cache.MaxAngle - Cache.DeltaAngle * spriteBorder.z;
values.radius1 = s_radius[1];
values.radius2 = Cache.OuterRadius;
values.uvDelta = (values.uvMax - values.uvMin) / values.segmentCount;
RenderArc(ref values);
// right bot
//values = new ArcRenderValues();
values.uvMin = new Vector2(innerUV.x, outerUV.y);
values.uvMax = new Vector2(outerUV.x, innerUV.y);
values.segmentCount = s_segmentsPerBorder[2];
values.angle1 = Cache.MaxAngle - Cache.DeltaAngle * spriteBorder.z;
values.angle2 = Cache.MaxAngle;
values.radius1 = Cache.InnerRadius;
values.radius2 = s_radius[0];
values.uvDelta = (values.uvMax - values.uvMin) / values.segmentCount;
RenderArc(ref values);
// right
//values = new ArcRenderValues();
values.uvMin = new Vector2(innerUV.x, innerUV.y);
values.uvMax = new Vector2(outerUV.x, innerUV.w);
values.segmentCount = s_segmentsPerBorder[2];
values.angle1 = Cache.MaxAngle - Cache.DeltaAngle * spriteBorder.z;
values.angle2 = Cache.MaxAngle;
values.radius1 = s_radius[0];
values.radius2 = s_radius[1];
values.uvDelta = (values.uvMax - values.uvMin) / values.segmentCount;
RenderArc(ref values);
// right top
//values = new ArcRenderValues();
values.uvMin = new Vector2(innerUV.x, innerUV.w);
values.uvMax = new Vector2(outerUV.x, outerUV.w);
values.segmentCount = s_segmentsPerBorder[2];
values.angle1 = Cache.MaxAngle - Cache.DeltaAngle * spriteBorder.z;
values.angle2 = Cache.MaxAngle;
values.radius1 = s_radius[1];
values.radius2 = Cache.OuterRadius;
values.uvDelta = (values.uvMax - values.uvMin) / values.segmentCount;
RenderArc(ref values);
}
void RenderArc(ref ArcRenderValues arc)
{
QuadRenderValues q = default;
Rect rect = Cache.TransformRect;
Vector2 uvMin = arc.uvMin;
arc.uvDelta = (arc.uvMax - uvMin) / arc.segmentCount;
arc.uvDeltaGradient = 1f / arc.segmentCount;
q.uvGradient = new(0, arc.uvDeltaGradient);
Vector2 uvMax = arc.uvMin + arc.uvDelta;
uvMax.y = arc.uvMax.y;
q.uvSprite = new Vector4(uvMin.x, uvMin.y, uvMax.x, uvMax.y);
float angle1 = arc.angle1;
float angleDelta = (arc.angle2 - arc.angle1) / arc.segmentCount;
float angle2 = angle1 + angleDelta;
float radius1X = arc.radius1 * rect.width / 2;
float radius1Y = arc.radius1 * rect.height / 2;
float radius2X = arc.radius2 * rect.width / 2;
float radius2Y = arc.radius2 * rect.height / 2;
q.leftBot = new Vector3(Math.Cos(angle1 * DEG2_RAD) * radius1X, Math.Sin(angle1 * DEG2_RAD) * radius1Y) + Cache.CircleCenter;
q.leftTop = new Vector3(Math.Cos(angle1 * DEG2_RAD) * radius2X, Math.Sin(angle1 * DEG2_RAD) * radius2Y) + Cache.CircleCenter;
q.rightTop = new Vector3(Math.Cos(angle2 * DEG2_RAD) * radius2X, Math.Sin(angle2 * DEG2_RAD) * radius2Y) + Cache.CircleCenter;
q.rightBot = new Vector3(Math.Cos(angle2 * DEG2_RAD) * radius1X, Math.Sin(angle2 * DEG2_RAD) * radius1Y) + Cache.CircleCenter;
q.angles = new(angle1, angle2);
q.radius = new(0, 1);
for (int i = 0; i < arc.segmentCount; i++)
{
q.leftBot.Set(Math.Cos(angle1 * DEG2_RAD) * radius1X + Cache.CircleCenter.x, Math.Sin(angle1 * DEG2_RAD) * radius1Y + Cache.CircleCenter.y, 0);
q.leftTop.Set(Math.Cos(angle1 * DEG2_RAD) * radius2X + Cache.CircleCenter.x, Math.Sin(angle1 * DEG2_RAD) * radius2Y + Cache.CircleCenter.y, 0);
q.rightTop.Set(Math.Cos(angle2 * DEG2_RAD) * radius2X + Cache.CircleCenter.x, Math.Sin(angle2 * DEG2_RAD) * radius2Y + Cache.CircleCenter.y, 0);
q.rightBot.Set(Math.Cos(angle2 * DEG2_RAD) * radius1X + Cache.CircleCenter.x, Math.Sin(angle2 * DEG2_RAD) * radius1Y + Cache.CircleCenter.y, 0);
if (Settings.LocalRescaleDelta != 0)
{
q.leftBot = Cache.Matrix4x4.MultiplyPoint(q.leftBot);
q.leftTop = Cache.Matrix4x4.MultiplyPoint(q.leftTop);
q.rightTop = Cache.Matrix4x4.MultiplyPoint(q.rightTop);
q.rightBot = Cache.Matrix4x4.MultiplyPoint(q.rightBot);
}
RenderQuad(ref arc, ref q);
angle1 += angleDelta;
angle2 += angleDelta;
q.uvSprite.x += arc.uvDelta.x;
q.uvSprite.z += arc.uvDelta.x;
q.uvGradient.x += arc.uvDeltaGradient;
q.uvGradient.y += arc.uvDeltaGradient;
q.angles.Set(angle1, angle2);
}
}
void RenderQuad(ref ArcRenderValues arc, ref QuadRenderValues q)
{
UIVertex[] vertices = _pool.Get();
vertices[0].position.Set(q.leftBot.x, q.leftBot.y, q.leftBot.z);
vertices[1].position.Set(q.rightBot.x, q.rightBot.y, q.rightBot.z);
vertices[2].position.Set(q.rightTop.x, q.rightTop.y, q.rightTop.z);
vertices[3].position.Set(q.leftTop.x, q.leftTop.y, q.leftTop.z);
Vector2 size = Cache.TransformRect.size;
Vector2 sizeMultiplied = size * multipliedPixelsPerUnit;
switch (Settings.Type)
{
case eType.Tiled:
{
vertices[0].uv0.Set(q.leftBot.x / sizeMultiplied.x, q.leftBot.y / sizeMultiplied.y, 0, 0);
vertices[1].uv0.Set(q.rightBot.x / sizeMultiplied.x, q.rightBot.y / sizeMultiplied.y, 0, 0);
vertices[2].uv0.Set(q.rightTop.x / sizeMultiplied.x, q.rightTop.y / sizeMultiplied.y, 0, 0);
vertices[3].uv0.Set(q.leftTop.x / sizeMultiplied.x, q.leftTop.y / sizeMultiplied.y, 0, 0);
break;
}
default:
{
vertices[0].uv0.Set(q.uvSprite.x, q.uvSprite.y, 0, 0);
vertices[1].uv0.Set(q.uvSprite.z, q.uvSprite.y, 0, 0);
vertices[2].uv0.Set(q.uvSprite.z, q.uvSprite.w, 0, 0);
vertices[3].uv0.Set(q.uvSprite.x, q.uvSprite.w, 0, 0);
break;
}
}
if (hasGradients)
{
Vector2 min = Cache.TransformRect.min + (Vector2)Cache.CircleCenter;
for (int v = 0; v < 4; v++)
{
Color color = Cache.Color;
for (int g = 0; g < Settings.Gradients.Length; g++)
{
SectorGradient gr = Settings.Gradients[g];
{
color = gr.Apply(color, vertices[v], Cache.TransformRect, q, arc, Settings, v > 1 ? 1 : 0, v is 1 or 2 ? 1 : 0);
}
}
vertices[v].color = color;
}
}
else
{
vertices[0].color = Cache.Color32;
vertices[1].color = Cache.Color32;
vertices[2].color = Cache.Color32;
vertices[3].color = Cache.Color32;
}
_vh.AddUIVertexQuad(vertices);
}
void SetDirty()
{
if (!_sector || !_sector.isActiveAndEnabled)
return;
#if UNITY_EDITOR
EditorUtility.SetDirty(_sector);
#endif
if (!CanvasUpdateRegistry.IsRebuildingGraphics())
_sector.SetVerticesDirty();
if (!CanvasUpdateRegistry.IsRebuildingLayout())
_sector.SetLayoutDirty();
else
_sector.StartCoroutine(DelayedSetDirty(_sector.rectTransform));
}
IEnumerator DelayedSetDirty(RectTransform rectTransform)
{
yield return null;
LayoutRebuilder.MarkLayoutForRebuild(rectTransform);
}
}
//[Serializable]
public struct QuadRenderValues
{
public Vector4 uvSprite;
public Vector2 uvGradient;
public Vector3 leftBot;
public Vector3 rightBot;
public Vector3 rightTop;
public Vector3 leftTop;
public Vector2 angles;
public Vector2 radius;
}
//[Serializable]
public struct ArcRenderValues
{
public Vector2 uvMin;
public Vector2 uvMax;
public Vector2 uvDelta;
public float uvDeltaGradient;
public float fill;
public float angle1;
public float angle2;
public float radius1;
public float radius2;
public int segmentCount;
}
[Serializable]
public class SectorGradient
{
public static Gradient WHITE => new()
{
alphaKeys = new[] { new GradientAlphaKey(0, 255), },
colorKeys = new[] { new GradientColorKey(Color.white, 0), }
};
public Gradient Gradient = WHITE;
public eColorOperation Operation;
public eFillMethod UV;
public eXY RectDirection;
public SectorGradient()
{
Gradient = WHITE;
}
public Color Apply(Color currentColor, UIVertex vertex, Rect rect, QuadRenderValues q, ArcRenderValues arc, Settings settings, int isOuterRadius, int isLastAngle)
{
int d = (int)RectDirection;
Color color = UV switch
{
eFillMethod.Radius => Gradient.Evaluate(q.radius[isOuterRadius]),
eFillMethod.Rect => Gradient.Evaluate((vertex.position[d] - rect.min[d]) / rect.size[d]),
eFillMethod.Degree => Gradient.Evaluate(q.angles[isLastAngle] / settings.DegreesTotal),
eFillMethod.DegreeSector => Gradient.Evaluate(q.uvGradient[isLastAngle]),
_ => Color.white,
};
for (int i = 0; i < 4; i++)
{
switch (Operation)
{
case eColorOperation.Multiply:
{
currentColor[i] *= color[i];
break;
}
case eColorOperation.Add:
{
currentColor[i] += color[i];
break;
}
case eColorOperation.Override:
{
currentColor[i] = color[i];
break;
}
case eColorOperation.Skip: { break; }
}
}
//return isLastAngle == 1 ? Color.white : Color.black;
return currentColor;
}
}
public class Raycasting
{
public bool IsRaycastLocationValid(Sector sector, Vector2 screenPoint, Camera eventCamera)
{
RectTransform rectTransform = sector.rectTransform;
Vector2 localPoint;
if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(rectTransform, screenPoint, eventCamera, out localPoint))
return false;
Vector2 center = sector.Cache.CircleCenter;
Vector2 pointerVector = localPoint - center;
float inner = sector.Cache.InnerRadius;
float outer = sector.Cache.OuterRadius;
float angle = Vector2.SignedAngle(Vector2.right, pointerVector);
Vector2 size = sector.Cache.TransformRect.size / 2;
Vector2 vector = new Vector2(Math.Cos(angle * Mathf.Deg2Rad), Math.Sin(angle * Mathf.Deg2Rad));
Vector2 innerVector = vector * size * inner;
Vector2 outerVector = vector * size * outer;
// Debug.DrawLine(screenPoint, rectTransform.TransformPoint(center), Color.black);
// Debug.DrawLine(rectTransform.TransformPoint(center), rectTransform.TransformPoint(center) + (Vector3)innerVector, Color.black);
// Debug.DrawLine(default, pointerVector, Color.black);
if (pointerVector.sqrMagnitude < innerVector.sqrMagnitude || pointerVector.sqrMagnitude > outerVector.sqrMagnitude)
{
return false;
}
Vector2 startVector = sector.Cache.StartVector;
Vector2 endVector = sector.Cache.EndVector;
if (sector.Settings.Clockwise == eClockwise.Clockwise)
{
(startVector, endVector) = (endVector, startVector);
}
if (sector.Cache.DeltaAngleAbs < 181)
{
if (IsLeft(center, center + startVector, localPoint))
{
return false;
}
// #if UNITY_EDITOR
// if (Application.isPlaying)
// {
// Vector2 drawCenter = rectTransform.TransformPoint(center);
// Vector2 drawVector = rectTransform.TransformVector(endVector);
// //if (sector.Settings.Gizmo)
// {
// Debug.DrawLine(drawCenter, drawCenter + drawVector * 1000, Color.red);
// }
// }
// #endif
if (!IsLeft(center, center + endVector, localPoint))
{
return false;
}
}
return true;
}
static bool IsLeft(Vector2 a, Vector2 b, Vector2 c)
{
Vector2 vector = a - b;
Vector2 originToPoint = c - b;
return -vector.x * originToPoint.y + vector.y * originToPoint.x < 0;
}
}
public class UIVertexPool
{
Stack<UIVertex[]> _pool;
List<UIVertex[]> _used;
public UIVertexPool(int size)
{
_pool = new Stack<UIVertex[]>(size);
_used = new List<UIVertex[]>();
for (int i = 0; i < size; i++)
{
_pool.Push(new UIVertex[4]);
}
}
public UIVertex[] Get()
{
UIVertex[] item = null;
if (_pool.Count > 0)
{
item = _pool.Pop();
}
else
{
item = new UIVertex[4];
for (int i = 0; i < 4; i++)
{
item[i] = UIVertex.simpleVert;
}
}
_used.Add(item);
return item;
}
public void ReleaseAll()
{
int count = _used.Count;
for (int i = 0; i < count; i++)
{
Release(_used[i]);
}
_used.Clear();
}
public void Release(UIVertex[] vrtx)
{
if (vrtx != null)
{
_pool.Push(vrtx);
}
}
public void Clear()
{
_pool.Clear();
}
}
[Serializable]
public class TrackedSprite
{
MaskableGraphic _graphic;
// To track textureless images, which will be rebuild if sprite atlas manager registered a Sprite Atlas that will give this image new texture
static List<TrackedSprite> m_TrackedTextureless = new();
static bool s_Initialized;
[SerializeField] Sprite m_Sprite;
// Whether this is being tracked for Atlas Binding.
bool m_Tracked;
public Sprite sprite
{
get { return m_Sprite; }
set
{
if (m_Sprite != null)
{
if (m_Sprite != value)
{
m_Sprite = value;
_graphic.SetAllDirty();
TrackSprite();
}
}
else if (value != null)
{
m_Sprite = value;
_graphic.SetAllDirty();
TrackSprite();
}
}
}
[NonSerialized] Sprite m_OverrideSprite;
public Sprite overrideSprite
{
get => activeSprite;
set
{
if (!SetClass(ref m_OverrideSprite, value)) return;
_graphic.SetAllDirty();
TrackSprite();
}
}
public Sprite activeSprite => m_OverrideSprite != null ? m_OverrideSprite : sprite;
public bool hasBorder
{
get
{
if (activeSprite != null)
{
Vector4 v = activeSprite.border;
return v.sqrMagnitude > 0f;
}
return false;
}
}
static void Rebuild(SpriteAtlas spriteAtlas)
{
for (var i = m_TrackedTextureless.Count - 1; i >= 0; i--)
{
var tracked = m_TrackedTextureless[i];
if (null != tracked.activeSprite && spriteAtlas.CanBindTo(tracked.activeSprite))
{
tracked._graphic.SetAllDirty();
m_TrackedTextureless.RemoveAt(i);
}
}
}
static void Track(TrackedSprite tracked)
{
if (!s_Initialized)
{
SpriteAtlasManager.atlasRegistered += Rebuild;
s_Initialized = true;
}
m_TrackedTextureless.Add(tracked);
}
static void UnTrack(TrackedSprite tracked)
{
m_TrackedTextureless.Remove(tracked);
}
public void OnEnable(MaskableGraphic graphic)
{
_graphic = graphic;
TrackSprite();
}
public void OnDisable()
{
if (m_Tracked) UnTrack(this);
}
void TrackSprite()
{
if (activeSprite == null || activeSprite.texture != null) return;
Track(this);
m_Tracked = true;
}
static bool SetClass<T>(ref T currentValue, T newValue) where T : class
{
if ((currentValue == null && newValue == null) || (currentValue != null && currentValue.Equals(newValue)))
return false;
currentValue = newValue;
return true;
}
}
[Serializable]
public class Layout
{
public Anchor[] Anchors = Array.Empty<Anchor>();
DrivenRectTransformTracker _tracker;
Sector _sector;
public void Update(Sector sector)
{
_sector ??= sector;
if (_sector.Cache.Root)
{
int newCount = _sector.Cache.Root.childCount;
if (newCount != _sector.Cache.childCount)
{
sector.SetAllDirty();
}
}
if (_sector.Cache.RootSectorTransform)
{
int siblingIndex = _sector.Cache.RootSectorTransform.GetSiblingIndex();
if (siblingIndex != _sector.Cache.SiblingIndex)
{
sector.SetAllDirty();
}
}
foreach (Anchor anchor in Anchors)
{
anchor.Execute(_sector);
}
}
public void SetLayoutHorizontal(Sector sector)
{
_sector ??= sector;
foreach (Anchor anchor in Anchors)
{
anchor.Execute(_sector);
}
_tracker.Clear();
DrivenTransformProperties flags = default;
if (_sector.Settings.PivotToSector)
{
flags |= DrivenTransformProperties.Pivot;
Vector2 pivot = ((Vector2)_sector.Cache.SectorCenter - _sector.Cache.TransformRect.min) / _sector.Cache.TransformRect.size;
if (!float.IsNaN(pivot.x) && !float.IsNaN(pivot.y))
{
sector.rectTransform.pivot = pivot;
}
}
if (flags != default)
{
_tracker.Add(sector, sector.rectTransform, flags);
}
foreach (Anchor anchor in Anchors)
{
anchor.Execute(_sector);
}
}
public void SetLayoutVertical(Sector sector) => SetLayoutHorizontal(sector);
[Serializable]
public class Anchor
{
static readonly Vector2 HALF_VECTOR = Vector2.one * .5f;
const float DEG2_RAD = Mathf.Deg2Rad;
Sector _sector;
public RectTransform rectTransform;
public bool Resize;
public Vector2 ResizeOffset;
public bool PivotToOuterCorner;
public Vector2 NormalizedAnchorAngleRadius = HALF_VECTOR;
public Vector2 OffsetAnchorAngleRadius;
public eRotation Rotation = eRotation.Root;
DrivenRectTransformTracker _tracker;
public void Execute(Sector sector)
{
_sector ??= sector;
_tracker.Clear();
if (!rectTransform) return;
float radius = Mathf.LerpUnclamped(_sector.Cache.InnerRadius, _sector.Cache.OuterRadius, NormalizedAnchorAngleRadius.y) + OffsetAnchorAngleRadius.y;
float angle = (Mathf.LerpUnclamped(_sector.Cache.MinAngle, _sector.Cache.MaxAngle, NormalizedAnchorAngleRadius.x) + OffsetAnchorAngleRadius.x) * DEG2_RAD;
Vector2 position = new(Math.Cos(angle) * radius * _sector.Cache.TransformRect.width / 2, Math.Sin(angle) * radius * _sector.Cache.TransformRect.height / 2);
if (!float.IsNaN(position.x) && !float.IsNaN(position.y))
{
rectTransform.anchoredPosition = position;
}
rectTransform.anchorMin = rectTransform.anchorMax = HALF_VECTOR;
var flags = DrivenTransformProperties.Anchors | DrivenTransformProperties.AnchoredPosition;
switch (Rotation)
{
case eRotation.Root:
{
flags |= DrivenTransformProperties.Rotation;
if (_sector.Cache.Root)
{
rectTransform.rotation = _sector.Cache.Root.rotation;
}
break;
}
case eRotation.ToCenter:
{
flags |= DrivenTransformProperties.Rotation;
rectTransform.localRotation = Quaternion.LookRotation(Vector3.forward, _sector.Cache.CircleCenter - rectTransform.localPosition);
break;
}
default:
case eRotation.None: break;
}
if (Resize)
{
flags |= DrivenTransformProperties.SizeDelta;
if (!float.IsNaN(_sector.Cache.ContentSize))
{
rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, _sector.Cache.ContentSize + ResizeOffset.x);
rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, _sector.Cache.ContentSize + ResizeOffset.y);
}
}
if (PivotToOuterCorner)
{
flags |= DrivenTransformProperties.Pivot;
Vector2 vector = _sector.Cache.MiddleVector;
Vector2 pivot = new Vector2(.5f, .5f);
pivot.x = vector.x > 0 ? 0 : 1;
pivot.y = vector.y > 0 ? 0 : 1;
rectTransform.pivot = pivot;
}
#if UNITY_EDITOR
EditorUtility.SetDirty(rectTransform);
#endif
_tracker.Add(_sector, rectTransform, flags);
}
}
public void SetDirty()
{
if (!_sector || !_sector.isActiveAndEnabled)
return;
if (!CanvasUpdateRegistry.IsRebuildingGraphics())
_sector.SetVerticesDirty();
if (!CanvasUpdateRegistry.IsRebuildingLayout())
_sector.SetLayoutDirty();
else
_sector.StartCoroutine(DelayedSetDirty(_sector.rectTransform));
}
IEnumerator DelayedSetDirty(RectTransform rectTransform)
{
yield return null;
LayoutRebuilder.MarkLayoutForRebuild(rectTransform);
}
public enum eRotation
{
None,
Root,
ToCenter,
}
}
public class Math
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static float Sin(float x)//x in radians
{
float sinn;
if (x < -3.14159265f)
x += 6.28318531f;
else if (x > 3.14159265f)
x -= 6.28318531f;
if (x < 0)
{
sinn = 1.27323954f * x + 0.405284735f * x * x;
if (sinn < 0)
sinn = 0.225f * (sinn * -sinn - sinn) + sinn;
else
sinn = 0.225f * (sinn * sinn - sinn) + sinn;
return sinn;
}
else
{
sinn = 1.27323954f * x - 0.405284735f * x * x;
if (sinn < 0)
sinn = 0.225f * (sinn * -sinn - sinn) + sinn;
else
sinn = 0.225f * (sinn * sinn - sinn) + sinn;
return sinn;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static float Cos(float x)//x in radians
{
return Sin(x + 1.5707963f);
}
}
#region Editor
#if UNITY_EDITOR
[CustomEditor(typeof(Sector)), CanEditMultipleObjects]
public class SectorEditor : GraphicEditor
{
HashSet<string> _excluded = new() { "m_OnCullStateChanged", "m_RaycastPadding" };
public override void OnInspectorGUI()
{
EditorGUI.BeginChangeCheck();
DrawPropertiesExcludingCustom(serializedObject);
if (EditorGUI.EndChangeCheck())
{
foreach (Sector sector in targets)
{
sector.SetAllDirty();
}
}
serializedObject.ApplyModifiedProperties();
}
void DrawPropertiesExcludingCustom(SerializedObject obj)
{
SerializedProperty iterator = obj.GetIterator();
bool enterChildren = true;
while (iterator.NextVisible(enterChildren))
{
if (DrawParentOffsetTransform(iterator)) continue;
DrawClockwise(iterator.name);
DrawPivotSector(iterator);
enterChildren = false;
if (iterator.name == "<Settings>k__BackingField")
{
enterChildren = true;
}
else
{
if (iterator.name == "m_Script") GUI.enabled = false;
if (!_excluded.Contains(iterator.name))
{
EditorGUILayout.PropertyField(iterator, iterator.name != "<Settings>k__BackingField");
}
if (iterator.name == "m_Script") GUI.enabled = true;
}
}
}
bool DrawParentOffsetTransform(SerializedProperty property)
{
if (property.name != nameof(Settings.ParentOffsetTransform)) return false;
var prop = serializedObject.FindProperty("<Settings>k__BackingField").FindPropertyRelative(nameof(Settings.CloneParentSectorSettings));
var prop2 = serializedObject.FindProperty("<Settings>k__BackingField").FindPropertyRelative(nameof(Settings.ShapeSource));
if (prop.boolValue || prop2.enumValueIndex == (int)eShapeSource.ParentOffsetTransform)
{
EditorGUILayout.PropertyField(property, false);
}
return true;
}
void DrawClockwise(string propertyName)
{
if (propertyName == nameof(Settings.Clockwise))
{
GUI.enabled = !serializedObject.FindProperty("<Settings>k__BackingField").FindPropertyRelative(nameof(Settings.CloneParentSectorSettings)).boolValue;
}
if (propertyName == nameof(Settings.GeometryResolution))
{
GUI.enabled = true;
}
}
void DrawPivotSector(SerializedProperty property)
{
if (property.name != nameof(Settings.PivotToSector)) return;
var prop = serializedObject.FindProperty("<Settings>k__BackingField").FindPropertyRelative(nameof(Settings.LocalRescaleDelta));
if (!property.boolValue && prop.floatValue != 0)
{
EditorGUILayout.HelpBox("LocalRescaleDelta not 0. PivotToSector should be 'true', for valid rescale", MessageType.Warning);
}
}
}
#endif
#endregion
}
@mitay-walle
Copy link
Author

mitay-walle commented Apr 21, 2024

uGUI Custom Graphic that draws an oval sector

Features

  • 1 script
  • no custom shaders
  • correct input handling
  • SpriteBorder support
  • 9-slice support
  • SpriteAtlas support
  • pixelPerUnitMultiplier support
  • Anchors for other transforms, including resizing to squeeze content into a sector
  • UV-by radius and tile by RectTransform
  • free pivot, or generated in the center of the sector
  • gradients
  • nested sectors can stick to parent
  • offset by angle
  • pixel offset
  • performance-wise

Known Issues

  • navigation not always recognize Selectable in selection wheel
  • Tiled mode not support SpriteAtlas, only Single-texture sprites

Usage Example | 9-slice sprite

sector_pixelPerUnitMultiplier
sector_count

Inspector Preview

photo_2024-04-21_03-00-49
photo_2024-04-21_03-00-45
photo_2024-04-21_03-00-42
photo_2024-04-21_03-00-58

photo_2024-04-21_03-01-19
photo_2024-04-21_03-01-16
photo_2024-04-21_03-01-14
photo_2024-04-21_03-01-08
photo_2024-04-21_03-01-03
photo_2024-04-21_03-01-00
photo_2024-04-21_03-01-06
photo_2024-04-21_03-00-51
photo_2024-04-21_02-59-55

@mitay-walle
Copy link
Author

Usage Examples

sector_resolution

sector_count
sector_inputRaycasting2
sector_inputRaycasting
sector_pixelPerUnitMultiplier

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment