diff --git a/Content.Client/Chat/UI/EmotesMenu.xaml b/Content.Client/Chat/UI/EmotesMenu.xaml index cc4d5bb77e..845b631617 100644 --- a/Content.Client/Chat/UI/EmotesMenu.xaml +++ b/Content.Client/Chat/UI/EmotesMenu.xaml @@ -1,4 +1,4 @@ - - - + + - - + + - - + + - + - + - + - + diff --git a/Content.Client/Chat/UI/EmotesMenu.xaml.cs b/Content.Client/Chat/UI/EmotesMenu.xaml.cs index f3b7837f21..80daa405a6 100644 --- a/Content.Client/Chat/UI/EmotesMenu.xaml.cs +++ b/Content.Client/Chat/UI/EmotesMenu.xaml.cs @@ -50,7 +50,6 @@ public sealed partial class EmotesMenu : RadialMenu var button = new EmoteMenuButton { - StyleClasses = { "RadialMenuButton" }, SetSize = new Vector2(64f, 64f), ToolTip = Loc.GetString(emote.Name), ProtoId = emote.ID, @@ -106,7 +105,7 @@ public sealed partial class EmotesMenu : RadialMenu } -public sealed class EmoteMenuButton : RadialMenuTextureButton +public sealed class EmoteMenuButton : RadialMenuTextureButtonWithSector { public ProtoId ProtoId { get; set; } } diff --git a/Content.Client/Ghost/GhostRoleRadioMenu.xaml.cs b/Content.Client/Ghost/GhostRoleRadioMenu.xaml.cs index 3897b1b949..1b65eac6ed 100644 --- a/Content.Client/Ghost/GhostRoleRadioMenu.xaml.cs +++ b/Content.Client/Ghost/GhostRoleRadioMenu.xaml.cs @@ -51,7 +51,6 @@ public sealed partial class GhostRoleRadioMenu : RadialMenu var button = new GhostRoleRadioMenuButton() { - StyleClasses = { "RadialMenuButton" }, SetSize = new Vector2(64, 64), ToolTip = Loc.GetString(ghostRoleProto.Name), ProtoId = ghostRoleProto.ID, @@ -100,7 +99,7 @@ public sealed partial class GhostRoleRadioMenu : RadialMenu } } -public sealed class GhostRoleRadioMenuButton : RadialMenuTextureButton +public sealed class GhostRoleRadioMenuButton : RadialMenuTextureButtonWithSector { public ProtoId ProtoId { get; set; } } diff --git a/Content.Client/RCD/RCDMenu.xaml b/Content.Client/RCD/RCDMenu.xaml index b3d5367a5f..d8ab0ac8f4 100644 --- a/Content.Client/RCD/RCDMenu.xaml +++ b/Content.Client/RCD/RCDMenu.xaml @@ -11,37 +11,37 @@ - - + + - - + + - - + + - - + + - - + + - + - + - + - + - + - + diff --git a/Content.Client/RCD/RCDMenu.xaml.cs b/Content.Client/RCD/RCDMenu.xaml.cs index f0d27d6b1f..7ea9894e41 100644 --- a/Content.Client/RCD/RCDMenu.xaml.cs +++ b/Content.Client/RCD/RCDMenu.xaml.cs @@ -74,7 +74,6 @@ public sealed partial class RCDMenu : RadialMenu var button = new RCDMenuButton() { - StyleClasses = { "RadialMenuButton" }, SetSize = new Vector2(64f, 64f), ToolTip = tooltip, ProtoId = protoId, @@ -99,9 +98,7 @@ public sealed partial class RCDMenu : RadialMenu // is visible in the main radial container (as these all start with Visible = false) foreach (var child in main.Children) { - var castChild = child as RadialMenuTextureButton; - - if (castChild is not RadialMenuTextureButton) + if (child is not RadialMenuTextureButton castChild) continue; if (castChild.TargetLayer == proto.Category) @@ -169,12 +166,7 @@ public sealed partial class RCDMenu : RadialMenu } } -public sealed class RCDMenuButton : RadialMenuTextureButton +public sealed class RCDMenuButton : RadialMenuTextureButtonWithSector { public ProtoId ProtoId { get; set; } - - public RCDMenuButton() - { - - } } diff --git a/Content.Client/Silicons/StationAi/StationAiMenu.xaml b/Content.Client/Silicons/StationAi/StationAiMenu.xaml index d56fc83289..cfa0b93234 100644 --- a/Content.Client/Silicons/StationAi/StationAiMenu.xaml +++ b/Content.Client/Silicons/StationAi/StationAiMenu.xaml @@ -1,4 +1,4 @@ - - + diff --git a/Content.Client/Silicons/StationAi/StationAiMenu.xaml.cs b/Content.Client/Silicons/StationAi/StationAiMenu.xaml.cs index b152f5ead8..a536d911f3 100644 --- a/Content.Client/Silicons/StationAi/StationAiMenu.xaml.cs +++ b/Content.Client/Silicons/StationAi/StationAiMenu.xaml.cs @@ -54,7 +54,6 @@ public sealed partial class StationAiMenu : RadialMenu // TODO: This radial boilerplate is quite annoying var button = new StationAiMenuButton(action.Event) { - StyleClasses = { "RadialMenuButton" }, SetSize = new Vector2(64f, 64f), ToolTip = action.Tooltip != null ? Loc.GetString(action.Tooltip) : null, }; @@ -121,7 +120,7 @@ public sealed partial class StationAiMenu : RadialMenu } } -public sealed class StationAiMenuButton(BaseStationAiAction action) : RadialMenuTextureButton +public sealed class StationAiMenuButton(BaseStationAiAction action) : RadialMenuTextureButtonWithSector { public BaseStationAiAction Action = action; } diff --git a/Content.Client/UserInterface/Controls/RadialContainer.cs b/Content.Client/UserInterface/Controls/RadialContainer.cs index be9b8817a0..72555aab5f 100644 --- a/Content.Client/UserInterface/Controls/RadialContainer.cs +++ b/Content.Client/UserInterface/Controls/RadialContainer.cs @@ -1,4 +1,3 @@ -using Robust.Client.Graphics; using Robust.Client.UserInterface.Controls; using System.Linq; using System.Numerics; @@ -8,6 +7,11 @@ namespace Content.Client.UserInterface.Controls; [Virtual] public class RadialContainer : LayoutContainer { + /// + /// Increment of radius per child element to be rendered. + /// + private const float RadiusIncrement = 5f; + /// /// Specifies the anglular range, in radians, in which child elements will be placed. /// The first value denotes the angle at which the first element is to be placed, and @@ -49,10 +53,30 @@ public class RadialContainer : LayoutContainer public RAlignment RadialAlignment { get; set; } = RAlignment.Clockwise; /// - /// Determines how far from the radial container's center that its child elements will be placed + /// Radial menu radius determines how far from the radial container's center its child elements will be placed. + /// To correctly display dynamic amount of elements control actually resizes depending on amount of child buttons, + /// but uses this property as base value for final radius calculation. /// [ViewVariables(VVAccess.ReadWrite)] - public float Radius { get; set; } = 100f; + public float InitialRadius { get; set; } = 100f; + + /// + /// Radial menu radius determines how far from the radial container's center its child elements will be placed. + /// This is dynamically calculated (based on child button count) radius, result of and + /// multiplied by currently visible child button count. + /// + [ViewVariables(VVAccess.ReadOnly)] + public float CalculatedRadius { get; private set; } + + /// + /// Determines radial menu button sectors inner radius, is a multiplier of . + /// + public float InnerRadiusMultiplier { get; set; } = 0.5f; + + /// + /// Determines radial menu button sectors outer radius, is a multiplier of . + /// + public float OuterRadiusMultiplier { get; set; } = 1.5f; /// /// Sets whether the container should reserve a space on the layout for child which are not currently visible @@ -67,37 +91,74 @@ public class RadialContainer : LayoutContainer { } - - protected override void Draw(DrawingHandleScreen handle) + + /// + protected override Vector2 ArrangeOverride(Vector2 finalSize) { - - const float baseRadius = 100f; - const float radiusIncrement = 5f; - - var children = ReserveSpaceForHiddenChildren ? Children : Children.Where(x => x.Visible); + var children = ReserveSpaceForHiddenChildren + ? Children + : Children.Where(x => x.Visible); + var childCount = children.Count(); - - // Add padding from the center at higher child counts so they don't overlap. - Radius = baseRadius + (childCount * radiusIncrement); + + // Add padding from the center at higher child counts so they don't overlap. + CalculatedRadius = InitialRadius + (childCount * RadiusIncrement); + + var isAntiClockwise = RadialAlignment == RAlignment.AntiClockwise; // Determine the size of the arc, accounting for clockwise and anti-clockwise arrangements var arc = AngularRange.Y - AngularRange.X; - arc = (arc < 0) ? MathF.Tau + arc : arc; - arc = (RadialAlignment == RAlignment.AntiClockwise) ? MathF.Tau - arc : arc; + arc = arc < 0 + ? MathF.Tau + arc + : arc; + arc = isAntiClockwise + ? MathF.Tau - arc + : arc; // Account for both circular arrangements and arc-based arrangements - var childMod = MathHelper.CloseTo(arc, MathF.Tau, 0.01f) ? 0 : 1; + var childMod = MathHelper.CloseTo(arc, MathF.Tau, 0.01f) + ? 0 + : 1; // Determine the separation between child elements var sepAngle = arc / (childCount - childMod); - sepAngle *= (RadialAlignment == RAlignment.AntiClockwise) ? -1f : 1f; + sepAngle *= isAntiClockwise + ? -1f + : 1f; + + var controlCenter = finalSize * 0.5f; // Adjust the positions of all the child elements - foreach (var (i, child) in children.Select((x, i) => (i, x))) + var query = children.Select((x, index) => (index, x)); + foreach (var (childIndex, child) in query) { - var position = new Vector2(Radius * MathF.Sin(AngularRange.X + sepAngle * i) + Width / 2f - child.Width / 2f, -Radius * MathF.Cos(AngularRange.X + sepAngle * i) + Height / 2f - child.Height / 2f); + const float angleOffset = MathF.PI * 0.5f; + + var targetAngleOfChild = AngularRange.X + sepAngle * (childIndex + 0.5f) + angleOffset; + + // flooring values for snapping float values to physical grid - + // it prevents gaps and overlapping between different button segments + var position = new Vector2( + MathF.Floor(CalculatedRadius * MathF.Cos(targetAngleOfChild)), + MathF.Floor(-CalculatedRadius * MathF.Sin(targetAngleOfChild)) + ) + controlCenter - child.DesiredSize * 0.5f + Position; + SetPosition(child, position); + + // radial menu buttons with sector need to also know in which sector and around which point + // they should be rendered, how much space sector should should take etc. + if (child is IRadialMenuItemWithSector tb) + { + tb.AngleSectorFrom = sepAngle * childIndex; + tb.AngleSectorTo = sepAngle * (childIndex + 1); + tb.AngleOffset = angleOffset; + tb.InnerRadius = CalculatedRadius * InnerRadiusMultiplier; + tb.OuterRadius = CalculatedRadius * OuterRadiusMultiplier; + tb.ParentCenter = controlCenter; + } } + + return base.ArrangeOverride(finalSize); } /// @@ -109,4 +170,5 @@ public class RadialContainer : LayoutContainer Clockwise, AntiClockwise, } + } diff --git a/Content.Client/UserInterface/Controls/RadialMenu.cs b/Content.Client/UserInterface/Controls/RadialMenu.cs index 5f56ad7f86..1b7f07aa2c 100644 --- a/Content.Client/UserInterface/Controls/RadialMenu.cs +++ b/Content.Client/UserInterface/Controls/RadialMenu.cs @@ -3,6 +3,9 @@ using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.CustomControls; using System.Linq; using System.Numerics; +using Content.Shared.Input; +using Robust.Client.Graphics; +using Robust.Shared.Input; namespace Content.Client.UserInterface.Controls; @@ -12,11 +15,16 @@ public class RadialMenu : BaseWindow /// /// Contextual button used to traverse through previous layers of the radial menu /// - public TextureButton? ContextualButton { get; set; } + public RadialMenuContextualCentralTextureButton ContextualButton { get; } + + /// + /// Button that represents outer area of menu (closes menu on outside clicks). + /// + public RadialMenuOuterAreaButton MenuOuterAreaButton { get; } /// /// Set a style class to be applied to the contextual button when it is set to move the user back through previous layers of the radial menu - /// + /// public string? BackButtonStyleClass { get @@ -52,7 +60,7 @@ public class RadialMenu : BaseWindow } } - private List _path = new(); + private readonly List _path = new(); private string? _backButtonStyleClass; private string? _closeButtonStyleClass; @@ -60,8 +68,8 @@ public class RadialMenu : BaseWindow /// A free floating menu which enables the quick display of one or more radial containers /// /// - /// Only one radial container is visible at a time (each container forming a separate 'layer' within - /// the menu), along with a contextual button at the menu center, which will either return the user + /// Only one radial container is visible at a time (each container forming a separate 'layer' within + /// the menu), along with a contextual button at the menu center, which will either return the user /// to the previous layer or close the menu if there are no previous layers left to traverse. /// To create a functional radial menu, simply parent one or more named radial containers to it, /// and populate the radial containers with RadialMenuButtons. Setting the TargetLayer field of these @@ -78,23 +86,56 @@ public class RadialMenu : BaseWindow } // Auto generate a contextual button for moving back through visited layers - ContextualButton = new TextureButton() + ContextualButton = new RadialMenuContextualCentralTextureButton { HorizontalAlignment = HAlignment.Center, VerticalAlignment = VAlignment.Center, SetSize = new Vector2(64f, 64f), }; + MenuOuterAreaButton = new RadialMenuOuterAreaButton(); ContextualButton.OnButtonUp += _ => ReturnToPreviousLayer(); + MenuOuterAreaButton.OnButtonUp += _ => Close(); AddChild(ContextualButton); + AddChild(MenuOuterAreaButton); // Hide any further add children, unless its promoted to the active layer - OnChildAdded += child => child.Visible = (GetCurrentActiveLayer() == child); + OnChildAdded += child => + { + child.Visible = GetCurrentActiveLayer() == child; + SetupContextualButtonData(child); + }; + } + + private void SetupContextualButtonData(Control child) + { + if (child is RadialContainer { Visible: true } container) + { + var parentCenter = MinSize * 0.5f; + ContextualButton.ParentCenter = parentCenter; + MenuOuterAreaButton.ParentCenter = parentCenter; + ContextualButton.InnerRadius = container.CalculatedRadius * container.InnerRadiusMultiplier; + MenuOuterAreaButton.OuterRadius = container.CalculatedRadius * container.OuterRadiusMultiplier; + } + } + + /// + protected override Vector2 ArrangeOverride(Vector2 finalSize) + { + var result = base.ArrangeOverride(finalSize); + + var currentLayer = GetCurrentActiveLayer(); + if (currentLayer != null) + { + SetupContextualButtonData(currentLayer); + } + + return result; } private Control? GetCurrentActiveLayer() { - var children = Children.Where(x => x != ContextualButton); + var children = Children.Where(x => x != ContextualButton && x != MenuOuterAreaButton); if (!children.Any()) return null; @@ -116,7 +157,7 @@ public class RadialMenu : BaseWindow foreach (var child in Children) { - if (child == ContextualButton) + if (child == ContextualButton || child == MenuOuterAreaButton) continue; // Hide layers which are not of interest @@ -129,6 +170,7 @@ public class RadialMenu : BaseWindow else { child.Visible = true; + SetupContextualButtonData(child); result = true; } } @@ -158,7 +200,7 @@ public class RadialMenu : BaseWindow // Hide all children except the contextual button foreach (var child in Children) { - if (child != ContextualButton) + if (child != ContextualButton && child != MenuOuterAreaButton) child.Visible = false; } @@ -172,49 +214,86 @@ public class RadialMenu : BaseWindow } } +/// +/// Base class for radial menu buttons. Excludes all actions except clicks and alt-clicks +/// from interactions. +/// [Virtual] -public class RadialMenuButton : Button +public class RadialMenuTextureButtonBase : TextureButton { - /// - /// Upon clicking this button the radial menu will transition to the named layer - /// - public string? TargetLayer { get; set; } - - /// - /// A simple button that can move the user to a different layer within a radial menu - /// - public RadialMenuButton() + /// + protected RadialMenuTextureButtonBase() { - OnButtonUp += OnClicked; + EnableAllKeybinds = true; } - private void OnClicked(ButtonEventArgs args) + /// + protected override void KeyBindUp(GUIBoundKeyEventArgs args) { - if (TargetLayer == null || TargetLayer == string.Empty) - return; - - var parent = FindParentMultiLayerContainer(this); - - if (parent == null) - return; - - parent.TryToMoveToNewLayer(TargetLayer); + if (args.Function == EngineKeyFunctions.UIClick + || args.Function == ContentKeyFunctions.AltActivateItemInWorld) + base.KeyBindUp(args); } +} - private RadialMenu? FindParentMultiLayerContainer(Control control) +/// +/// Special button for closing radial menu or going back between radial menu levels. +/// Is looking like just but considers whole space around +/// itself (til radial menu buttons) as itself in case of clicking. But this 'effect' +/// works only if control have parent, and ActiveContainer property is set. +/// Also considers all space outside of radial menu buttons as itself for clicking. +/// +public sealed class RadialMenuContextualCentralTextureButton : RadialMenuTextureButtonBase +{ + public float InnerRadius { get; set; } + + public Vector2? ParentCenter { get; set; } + + /// + protected override bool HasPoint(Vector2 point) { - foreach (var ancestor in control.GetSelfAndLogicalAncestors()) + if (ParentCenter == null) { - if (ancestor is RadialMenu) - return ancestor as RadialMenu; + return base.HasPoint(point); } - return null; + var distSquared = (point + Position - ParentCenter.Value).LengthSquared(); + + var innerRadiusSquared = InnerRadius * InnerRadius; + + // comparing to squared values is faster then making sqrt + return distSquared < innerRadiusSquared; + } +} + +/// +/// Menu button for outer area of radial menu (covers everything 'outside'). +/// +public sealed class RadialMenuOuterAreaButton : RadialMenuTextureButtonBase +{ + public float OuterRadius { get; set; } + + public Vector2? ParentCenter { get; set; } + + /// + protected override bool HasPoint(Vector2 point) + { + if (ParentCenter == null) + { + return base.HasPoint(point); + } + + var distSquared = (point + Position - ParentCenter.Value).LengthSquared(); + + var outerRadiusSquared = OuterRadius * OuterRadius; + + // comparing to squared values is faster, then making sqrt + return distSquared > outerRadiusSquared; } } [Virtual] -public class RadialMenuTextureButton : TextureButton +public class RadialMenuTextureButton : RadialMenuTextureButtonBase { /// /// Upon clicking this button the radial menu will be moved to the named layer @@ -226,6 +305,7 @@ public class RadialMenuTextureButton : TextureButton /// public RadialMenuTextureButton() { + EnableAllKeybinds = true; OnButtonUp += OnClicked; } @@ -246,10 +326,329 @@ public class RadialMenuTextureButton : TextureButton { foreach (var ancestor in control.GetSelfAndLogicalAncestors()) { - if (ancestor is RadialMenu) - return ancestor as RadialMenu; + if (ancestor is RadialMenu menu) + return menu; } return null; } } + +public interface IRadialMenuItemWithSector +{ + /// + /// Angle in radian where button sector should start. + /// + public float AngleSectorFrom { set; } + + /// + /// Angle in radian where button sector should end. + /// + public float AngleSectorTo { set; } + + /// + /// Outer radius for drawing segment and pointer detection. + /// + public float OuterRadius { set; } + + /// + /// Outer radius for drawing segment and pointer detection. + /// + public float InnerRadius { set; } + + /// + /// Offset in radian by which menu button should be rotated. + /// + public float AngleOffset { set; } + + /// + /// Coordinates of center in parent component - button container. + /// + public Vector2 ParentCenter { set; } +} + +[Virtual] +public class RadialMenuTextureButtonWithSector : RadialMenuTextureButton, IRadialMenuItemWithSector +{ + private Vector2[]? _sectorPointsForDrawing; + + private float _angleSectorFrom; + private float _angleSectorTo; + private float _outerRadius; + private float _innerRadius; + private float _angleOffset; + + private bool _isWholeCircle; + private Vector2? _parentCenter; + + private Color _backgroundColorSrgb = Color.ToSrgb(new Color(70, 73, 102, 128)); + private Color _hoverBackgroundColorSrgb = Color.ToSrgb(new Color(87, 91, 127, 128)); + private Color _borderColorSrgb = Color.ToSrgb(new Color(173, 216, 230, 70)); + private Color _hoverBorderColorSrgb = Color.ToSrgb(new Color(87, 91, 127, 128)); + + /// + /// Marker, that control should render border of segment. Is false by default. + /// + /// + /// By default color of border is same as color of background. Use + /// and to change it. + /// + public bool DrawBorder { get; set; } = false; + + /// + /// Marker, that control should render background of all sector. Is true by default. + /// + public bool DrawBackground { get; set; } = true; + + /// + /// Marker, that control should render separator lines. + /// Separator lines are used to visually separate sector of radial menu items. + /// Is true by default + /// + public bool DrawSeparators { get; set; } = true; + + /// + /// Color of background in non-hovered state. Accepts RGB color, works with sRGB for DrawPrimitive internally. + /// + public Color BackgroundColor + { + get => Color.FromSrgb(_backgroundColorSrgb); + set => _backgroundColorSrgb = Color.ToSrgb(value); + } + + /// + /// Color of background in hovered state. Accepts RGB color, works with sRGB for DrawPrimitive internally. + /// + public Color HoverBackgroundColor + { + get => Color.FromSrgb(_hoverBackgroundColorSrgb); + set => _hoverBackgroundColorSrgb = Color.ToSrgb(value); + } + + /// + /// Color of button border. Accepts RGB color, works with sRGB for DrawPrimitive internally. + /// + public Color BorderColor + { + get => Color.FromSrgb(_borderColorSrgb); + set => _borderColorSrgb = Color.ToSrgb(value); + } + + /// + /// Color of button border when button is hovered. Accepts RGB color, works with sRGB for DrawPrimitive internally. + /// + public Color HoverBorderColor + { + get => Color.FromSrgb(_hoverBorderColorSrgb); + set => _hoverBorderColorSrgb = Color.ToSrgb(value); + } + + /// + /// Color of separator lines. + /// Separator lines are used to visually separate sector of radial menu items. + /// + public Color SeparatorColor { get; set; } = new Color(128, 128, 128, 128); + + /// + float IRadialMenuItemWithSector.AngleSectorFrom + { + set + { + _angleSectorFrom = value; + _isWholeCircle = IsWholeCircle(value, _angleSectorTo); + } + } + + /// + float IRadialMenuItemWithSector.AngleSectorTo + { + set + { + _angleSectorTo = value; + _isWholeCircle = IsWholeCircle(_angleSectorFrom, value); + } + } + + /// + float IRadialMenuItemWithSector.OuterRadius { set => _outerRadius = value; } + + /// + float IRadialMenuItemWithSector.InnerRadius { set => _innerRadius = value; } + + /// + public float AngleOffset { set => _angleOffset = value; } + + /// + Vector2 IRadialMenuItemWithSector.ParentCenter { set => _parentCenter = value; } + + /// + /// A simple texture button that can move the user to a different layer within a radial menu + /// + public RadialMenuTextureButtonWithSector() + { + } + + /// + protected override void Draw(DrawingHandleScreen handle) + { + base.Draw(handle); + + if (_parentCenter == null) + { + return; + } + + // draw sector where space that button occupies actually is + var containerCenter = (_parentCenter.Value - Position) * UIScale; + + var angleFrom = _angleSectorFrom + _angleOffset; + var angleTo = _angleSectorTo + _angleOffset; + if (DrawBackground) + { + var segmentColor = DrawMode == DrawModeEnum.Hover + ? _hoverBackgroundColorSrgb + : _backgroundColorSrgb; + + DrawAnnulusSector(handle, containerCenter, _innerRadius * UIScale, _outerRadius * UIScale, angleFrom, angleTo, segmentColor); + } + + if (DrawBorder) + { + var borderColor = DrawMode == DrawModeEnum.Hover + ? _hoverBorderColorSrgb + : _borderColorSrgb; + DrawAnnulusSector(handle, containerCenter, _innerRadius * UIScale, _outerRadius * UIScale, angleFrom, angleTo, borderColor, false); + } + + if (!_isWholeCircle && DrawSeparators) + { + DrawSeparatorLines(handle, containerCenter, _innerRadius * UIScale, _outerRadius * UIScale, angleFrom, angleTo, SeparatorColor); + } + } + + /// + protected override bool HasPoint(Vector2 point) + { + if (_parentCenter == null) + { + return base.HasPoint(point); + } + + var outerRadiusSquared = _outerRadius * _outerRadius; + var innerRadiusSquared = _innerRadius * _innerRadius; + + var distSquared = (point + Position - _parentCenter.Value).LengthSquared(); + var isInRadius = distSquared < outerRadiusSquared && distSquared > innerRadiusSquared; + if (!isInRadius) + { + return false; + } + + // difference from the center of the parent to the `point` + var pointFromParent = point + Position - _parentCenter.Value; + + // Flip Y to get from ui coordinates to natural coordinates + var angle = MathF.Atan2(-pointFromParent.Y, pointFromParent.X) - _angleOffset; + if (angle < 0) + { + // atan2 range is -pi->pi, while angle sectors are + // 0->2pi, so remap the result into that range + angle = MathF.PI * 2 + angle; + } + + var isInAngle = angle >= _angleSectorFrom && angle < _angleSectorTo; + return isInAngle; + } + + /// + /// Draw segment between two concentrated circles from and to certain angles. + /// + /// Drawing handle, to which rendering should be delegated. + /// Point where circle center should be. + /// Radius of internal circle. + /// Radius of external circle. + /// Angle in radian, from which sector should start. + /// Angle in radian, from which sector should start. + /// Color for drawing. + /// Should figure be filled, or have only border. + private void DrawAnnulusSector( + DrawingHandleScreen drawingHandleScreen, + Vector2 center, + float radiusInner, + float radiusOuter, + float angleSectorFrom, + float angleSectorTo, + Color color, + bool filled = true + ) + { + const float minimalSegmentSize = MathF.Tau / 128f; + + var requestedSegmentSize = angleSectorTo - angleSectorFrom; + var segmentCount = (int)(requestedSegmentSize / minimalSegmentSize) + 1; + var anglePerSegment = requestedSegmentSize / (segmentCount - 1); + + var bufferSize = segmentCount * 2; + if (_sectorPointsForDrawing == null || _sectorPointsForDrawing.Length != bufferSize) + { + _sectorPointsForDrawing ??= new Vector2[bufferSize]; + } + + for (var i = 0; i < segmentCount; i++) + { + var angle = angleSectorFrom + anglePerSegment * i; + + // Flip Y to get from ui coordinates to natural coordinates + var unitPos = new Vector2(MathF.Cos(angle), -MathF.Sin(angle)); + var outerPoint = center + unitPos * radiusOuter; + var innerPoint = center + unitPos * radiusInner; + if (filled) + { + // to make filled sector we need to create strip from triangles + _sectorPointsForDrawing[i * 2] = outerPoint; + _sectorPointsForDrawing[i * 2 + 1] = innerPoint; + } + else + { + // to make border of sector we need points ordered as sequences on radius + _sectorPointsForDrawing[i] = outerPoint; + _sectorPointsForDrawing[bufferSize - 1 - i] = innerPoint; + } + } + + var type = filled + ? DrawPrimitiveTopology.TriangleStrip + : DrawPrimitiveTopology.LineStrip; + drawingHandleScreen.DrawPrimitives(type, _sectorPointsForDrawing, color); + } + + private static void DrawSeparatorLines( + DrawingHandleScreen drawingHandleScreen, + Vector2 center, + float radiusInner, + float radiusOuter, + float angleSectorFrom, + float angleSectorTo, + Color color + ) + { + var fromPoint = new Angle(-angleSectorFrom).RotateVec(Vector2.UnitX); + drawingHandleScreen.DrawLine( + center + fromPoint * radiusOuter, + center + fromPoint * radiusInner, + color + ); + + var toPoint = new Angle(-angleSectorTo).RotateVec(Vector2.UnitX); + drawingHandleScreen.DrawLine( + center + toPoint * radiusOuter, + center + toPoint * radiusInner, + color + ); + } + + private static bool IsWholeCircle(float angleSectorFrom, float angleSectorTo) + { + return new Angle(angleSectorFrom).EqualsApprox(new Angle(angleSectorTo)); + } +}