diff --git a/Examples/GameloopExamples.cs b/Examples/GameloopExamples.cs index 437ae81c..45fa2be9 100644 --- a/Examples/GameloopExamples.cs +++ b/Examples/GameloopExamples.cs @@ -533,7 +533,8 @@ private void DrawRoundedCursor(Vector2 tip, float size, ColorRgba colorRgba) var top = circleCenter + new Vector2(0, -1) * size; SegmentDrawing.DrawSegment(tip, left, 1f, colorRgba, LineCapType.CappedExtended, 3); SegmentDrawing.DrawSegment(tip, top, 1f, colorRgba, LineCapType.CappedExtended, 3); - CircleDrawing.DrawCircleSectorLines(circleCenter, size, 180, 270, 1f, colorRgba, false, 4f); + var circle = new Circle(circleCenter, size); + circle.DrawSectorLines(180, 270, 0f, 1f, colorRgba, 0.65f); } protected override void Update(GameTime time, ScreenInfo game, ScreenInfo gameUi, ScreenInfo ui) diff --git a/Examples/Scenes/ExampleScenes/AsteroidMiningExample.cs b/Examples/Scenes/ExampleScenes/AsteroidMiningExample.cs index 4945b3c4..4783d0ba 100644 --- a/Examples/Scenes/ExampleScenes/AsteroidMiningExample.cs +++ b/Examples/Scenes/ExampleScenes/AsteroidMiningExample.cs @@ -16,9 +16,10 @@ using ShapeEngine.Geometry.TriangleDef; using ShapeEngine.Input; using ShapeEngine.Random; +using ShapeEngine.ShapeClipper; + namespace Examples.Scenes.ExampleScenes { - public class AsteroidShard : GameObject { // private Polygon shape; @@ -81,10 +82,13 @@ public override void Update(GameTime time, ScreenInfo game, ScreenInfo gameUi, S public override void DrawGame(ScreenInfo game) { - //SDrawing.DrawCircleFast(pos, 4f, RED); - var c = this.color.ColorRgba.ChangeAlpha((byte)(255 * lifetimeF)); - //color = this.color; - shape.DrawLines(2f * lifetimeF, c); + var alpha = (int)(400 * lifetimeF); + if(alpha > 255) alpha = 255; + var c = color.ColorRgba.SetAlpha((byte)alpha); + + // shape.DrawLines(2f * lifetimeF, c); + + shape.Draw(c); } public override void DrawGameUI(ScreenInfo gameUi) @@ -94,6 +98,7 @@ public override void DrawGameUI(ScreenInfo gameUi) public override Rect GetBoundingBox() { return shape.GetBoundingBox(); } } + public class Asteroid : CollisionObject { internal class DamagedSegment @@ -448,7 +453,8 @@ public override void DrawGame(ScreenInfo game) if (hybernate) return; var c = Colors.Cold; shape.DrawLines(4f, c); - CircleDrawing.DrawCircle(tip, 8f, c); + var circle = new Circle(tip, 8f); + circle.Draw(c, 0.1f); if (laserEnabled && laserPoints.Count > 1) { @@ -456,7 +462,8 @@ public override void DrawGame(ScreenInfo game) { Segment laserSegment = new(laserPoints[i], laserPoints[i + 1]); laserSegment.Draw(4f, c); - CircleDrawing.DrawCircle(laserPoints[i + 1], Rng.Instance.RandF(6f, 12f), c, 12); + var laserPointCircle = new Circle(laserPoints[i + 1], Rng.Instance.RandF(6f, 12f)); + laserPointCircle.Draw(c, 0.1f); } } @@ -529,7 +536,8 @@ internal enum ShapeType { None = 0, Triangle = 1, Rect = 2, Poly = 3}; //Polygons testShapes = new(); //Rect clipRect = new(); //RectD clipperRect = new(); - + private static Polygon cutShapeBuffer = new(); + private static FractureInfo fractureInfoBuffer = new(); private readonly InputAction iaModeChange; private readonly InputAction iaAddShape; private readonly InputAction iaCutShape; @@ -658,36 +666,36 @@ protected override void OnUpdateExample(GameTime time, ScreenInfo game, ScreenIn } private void OnAsteroidFractured(Asteroid a, Vector2 point) { - var cutShape = Polygon.Generate(point, Rng.Instance.RandI(6, 12), 35, 100); - if(cutShape != null) FractureAsteroid(a, cutShape); + cutShapeBuffer.Clear(); + if (!Polygon.Generate(point, Rng.Instance.RandI(6, 12), 35, 100, cutShapeBuffer)) return; + if(cutShapeBuffer.Count > 0) FractureAsteroid(a, cutShapeBuffer); } private void FractureAsteroid(Asteroid a, Polygon cutShape) { RemoveAsteroid(a); var asteroidShape = a.GetPolygon(); var color = a.GetColor(); - var fracture = fractureHelper.Fracture(asteroidShape, cutShape); + fractureHelper.Fracture(asteroidShape, cutShape, fractureInfoBuffer); - foreach (var cutoutShape in fracture.Cutouts) + foreach (var cutoutShape in fractureInfoBuffer.Cutouts) { lastCutOuts.Add(new Cutout(cutoutShape)); } var center = cutShape.GetCentroid(); - foreach (var piece in fracture.Pieces) + foreach (var piece in fractureInfoBuffer.Pieces) { // Vector2 center = piece.GetCentroid(); AsteroidShard shard = new(piece, center, color); SpawnArea?.AddGameObject(shard); } - if (fracture.NewShapes.Count > 0) + if (fractureInfoBuffer.NewShapes.Count > 0) { - foreach (var shape in fracture.NewShapes) + foreach (var shape in fractureInfoBuffer.NewShapes) { float shapeArea = shape.GetArea(); if (shapeArea > MinPieceArea) { - var relativeInfo = shape.ToRelative(); Asteroid newAsteroid = new(relativeInfo.shape, relativeInfo.transform); @@ -743,8 +751,7 @@ private void RegenerateShape() } private void GenerateTriangle() { - var shape = Polygon.Generate(curPos, 3, curSize / 2, curSize); - if (shape != null) curShape = shape; + Polygon.Generate(curPos, 3, curSize / 2, curSize, curShape); } private void GenerateRect() { @@ -753,8 +760,7 @@ private void GenerateRect() } private void GeneratePoly() { - var shape = Polygon.Generate(curPos, 16, curSize * 0.25f, curSize); - if(shape != null) curShape = shape; + Polygon.Generate(curPos, 16, curSize * 0.25f, curSize, curShape); } private void PolyModeStarted() { @@ -771,23 +777,6 @@ private void PolyModeEnded() } protected override void OnHandleInputExample(float dt, Vector2 mousePosGame, Vector2 mousePosGameUi, Vector2 mousePosUI) { - //clipRect = new(mousePosGame, new Vector2(100, 300), new Vector2(0f, 1f)); - //clipperRect = clipRect.ToClipperRect(); - //if (IsKeyPressed(KeyboardKey.KEY_NINE)) - //{ - // Polygons newShapes = new Polygons(); - // foreach (var shape in testShapes) - // { - // if (shape.OverlapShape(clipRect)) - // { - // var result = SClipper.ClipRect(clipRect, shape, 2, false).ToPolygons(true); - // if (result.Count > 0) newShapes.AddRange(result); - // } - // else newShapes.Add(shape); - // } - // testShapes = newShapes; - //} - if (CollisionHandler == null) return; var gamepad = Input.GamepadManager.LastUsedGamepad; @@ -818,7 +807,6 @@ protected override void OnHandleInputExample(float dt, Vector2 mousePosGame, Vec asteroid.Overlapped(); } } - if (iaAddShape.State.Pressed) //add polygon (merge) { @@ -835,7 +823,12 @@ protected override void OnHandleInputExample(float dt, Vector2 mousePosGame, Vec } } - var finalShapes = ShapeClipper.UnionMany(curShape.ToPolygon(), polys, Clipper2Lib.FillRule.NonZero).ToPolygons(true); + + Polygons finalShapes = new(); + var curPoly = curShape.ToPolygon(); + curPoly.ClipUnionMany(polys, finalShapes); + finalShapes.RemoveAllHoles(); + // var finalShapes = curShape.ToPolygon().UnionMany(polys).ToPolygons(true); if (finalShapes.Count > 0) { foreach (var f in finalShapes) @@ -873,8 +866,8 @@ protected override void OnHandleInputExample(float dt, Vector2 mousePosGame, Vec { if (candidate is Asteroid asteroid) { + allCutOuts.Add(cutShape); FractureAsteroid(asteroid, cutShape); - } } if (allCutOuts.Count > 0) diff --git a/Examples/Scenes/ExampleScenes/BouncyCircles.cs b/Examples/Scenes/ExampleScenes/BouncyCircles.cs index 24aabe5f..cc92e07a 100644 --- a/Examples/Scenes/ExampleScenes/BouncyCircles.cs +++ b/Examples/Scenes/ExampleScenes/BouncyCircles.cs @@ -3,6 +3,7 @@ using ShapeEngine.Core.GameDef; using ShapeEngine.Core.Structs; using ShapeEngine.Geometry.CircleDef; +using ShapeEngine.Geometry.PointsDef; using ShapeEngine.Geometry.PolygonDef; using ShapeEngine.Geometry.RectDef; using ShapeEngine.Input; @@ -52,7 +53,7 @@ public override void DrawGame(ScreenInfo game) _ => Colors.Medium }; - CircleDrawing.DrawCircleFast(Transform.Position, Transform.ScaledSize.Width, color); + Transform.GetCircle().DrawFast(color); } public override void DrawGameUI(ScreenInfo gameUi) @@ -89,13 +90,15 @@ public class BouncyCircles : ExampleScene { Rect boundaryRect; + private Polygon hull = new(); + private InputAction iaAdd; private InputAction iaToggleConvexHull; private readonly InputActionTree inputActionTree; private bool showConvexHull = false; private readonly List circles = new(65536); - private readonly List circlePoints = new(65536); + private readonly Points circlePoints = new(65536); public BouncyCircles() { @@ -180,8 +183,13 @@ protected override void OnDrawGameExample(ScreenInfo game) { circlePoints.Add(circ.Transform.Position); } - var hull = Polygon.FindConvexHull(circlePoints); - hull?.DrawLines(4f, Colors.Special); + + if (circlePoints.Count > 0) + { + hull.Clear(); + circlePoints.FindConvexHull(hull); + hull.DrawLines(4f, Colors.Special); + } } } diff --git a/Examples/Scenes/ExampleScenes/CameraExample.cs b/Examples/Scenes/ExampleScenes/CameraExample.cs index a9513242..7d4a4bdb 100644 --- a/Examples/Scenes/ExampleScenes/CameraExample.cs +++ b/Examples/Scenes/ExampleScenes/CameraExample.cs @@ -150,8 +150,11 @@ protected override void OnDrawGameExample(ScreenInfo game) var c = Colors.Cold; // new ColorRgba(Color.CornflowerBlue); float f = camera.ZoomFactor; - CircleDrawing.DrawCircle(camera.BasePosition, 8f * f, c); - CircleDrawing.DrawCircleLines(camera.BasePosition, 64 * f, 2f * f, c); + var circle = new Circle(camera.BasePosition, 8f * f); + circle.Draw(c, 0.2f); + circle = circle.SetRadius(64 * f); + circle.DrawLines(2f * f, c, 0.65f); + Segment hor = new(camera.BasePosition - new Vector2(3000 * f, 0), camera.BasePosition + new Vector2(3000 * f, 0)); hor.Draw(2f * f, c); Segment ver = new(camera.BasePosition - new Vector2(0, 3000 * f), camera.BasePosition + new Vector2(0, 3000 * f)); diff --git a/Examples/Scenes/ExampleScenes/CameraGroupFollowExample.cs b/Examples/Scenes/ExampleScenes/CameraGroupFollowExample.cs index 78ea1d4d..fdbbe667 100644 --- a/Examples/Scenes/ExampleScenes/CameraGroupFollowExample.cs +++ b/Examples/Scenes/ExampleScenes/CameraGroupFollowExample.cs @@ -214,10 +214,13 @@ public void Draw() var outlineColor = Selected ? outlineColorActive : outlineColorInactive; var hullColor = Selected ? hullColorActive : hullColorInactive; var cockpitColor = Selected ? cockpitColorActive : cockpitColorInactive; - CircleDrawing.DrawCircle(hull.Center - rightThruster * hull.Radius, hull.Radius / 6, outlineColor.ColorRgba, 12); - CircleDrawing.DrawCircle(hull.Center - leftThruster * hull.Radius, hull.Radius / 6, outlineColor.ColorRgba, 12); + var rightThrusterCircle = new Circle(hull.Center - rightThruster * hull.Radius, hull.Radius / 6); + var leftThrusterCircle = new Circle(hull.Center - leftThruster * hull.Radius, hull.Radius / 6); + rightThrusterCircle.Draw(outlineColor.ColorRgba, 0.25f); + leftThrusterCircle.Draw(outlineColor.ColorRgba, 0.25f); hull.Draw(hullColor.ColorRgba); - CircleDrawing.DrawCircle(hull.Center + movementDir * hull.Radius * 0.66f, hull.Radius * 0.33f, cockpitColor.ColorRgba, 12); + var circle = new Circle(hull.Center + movementDir * hull.Radius * 0.66f, hull.Radius * 0.33f); + circle.Draw(cockpitColor.ColorRgba, 0.25f); hull.DrawLines(4f, outlineColor.ColorRgba); diff --git a/Examples/Scenes/ExampleScenes/ControlNodeExampleScene.cs b/Examples/Scenes/ExampleScenes/ControlNodeExampleScene.cs index c8b944de..70579394 100644 --- a/Examples/Scenes/ExampleScenes/ControlNodeExampleScene.cs +++ b/Examples/Scenes/ExampleScenes/ControlNodeExampleScene.cs @@ -73,24 +73,39 @@ public ControlNodeButton(string text, AnchorPoint anchor, Vector2 stretch) this.InputFilter = InputFilter.All; } - protected override bool GetPressedState() + protected override bool GetButtonPressedState() { if (!Selected) return false; - var acceptState = GameloopExamples.Instance.InputActionUIAccept.Consume(out _); - return acceptState is { Consumed: false, Pressed: true }; - - // if (!Selected) return false; - // return Raylib.IsKeyDown(KeyboardKey.Space); + //Always consumed for some reason... + // var acceptState = GameloopExamples.Instance.InputActionUIAccept.Consume(out _); + // return acceptState is { Consumed: false, Pressed: true }; + return GameloopExamples.Instance.InputActionUIAccept.State.Pressed; } - protected override bool GetMousePressedState() + protected override bool GetMouseButtonPressedState() { if (!MouseInside) return false; - var acceptState = GameloopExamples.Instance.InputActionUIAcceptMouse.Consume(out _); - return acceptState is { Consumed: false, Pressed: true }; - - // if (!MouseInside) return false; - // return Raylib.IsMouseButtonDown(MouseButton.Left); + //Always consumed for some reason... + // var acceptState = GameloopExamples.Instance.InputActionUIAcceptMouse.Consume(out _); + // return acceptState is { Consumed: false, Pressed: true }; + return GameloopExamples.Instance.InputActionUIAcceptMouse.State.Pressed; + } + protected override bool GetButtonReleasedState() + { + if (!Selected) return false; + //Always consumed for some reason... + // var acceptState = GameloopExamples.Instance.InputActionUIAccept.Consume(out _); + // return acceptState is { Consumed: false, Released: true }; + return GameloopExamples.Instance.InputActionUIAccept.State.Released; + } + + protected override bool GetMouseButtonReleasedState() + { + if (!MouseInside) return false; + //Always consumed for some reason... + // var acceptState = GameloopExamples.Instance.InputActionUIAcceptMouse.Consume(out _); + // return acceptState is { Consumed: false, Released: true }; + return GameloopExamples.Instance.InputActionUIAcceptMouse.State.Released; } public override Direction GetNavigationDirection() diff --git a/Examples/Scenes/ExampleScenes/DataExample.cs b/Examples/Scenes/ExampleScenes/DataExample.cs index 4544f5e2..4c2b5ca8 100644 --- a/Examples/Scenes/ExampleScenes/DataExample.cs +++ b/Examples/Scenes/ExampleScenes/DataExample.cs @@ -44,7 +44,9 @@ public void Draw() var startC = Colors.Warm; var endC = Colors.Medium.SetAlpha(150); var c = startC.Lerp(endC, f); - CircleDrawing.DrawCircle(pos, ShapeMath.LerpFloat(size * 0.5f, size, f), c, 24); + var r = ShapeMath.LerpFloat(size * 0.5f, size, f); + var circle = new Circle(pos, r); + circle.Draw(c, 0.75f); } } private class Planet @@ -263,8 +265,8 @@ protected override void OnUpdateExample(GameTime time, ScreenInfo game, ScreenIn } protected override void OnDrawGameExample(ScreenInfo game) { - - CircleDrawing.DrawCircleLines(planet.Shape.Center, AsteroidSpawnRadius, 12f, Colors.Highlight, 4f); + var circle = new Circle(planet.Shape.Center, AsteroidSpawnRadius); + circle.DrawLines(12f, Colors.Highlight, 1f); planet.Draw(); planet.DrawGameUI(textFont); diff --git a/Examples/Scenes/ExampleScenes/DelaunayExample.cs b/Examples/Scenes/ExampleScenes/DelaunayExample.cs index 50d91ae2..d9c71f71 100644 --- a/Examples/Scenes/ExampleScenes/DelaunayExample.cs +++ b/Examples/Scenes/ExampleScenes/DelaunayExample.cs @@ -1,12 +1,7 @@ - - -using System.Diagnostics; +using System.Diagnostics; using Raylib_cs; -using ShapeEngine.Core; -using ShapeEngine.StaticLib; using System.Numerics; using System.Text; -using ShapeEngine.Color; using ShapeEngine.Core.Structs; using ShapeEngine.Geometry; using ShapeEngine.Geometry.PointsDef; @@ -15,14 +10,11 @@ using ShapeEngine.Geometry.TriangleDef; using ShapeEngine.Geometry.TriangulationDef; using ShapeEngine.Input; -using Color = System.Drawing.Color; namespace Examples.Scenes.ExampleScenes { public class DelaunayExample : ExampleScene { - //private const float PointDistance = 10f; - private readonly Font font; private readonly Points points = new(); @@ -39,6 +31,8 @@ public class DelaunayExample : ExampleScene private float lineThicknessBig = 0f; private float vertexSize = 0f; private float vertexSizeBig = 0f; + + private static Polygon shapeBuffer = new(); public DelaunayExample() { @@ -68,16 +62,19 @@ public override void Reset() } - private Triangle GenerateTriangle(Vector2 pos, float size) - { - var poly = Polygon.Generate(pos, 3, size / 2, size); - if(poly == null) - { - Debug.WriteLine("Warning: Failed to generate polygon. Returning null."); - return new(); - } - return new(poly[0], poly[1], poly[2]); - } + // private Triangle GenerateTriangle(Vector2 pos, float size) + // { + // if (Polygon.Generate(pos, 3, size / 2, size, shapeBuffer)) + // { + // if (shapeBuffer.Count >= 3) + // { + // return new(shapeBuffer[0], shapeBuffer[1], shapeBuffer[2]); + // } + // } + // + // Debug.WriteLine("Warning: Failed to generate polygon. Returning null."); + // return new(); + // } protected override void OnHandleInputExample(float dt, Vector2 mousePosGame, Vector2 mousePosGameUi, Vector2 mousePosUI) { @@ -147,8 +144,7 @@ protected override void OnHandleInputExample(float dt, Vector2 mousePosGame, Vec private void Triangulate() { if (points.Count < 3) return; - curTriangulation = Polygon.TriangulateDelaunay(points); - + points.TriangulatePointCloud(curTriangulation); } protected override void OnDrawGameExample(ScreenInfo game) @@ -158,11 +154,11 @@ protected override void OnDrawGameExample(ScreenInfo game) { var tri = curTriangulation[i]; if (i == closeTriangleIndex) continue; - tri.DrawLines(lineThickness, Colors.Light, LineCapType.CappedExtended, 4); + tri.DrawLines(lineThickness, Colors.Light, 4); } - if(closeTriangleIndex >= 0) curTriangulation[closeTriangleIndex].DrawLines(lineThicknessBig, Colors.Highlight, LineCapType.CappedExtended, 4); + if(closeTriangleIndex >= 0) curTriangulation[closeTriangleIndex].DrawLines(lineThicknessBig, Colors.Highlight, 4); for (int i = 0; i < points.Count; i++) { diff --git a/Examples/Scenes/ExampleScenes/EndlessSpaceCollision.cs b/Examples/Scenes/ExampleScenes/EndlessSpaceCollision.cs index 88d579e6..31cf58e0 100644 --- a/Examples/Scenes/ExampleScenes/EndlessSpaceCollision.cs +++ b/Examples/Scenes/ExampleScenes/EndlessSpaceCollision.cs @@ -55,7 +55,7 @@ public override Rect GetSourceRect(Dimensions screenDimensions, Dimensions textu ); } - public override (ColorRgba color, bool clear) GetBackgroundClearColor() => (ColorRgba.Clear, true); + public override (ColorRgba color, bool clear) GetBackgroundClearColor() => (ColorRgba.Transparent, true); } private List starTextures = new(5); @@ -252,7 +252,8 @@ private void OnDrawStarTexture(ScreenInfo screeninfo, ScreenTexture texture) { var pos = screeninfo.Area.GetRandomPointInside(); - CircleDrawing.DrawCircleFast(pos, Rng.Instance.RandF(1, 5), Colors.Highlight.SetAlpha(alpha)); + var circle = new Circle(pos, Rng.Instance.RandF(1, 5)); + circle.DrawFast(Colors.Highlight.SetAlpha(alpha)); } } @@ -379,13 +380,11 @@ private void AddAsteroids(int amount) private void AddAsteroid(bool big) { var pos = GetRandomUniversePosition(2500); - - // var minSize = big ? AsteroidMinSize : AsteroidMinSize / 4f; var maxSize = big ? AsteroidMaxSize : AsteroidMaxSize / 4f; - // var shape = Polygon.Generate(pos, AsteroidPointCount, minSize, maxSize); - var shape = Polygon.GenerateRelative(AsteroidPointCount, 0.5f, 1f); - if (shape == null) return; + Polygon shape = new(); + if (!Polygon.GenerateRelative(AsteroidPointCount, 0.5f, 1f, shape) || shape.Count < 3) return; + var a = new AsteroidObstacle(shape, pos, maxSize, big); if (!big) a.target = ship; asteroids.Add(a); @@ -393,12 +392,11 @@ private void AddAsteroid(bool big) } private void AddAsteroid(Vector2 pos, bool big) { - // var minSize = big ? AsteroidMinSize : AsteroidMinSize / 4f; var maxSize = big ? AsteroidMaxSize : AsteroidMaxSize / 4f; - // var shape = Polygon.Generate(pos, AsteroidPointCount, minSize, maxSize); - var shape = Polygon.GenerateRelative(AsteroidPointCount, 0.5f, 1f); - if (shape == null) return; + Polygon shape = new(); + if (!Polygon.GenerateRelative(AsteroidPointCount, 0.5f, 1f, shape) || shape.Count < 3) return; + var a = new AsteroidObstacle(shape, pos, maxSize, big); if (!big) a.target = ship; asteroids.Add(a); @@ -786,26 +784,17 @@ protected override void OnDrawGameExample(ScreenInfo game) // { // cutShape.Draw(cutShapeColor); // } - - - CircleDrawing.DrawCircleSectorLines(ship.Transform.Position, 250f, -80, -10, 12f, Colors.PcDark.ColorRgba, false, 8f); - CircleDrawing.DrawCircleSectorLines(ship.Transform.Position, 250f, -100, -170, 12f, Colors.PcDark.ColorRgba, false, 8f); - CircleDrawing.DrawCircleSectorLines(ship.Transform.Position, 250f, 170, 10, 12f, Colors.PcDark.ColorRgba, false, 8f); - if (minigun.ReloadF > 0f) - { - CircleDrawing.DrawCircleSectorLines(ship.Transform.Position, 250f, -80, ShapeMath.LerpFloat(-80, -10, minigun.ReloadF), 4f, Colors.PcWarm.ColorRgba, false, 8f); - } - else CircleDrawing.DrawCircleSectorLines(ship.Transform.Position, 250f, -80, ShapeMath.LerpFloat(-80, -10, 1f - minigun.ClipSizeF), 4f, Colors.PcCold.ColorRgba, false, 8f); + var sectorLineInfo = new LineDrawingInfo(12f, Colors.PcDark.ColorRgba); + var circle = new Circle(ship.Transform.Position, 250f); + circle.DrawSectorLines(-80f, -10f, 0f, sectorLineInfo, 0.75f); + circle.DrawSectorLines(-100f, -170f, 0f, sectorLineInfo, 0.75f); + circle.DrawSectorLines(170f, 10f, 0f, sectorLineInfo, 0.75f); - if (cannon.ReloadF > 0f) - { - CircleDrawing.DrawCircleSectorLines(ship.Transform.Position, 250f, -100, ShapeMath.LerpFloat(-100, -170, cannon.ReloadF), 4f, Colors.PcWarm.ColorRgba, false, 8f); - } - else CircleDrawing.DrawCircleSectorLines(ship.Transform.Position, 250f, -100, ShapeMath.LerpFloat(-100, -170, 1f - cannon.ClipSizeF), 4f, Colors.PcCold.ColorRgba, false, 8f); - - - CircleDrawing.DrawCircleSectorLines(ship.Transform.Position, 250f, 170, ShapeMath.LerpFloat(170, 10, ship.HealthF), 4f, Colors.PcWarm.ColorRgba, false, 8f); + sectorLineInfo = new LineDrawingInfo(4f, Colors.PcWarm.ColorRgba); + circle.DrawSectorLines(-80, ShapeMath.LerpFloat(-80, -10, minigun.ReloadF > 0f ? minigun.ReloadF : 1f - minigun.ClipSizeF), 0f, sectorLineInfo, 0.75f); + circle.DrawSectorLines(-100, ShapeMath.LerpFloat(-100, -170, cannon.ReloadF > 0f ? cannon.ReloadF : 1f - cannon.ClipSizeF), 0f, sectorLineInfo, 0.75f); + circle.DrawSectorLines(170, ShapeMath.LerpFloat(170, 10, ship.HealthF), 0f, sectorLineInfo, 0.75f); } protected override void OnDrawGameUIExample(ScreenInfo gameUi) { @@ -853,12 +842,14 @@ protected override void OnDrawUIExample(ScreenInfo ui) var multiDestructorStripedBarRect = multiDestructorRectBar.GetProgressRect(multiDestructorF, 0f, 1f, 0f, 0f).ApplyMargins(0.01f, 0.01f, 0.04f, 0.04f); LineDrawingInfo stripedBarInfo = new LineDrawingInfo(thickness, Colors.Warm, LineCapType.Capped, 4); - singleDestructorRect.DrawCorners(new LineDrawingInfo(thickness, Colors.Warm, LineCapType.Capped, 4), cornerLength); + // singleDestructorRect.DrawCorners(new LineDrawingInfo(thickness, Colors.Warm, LineCapType.Capped, 4), cornerLength); + singleDestructorRect.DrawCorners(thickness, Colors.Warm, cornerLength); singleDestructorRectBar.Draw(Colors.Medium); singleDestructorStripedBarRect.DrawStriped(singleDestructorRectBar.Width * 0.015f, -15, stripedBarInfo); - multiDestructorRect.DrawCorners(new LineDrawingInfo(thickness, Colors.Warm, LineCapType.Capped, 4), cornerLength); + // multiDestructorRect.DrawCorners(new LineDrawingInfo(thickness, Colors.Warm, LineCapType.Capped, 4), cornerLength); + multiDestructorRect.DrawCorners(thickness, Colors.Warm, cornerLength); multiDestructorRectBar.Draw(Colors.Medium); multiDestructorStripedBarRect.DrawStriped(multiDestructorRectBar.Width * 0.015f, 15, stripedBarInfo); diff --git a/Examples/Scenes/ExampleScenes/EndlessSpaceExampleSource/AsteroidObstacle.cs b/Examples/Scenes/ExampleScenes/EndlessSpaceExampleSource/AsteroidObstacle.cs index 128eb1d1..fa3de46c 100644 --- a/Examples/Scenes/ExampleScenes/EndlessSpaceExampleSource/AsteroidObstacle.cs +++ b/Examples/Scenes/ExampleScenes/EndlessSpaceExampleSource/AsteroidObstacle.cs @@ -8,10 +8,10 @@ using ShapeEngine.Geometry.PolygonDef; using ShapeEngine.Geometry.RectDef; using ShapeEngine.Geometry.StripedDrawingDef; -using ShapeEngine.Geometry.TriangleDef; using ShapeEngine.Geometry.TriangulationDef; using ShapeEngine.StaticLib; using ShapeEngine.Random; +using ShapeEngine.ShapeClipper; namespace Examples.Scenes.ExampleScenes.EndlessSpaceExampleSource; @@ -26,7 +26,8 @@ internal class AsteroidObstacle : CollisionObject private PolygonCollider collider; private readonly Polygon? outsideShape; - public Triangulation Triangulation; + public Triangulation Triangulation = new(); + public Triangulation OutlineTriangulation = new(); private Rect bb; private float damageFlashTimer = 0f; @@ -83,18 +84,20 @@ public AsteroidObstacle(Polygon relativeShape, Vector2 pos, float size, bool big AddCollider(collider); var shape = collider.GetPolygonShape(); bb = shape.GetBoundingBox(); - Triangulation = shape.Triangulate(); + shape.Triangulate(Triangulation); + shape.TriangulateOutline(OutlineTriangulation, EndlessSpaceCollision.AsteroidLineThickness, 2f, false, false); + outsideShape = new Polygon(); if (big) { - outsideShape = shape.ScaleSizeCopy(1.5f); + shape.ScaleSizeCopy(outsideShape, 1.5f); Health = ShapeMath.LerpFloat(300, 650, EndlessSpaceCollision.DifficultyFactor) * Rng.Instance.RandF(0.9f, 1.1f); gappedOutlineInfo = BigAsteroidGappedOutlineInfo.ChangeStartOffset(Rng.Instance.RandF()); } else { - outsideShape = shape.ScaleSizeCopy(1.25f); + shape.ScaleSizeCopy(outsideShape, 1.25f); Health = ShapeMath.LerpFloat(25, 100, EndlessSpaceCollision.DifficultyFactor) * Rng.Instance.RandF(0.9f, 1.1f); gappedOutlineInfo = SmallAsteroidGappedOutlineInfo.ChangeStartOffset(Rng.Instance.RandF()); } @@ -134,6 +137,7 @@ public void MoveTo(Vector2 newPosition) var moved = newPosition - Transform.Position; Transform = Transform.SetPosition(newPosition); Triangulation.ChangePosition(moved); + OutlineTriangulation.ChangePosition(moved); outsideShape?.ChangePosition(moved); // moved = true; } @@ -182,6 +186,7 @@ public override void Update(GameTime time, ScreenInfo game, ScreenInfo gameUi, var moved = Transform.Position - prevPosition; Triangulation.ChangePosition(moved); + OutlineTriangulation.ChangePosition(moved); outsideShape?.ChangePosition(moved); } public Polygon GetShape() => collider.GetPolygonShape(); @@ -220,7 +225,8 @@ public override void DrawGame(ScreenInfo game) if (EndlessSpaceCollision.AsteroidLineThickness > 1 && outsideShape != null) { var c = damageFlashTimer > 0f ? Colors.PcWarm.ColorRgba : Colors.PcHighlight.ColorRgba; - collider.GetPolygonShape().DrawLines(EndlessSpaceCollision.AsteroidLineThickness, c); + OutlineTriangulation.Draw(c); + // ClipperImmediate2D.DrawPolygonOutline(collider.GetPolygonShape(), EndlessSpaceCollision.AsteroidLineThickness, c, 4f, false, true, false); if (Big) { perimeter = outsideShape.DrawGappedOutline(perimeter, GappedLineInfo, gappedOutlineInfo); diff --git a/Examples/Scenes/ExampleScenes/EndlessSpaceExampleSource/Autogun.cs b/Examples/Scenes/ExampleScenes/EndlessSpaceExampleSource/Autogun.cs index fc1e2b64..45f4c5bb 100644 --- a/Examples/Scenes/ExampleScenes/EndlessSpaceExampleSource/Autogun.cs +++ b/Examples/Scenes/ExampleScenes/EndlessSpaceExampleSource/Autogun.cs @@ -5,6 +5,7 @@ using ShapeEngine.Geometry.CircleDef; using ShapeEngine.Geometry.CollisionSystem; using ShapeEngine.Geometry.CollisionSystem.CollisionHandlerDef; +using ShapeEngine.Geometry.QuadDef; using ShapeEngine.Geometry.RectDef; using ShapeEngine.Geometry.SegmentDef; using ShapeEngine.StaticLib; @@ -141,7 +142,8 @@ public void Draw() float sideScaleFactor = ShapeMath.LerpFloat(0.2f, 0.5f, targetingAnimationF); // float targetingAnimationRotation = ShapeMath.LerpFloat(-15, 15, targetingAnimationF); var bb = curTarget.GetBoundingBox(); - bb.DrawLinesScaled(new LineDrawingInfo(6f, c), 45f, bb.Center, sideScaleFactor, 0.5f); //DrawLines(4f, c); + var bbQuad = new Quad(bb, 45f, bb.Center); + bbQuad.DrawLinesScaled(new LineDrawingInfo(6f, c), sideScaleFactor, 0.5f); var targetPos = curTarget.GetBoundingBox().Center; pos.Draw(5f, c); diff --git a/Examples/Scenes/ExampleScenes/EndlessSpaceExampleSource/Destructor.cs b/Examples/Scenes/ExampleScenes/EndlessSpaceExampleSource/Destructor.cs index ded7b42d..c951b138 100644 --- a/Examples/Scenes/ExampleScenes/EndlessSpaceExampleSource/Destructor.cs +++ b/Examples/Scenes/ExampleScenes/EndlessSpaceExampleSource/Destructor.cs @@ -1,6 +1,7 @@ using System.Numerics; using ShapeEngine.Color; using ShapeEngine.Core.Structs; +using ShapeEngine.Geometry; using ShapeEngine.Geometry.CircleDef; using ShapeEngine.Geometry.CollisionSystem; using ShapeEngine.StaticLib; @@ -111,7 +112,7 @@ public override void DrawGame(ScreenInfo game) var angle = Velocity.AngleDeg(); var startAngle = angle - 50f; var endAngle = angle + 50f; - RingDrawing.DrawSectorRingLines(circle.Center, circle.Radius * 0.75f, circle.Radius, startAngle, endAngle, thickness, color, 4f); + circle.DrawRingSectorLines(circle.Radius * 0.25f, startAngle, endAngle, 0f, thickness * 0.5f, color, 0.6f); } public override void DrawGameUI(ScreenInfo gameUi) diff --git a/Examples/Scenes/ExampleScenes/EndlessSpaceExampleSource/ExplosivePayload.cs b/Examples/Scenes/ExampleScenes/EndlessSpaceExampleSource/ExplosivePayload.cs index a3c41c3b..f6e3b452 100644 --- a/Examples/Scenes/ExampleScenes/EndlessSpaceExampleSource/ExplosivePayload.cs +++ b/Examples/Scenes/ExampleScenes/EndlessSpaceExampleSource/ExplosivePayload.cs @@ -61,7 +61,8 @@ protected virtual void DrawSmoke() var f = 1f - (SmokeTimer / Info.SmokeDuration); var color = Colors.Warm.Lerp(Colors.PcMedium.ColorRgba.SetAlpha(50), f); var size = ShapeMath.LerpFloat(Info.Radius * 0.5f, Info.Radius * 3f, f); - CircleDrawing.DrawCircle(CurPosition, size, color, 24); + var circle = new Circle(CurPosition, size); + circle.Draw(color, 0.6f); } public bool IsFinished() => finished; @@ -97,14 +98,10 @@ public void Draw() if (travelTimer > 0f) { var f = TravelF; - CircleDrawing.DrawCircle(TargetLocation, 12f, Colors.PcCold.ColorRgba, 24); - CircleDrawing.DrawCircleLines(TargetLocation, Info.Radius * (1f - f), 6f, Colors.PcMedium.ColorRgba, 6); - - // var f = TravelF; - // ShapeDrawing.DrawCircleLines(targetLocation, Size, 6f, Colors.Dark, 6); - // ShapeDrawing.DrawCircleSectorLines(targetLocation, Size, 0f, 359f * f, 6f, Colors.Special, false, 4f); - // ShapeDrawing.DrawCircleSectorLines(targetLocation, Size / 12, 0f, 359f * f, 6f, Colors.Special, false, 4f); - // ShapeDrawing.DrawCircle(targetLocation, 12f, Colors.Special, 24); + var circle = new Circle(TargetLocation, 12f); + circle.Draw(Colors.PcCold.ColorRgba, 0.2f); + circle = circle.SetRadius(Info.Radius * (1f - f)); + circle.DrawLines(6f, Colors.PcMedium.ColorRgba, 0.6f); var lineEnd = ShapeVec.Lerp(StartLocation, TargetLocation, f); var w = lineEnd - EndlessSpaceCollision.DestroyerPosition; diff --git a/Examples/Scenes/ExampleScenes/EndlessSpaceExampleSource/LaserBeamParticle.cs b/Examples/Scenes/ExampleScenes/EndlessSpaceExampleSource/LaserBeamParticle.cs index 55e718be..ac9569ae 100644 --- a/Examples/Scenes/ExampleScenes/EndlessSpaceExampleSource/LaserBeamParticle.cs +++ b/Examples/Scenes/ExampleScenes/EndlessSpaceExampleSource/LaserBeamParticle.cs @@ -46,6 +46,8 @@ public void Draw() var color = paletteColor.ColorRgba; var s = ShapeMath.LerpFloat(size, size * 0.25f, lifetimeF); var c = color.Lerp(color.ChangeAlpha(150), lifetimeF); - CircleDrawing.DrawCircleFast(position, s, c); + + var circle = new Circle(position, s); + circle.DrawFast(c); } } \ No newline at end of file diff --git a/Examples/Scenes/ExampleScenes/EndlessSpaceExampleSource/PayloadMarkerSimple.cs b/Examples/Scenes/ExampleScenes/EndlessSpaceExampleSource/PayloadMarkerSimple.cs index 6535c166..51bd5d76 100644 --- a/Examples/Scenes/ExampleScenes/EndlessSpaceExampleSource/PayloadMarkerSimple.cs +++ b/Examples/Scenes/ExampleScenes/EndlessSpaceExampleSource/PayloadMarkerSimple.cs @@ -1,4 +1,5 @@ using Examples.PayloadSystem; +using ShapeEngine.Geometry; using ShapeEngine.Geometry.CircleDef; using ShapeEngine.StaticLib; @@ -15,17 +16,22 @@ public override void Draw() { if (!Launched) return; + if (TravelF > 0f) { + var circle = new Circle(Location, 15f); if (visible) { - CircleDrawing.DrawCircle(Location, 15, Colors.Cold, 24); - } - CircleDrawing.DrawCircleSectorLines(Location, 25f, 0, 359 * TravelF, 4f, Colors.Cold, false, 4f); + circle.Draw(Colors.Cold, 0.2f); + } + + circle = circle.SetRadius(25f); + circle.DrawSectorLines(0, 359 * TravelF, 0f, new LineDrawingInfo(4f, Colors.Cold), 0.2f); } else { - CircleDrawing.DrawCircle(Location, 15, Colors.Cold, 24); + var circle = new Circle(Location, 15f); + circle.Draw(Colors.Cold, 0.2f); } diff --git a/Examples/Scenes/ExampleScenes/EndlessSpaceExampleSource/PayloadTargetingSystemBarrage.cs b/Examples/Scenes/ExampleScenes/EndlessSpaceExampleSource/PayloadTargetingSystemBarrage.cs index 697799bc..739c7fad 100644 --- a/Examples/Scenes/ExampleScenes/EndlessSpaceExampleSource/PayloadTargetingSystemBarrage.cs +++ b/Examples/Scenes/ExampleScenes/EndlessSpaceExampleSource/PayloadTargetingSystemBarrage.cs @@ -28,6 +28,7 @@ public Vector2 GetTargetPosition(int curActivation, int maxActivations) public void DrawTargetArea(float f, ColorRgba color) { - CircleDrawing.DrawCircleLines(curPosition, radius * f, 6f, color); + var circle = new Circle(curPosition, radius * f); + circle.DrawLines(6f, color, 0.1f); } } \ No newline at end of file diff --git a/Examples/Scenes/ExampleScenes/EndlessSpaceExampleSource/Penetrator.cs b/Examples/Scenes/ExampleScenes/EndlessSpaceExampleSource/Penetrator.cs index af8bd5f5..ebe40fe2 100644 --- a/Examples/Scenes/ExampleScenes/EndlessSpaceExampleSource/Penetrator.cs +++ b/Examples/Scenes/ExampleScenes/EndlessSpaceExampleSource/Penetrator.cs @@ -50,9 +50,9 @@ protected override void DrawSmoke() { var f = 1f - (SmokeTimer / Info.SmokeDuration); var color = Colors.PcWarm.ColorRgba.Lerp(Colors.PcMedium.ColorRgba.SetAlpha(50), f); - var size = Info.Radius; // ShapeMath.LerpFloat(Info.Radius * 0.5f, Info.Radius * 3f, f); - CircleDrawing.DrawCircle(CurPosition, size, color, 24); - // ShapeDrawing.DrawCircle(CurPosition, Info.Radius * 0.05f , color, 18); + var size = Info.Radius; + var circle = new Circle(CurPosition, size); + circle.Draw(color, 0.65f); } } diff --git a/Examples/Scenes/ExampleScenes/EndlessSpaceExampleSource/Ship.cs b/Examples/Scenes/ExampleScenes/EndlessSpaceExampleSource/Ship.cs index 549e5505..79c79514 100644 --- a/Examples/Scenes/ExampleScenes/EndlessSpaceExampleSource/Ship.cs +++ b/Examples/Scenes/ExampleScenes/EndlessSpaceExampleSource/Ship.cs @@ -49,6 +49,9 @@ internal class Ship : CollisionObject, ICameraFollowTarget public float HealthF => (float)Health / (float)MaxHp; public const int MaxHp = 3; private LaserBeam laserBeam; + + private static Polygon cutShapeBuffer = new(); + public CollisionHandler? collisionHandler; public Ship(Vector2 pos, float shipSize) @@ -102,8 +105,10 @@ protected override void Collision(CollisionInformation info) if(info.Count <= 0 || info.Other is not AsteroidObstacle a) return; if(!info.Validate(out IntersectionPoint combined)) return; - var cs = GetCutShape(); - if(cs != null) a.Cut(cs); + if (GetCutShape(cutShapeBuffer) && cutShapeBuffer.Count >= 3) + { + a.Cut(cutShapeBuffer); + } if (collisionStunTimer <= 0f) { @@ -125,9 +130,9 @@ protected override void Collision(CollisionInformation info) } - public Polygon? GetCutShape() + public bool GetCutShape(Polygon cutShape) { - return Polygon.Generate(Transform.Position, 12, shipSize * 1.5f, shipSize * 3); + return Polygon.Generate(Transform.Position, 12, shipSize * 1.5f, shipSize * 3, cutShape); } private Triangle CreateHull() diff --git a/Examples/Scenes/ExampleScenes/EndlessSpaceExampleSource/TurretPayload.cs b/Examples/Scenes/ExampleScenes/EndlessSpaceExampleSource/TurretPayload.cs index 84721121..df0eb22d 100644 --- a/Examples/Scenes/ExampleScenes/EndlessSpaceExampleSource/TurretPayload.cs +++ b/Examples/Scenes/ExampleScenes/EndlessSpaceExampleSource/TurretPayload.cs @@ -96,7 +96,8 @@ public void Draw() { Turret.Draw(); Raylib.DrawPoly(targetLocation, 6, info.Size / 2, 0f, Colors.Cold.ToRayColor()); - CircleDrawing.DrawCircleLines(targetLocation, info.Size, 6f, Colors.PcCold.ColorRgba, 6); + var circle = new Circle(targetLocation, info.Size); + circle.DrawLines(6f, Colors.PcCold.ColorRgba, 0.65f); var barrelPos = targetLocation + Turret.AimDir * info.Size; barrelPos.Draw(info.Size / 6, Colors.Cold, 24); } @@ -104,8 +105,11 @@ public void Draw() if (travelTimer > 0f) { var f = TravelF; - CircleDrawing.DrawCircle(targetLocation, 12f, Colors.PcCold.ColorRgba, 24); - CircleDrawing.DrawCircleLines(targetLocation, info.ImpactSize * (1f - f), 6f, Colors.PcMedium.ColorRgba, 6); + + var circle = new Circle(targetLocation, 12f); + circle.Draw(Colors.PcCold.ColorRgba, 0.1f); + var impactCircle = new Circle(targetLocation, info.ImpactSize * (1f - f)); + impactCircle.DrawLines(6f, Colors.PcMedium.ColorRgba, 0.75f); var lineEnd = ShapeVec.Lerp(startLocation, targetLocation, f); var w = lineEnd - EndlessSpaceCollision.DestroyerPosition; @@ -120,7 +124,8 @@ public void Draw() var f = 1f - (smokeTimer / info.SmokeDuration); var color = Colors.Warm.Lerp(Colors.PcMedium.ColorRgba.SetAlpha(50), f); var size = ShapeMath.LerpFloat(info.ImpactSize * 0.5f, info.ImpactSize * 3f, f); - CircleDrawing.DrawCircle(curPosition, size, color, 24); + var circle = new Circle(curPosition, size); + circle.Draw(color, 0.65f); } } diff --git a/Examples/Scenes/ExampleScenes/GameObjectHandlerExample.cs b/Examples/Scenes/ExampleScenes/GameObjectHandlerExample.cs index 23700604..8a920ef4 100644 --- a/Examples/Scenes/ExampleScenes/GameObjectHandlerExample.cs +++ b/Examples/Scenes/ExampleScenes/GameObjectHandlerExample.cs @@ -236,7 +236,7 @@ protected override void Collision(CollisionInformation info) public override void DrawGame(ScreenInfo game) { var c = circleCollider.GetCircleShape(); - c.DrawLines(4f, Colors.Warm); + c.DrawLines(4f, Colors.Warm, 1f); } public override bool HasLeftBounds(Rect bounds) => !bounds.OverlapShape(circleCollider.GetCircleShape()); @@ -329,8 +329,9 @@ internal class Rock : CollisionObject private Rect boundingBox; public Rock(Vector2 pos) : base(new Transform2D(pos, 0f, new Size(Size, 0f), 1f)) { - var shape = Polygon.GenerateRelative(6, 0.5f, 1f); - var col = new PolygonCollider(new(), shape ?? []) + Polygon shape = new(); + Polygon.GenerateRelative(6, 0.5f, 1f, shape); + var col = new PolygonCollider(new(), shape) { ComputeCollision = true, ComputeIntersections = true, @@ -859,7 +860,8 @@ private void DrawWalls(Vector2 mousePos) { if (segmentStarted) { - CircleDrawing.DrawCircle(startPoint, 15f, Colors.Highlight); + var circle = new Circle(startPoint, 15f); + circle.Draw(Colors.Highlight); Segment s = new(startPoint, mousePos); s.Draw(4, Colors.Highlight); diff --git a/Examples/Scenes/ExampleScenes/JsonDataExample.cs b/Examples/Scenes/ExampleScenes/JsonDataExample.cs index 47f3e8a4..a8fdbbe3 100644 --- a/Examples/Scenes/ExampleScenes/JsonDataExample.cs +++ b/Examples/Scenes/ExampleScenes/JsonDataExample.cs @@ -50,7 +50,8 @@ public void Draw() var startC = Colors.Warm; var endC = Colors.Medium.SetAlpha(150); var c = startC.Lerp(endC, f); - CircleDrawing.DrawCircle(pos, ShapeMath.LerpFloat(size * 0.5f, size, f), c, 24); + var circle = new Circle(pos, ShapeMath.LerpFloat(size * 0.5f, size, f)); + circle.Draw(c, 0.55f); } } private class Planet @@ -342,8 +343,8 @@ protected override void OnUpdateExample(GameTime time, ScreenInfo game, ScreenIn } protected override void OnDrawGameExample(ScreenInfo game) { - - CircleDrawing.DrawCircleLines(planet.Shape.Center, AsteroidSpawnRadius, 12f, Colors.Highlight, 4f); + var circle = new Circle(planet.Shape.Center, AsteroidSpawnRadius); + circle.DrawLines(12f, Colors.Highlight, 0.8f); planet.Draw(); planet.DrawGameUI(textFont); diff --git a/Examples/Scenes/ExampleScenes/OutlineDrawingExample.cs b/Examples/Scenes/ExampleScenes/OutlineDrawingExample.cs index 66f897f7..e4204594 100644 --- a/Examples/Scenes/ExampleScenes/OutlineDrawingExample.cs +++ b/Examples/Scenes/ExampleScenes/OutlineDrawingExample.cs @@ -33,15 +33,26 @@ public ValueSlider(string title, float startValue, float minValue, float maxValu this.title = title; } - protected override bool GetPressedState() + protected override bool GetButtonPressedState() { - return ShapeKeyboardButton.SPACE.GetInputState().Down; + return ShapeKeyboardButton.SPACE.GetInputState().Pressed; } - protected override bool GetMousePressedState() + protected override bool GetMouseButtonPressedState() { - return ShapeMouseButton.LEFT.GetInputState().Down; + return ShapeMouseButton.LEFT.GetInputState().Pressed; } + + protected override bool GetButtonReleasedState() + { + return ShapeKeyboardButton.SPACE.GetInputState().Released; + } + + protected override bool GetMouseButtonReleasedState() + { + return ShapeMouseButton.LEFT.GetInputState().Released; + } + protected override bool GetDecreaseValuePressed() { @@ -111,8 +122,8 @@ protected override void OnDraw() private LineDrawingInfo lineInfo; private LineDrawingInfo lineInfoOutline; - private int curCircleSides = 36; - + // private int curCircleSides = 36; + private float curCircleSmoothness = 0.5f; private float curSideScalingFactor = 0.5f; private float curSideScalingOriginFactor = 0.5f; @@ -162,10 +173,11 @@ public OutlineDrawingExample() triangle = Triangle.Generate(center, size / 2, size); rect = new Rect(center, new Size(size, size), new AnchorPoint(0.5f, 0.5f)); quad = new Quad(center, new Size(size, size), 45 * ShapeMath.DEGTORAD, new AnchorPoint(0.5f, 0.5f)); - var generatedPolygon = Polygon.Generate(center, 16, radius / 2, radius); - var generatedPolyline = Polygon.Generate(center, 16, radius / 2, radius)?.ToPolyline(); - poly = generatedPolygon ?? []; - polyline = generatedPolyline ?? []; + poly = new(); + Polygon.Generate(center, 16, radius / 2, radius, poly); + polyline = poly.ToPolyline(); + poly.Clear(); + Polygon.Generate(center, 16, radius / 2, radius, poly); sideScalingFactorSlider = new("Scaling", 0.5f, 0f, 1f, true); // new(0.5f, "Scaling", font); sideScalingOriginFactorSlider = new("Origin", 0.5f, 0f, 1f, true); @@ -175,7 +187,7 @@ public OutlineDrawingExample() { Percentage = false }; - circleSideSlider = new( "Sides", 18, 3, 120, true) + circleSideSlider = new("Smoothness", 0.5f, 0f, 1f, true) { Percentage = false }; @@ -191,7 +203,8 @@ private void ActualizeSliderValues() curGaps = (int)(gapsSlider.CurValue); curGapPerimeterPercentage = gapPerimeterPercentageSlider.CurValue; - curCircleSides = (int)circleSideSlider.CurValue; + // curCircleSides = (int)circleSideSlider.CurValue; + curCircleSmoothness = circleSideSlider.CurValue; } public override void Reset() { @@ -210,7 +223,7 @@ public override void Reset() curGaps = 4; gapsSlider.SetCurValue(4); - curCircleSides = 18; + curCircleSmoothness = 0.5f; circleSideSlider.SetCurValue(18); curGapPerimeterPercentage = 0.5f; @@ -291,8 +304,8 @@ protected override void OnHandleInputExample(float dt, Vector2 mousePosGame, Vec } protected override void OnDrawGameExample(ScreenInfo game) { - lineInfo = lineInfo.ChangeColor(Colors.Highlight); - lineInfoOutline = lineInfoOutline.ChangeColor(Colors.Dark); + lineInfo = lineInfo.SetColor(Colors.Highlight); + lineInfoOutline = lineInfoOutline.SetColor(Colors.Dark); var curGappedOutlineInfo = new GappedOutlineDrawingInfo(curGaps, curStartOffset, curGapPerimeterPercentage); @@ -313,14 +326,14 @@ protected override void OnDrawGameExample(ScreenInfo game) } else if (shapeIndex == 1) // Circle { - circle.DrawLines(lineInfoOutline, curCircleSides); + circle.DrawLines(lineInfoOutline, curCircleSmoothness); if (gappedMode) { - circle.DrawGappedOutline(lineInfo, curGappedOutlineInfo, 0f, curCircleSides); + circle.DrawGappedOutline(lineInfo, curGappedOutlineInfo, 0f, curCircleSmoothness); } else { - circle.DrawLinesScaled(lineInfo, 0f, curCircleSides, curSideScalingFactor, curSideScalingOriginFactor); + circle.DrawLinesScaled(0f, lineInfo, curCircleSmoothness, curSideScalingFactor, curSideScalingOriginFactor); } } else if (shapeIndex == 2) // Triangle @@ -332,7 +345,7 @@ protected override void OnDrawGameExample(ScreenInfo game) } else { - triangle.DrawLinesScaled(lineInfo, 0f, new(), curSideScalingFactor, curSideScalingOriginFactor); + triangle.DrawLinesScaled(lineInfo, curSideScalingFactor, curSideScalingOriginFactor); } } else if (shapeIndex == 3) // Rect @@ -344,7 +357,7 @@ protected override void OnDrawGameExample(ScreenInfo game) } else { - rect.DrawLinesScaled(lineInfo, 0f, new(), curSideScalingFactor, curSideScalingOriginFactor); + rect.DrawLinesScaled(lineInfo, curSideScalingFactor, curSideScalingOriginFactor); } } else if (shapeIndex == 4) // Quad @@ -356,7 +369,7 @@ protected override void OnDrawGameExample(ScreenInfo game) } else { - quad.DrawLinesScaled(lineInfo, 0f, new(), curSideScalingFactor, curSideScalingOriginFactor); + quad.DrawLinesScaled(lineInfo, curSideScalingFactor, curSideScalingOriginFactor); } } else if (shapeIndex == 5) // Polygon diff --git a/Examples/Scenes/ExampleScenes/PathfinderExample.cs b/Examples/Scenes/ExampleScenes/PathfinderExample.cs index 3385298d..90efcd08 100644 --- a/Examples/Scenes/ExampleScenes/PathfinderExample.cs +++ b/Examples/Scenes/ExampleScenes/PathfinderExample.cs @@ -23,7 +23,8 @@ public Vector2 Position public void Draw(ColorRgba color) { circle.Draw(color.ChangeBrightness(-0.05f)); - CircleDrawing.DrawCircleLines(circle.Center, circle.Radius * 1.5f, circle.Radius * 0.15f, Colors.PcText.ColorRgba, 4f); + var outerCircle = circle.SetRadius(circle.Radius * 1.5f); + outerCircle.DrawLines(circle.Radius * 0.15f, Colors.PcText.ColorRgba, 0.3f); } } diff --git a/Examples/Scenes/ExampleScenes/PathfinderExample2.cs b/Examples/Scenes/ExampleScenes/PathfinderExample2.cs index 21b38fac..ca36cc95 100644 --- a/Examples/Scenes/ExampleScenes/PathfinderExample2.cs +++ b/Examples/Scenes/ExampleScenes/PathfinderExample2.cs @@ -17,6 +17,7 @@ using ShapeEngine.Pathfinding; using Path = ShapeEngine.Pathfinding.Path; using ShapeEngine.Random; +using ShapeEngine.ShapeClipper; using Size = ShapeEngine.Core.Structs.Size; namespace Examples.Scenes.ExampleScenes; @@ -71,16 +72,16 @@ public Ship(Vector2 pos, float shipSize, Pathfinder pathfinder) inputActionTree = [iaMoveHor, iaMoveVer]; } - public Polygon? GetCutShape(float minSize) - { - var s = MathF.Max(minSize, shipSize); - return Polygon.Generate(pivot, 12, s, s * 2); - } + // public Polygon? GetCutShape(float minSize) + // { + // var s = MathF.Max(minSize, shipSize); + // return Polygon.Generate(pivot, 12, s, s * 2); + // } - public bool Overlaps(Polygon poly) - { - return hull.OverlapShape(poly); - } + // public bool Overlaps(Polygon poly) + // { + // return hull.OverlapShape(poly); + // } private void CreateHull(Vector2 pos, float size) { var a = pos + new Vector2(size, 0); @@ -402,13 +403,22 @@ public void Draw(ScreenInfo game) private void DrawInterpolated(float factor) { var c = Predictor ? Colors.PcHighlight : Colors.PcSpecial; - if(factor <= 0f) CircleDrawing.DrawCircleFast(prevBody.Center, prevBody.Radius, c.ColorRgba); - else if(factor >= 1f) CircleDrawing.DrawCircleFast(body.Center, body.Radius, c.ColorRgba); + if (factor <= 0f) + { + var circle = new Circle(body.Center, prevBody.Radius); + circle.DrawFast(c.ColorRgba); + } + else if (factor >= 1f) + { + var circle = new Circle(body.Center, body.Radius); + circle.DrawFast(c.ColorRgba); + } else { var interpPos = prevBody.Center.Lerp(body.Center, factor); float interpRadius = ShapeMath.LerpFloat(prevBody.Radius, body.Radius, factor); - CircleDrawing.DrawCircleFast(interpPos, interpRadius, c.ColorRgba); + var circle = new Circle(interpPos, interpRadius); + circle.DrawFast(c.ColorRgba); } } @@ -515,9 +525,11 @@ private class AsteroidObstacle public AsteroidObstacle(Vector2 center) { this.center = center; - this.shape = GenerateShape(center); - this.bb = this.shape.GetBoundingBox(); - this.triangulation = shape.Triangulate(); + shape = new(); + GenerateShape(center, shape); + bb = this.shape.GetBoundingBox(); + triangulation = new(); + shape.Triangulate(triangulation); } @@ -526,7 +538,8 @@ public AsteroidObstacle(Polygon shape) this.shape = shape; this.center = shape.GetCentroid(); this.bb = this.shape.GetBoundingBox(); - this.triangulation = shape.Triangulate(); + this.triangulation = new(); + this.shape.Triangulate(triangulation); } @@ -534,18 +547,21 @@ public void Draw(Rect cameraRect, bool drawFilled) { if (!bb.OverlapShape(cameraRect)) return; - if(drawFilled) triangulation.Draw(Colors.PcBackground.ColorRgba); + if (drawFilled) triangulation.Draw(Colors.PcBackground.ColorRgba); if (AsteroidLineThickness > 1) { - shape.DrawLines(AsteroidLineThickness, Colors.PcHighlight.ColorRgba); + TriMesh result = new(); + shape.TriangulateOutline(result, AsteroidLineThickness, 4f, true, false); + result.Draw(Colors.PcHighlight.ColorRgba); + // shape.DrawLines(AsteroidLineThickness, Colors.PcHighlight.ColorRgba); } } - public static Polygon? GenerateShape(Vector2 position) + public static bool GenerateShape(Vector2 position, Polygon shape) { - return Polygon.Generate(position, AsteroidPointCount, AsteroidMinSize, AsteroidMaxSize); + return Polygon.Generate(position, AsteroidPointCount, AsteroidMinSize, AsteroidMaxSize, shape); } public ShapeType GetShapeType() => ShapeType.Poly; @@ -583,7 +599,8 @@ public void Draw(Rect cameraRect, bool drawFilled) private const float MinPathRequestDistance = CellSize * 2; private const float MinPathRequestDistanceSquared = MinPathRequestDistance * MinPathRequestDistance; - + Polygons cutOutsBuffer = new(); + Polygons newShapesBuffer = new(); public PathfinderExample2() @@ -703,9 +720,11 @@ private void AddAsteroids(int amount) for (int i = 0; i < amount; i++) { var center = universe.GetRandomPointInside(); - var newShape = AsteroidObstacle.GenerateShape(center); + Polygon newShape = new(); + if(!AsteroidObstacle.GenerateShape(center, newShape) || newShape.Count < 3) continue; - var result = newShape.Intersect(universeShape); + Polygons result = new(); + newShape.ClipIntersection(universeShape, result); if (result.Count > 0) { newShape = result[0].ToPolygon(); @@ -715,11 +734,11 @@ private void AddAsteroids(int amount) { var existingShape = shapes[j]; - if(newShape == existingShape) continue; + if(newShape.Equals(existingShape)) continue; if (newShape.OverlapShape(existingShape)) { - newShape.UnionShapeSelf(existingShape, FillRule.NonZero); + newShape.ClipUnion(existingShape, newShape); shapes.RemoveAt(j); } else @@ -727,11 +746,12 @@ private void AddAsteroids(int amount) var cd = newShape.GetClosestPoint(existingShape); if (cd.DistanceSquared <= cellDisSq) { - var fillShape = Polygon.Generate(cd.Self.Point, 7, cellDistance, cellDistance * 2); - if (fillShape != null) + Polygon fillShape = new(); + Polygon.Generate(cd.Self.Point, 7, cellDistance, cellDistance * 2, fillShape); + if (fillShape.Count > 3) { - newShape.UnionShapeSelf(fillShape, FillRule.NonZero); - newShape.UnionShapeSelf(existingShape, FillRule.NonZero); + newShape.ClipUnion(fillShape, newShape); + newShape.ClipUnion(existingShape, newShape); shapes.RemoveAt(j); } @@ -751,6 +771,7 @@ private void AddAsteroids(int amount) } } + /* private void AddAsteroid(Vector2 position) { var asteroidShape = AsteroidObstacle.GenerateShape(position); @@ -858,8 +879,9 @@ private void AddAsteroid(Vector2 position) var newAsteroid = new AsteroidObstacle(asteroidShape); asteroids.Add(newAsteroid); } + */ - private void RemoveChasers(int amount) + /*private void RemoveChasers(int amount) { if (amount >= chasers.Count) { @@ -874,7 +896,8 @@ private void RemoveChasers(int amount) chasers.RemoveAt(i); pathfinder.RemoveAgent(chaser); } - } + }*/ + protected override void OnHandleInputExample(float dt, Vector2 mousePosGame, Vector2 mousePosGameUi, Vector2 mousePosUI) { var gamepad = Input.GamepadManager.LastUsedGamepad; @@ -935,16 +958,18 @@ protected override void OnUpdateExample(GameTime time, ScreenInfo game, ScreenIn var asteroid = asteroids[i]; var asteroidShape = asteroid.GetShape(); - var result = asteroidShape.CutShape(cutShape); + cutOutsBuffer.Clear(); + newShapesBuffer.Clear(); + asteroidShape.Cut(cutShape, cutOutsBuffer, newShapesBuffer); - if (result.cutOuts.Count > 0 ) + if (cutOutsBuffer.Count > 0) { lastCutShapes.Add(cutRect); lastCutShapeTimers.Add(LastCutShapeDuration); pathfinder.ApplyNodeValue(nodeValueRect, AsteroidObstacle.NodeCostReset); asteroids.RemoveAt(i); - foreach (var shape in result.newShapes) + foreach (var shape in newShapesBuffer) { if (shape.GetArea() <= CellSize * CellSize) { @@ -955,37 +980,7 @@ protected override void OnUpdateExample(GameTime time, ScreenInfo game, ScreenIn asteroids.Add(newAsteroid); } } - - // if (ship.Overlaps(asteroid.GetShape())) - // { - // var asteroidShape = asteroid.GetShape(); - // var result = asteroidShape.Cut(cutShape); - // - // if (result.newShapes.Count > 0) - // { - // lastCutShapes.Add(cutRect); - // lastCutShapeTimers.Add(LastCutShapeDuration); - // pathfinder.ApplyNodeValue(nodeValueRect, AsteroidObstacle.NodeValueReset); - // asteroids.RemoveAt(i); - // - // foreach (var shape in result.newShapes) - // { - // if (shape.GetArea() <= CellSize * CellSize) - // { - // pathfinder.ApplyNodeValue(shape, AsteroidObstacle.NodeValueReset); - // continue; - // } - // var newAsteroid = new AsteroidObstacle(shape); - // asteroids.Add(newAsteroid); - // } - // } - // - // } - // - - // asteroid.Update(time.Delta); } - } @@ -1037,9 +1032,9 @@ protected override void OnDrawGameExample(ScreenInfo game) var outerColor = Colors.PcSpecial.ColorRgba.ChangeAlpha((byte)150); outerBoundary.DrawLines(thickness, outerColor); } - - CircleDrawing.DrawCircleLines(ship.GetChasePosition(), MinPathRequestDistance, 8f, Colors.PcCold.ColorRgba); - // ShapeDrawing.DrawCircleLines(ship.GetChasePosition(), Chaser.MaxPathRequestDistance, 8f, new ColorRgba(Color.Aqua)); + + var circle = new Circle(ship.GetChasePosition(), MinPathRequestDistance); + circle.DrawLines(8f, Colors.PcCold.ColorRgba, 0.8f); } diff --git a/Examples/Scenes/ExampleScenes/PhysicsExample.cs b/Examples/Scenes/ExampleScenes/PhysicsExample.cs index ac69006d..cde24567 100644 --- a/Examples/Scenes/ExampleScenes/PhysicsExample.cs +++ b/Examples/Scenes/ExampleScenes/PhysicsExample.cs @@ -67,14 +67,15 @@ public PhysicsExample() var sizeMax = ShapeMath.LerpFloat(1.5f, 3f, f); var sizeRange = new ValueRange(sizeMin, sizeMax); var color = ColorRgba.Lerp(ColorRgba.White, ColorRgba.White.SetAlpha(220), f); - t.BeginDraw(ColorRgba.Clear); + t.BeginDraw(ColorRgba.Transparent); for (int j = 0; j < starCount; j++) { var x = Rng.Instance.RandF(0f, 2048); var y = Rng.Instance.RandF(0f, 2048); var pos = new Vector2(x, y); var size = sizeRange.Rand(); - CircleDrawing.DrawCircle(pos, size, color); + var circle = new Circle(pos, size); + circle.Draw(color); } t.EndDraw(); @@ -232,8 +233,10 @@ private void DrawCircleSector() StripedDrawing.DrawStripedRing(Vector2.Zero, SectorRadiusInside, SectorRadiusOutside, 1f, stripedLineInfo, 0f); - CircleDrawing.DrawCircleLines(Vector2.Zero, SectorRadiusInside, universeLineInfo , 0f, 24f); - CircleDrawing.DrawCircleLines(Vector2.Zero, SectorRadiusOutside, universeLineInfo, 0f, 24f); + var insideCircle = new Circle(Vector2.Zero, SectorRadiusInside); + var outsideCircle = new Circle(Vector2.Zero, SectorRadiusOutside); + insideCircle.DrawLines(universeLineInfo, 0.75f); + outsideCircle.DrawLines(universeLineInfo, 0.75f); var textCount = 12; var angleStepRad = (360f / textCount) * ShapeMath.DEGTORAD; diff --git a/Examples/Scenes/ExampleScenes/PhysicsExampleSource/Asteroid.cs b/Examples/Scenes/ExampleScenes/PhysicsExampleSource/Asteroid.cs index 5a2993e0..324024d6 100644 --- a/Examples/Scenes/ExampleScenes/PhysicsExampleSource/Asteroid.cs +++ b/Examples/Scenes/ExampleScenes/PhysicsExampleSource/Asteroid.cs @@ -44,7 +44,8 @@ public void Draw() float f = timer / lifetime; if(reversed) f = 1f - f; var radius = radiusRange.Lerp(f); - CircleDrawing.DrawCircleLines(curPosition, radius, thickness, color.ColorRgba.SetAlpha(200), 8f); + var circle = new Circle(curPosition, radius); + circle.DrawLines(thickness, color.ColorRgba.SetAlpha(200), 0.1f); } } @@ -83,8 +84,9 @@ public Asteroid(Vector2 position, PaletteColor color) Transform = new Transform2D(position, 0f, new Size(randSize), 1f); paletteColor = color; - var relativePoints = Polygon.GenerateRelative(15, 0.4f, 1f); - collider = new PolygonCollider(new(),relativePoints ?? []) + Polygon relativePoints = new(); + Polygon.GenerateRelative(15, 0.4f, 1f, relativePoints); + collider = new PolygonCollider(new(),relativePoints) { ComputeCollision = true, ComputeIntersections = true, diff --git a/Examples/Scenes/ExampleScenes/PhysicsExampleSource/Ship.cs b/Examples/Scenes/ExampleScenes/PhysicsExampleSource/Ship.cs index 4a9c0939..d68f4f72 100644 --- a/Examples/Scenes/ExampleScenes/PhysicsExampleSource/Ship.cs +++ b/Examples/Scenes/ExampleScenes/PhysicsExampleSource/Ship.cs @@ -190,7 +190,7 @@ public override void DrawGame(ScreenInfo game) hullAbsolute.DrawStriped(10f, Transform.RotationDeg + 90, new LineDrawingInfo(2f,c, LineCapType.None, 0), 0f); - hullAbsolute.DrawLines(thickness, c, LineCapType.Capped, 6); + hullAbsolute.DrawLines(thickness, c, 6); var radius = hullSize.Radius * 2; var center = Transform.Position; diff --git a/Examples/Scenes/ExampleScenes/PolygonHolesExample.cs b/Examples/Scenes/ExampleScenes/PolygonHolesExample.cs deleted file mode 100644 index c074f349..00000000 --- a/Examples/Scenes/ExampleScenes/PolygonHolesExample.cs +++ /dev/null @@ -1,283 +0,0 @@ - -using Raylib_cs; -using ShapeEngine.StaticLib; -using System.Numerics; -using System.Text; -using Clipper2Lib; -using ShapeEngine.Color; -using ShapeEngine.Core.Structs; -using ShapeEngine.Geometry; -using ShapeEngine.Geometry.PointsDef; -using ShapeEngine.Geometry.PolygonDef; -using ShapeEngine.Geometry.RectDef; -using ShapeEngine.Geometry.TriangleDef; -using ShapeEngine.Geometry.TriangulationDef; -using ShapeEngine.Input; -using Color = System.Drawing.Color; - -namespace Examples.Scenes.ExampleScenes; - - -public class PolygonHolesExample : ExampleScene -{ - - private class PolygonWithHoles - { - public Polygon Polygon; - public Triangulation Triangulation; - public Polygons Holes; - - public PolygonWithHoles(Polygon polygon, Polygons holes) - { - this.Polygon = polygon; - this.Holes = holes; - List holeTriangles = new(holes.Count * 2); - Points points = new(); - points.AddRange(polygon); - foreach (var hole in holes) - { - points.AddRange(hole); - holeTriangles.AddRange(hole.Triangulate()); - } - - var triangulation = Polygon.TriangulateDelaunay(points); - for (int i = triangulation.Count - 1; i >= 0; i--) - { - var triangle = triangulation[i]; - foreach (var t in holeTriangles) - { - if (triangle.OverlapShape(t)) - { - triangulation.RemoveAt(i); - break; - } - } - - } - - Triangulation = triangulation; - - } - public PolygonWithHoles(Polygon polygon) - { - this.Polygon = polygon; - this.Holes = new(); - this.Triangulation = polygon.Triangulate(); - - } - - public bool Overlap(Polygon other) - { - foreach (var t in Triangulation) - { - if (t.OverlapShape(other)) return true; - } - - return false; - } - - public void Draw(ColorRgba color, ColorRgba outlineColor) - { - Triangulation.Draw(color); - Polygon.DrawLines(2f, outlineColor); - foreach (var hole in Holes) - { - hole.DrawLines(2f, outlineColor); - } - } - - } - - private Vector2 curPolygonPosition; - private Polygon curPolygon; - private Polygon main; - private Triangulation triangulation; - private readonly Polygons holes = new(); - - private InputAction iaAddPolygon; - private readonly InputActionTree inputActionTree; - - public PolygonHolesExample() - { - Title = "Polygon Holes Example"; - - InputActionSettings defaultSettings = new(); - var addPolygonKB = new InputTypeKeyboardButton(ShapeKeyboardButton.SPACE); - var addPolygonGP = new InputTypeGamepadButton(ShapeGamepadButton.RIGHT_FACE_DOWN); - var addPolygonMB = new InputTypeMouseButton(ShapeMouseButton.LEFT); - iaAddPolygon = new(defaultSettings,addPolygonKB, addPolygonGP, addPolygonMB); - - inputActionTree = [iaAddPolygon]; - - textFont.FontSpacing = 1f; - textFont.ColorRgba = Colors.Light; - GenerateCurPolygon(new()); - - var mainRect = new Rect(new Vector2(0f), new Size(1000), new AnchorPoint(0.5f)); - main = mainRect.ToPolygon(); - triangulation = main.Triangulate(); - } - public override void Reset() - { - var mainRect = new Rect(new Vector2(0f), new Size(1000), new AnchorPoint(0.5f)); - main = mainRect.ToPolygon(); - triangulation = main.Triangulate(); - holes.Clear(); - } - - - protected override void OnHandleInputExample(float dt, Vector2 mousePosGame, Vector2 mousePosGameUi, Vector2 mousePosUI) - { - var gamepad = Input.GamepadManager.LastUsedGamepad; - inputActionTree.CurrentGamepad = gamepad; - inputActionTree.Update(dt); - - if (iaAddPolygon.State.Pressed) - { - PlaceCurPolygon(mousePosGame); - } - } - - protected override void OnUpdateExample(GameTime time, ScreenInfo game, ScreenInfo gameUi, ScreenInfo ui) - { - var translation = (game.MousePos - curPolygonPosition); - curPolygonPosition = game.MousePos; - curPolygon.ChangePosition(translation); - // for (int i = 0; i < curPolygonTriangulation.Count; i++) - // { - // curPolygonTriangulation[i] = curPolygonTriangulation[i].Move(translation); - // } - } - - protected override void OnDrawGameExample(ScreenInfo game) - { - // triangulation.Draw(Colors.Cold); - foreach (var tri in triangulation) - { - tri.Draw(Colors.Medium); - // tri.DrawLines(2f, Colors.Cold); - } - - curPolygon.DrawLines(4f, Colors.Warm); - foreach (var hole in holes) - { - hole.DrawLines(2f, Colors.Warm); - } - - // main.DrawLines(6f, Colors.Medium); - - // main.DrawLines(6f, Colors.Medium); - // - // - // var holeTriangulation = new Triangulation(); - // var points = new Points(); - // foreach (var p in difference) - // { - // var poly = p.ToPolygon(); - // points.AddRange(poly); - // if (p.IsHole()) - // { - // poly.FixWindingOrder(); - // holeTriangulation.AddRange(poly.Triangulate()); - // } - // } - // holeTriangulation.Draw(Colors.Cold); - // Triangulation triangulation = Polygon.TriangulateDelaunay(points); - // triangulation.DrawLines(4f, Colors.Warm); - // points.Draw(12, Colors.Light, 16); - // - // for (int i = triangulation.Count - 1; i >= 0; i--) - // { - // var tri = triangulation[i]; - // bool draw = true; - // foreach (var h in holeTriangulation) - // { - // if (Math.Abs(tri.GetArea() - h.GetArea()) < 1) - // { - // draw = false; - // break; - // } - // } - // if(draw) tri.Draw(Colors.Warm); - // } - // - // // triangulation.Draw(Colors.Warm); - // - // curPolygon.DrawLines(5f, Colors.Highlight); - } - protected override void OnDrawGameUIExample(ScreenInfo gameUi) - { - - } - - protected override void OnDrawUIExample(ScreenInfo ui) - { - var bottomCenter = GameloopExamples.Instance.UIRects.GetRect("bottom center"); - DrawInputText(bottomCenter); - } - - - private void GenerateCurPolygon(Vector2 pos) - { - var shape = Polygon.Generate(pos, 12, 50, 100); - if(shape == null) return; - curPolygon = shape; - curPolygonPosition = pos; - } - private void PlaceCurPolygon(Vector2 mousePos) - { - // intersection = Clipper.Intersect(main.ToClipperPaths(), curPolygon.ToClipperPaths(), FillRule.NonZero, 2); - // difference = Clipper.Difference(main.ToClipperPaths(), curPolygon.ToClipperPaths(), FillRule.NonZero, 2); - holes.Add(curPolygon); - UpdateTriangulation(); - GenerateCurPolygon(mousePos); - } - - private void UpdateTriangulation() - { - var points = main.ToPoints(); - foreach (var hole in holes) - { - foreach (var p in hole) - { - if(main.ContainsPoint(p)) points.Add(p); - } - // points.AddRange(hole); - } - - var newTriangulation = Polygon.TriangulateDelaunay(points); - triangulation.Clear(); - foreach (var tri in newTriangulation) - { - bool outside = true; - foreach (var hole in holes) - { - // var count = 0; - // if (hole.ContainsPoint(tri.A)) count++; - // if (hole.ContainsPoint(tri.B)) count++; - // if (hole.ContainsPoint(tri.C)) count++; - if (hole.ContainsPoint(tri.GetCentroid())) - { - outside = false; - break; - } - } - if(outside) triangulation.Add(tri); - } - - } - - private void DrawInputText(Rect rect) - { - var sb = new StringBuilder(); - var curInputDeviceAll = Input.CurrentInputDeviceType; - - string addPointText = iaAddPolygon.GetInputTypeDescription(curInputDeviceAll, true, 1, false); - - sb.Append($"Add Point{addPointText} | "); - - textFont.ColorRgba = Colors.Light; - textFont.DrawTextWrapNone(sb.ToString(), rect, new(0.5f)); - - } -} \ No newline at end of file diff --git a/Examples/Scenes/ExampleScenes/PolylineInflationExample.cs b/Examples/Scenes/ExampleScenes/PolylineInflationExample.cs index 0aae2291..a567a6cf 100644 --- a/Examples/Scenes/ExampleScenes/PolylineInflationExample.cs +++ b/Examples/Scenes/ExampleScenes/PolylineInflationExample.cs @@ -10,6 +10,7 @@ using ShapeEngine.Geometry.PolylineDef; using ShapeEngine.Geometry.RectDef; using ShapeEngine.Geometry.SegmentDef; +using ShapeEngine.ShapeClipper; namespace Examples.Scenes.ExampleScenes { @@ -136,10 +137,15 @@ protected override void OnDrawGameExample(ScreenInfo game) float disSq = (mousePos - p).LengthSquared(); if (pickedVertex == -1 && disSq < (vertexRadius * vertexRadius) * 2f) { - CircleDrawing.DrawCircle(p, vertexRadius * 2f, Colors.Highlight); + var circle = new Circle(p, vertexRadius * 2f); + circle.Draw(Colors.Highlight); pickedVertex = i; } - else CircleDrawing.DrawCircle(p, vertexRadius, Colors.Medium); + else + { + var circle = new Circle(p, vertexRadius); + circle.Draw(Colors.Medium); + } if (drawClosest) { disSq = (closest.Point - p).LengthSquared(); @@ -218,13 +224,19 @@ protected override void OnDrawGameExample(ScreenInfo game) } } - if (drawClosest) CircleDrawing.DrawCircle(closest.Point, vertexRadius, Colors.Warm); + if (drawClosest) + { + var circle = new Circle(closest.Point, vertexRadius); + circle.Draw(Colors.Warm); + } - Polygons? inflatedPolygons = null; - if (lerpOffsetDelta > 10f) + Polygons? inflationResult = null; + if (lerpOffsetDelta > 10f && polyline.Count > 1) { - inflatedPolygons = ShapeClipper.Inflate(polyline, lerpOffsetDelta).ToPolygons(); - foreach (var polygon in inflatedPolygons) + inflationResult = new(); + polyline.InflatePolyline(inflationResult, lerpOffsetDelta, 2f, false, ShapeClipperEndType.Round); + // inflatedPolygons = ShapeClipper.Inflate(polyline, lerpOffsetDelta).ToPolygons(); + foreach (var polygon in inflationResult) { polygon.DrawLines(relativeSize, Colors.Special); } @@ -234,9 +246,9 @@ protected override void OnDrawGameExample(ScreenInfo game) if (collisionSegmentValid) { var intersectionHappend = false; - if (inflatedPolygons != null) + if (inflationResult != null) { - foreach (var polygon in inflatedPolygons) + foreach (var polygon in inflationResult) { var intersectionPoints = collisionSegment.IntersectShape(polygon); if (intersectionPoints != null && intersectionPoints.Count > 0) diff --git a/Examples/Scenes/ExampleScenes/SavegameExample.cs b/Examples/Scenes/ExampleScenes/SavegameExample.cs index 2f31c660..2e92927f 100644 --- a/Examples/Scenes/ExampleScenes/SavegameExample.cs +++ b/Examples/Scenes/ExampleScenes/SavegameExample.cs @@ -330,28 +330,38 @@ protected override void OnDrawUIExample(ScreenInfo ui) if (draggingTopLeft) { - CircleDrawing.DrawCircleFast(rect.TopLeft, rectCornerSelectionRadius, ColorRgba.White); - CircleDrawing.DrawCircleFast(rect.BottomRight, rectCornerSize, color); + var circle1 = new Circle(rect.TopLeft, rectCornerSelectionRadius); + var circle2 = new Circle(rect.BottomRight, rectCornerSize); + circle1.DrawFast(ColorRgba.White); + circle2.DrawFast(color); } else if (draggingBottomRight) { - CircleDrawing.DrawCircleFast(rect.TopLeft, rectCornerSize, color); - CircleDrawing.DrawCircleFast(rect.BottomRight, rectCornerSelectionRadius, ColorRgba.White); + var circle1 = new Circle(rect.TopLeft, rectCornerSize); + var circle2 = new Circle(rect.BottomRight, rectCornerSelectionRadius); + circle1.DrawFast(color); + circle2.DrawFast(ColorRgba.White); } else if (nearTopLeft) { - CircleDrawing.DrawCircleFast(rect.TopLeft, rectCornerSelectionRadius, color); - CircleDrawing.DrawCircleFast(rect.BottomRight, rectCornerSize, color); + var circle1 = new Circle(rect.TopLeft, rectCornerSelectionRadius); + var circle2 = new Circle(rect.BottomRight, rectCornerSize); + circle1.DrawFast(color); + circle2.DrawFast(color); } else if (nearBottomRight) { - CircleDrawing.DrawCircleFast(rect.TopLeft, rectCornerSize, color); - CircleDrawing.DrawCircleFast(rect.BottomRight, rectCornerSelectionRadius, color); + var circle1 = new Circle(rect.TopLeft, rectCornerSize); + var circle2 = new Circle(rect.BottomRight, rectCornerSelectionRadius); + circle1.DrawFast(color); + circle2.DrawFast(color); } else { - CircleDrawing.DrawCircleFast(rect.TopLeft, rectCornerSize, color); - CircleDrawing.DrawCircleFast(rect.BottomRight, rectCornerSize, color); + var circle1 = new Circle(rect.TopLeft, rectCornerSize); + var circle2 = new Circle(rect.BottomRight, rectCornerSize); + circle1.DrawFast(color); + circle2.DrawFast(color); } var lmbState = ShapeMouseButton.LEFT.GetInputState(); @@ -370,7 +380,7 @@ protected override void OnDrawUIExample(ScreenInfo ui) var slotButtonArea = slotButtonsAreas[i].ApplyMargins(buttonMargin); if (i == currentSavegameSlot) { - slotButtonArea.DrawCornersRelative(new LineDrawingInfo(rectLineThickness / 2, ColorRgba.White), 0.15f); + slotButtonArea.DrawCornersRelative(rectLineThickness / 2, ColorRgba.White, 0.15f); } ColorRgba slotColor; diff --git a/Examples/Scenes/ExampleScenes/ScreenEffectsExample.cs b/Examples/Scenes/ExampleScenes/ScreenEffectsExample.cs index 42139f4d..07355d40 100644 --- a/Examples/Scenes/ExampleScenes/ScreenEffectsExample.cs +++ b/Examples/Scenes/ExampleScenes/ScreenEffectsExample.cs @@ -26,12 +26,15 @@ public Star(Vector2 pos, float size) public void Draw() { - var color = Colors.Dark; // new ColorRgba(System.Drawing.Color.DarkGray); - if (circle.Radius > 2f && circle.Radius <= 3f) color = Colors.Dark.ChangeBrightness(0.025f); // new(System.Drawing.Color.LightGray); - else if (circle.Radius > 3f) color = Colors.Dark.ChangeBrightness(0.05f); // new(System.Drawing.Color.AntiqueWhite); - CircleDrawing.DrawCircleFast(circle.Center, circle.Radius, color); + var color = Colors.Dark; + if (circle.Radius > 2f && circle.Radius <= 3f) color = Colors.Dark.ChangeBrightness(0.025f); + else if (circle.Radius > 3f) color = Colors.Dark.ChangeBrightness(0.05f); + circle.DrawFast(color); + } + public void Draw(ColorRgba c) + { + circle.DrawFast(c); } - public void Draw(ColorRgba c) => CircleDrawing.DrawCircleFast(circle.Center, circle.Radius, c); } internal class Comet { @@ -119,8 +122,8 @@ public void Update(float dt, Rect r, Vector2 mousePos) } public void Draw() { - background.DrawRounded(4f, 4, Colors.Dark); - fill.DrawRounded(4f, 4, Colors.Medium); + background.Draw(Colors.Dark); + fill.Draw(Colors.Medium); int textValue = (int)(CurValue * TextValueMax); font.ColorRgba = mouseInside ? Colors.Highlight: Colors.Special; @@ -228,11 +231,16 @@ public void Draw() { var rightThruster = movementDir.RotateDeg(-25); var leftThruster = movementDir.RotateDeg(25); - CircleDrawing.DrawCircle(Hull.Center - rightThruster * Hull.Radius, Hull.Radius / 6, outlineColor.ColorRgba, 12); - CircleDrawing.DrawCircle(Hull.Center - leftThruster * Hull.Radius, Hull.Radius / 6, outlineColor.ColorRgba, 12); - Hull.Draw(hullColor.ColorRgba); - CircleDrawing.DrawCircle(Hull.Center + movementDir * Hull.Radius * 0.66f, Hull.Radius * 0.33f, cockpitColor.ColorRgba, 12); + var rightThrusterCircle = new Circle(Hull.Center - rightThruster * Hull.Radius, Hull.Radius / 6); + var leftThrusterCircle = new Circle(Hull.Center - leftThruster * Hull.Radius, Hull.Radius / 6); + var hullCircle = new Circle(Hull.Center + movementDir * Hull.Radius * 0.66f, Hull.Radius * 0.33f); + + rightThrusterCircle.Draw(outlineColor.ColorRgba, 0.25f); + leftThrusterCircle.Draw(outlineColor.ColorRgba, 0.25f); + Hull.Draw(hullColor.ColorRgba); + hullCircle.Draw(cockpitColor.ColorRgba, 0.25f); + Hull.DrawLines(4f, outlineColor.ColorRgba); } diff --git a/Examples/Scenes/ExampleScenes/ShapeDrawingTestExample.cs b/Examples/Scenes/ExampleScenes/ShapeDrawingTestExample.cs new file mode 100644 index 00000000..2b4d3b0c --- /dev/null +++ b/Examples/Scenes/ExampleScenes/ShapeDrawingTestExample.cs @@ -0,0 +1,450 @@ +using System.Numerics; +using ShapeEngine.Color; +using ShapeEngine.Core; +using ShapeEngine.Core.Structs; +using ShapeEngine.Geometry; +using ShapeEngine.Geometry.CircleDef; +using ShapeEngine.Geometry.PolygonDef; +using ShapeEngine.Geometry.PolylineDef; +using ShapeEngine.Geometry.QuadDef; +using ShapeEngine.Geometry.RectDef; +using ShapeEngine.Geometry.SegmentDef; +using ShapeEngine.Geometry.TriangleDef; +using ShapeEngine.Input; +using ShapeEngine.StaticLib; +using ShapeEngine.Text; +using ShapeEngine.UI; + +namespace Examples.Scenes.ExampleScenes; + +public class ShapeDrawingTestExample : ExampleScene +{ + private enum ShapeType + { + Segment, + Circle, + Triangle, + Rect, + Quad, + Polygon, + Polyline + } + + private enum ParamId + { + Thickness, + Smoothness, + Percentage, + Scale, + Origin, + StartIndex, + CornerLength, + CornerFactor, + RotationDeg, + Roundness, + Segments, + RingThickness, + StartAngleDeg, + EndAngleDeg, + DrawType, + Width, + EndWidth, + Steps, + VertexRadius, + MiterLimit + } + + private sealed class ValueSlider : ControlNodeSlider + { + private readonly TextFont font; + public string Label; + public bool Percentage = false; + public bool Integer = false; + public bool ShowValue = true; + + public ValueSlider(string label, float value, float min, float max, bool horizontal = true) + : base(value, min, max, horizontal) + { + Label = label; + font = new(GameloopExamples.Instance.GetFont(FontIDs.JetBrains), 1f, ColorRgba.White); + } + + protected override bool GetButtonPressedState() => ShapeKeyboardButton.SPACE.GetInputState().Pressed; + protected override bool GetMouseButtonPressedState() => ShapeMouseButton.LEFT.GetInputState().Pressed; + + protected override bool GetButtonReleasedState() => ShapeKeyboardButton.SPACE.GetInputState().Released; + protected override bool GetMouseButtonReleasedState() => ShapeMouseButton.LEFT.GetInputState().Released; + + protected override bool GetDecreaseValuePressed() => ShapeKeyboardButton.LEFT.GetInputState().Pressed; + protected override bool GetIncreaseValuePressed() => ShapeKeyboardButton.RIGHT.GetInputState().Pressed; + + protected override void OnDraw() + { + var bgColor = Colors.Dark; + var fillColor = MouseInside ? Colors.Special : Colors.Medium; + var textColor = Colors.Highlight; + var margin = Rect.Size.Min() * 0.1f; + var fillRect = Fill.ApplyMarginsAbsolute(margin); + + Rect.Draw(bgColor); + fillRect.Draw(fillColor); + + if (Selected) Rect.DrawLines(2f, Colors.Special); + else Rect.DrawLines(2f, Colors.Medium); + + font.ColorRgba = textColor; + + if (!ShowValue) + { + font.DrawTextWrapNone(Label, Rect, new AnchorPoint(0.5f, 0.5f)); + return; + } + + string valueText; + if (Percentage) valueText = $"{(int)(CurValue * 100f)}%"; + else if (Integer) valueText = $"{(int)CurValue}"; + else valueText = $"{CurValue:0.##}"; + + font.DrawTextWrapNone($"{Label} {valueText}", Rect, new AnchorPoint(0.5f, 0.5f)); + } + } + + private sealed record DrawingCase(string Name, Action Draw, params ParamId[] Params); + + private static readonly ColorRgba DrawColor = ColorRgba.White.SetAlpha(200); + + private readonly ValueSlider shapeSlider; + private readonly ValueSlider methodSlider; + private readonly Dictionary paramSliders = new(); + private readonly Dictionary> cases = new(); + + private Segment segment; + private Circle circle; + private Triangle triangle; + private Rect rect; + private Quad quad; + private Triangle triangleTemplate; + private Polygon polygonTemplate = []; + private Polygon polygon = []; + private Polyline polyline = []; + + public ShapeDrawingTestExample() + { + Title = "Shape Drawing Test"; + + textFont.FontSpacing = 1f; + textFont.ColorRgba = Colors.Light; + + BuildParamSliders(); + InitializeStableShapes(); + BuildShapes(); + BuildCases(); + + shapeSlider = new("Shape", 0, 0, Enum.GetValues().Length - 1, false) + { + Integer = true + }; + + methodSlider = new("Method", 0, 0, 0, false) + { + Integer = true + }; + + SyncMethodSlider(); + } + + public override void Reset() + { + shapeSlider.SetCurValue(0); + methodSlider.SetCurValue(0); + + foreach (var slider in paramSliders.Values) + { + slider.SetCurValue(slider.MinValue); + } + + paramSliders[ParamId.Thickness].SetCurValue(6f); + paramSliders[ParamId.Smoothness].SetCurValue(0.5f); + paramSliders[ParamId.Percentage].SetCurValue(0.6f); + paramSliders[ParamId.Scale].SetCurValue(0.6f); + paramSliders[ParamId.Origin].SetCurValue(0.5f); + paramSliders[ParamId.StartIndex].SetCurValue(0); + paramSliders[ParamId.CornerLength].SetCurValue(50f); + paramSliders[ParamId.CornerFactor].SetCurValue(0.2f); + paramSliders[ParamId.RotationDeg].SetCurValue(45f); + paramSliders[ParamId.Roundness].SetCurValue(0.25f); + paramSliders[ParamId.Segments].SetCurValue(8); + paramSliders[ParamId.RingThickness].SetCurValue(40f); + paramSliders[ParamId.StartAngleDeg].SetCurValue(0f); + paramSliders[ParamId.EndAngleDeg].SetCurValue(220f); + paramSliders[ParamId.DrawType].SetCurValue(0); + paramSliders[ParamId.Width].SetCurValue(20f); + paramSliders[ParamId.EndWidth].SetCurValue(4f); + paramSliders[ParamId.Steps].SetCurValue(8); + paramSliders[ParamId.VertexRadius].SetCurValue(12f); + paramSliders[ParamId.MiterLimit].SetCurValue(4f); + + InitializeStableShapes(); + BuildShapes(); + SyncMethodSlider(); + } + + protected override void OnUpdateExample(GameTime time, ScreenInfo game, ScreenInfo gameUi, ScreenInfo ui) + { + var leftRect = ui.Area.ApplyMargins(0.01f, 0.92f, 0.14f, 0.18f); + var rightRect = ui.Area.ApplyMargins(0.92f, 0.01f, 0.14f, 0.18f); + var bottomRect = ui.Area.ApplyMargins(0.12f, 0.12f, 0.82f, 0.02f); + + shapeSlider.SetRect(leftRect); + shapeSlider.Update(time.Delta, ui.MousePos); + + SyncMethodSlider(); + + methodSlider.SetRect(rightRect); + methodSlider.Update(time.Delta, ui.MousePos); + + var active = GetCurrentCase().Params.ToList(); + if (UsesRotationSlider()) active.Add(ParamId.RotationDeg); + + if (active.Count > 0) + { + var rects = bottomRect.SplitV(active.Count); + for (int i = 0; i < active.Count; i++) + { + var slider = paramSliders[active[i]]; + slider.SetRect(rects[i].ApplyMargins(0.02f, 0.02f, 0f, 0f)); + slider.Update(time.Delta, ui.MousePos); + } + } + + BuildShapes(); + + Title = $"{GetCurrentShape()} :: {GetCurrentCase().Name}"; + } + + protected override void OnDrawGameExample(ScreenInfo game) + { + GetCurrentCase().Draw(); + } + + protected override void OnDrawUIExample(ScreenInfo ui) + { + var shapeLabel = GetCurrentShape().ToString(); + var methodLabel = GetCurrentCase().Name; + + shapeSlider.Label = string.Empty; + methodSlider.Label = string.Empty; + + shapeSlider.ShowValue = false; + methodSlider.ShowValue = false; + + shapeSlider.Draw(); + methodSlider.Draw(); + + var leftRect = ui.Area.ApplyMargins(0.01f, 0.92f, 0.14f, 0.18f); + var rightRect = ui.Area.ApplyMargins(0.92f, 0.01f, 0.14f, 0.18f); + var leftLabelRect = leftRect; + var rightLabelRect = rightRect; + + textFont.ColorRgba = Colors.Highlight; + textFont.Draw(shapeLabel, leftLabelRect, 90f, new AnchorPoint(0.5f, 0.5f)); + textFont.Draw(methodLabel, rightLabelRect, -90f, new AnchorPoint(0.5f, 0.5f)); + + + var active = GetCurrentCase().Params.ToList(); + if (UsesRotationSlider()) active.Add(ParamId.RotationDeg); + foreach (var id in active) paramSliders[id].Draw(); + } + + private void InitializeStableShapes() + { + float size = 420f; + float radius = size * 0.5f; + var center = Vector2.Zero; + + triangleTemplate = Triangle.Generate(center, size * 0.5f, size); + polygonTemplate = new(); + Polygon.Generate(center, 10, radius * 0.5f, radius, polygonTemplate); + } + + private void BuildShapes() + { + float size = 420f; + float radius = size * 0.5f; + float rotRad = RotationDeg * ShapeMath.DEGTORAD; + var center = Vector2.Zero; + + segment = new(center, size, rotRad); + circle = new(center, radius); + triangle = triangleTemplate; + rect = new Rect(center, new Size(size, size), new AnchorPoint(0.5f, 0.5f)); + quad = new Quad(center, new Size(size, size), rotRad, new AnchorPoint(0.5f, 0.5f)); + + polygon = new Polygon(polygonTemplate); + polygon.SetRotation(rotRad, center); + + polyline = polygon.ToPolyline(); + } + + private void BuildParamSliders() + { + paramSliders[ParamId.Thickness] = new("Thickness", 6f, 1f, 32f) { Integer = false }; + paramSliders[ParamId.Smoothness] = new("Smoothness", 0.5f, 0f, 1f) { Percentage = true }; + paramSliders[ParamId.Percentage] = new("Percentage", 0.6f, -1f, 1f) { Percentage = true }; + paramSliders[ParamId.Scale] = new("Scale", 0.6f, 0f, 1f) { Percentage = true }; + paramSliders[ParamId.Origin] = new("Origin", 0.5f, 0f, 1f) { Percentage = true }; + paramSliders[ParamId.StartIndex] = new("Start", 0, 0, 15) { Integer = true }; + paramSliders[ParamId.CornerLength] = new("Corner", 50f, 1f, 120f); + paramSliders[ParamId.CornerFactor] = new("Corner", 0.2f, 0f, 1f) { Percentage = true }; + paramSliders[ParamId.RotationDeg] = new("Rotation", 45f, 0f, 360f); + paramSliders[ParamId.Roundness] = new("Roundness", 0.25f, 0f, 1f) { Percentage = true }; + paramSliders[ParamId.Segments] = new("Segments", 8, 1, 32) { Integer = true }; + paramSliders[ParamId.RingThickness] = new("Ring", 40f, 4f, 120f); + paramSliders[ParamId.StartAngleDeg] = new("Start°", 0f, 0f, 360f); + paramSliders[ParamId.EndAngleDeg] = new("End°", 220f, 0f, 360f); + paramSliders[ParamId.DrawType] = new("Type", 0, 0, 2) { Integer = true }; + paramSliders[ParamId.Width] = new("Width", 20f, 2f, 40f); + paramSliders[ParamId.EndWidth] = new("End", 4f, 1f, 20f); + paramSliders[ParamId.Steps] = new("Steps", 8, 1, 16) { Integer = true }; + paramSliders[ParamId.VertexRadius] = new("Vertex", 12f, 2f, 32f); + paramSliders[ParamId.MiterLimit] = new("Miter", 4f, 2f, 8f); + } + + private void BuildCases() + { + cases[ShapeType.Segment] = + [ + new("Draw", () => segment.Draw(LineInfo)), + new("Draw Separate Caps", () => segment.DrawSeparateCaps(LineThickness, DrawColor, LineCapType.Capped, 4, LineCapType.CappedExtended, 4), ParamId.Thickness), + new("Draw Percentage", () => segment.DrawPercentage(Percentage, LineInfo), ParamId.Thickness, ParamId.Percentage), + new("Draw Scaled", () => segment.DrawScaled(LineInfo, Scale, Origin), ParamId.Thickness, ParamId.Scale, ParamId.Origin), + new("Draw Glow", () => segment.DrawGlow(Width, EndWidth, DrawColor, DrawColor.SetAlpha(0), Steps, LineCapType.CappedExtended, 4), ParamId.Width, ParamId.EndWidth, ParamId.Steps), + new("Draw Vertices", () => { segment.Draw(LineInfo); segment.DrawVertices(VertexRadius, DrawColor, Smoothness); }, ParamId.Thickness, ParamId.VertexRadius, ParamId.Smoothness), + ]; + + cases[ShapeType.Circle] = + [ + new("Draw", () => circle.Draw(DrawColor, Smoothness), ParamId.Smoothness), + new("Draw Fast", () => circle.DrawFast(DrawColor)), + new("Draw Scaled", () => circle.DrawScaled(0f, DrawColor, Smoothness, Scale, Origin), ParamId.Smoothness, ParamId.Scale, ParamId.Origin), + new("Draw Percentage", () => circle.DrawPercentage(Percentage, 0f, DrawColor, Smoothness), ParamId.Smoothness, ParamId.Percentage), + new("Draw Lines", () => circle.DrawLines(LineInfo, Smoothness), ParamId.Thickness, ParamId.Smoothness), + new("Draw Lines Percentage", () => circle.DrawLinesPercentage(Percentage, 0f, LineInfo, Smoothness), ParamId.Thickness, ParamId.Smoothness, ParamId.Percentage), + new("Draw Sector", () => circle.DrawSector(StartAngleDeg, EndAngleDeg, 0f, DrawColor, Smoothness), ParamId.StartAngleDeg, ParamId.EndAngleDeg, ParamId.Smoothness), + new("Draw Sector Scaled", () => circle.DrawSectorScaled(StartAngleDeg, EndAngleDeg, 0f, DrawColor, Smoothness, Scale, Origin), ParamId.StartAngleDeg, ParamId.EndAngleDeg, ParamId.Smoothness, ParamId.Scale, ParamId.Origin), + new("Draw Sector Lines", () => circle.DrawSectorLines(StartAngleDeg, EndAngleDeg, 0f, LineInfo, Smoothness), ParamId.StartAngleDeg, ParamId.EndAngleDeg, ParamId.Thickness, ParamId.Smoothness), + new("Draw Sector Lines Closed", () => circle.DrawSectorLinesClosed(StartAngleDeg, EndAngleDeg, 0f, LineInfo, Smoothness), ParamId.StartAngleDeg, ParamId.EndAngleDeg, ParamId.Thickness, ParamId.Smoothness), + new("Draw Ring Lines", () => circle.DrawRingLines(RingThickness, 0f, LineInfo, Smoothness), ParamId.RingThickness, ParamId.Thickness, ParamId.Smoothness), + new("Draw Ring Lines Percentage", () => circle.DrawRingLinesPercentage(RingThickness, Percentage, 0f, LineInfo, Smoothness), ParamId.RingThickness, ParamId.Thickness, ParamId.Smoothness, ParamId.Percentage), + new("Draw Ring Sector Lines", () => circle.DrawRingSectorLines(RingThickness, StartAngleDeg, EndAngleDeg, 0f, LineInfo, Smoothness), ParamId.RingThickness, ParamId.StartAngleDeg, ParamId.EndAngleDeg, ParamId.Thickness, ParamId.Smoothness), + ]; + + cases[ShapeType.Triangle] = + [ + new("Draw", () => triangle.Draw(DrawColor)), + new("Draw Scaled", () => triangle.DrawScaled(DrawColor, Scale, Origin, DrawType), ParamId.Scale, ParamId.Origin, ParamId.DrawType), + new("Draw Lines", () => triangle.DrawLines(LineThickness, DrawColor, MiterLimit), ParamId.Thickness, ParamId.MiterLimit), + new("Draw Lines Percentage", () => triangle.DrawLinesPercentage(Percentage, StartIndex, LineThickness, DrawColor, MiterLimit), ParamId.Thickness, ParamId.Percentage, ParamId.StartIndex, ParamId.MiterLimit), + new("Draw Corners", () => triangle.DrawCorners(LineThickness, DrawColor, CornerLength, MiterLimit), ParamId.Thickness, ParamId.CornerLength, ParamId.MiterLimit), + new("Draw Corners Relative", () => triangle.DrawCornersRelative(LineThickness, DrawColor, CornerFactor, MiterLimit), ParamId.Thickness, ParamId.CornerFactor, ParamId.MiterLimit), + new("Draw Vertices", () => + { + triangle.DrawVertices(VertexRadius, DrawColor, Smoothness); + }, ParamId.VertexRadius, ParamId.Smoothness), + ]; + + cases[ShapeType.Rect] = + [ + new("Draw", () => rect.Draw(DrawColor)), + new("Draw Rounded", () => rect.DrawRounded(DrawColor, Roundness, Segments), ParamId.Roundness, ParamId.Segments), + new("Draw Scaled", () => rect.DrawScaled(DrawColor, Scale, Origin, DrawType), ParamId.Scale, ParamId.Origin, ParamId.DrawType), + new("Draw Lines", () => rect.DrawLines(LineInfo), ParamId.Thickness), + new("Draw Lines Rounded", () => rect.DrawLinesRounded(LineThickness, DrawColor, Roundness, Segments), ParamId.Thickness, ParamId.Roundness, ParamId.Segments), + new("Draw Lines Percentage", () => rect.DrawLinesPercentage(Percentage, StartIndex, LineInfo), ParamId.Thickness, ParamId.Percentage, ParamId.StartIndex), + new("Draw Corners", () => rect.DrawCorners(LineThickness, DrawColor, CornerLength), ParamId.Thickness, ParamId.CornerLength), + new("Draw Corners Relative", () => rect.DrawCornersRelative(LineThickness, DrawColor, CornerFactor), ParamId.Thickness, ParamId.CornerFactor), + new("Draw Chamfered Corners", () => rect.DrawChamferedCorners(DrawColor, CornerLength), ParamId.CornerLength), + new("Draw Chamfered Corners Relative", () => rect.DrawChamferedCornersRelative(DrawColor, CornerFactor), ParamId.CornerFactor), + new("Draw Chamfered Corners Lines", () => rect.DrawChamferedCornersLines(LineThickness, DrawColor, CornerLength), ParamId.Thickness, ParamId.CornerLength), + new("Draw Chamfered Corners Lines Relative", () => rect.DrawChamferedCornersLinesRelative(LineThickness, DrawColor, CornerFactor), ParamId.Thickness, ParamId.CornerFactor), + ]; + + cases[ShapeType.Quad] = + [ + new("Draw", () => quad.Draw(DrawColor)), + new("Draw Scaled", () => quad.DrawScaled(DrawColor, Scale, Origin, DrawType), ParamId.Scale, ParamId.Origin, ParamId.DrawType), + new("Draw Lines", () => quad.DrawLines(LineInfo), ParamId.Thickness), + new("Draw Lines Percentage", () => quad.DrawLinesPercentage(Percentage, StartIndex, LineInfo), ParamId.Thickness, ParamId.Percentage, ParamId.StartIndex), + new("Draw Vignette", () => quad.DrawVignette(CornerLength, RotationDeg, DrawColor, Smoothness), ParamId.CornerLength, ParamId.Smoothness), + new("Draw Corners", () => quad.DrawCorners(LineThickness, DrawColor, CornerLength), ParamId.Thickness, ParamId.CornerLength), + new("Draw Corners Relative", () => quad.DrawCornersRelative(LineThickness, DrawColor, CornerFactor), ParamId.Thickness, ParamId.CornerFactor), + new("Draw Chamfered Corners", () => quad.DrawChamferedCorners(DrawColor, CornerLength), ParamId.CornerLength), + new("Draw Chamfered Corners Relative", () => quad.DrawChamferedCornersRelative(DrawColor, CornerFactor), ParamId.CornerFactor), + new("Draw Chamfered Corners Lines", () => quad.DrawChamferedCornersLines(LineThickness, DrawColor, CornerLength), ParamId.Thickness, ParamId.CornerLength), + new("Draw Chamfered Corners Lines Relative", () => quad.DrawChamferedCornersLinesRelative(LineThickness, DrawColor, CornerFactor), ParamId.Thickness, ParamId.CornerFactor), + ]; + + cases[ShapeType.Polygon] = + [ + new("Draw", () => polygon.Draw(DrawColor)), + new("Draw Polygon Convex", () => polygon.DrawPolygonConvex(DrawColor)), + new("Draw Lines", () => polygon.DrawLines(LineThickness, DrawColor, MiterLimit), ParamId.Thickness, ParamId.MiterLimit), + new("Draw Lines Convex", () => polygon.DrawLinesConvex(LineThickness, DrawColor, MiterLimit), ParamId.Thickness, ParamId.MiterLimit), + new("Draw Lines Perimeter", () => polygon.DrawLinesPerimeter(polygon.GetPerimeter() * Percentage, StartIndex, LineThickness, DrawColor, MiterLimit), ParamId.Thickness, ParamId.Percentage, ParamId.StartIndex, ParamId.MiterLimit), + new("Draw Lines Percentage", () => polygon.DrawLinesPercentage(Percentage, StartIndex, LineThickness, DrawColor, MiterLimit), ParamId.Thickness, ParamId.Percentage, ParamId.StartIndex, ParamId.MiterLimit), + new("Draw Lines Perimeter Capped", () => polygon.DrawLinesPerimeterCapped(polygon.GetPerimeter() * Percentage, StartIndex, LineThickness, DrawColor, LineCapType.CappedExtended, 4), ParamId.Thickness, ParamId.Percentage, ParamId.StartIndex), + new("Draw Lines Percentage Capped", () => polygon.DrawLinesPercentageCapped(Percentage, StartIndex, LineInfo), ParamId.Thickness, ParamId.Percentage, ParamId.StartIndex), + new("Draw Cornered Absolute Transparent", () => polygon.DrawCorneredAbsoluteTransparent(CornerLength, LineInfo, MiterLimit), ParamId.Thickness, ParamId.CornerLength, ParamId.MiterLimit), + new("Draw Cornered Relative Transparent", () => polygon.DrawCorneredRelativeTransparent(CornerFactor, LineInfo, MiterLimit), ParamId.Thickness, ParamId.CornerFactor, ParamId.MiterLimit), + new("Draw Cornered", () => polygon.DrawCornered(CornerLength, LineInfo), ParamId.Thickness, ParamId.CornerLength), + ]; + + cases[ShapeType.Polyline] = + [ + new("Draw", () => polyline.Draw(LineInfo), ParamId.Thickness), + new("Draw Perimeter", () => polyline.DrawPerimeter(polyline.GetLength() * Percentage, LineInfo), ParamId.Thickness, ParamId.Percentage), + new("Draw Percentage", () => polyline.DrawPercentage(Percentage, LineInfo), ParamId.Thickness, ParamId.Percentage), + new("Draw Glow", () => polyline.DrawGlow(Width, EndWidth, DrawColor, DrawColor.SetAlpha(0), Steps, LineCapType.CappedExtended, 4), ParamId.Width, ParamId.EndWidth, ParamId.Steps), + ]; + } + + private void SyncMethodSlider() + { + var list = cases[GetCurrentShape()]; + methodSlider.MaxValue = Math.Max(0, list.Count - 1); + if (methodSlider.CurValue > methodSlider.MaxValue) methodSlider.SetCurValue(methodSlider.MaxValue); + } + + private int GetCurrentShapeIndex() => ShapeMath.Clamp((int)MathF.Round(shapeSlider.CurValue), 0, Enum.GetValues().Length - 1); + private int GetCurrentMethodIndex() + { + var shape = GetCurrentShape(); + return ShapeMath.Clamp((int)MathF.Round(methodSlider.CurValue), 0, cases[shape].Count - 1); + } + private ShapeType GetCurrentShape() => (ShapeType)GetCurrentShapeIndex(); + private DrawingCase GetCurrentCase() => cases[GetCurrentShape()][GetCurrentMethodIndex()]; + private bool UsesRotationSlider() => GetCurrentShape() is ShapeType.Segment or ShapeType.Quad or ShapeType.Polygon; + + private float LineThickness => paramSliders[ParamId.Thickness].CurValue; + private float Smoothness => paramSliders[ParamId.Smoothness].CurValue; + private float Percentage => paramSliders[ParamId.Percentage].CurValue; + private float Scale => paramSliders[ParamId.Scale].CurValue; + private float Origin => paramSliders[ParamId.Origin].CurValue; + private int StartIndex => (int)paramSliders[ParamId.StartIndex].CurValue; + private float CornerLength => paramSliders[ParamId.CornerLength].CurValue; + private float CornerFactor => paramSliders[ParamId.CornerFactor].CurValue; + private float RotationDeg => paramSliders[ParamId.RotationDeg].CurValue; + private float Roundness => paramSliders[ParamId.Roundness].CurValue; + private int Segments => (int)paramSliders[ParamId.Segments].CurValue; + private float RingThickness => paramSliders[ParamId.RingThickness].CurValue; + private float StartAngleDeg => paramSliders[ParamId.StartAngleDeg].CurValue; + private float EndAngleDeg => paramSliders[ParamId.EndAngleDeg].CurValue; + private int DrawType => (int)paramSliders[ParamId.DrawType].CurValue; + private float Width => paramSliders[ParamId.Width].CurValue; + private float EndWidth => paramSliders[ParamId.EndWidth].CurValue; + private int Steps => (int)paramSliders[ParamId.Steps].CurValue; + private float VertexRadius => paramSliders[ParamId.VertexRadius].CurValue; + private float MiterLimit => paramSliders[ParamId.MiterLimit].CurValue; + + private LineDrawingInfo LineInfo => new(LineThickness, DrawColor, LineCapType.CappedExtended, 4); +} diff --git a/Examples/Scenes/ExampleScenes/ShapeIntersectionExample.cs b/Examples/Scenes/ExampleScenes/ShapeIntersectionExample.cs index f67dc70b..460e48b0 100644 --- a/Examples/Scenes/ExampleScenes/ShapeIntersectionExample.cs +++ b/Examples/Scenes/ExampleScenes/ShapeIntersectionExample.cs @@ -1114,8 +1114,8 @@ private class PolygonShape : Shape public PolygonShape(Vector2 pos, float size) { - var shape = Polygon.Generate(pos, Rng.Instance.RandI(8, 16), size / 4, size); - Polygon = shape ?? []; + Polygon = new(); + Polygon.Generate(pos, Rng.Instance.RandI(8, 16), size / 4, size, Polygon); position = pos; } public override void Move(Vector2 newPosition) @@ -1275,8 +1275,9 @@ private class PolylineShape : Shape public PolylineShape(Vector2 pos, float size) { - var shape = Polygon.Generate(pos, Rng.Instance.RandI(8, 16), size / 2, size); - Polyline = shape == null ? [] : shape.ToPolyline(); + Polygon shapeBuffer = new(); + Polygon.Generate(pos, Rng.Instance.RandI(8, 16), size / 2, size, shapeBuffer); + Polyline = shapeBuffer.Count < 2 ? [] : shapeBuffer.ToPolyline(); position = pos; } public override void Move(Vector2 newPosition) diff --git a/Examples/Scenes/ExampleScenes/ShipInputExample.cs b/Examples/Scenes/ExampleScenes/ShipInputExample.cs index f7395f3b..995b824b 100644 --- a/Examples/Scenes/ExampleScenes/ShipInputExample.cs +++ b/Examples/Scenes/ExampleScenes/ShipInputExample.cs @@ -152,10 +152,13 @@ public void Draw() var hullColor = colorScheme.Hull; var cockpitColor = colorScheme.Cockpit; - CircleDrawing.DrawCircle(hull.Center - rightThruster * hull.Radius, hull.Radius / 6, outlineColor, 12); - CircleDrawing.DrawCircle(hull.Center - leftThruster * hull.Radius, hull.Radius / 6, outlineColor, 12); + var rightThrusterCircle = new Circle(hull.Center - rightThruster * hull.Radius, hull.Radius / 6); + var leftThrusterCircle = new Circle(hull.Center - leftThruster * hull.Radius, hull.Radius / 6); + var hullCircle = new Circle(hull.Center + movementDir * hull.Radius * 0.66f, hull.Radius * 0.33f); + rightThrusterCircle.Draw(outlineColor, 0.2f); + leftThrusterCircle.Draw(outlineColor, 0.2f); hull.Draw(hullColor); - CircleDrawing.DrawCircle(hull.Center + movementDir * hull.Radius * 0.66f, hull.Radius * 0.33f, cockpitColor, 12); + hullCircle.Draw(cockpitColor, 0.2f); hull.DrawLines(4f, outlineColor); } diff --git a/Examples/Scenes/ExampleScenes/StripedShapeDrawingExample.cs b/Examples/Scenes/ExampleScenes/StripedShapeDrawingExample.cs index d6ad8821..4ea2d9b0 100644 --- a/Examples/Scenes/ExampleScenes/StripedShapeDrawingExample.cs +++ b/Examples/Scenes/ExampleScenes/StripedShapeDrawingExample.cs @@ -32,14 +32,24 @@ public ValueSlider(string title, float startValue, float minValue, float maxValu this.title = title; } - protected override bool GetPressedState() + protected override bool GetButtonPressedState() { - return ShapeKeyboardButton.SPACE.GetInputState().Down; + return ShapeKeyboardButton.SPACE.GetInputState().Pressed; } - protected override bool GetMousePressedState() + protected override bool GetMouseButtonPressedState() { - return ShapeMouseButton.LEFT.GetInputState().Down; + return ShapeMouseButton.LEFT.GetInputState().Pressed; + } + + protected override bool GetButtonReleasedState() + { + return ShapeKeyboardButton.SPACE.GetInputState().Released; + } + + protected override bool GetMouseButtonReleasedState() + { + return ShapeMouseButton.LEFT.GetInputState().Released; } protected override bool GetDecreaseValuePressed() @@ -193,8 +203,8 @@ public StripedShapeDrawingExample() outsideTriangle = Triangle.Generate(center, size / 2, size); outsideRect = new Rect(center, new Size(size, size), new AnchorPoint(0.5f, 0.5f)); outsideQuad = new Quad(center, new Size(size, size), 45 * ShapeMath.DEGTORAD, new AnchorPoint(0.5f, 0.5f)); - var generatedPoly = Polygon.Generate(center, 16, size / 4, size); - outsidePoly = generatedPoly ?? []; + outsidePoly = new(); + Polygon.Generate(center, 16, size / 4, size, outsidePoly); size = 100; radius = size / 2; @@ -202,8 +212,8 @@ public StripedShapeDrawingExample() insideTriangle = Triangle.Generate(center, size / 2, size); insideRect = new Rect(center, new Size(size, size), new AnchorPoint(0.5f, 0.5f)); insideQuad = new Quad(center, new Size(size, size), 45 * ShapeMath.DEGTORAD, new AnchorPoint(0.5f, 0.5f)); - generatedPoly = Polygon.Generate(center, 16, size / 4, size); - insidePoly = generatedPoly ?? []; + insidePoly = new(); + Polygon.Generate(center, 16, size / 4, size, insidePoly); curInsidePolygonSize = size; insideShapeRotDegSlider = new("Inside Rotation", 0, 0f, 360, true) @@ -241,8 +251,8 @@ private void ActualizeSliderValues() curRotationDeg = rotationDegSlider.CurValue; float t = lineThicknessSlider.CurValue; - lineInfoStriped = lineInfoStriped.ChangeThickness(t); - lineInfoOutline = lineInfoOutline.ChangeThickness(t * 1.25f); + lineInfoStriped = lineInfoStriped.SetThickness(t); + lineInfoOutline = lineInfoOutline.SetThickness(t * 1.25f); curInsideShapeRotDeg = insideShapeRotDegSlider.CurValue; curInsideShapeSize = insideShapeSizeSlider.CurValue; @@ -284,8 +294,7 @@ private void RegenerateOutsideShape() } else if (outsideShapeIndex == 4) { - var shape = Polygon.Generate(center, 16, size / 4, size); - if (shape != null) outsidePoly = shape; + Polygon.Generate(center, 16, size / 4, size, outsidePoly); } } private void RegenerateInsideShape() @@ -299,8 +308,7 @@ private void RegenerateInsideShape() } else if (insideShapeIndex == 4) { - var shape = Polygon.Generate(center, 16, size / 2, size); - if(shape != null) insidePoly = shape; + Polygon.Generate(center, 16, size / 2, size, insidePoly); } } protected override void OnUpdateExample(GameTime time, ScreenInfo game, ScreenInfo gameUi, ScreenInfo ui) @@ -426,8 +434,8 @@ protected override void OnHandleInputExample(float dt, Vector2 mousePosGame, Vec } protected override void OnDrawGameExample(ScreenInfo game) { - lineInfoStriped = lineInfoStriped.ChangeColor(stripedColor.ColorRgba); - lineInfoOutline = lineInfoOutline.ChangeColor(outlineColor.ColorRgba); + lineInfoStriped = lineInfoStriped.SetColor(stripedColor.ColorRgba); + lineInfoOutline = lineInfoOutline.SetColor(outlineColor.ColorRgba); var lineInfoOutlineMasked = new LineDrawingInfo(lineInfoOutline.Thickness / 3, outlineColor.ColorRgba.ChangeBrightness(-0.5f), LineCapType.CappedExtended, 4); const bool reverseMask = false; const bool doubleMask = false; //if true draws the outsideShape outline different inside and outside the insideShape diff --git a/Examples/Scenes/ExampleScenes/XmlDataExample.cs b/Examples/Scenes/ExampleScenes/XmlDataExample.cs index 7568583d..128ce0ad 100644 --- a/Examples/Scenes/ExampleScenes/XmlDataExample.cs +++ b/Examples/Scenes/ExampleScenes/XmlDataExample.cs @@ -49,7 +49,9 @@ public void Draw() var startC = Colors.Warm; var endC = Colors.Medium.SetAlpha(150); var c = startC.Lerp(endC, f); - CircleDrawing.DrawCircle(pos, ShapeMath.LerpFloat(size * 0.5f, size, f), c, 24); + var r = ShapeMath.LerpFloat(size * 0.5f, size, f); + var circle = new Circle(pos, r); + circle.Draw(c); } } private class Planet @@ -368,8 +370,8 @@ protected override void OnUpdateExample(GameTime time, ScreenInfo game, ScreenIn } protected override void OnDrawGameExample(ScreenInfo game) { - - CircleDrawing.DrawCircleLines(planet.Shape.Center, AsteroidSpawnRadius, 12f, Colors.Highlight, 4f); + var circle = new Circle(planet.Shape.Center, AsteroidSpawnRadius); + circle.DrawLines(12f, Colors.Highlight); planet.Draw(); planet.DrawGameUI(textFont); diff --git a/Examples/Scenes/MainScene.cs b/Examples/Scenes/MainScene.cs index f67f0322..195e3e77 100644 --- a/Examples/Scenes/MainScene.cs +++ b/Examples/Scenes/MainScene.cs @@ -44,6 +44,7 @@ public MainScene() { examples.Add(new OutlineDrawingExample()); examples.Add(new StripedShapeDrawingExample()); + examples.Add(new ShapeDrawingTestExample()); examples.Add(new ShapeIntersectionExample()); examples.Add(new CurveDataExample()); examples.Add(new PhysicsExample()); @@ -99,7 +100,7 @@ public MainScene() textureSurface = new TextureSurface(2048, 2048); textureSurface.SetTextureFilter(TextureFilter.Trilinear); - textureSurface.BeginDraw(ColorRgba.Clear); + textureSurface.BeginDraw(ColorRgba.Transparent); LineDrawingInfo stripedInfo = new(2f, ColorRgba.White, LineCapType.Capped, 6); textureSurface.Rect.DrawStriped(16f, 30f, stripedInfo); textureSurface.EndDraw(); diff --git a/Examples/UIElements/ExampleSelectionButton.cs b/Examples/UIElements/ExampleSelectionButton.cs index 04c9bf1f..b137e963 100644 --- a/Examples/UIElements/ExampleSelectionButton.cs +++ b/Examples/UIElements/ExampleSelectionButton.cs @@ -2,7 +2,10 @@ using Examples.Scenes; using ShapeEngine.Core.Structs; using ShapeEngine.Geometry; +using ShapeEngine.Geometry.RectDef; +using ShapeEngine.Geometry.SegmentDef; using ShapeEngine.Geometry.StripedDrawingDef; +using ShapeEngine.StaticLib; using ShapeEngine.UI; using ShapeEngine.Text; @@ -19,6 +22,9 @@ public class ExampleSelectionButton : ControlNode private float pressDelayTimer = 0f; private const float PressDelay = 0.1f; + + private float mouseInsideAnimationTimer = 0f; + private const float mouseInsideAnimationDuration = 3f; public ExampleSelectionButton() { @@ -39,20 +45,44 @@ public void SetScene(ExampleScene? newScene) Visible = Active; } - protected override bool GetMousePressedState() + protected override bool GetMouseButtonPressedState() + { + // if (!MouseInside) return false; + if (Scene == null) return false; + //Always consumed for some reason... + // var acceptState = GameloopExamples.Instance.InputActionUIAcceptMouse.Consume(out _); + // return acceptState is { Consumed: false, Pressed: true }; + return GameloopExamples.Instance.InputActionUIAcceptMouse.State.Pressed; + } + + protected override bool GetButtonPressedState() + { + if (!Selected) return false; + if (Scene == null) return false; + //Always consumed for some reason... + // var acceptState = GameloopExamples.Instance.InputActionUIAccept.Consume(out _); + // return acceptState is { Consumed: false, Pressed: true }; + return GameloopExamples.Instance.InputActionUIAccept.State.Pressed; + } + + protected override bool GetMouseButtonReleasedState() { - if (!MouseInside) return false; + // if (!MouseInside) return false; if (Scene == null) return false; - var acceptState = GameloopExamples.Instance.InputActionUIAcceptMouse.Consume(out _); - return acceptState is { Consumed: false, Released: true }; + //Always consumed for some reason... + // var acceptState = GameloopExamples.Instance.InputActionUIAcceptMouse.Consume(out _); + // return acceptState is { Consumed: false, Released: true }; + return GameloopExamples.Instance.InputActionUIAcceptMouse.State.Released; } - protected override bool GetPressedState() + protected override bool GetButtonReleasedState() { if (!Selected) return false; if (Scene == null) return false; - var acceptState = GameloopExamples.Instance.InputActionUIAccept.Consume(out _); - return acceptState is { Consumed: false, Released: true }; + //Always consumed for some reason... + // var acceptState = GameloopExamples.Instance.InputActionUIAccept.Consume(out _); + // return acceptState is { Consumed: false, Released: true }; + return GameloopExamples.Instance.InputActionUIAccept.State.Released; } public override Direction GetNavigationDirection() @@ -100,7 +130,7 @@ protected override void SelectedWasChanged(bool value) protected override void PressedWasChanged(bool value) { if (Scene == null) return; - if (value) + if (!value) { // Console.WriteLine($"Button Pressed - Scene {Scene.Title}"); // GAMELOOP.GoToScene(Scene); @@ -110,6 +140,7 @@ protected override void PressedWasChanged(bool value) } else { + mouseInsideAnimationTimer = 0f; GameloopExamples.Instance.GoToScene(Scene); } @@ -129,10 +160,16 @@ protected override void OnUpdate(float dt, Vector2 mousePos, bool mousePosValid) pressDelayTimer -= dt; if (pressDelayTimer <= 0f) { - if (Scene != null) GameloopExamples.Instance.GoToScene(Scene); pressDelayTimer = 0f; + mouseInsideAnimationTimer = 0f; + if (Scene != null) GameloopExamples.Instance.GoToScene(Scene); } } + + if (MouseInside) + { + mouseInsideAnimationTimer += dt; + } } protected override void OnDraw() @@ -141,17 +178,21 @@ protected override void OnDraw() var r = Rect; var text = Scene.Title; - + if (MouseInside) { var amount = Rect.Size.Min() * 0.25f; var outside = Rect.ChangeSize(amount, new AnchorPoint(0.5f, 0.5f)); // outside.DrawLines(2f, Colors.Medium); - var lineThickness = outside.Size.Min() * 0.02f; - var spacing = lineThickness * 12f; - var info = new LineDrawingInfo(lineThickness, Colors.Dark, LineCapType.Capped, 6); - outside.DrawStriped(spacing, 35f, info); + var animationFactor = mouseInsideAnimationTimer / mouseInsideAnimationDuration; + var lineThickness = outside.Size.Min() * 0.04f; + // var spacing = lineThickness * ShapeMath.LerpFloat(4f, 5f, ShapeTween.PingPong(animationFactor)); + // var info = new LineDrawingInfo(lineThickness, Colors.Dark, LineCapType.Capped, 6); + // outside.DrawStriped(spacing, 35f, info); + var cornerLength = outside.Size.Min() * ShapeMath.LerpFloat(0.15f, 0.35f, ShapeTween.PingPong(animationFactor)); + outside.DrawCorners(lineThickness, Colors.Highlight, cornerLength); + // outside.LeftSegment.Draw(lineThickness, Colors.Highlight); } if (Selected) diff --git a/ShapeEngine/Color/ColorRgba.cs b/ShapeEngine/Color/ColorRgba.cs index 8dd55f82..6eab633a 100644 --- a/ShapeEngine/Color/ColorRgba.cs +++ b/ShapeEngine/Color/ColorRgba.cs @@ -36,7 +36,715 @@ namespace ShapeEngine.Color; /// public readonly byte A; #endregion + + #region Predefined Colors + + /// + /// Gets a system-defined color that has an ARGB value of #00FFFFFF. + /// + public static ColorRgba Transparent => new(System.Drawing.Color.Transparent); + + /// + /// Gets a system-defined color that has an ARGB value of #FFF0F8FF. + /// + public static ColorRgba AliceBlue => new(System.Drawing.Color.AliceBlue); + + /// + /// Gets a system-defined color that has an ARGB value of #FFFAEBD7. + /// + public static ColorRgba AntiqueWhite => new(System.Drawing.Color.AntiqueWhite); + + /// + /// Gets a system-defined color that has an ARGB value of #FF00FFFF. + /// + public static ColorRgba Aqua => new(System.Drawing.Color.Aqua); + + /// + /// Gets a system-defined color that has an ARGB value of #FF7FFFD4. + /// + public static ColorRgba Aquamarine => new(System.Drawing.Color.Aquamarine); + + /// + /// Gets a system-defined color that has an ARGB value of #FFF0FFFF. + /// + public static ColorRgba Azure => new(System.Drawing.Color.Azure); + + /// + /// Gets a system-defined color that has an ARGB value of #FFF5F5DC. + /// + public static ColorRgba Beige => new(System.Drawing.Color.Beige); + + /// + /// Gets a system-defined color that has an ARGB value of #FFFFE4C4. + /// + public static ColorRgba Bisque => new(System.Drawing.Color.Bisque); + + /// + /// Gets a system-defined color that has an ARGB value of #FF000000. + /// + public static ColorRgba Black => new(System.Drawing.Color.Black); + + /// + /// Gets a system-defined color that has an ARGB value of #FFFFEBCD. + /// + public static ColorRgba BlanchedAlmond => new(System.Drawing.Color.BlanchedAlmond); + + /// + /// Gets a system-defined color that has an ARGB value of #FF0000FF. + /// + public static ColorRgba Blue => new(System.Drawing.Color.Blue); + + /// + /// Gets a system-defined color that has an ARGB value of #FF8A2BE2. + /// + public static ColorRgba BlueViolet => new(System.Drawing.Color.BlueViolet); + + /// + /// Gets a system-defined color that has an ARGB value of #FFA52A2A. + /// + public static ColorRgba Brown => new(System.Drawing.Color.Brown); + + /// + /// Gets a system-defined color that has an ARGB value of #FFDEB887. + /// + public static ColorRgba BurlyWood => new(System.Drawing.Color.BurlyWood); + + /// + /// Gets a system-defined color that has an ARGB value of #FF5F9EA0. + /// + public static ColorRgba CadetBlue => new(System.Drawing.Color.CadetBlue); + + /// + /// Gets a system-defined color that has an ARGB value of #FF7FFF00. + /// + public static ColorRgba Chartreuse => new(System.Drawing.Color.Chartreuse); + + /// + /// Gets a system-defined color that has an ARGB value of #FFD2691E. + /// + public static ColorRgba Chocolate => new(System.Drawing.Color.Chocolate); + + /// + /// Gets a system-defined color that has an ARGB value of #FFFF7F50. + /// + public static ColorRgba Coral => new(System.Drawing.Color.Coral); + + /// + /// Gets a system-defined color that has an ARGB value of #FF6495ED. + /// + public static ColorRgba CornflowerBlue => new(System.Drawing.Color.CornflowerBlue); + + /// + /// Gets a system-defined color that has an ARGB value of #FFFFF8DC. + /// + public static ColorRgba Cornsilk => new(System.Drawing.Color.Cornsilk); + /// + /// Gets a system-defined color that has an ARGB value of #FFDC143C. + /// + public static ColorRgba Crimson => new(System.Drawing.Color.Crimson); + + /// + /// Gets a system-defined color that has an ARGB value of #FF00FFFF. + /// + public static ColorRgba Cyan => new(System.Drawing.Color.Cyan); + + /// + /// Gets a system-defined color that has an ARGB value of #FF00008B. + /// + public static ColorRgba DarkBlue => new(System.Drawing.Color.DarkBlue); + + /// + /// Gets a system-defined color that has an ARGB value of #FF008B8B. + /// + public static ColorRgba DarkCyan => new(System.Drawing.Color.DarkCyan); + + /// + /// Gets a system-defined color that has an ARGB value of #FFB8860B. + /// + public static ColorRgba DarkGoldenrod => new(System.Drawing.Color.DarkGoldenrod); + + /// + /// Gets a system-defined color that has an ARGB value of #FFA9A9A9. + /// + public static ColorRgba DarkGray => new(System.Drawing.Color.DarkGray); + + /// + /// Gets a system-defined color that has an ARGB value of #FF006400. + /// + public static ColorRgba DarkGreen => new(System.Drawing.Color.DarkGreen); + + /// + /// Gets a system-defined color that has an ARGB value of #FFBDB76B. + /// + public static ColorRgba DarkKhaki => new(System.Drawing.Color.DarkKhaki); + + /// + /// Gets a system-defined color that has an ARGB value of #FF8B008B. + /// + public static ColorRgba DarkMagenta => new(System.Drawing.Color.DarkMagenta); + + /// + /// Gets a system-defined color that has an ARGB value of #FF556B2F. + /// + public static ColorRgba DarkOliveGreen => new(System.Drawing.Color.DarkOliveGreen); + + /// + /// Gets a system-defined color that has an ARGB value of #FFFF8C00. + /// + public static ColorRgba DarkOrange => new(System.Drawing.Color.DarkOrange); + + /// + /// Gets a system-defined color that has an ARGB value of #FF9932CC. + /// + public static ColorRgba DarkOrchid => new(System.Drawing.Color.DarkOrchid); + + /// + /// Gets a system-defined color that has an ARGB value of #FF8B0000. + /// + public static ColorRgba DarkRed => new(System.Drawing.Color.DarkRed); + + /// + /// Gets a system-defined color that has an ARGB value of #FFE9967A. + /// + public static ColorRgba DarkSalmon => new(System.Drawing.Color.DarkSalmon); + + /// + /// Gets a system-defined color that has an ARGB value of #FF8FBC8F. + /// + public static ColorRgba DarkSeaGreen => new(System.Drawing.Color.DarkSeaGreen); + + /// + /// Gets a system-defined color that has an ARGB value of #FF483D8B. + /// + public static ColorRgba DarkSlateBlue => new(System.Drawing.Color.DarkSlateBlue); + + /// + /// Gets a system-defined color that has an ARGB value of #FF2F4F4F. + /// + public static ColorRgba DarkSlateGray => new(System.Drawing.Color.DarkSlateGray); + + /// + /// Gets a system-defined color that has an ARGB value of #FF00CED1. + /// + public static ColorRgba DarkTurquoise => new(System.Drawing.Color.DarkTurquoise); + + /// + /// Gets a system-defined color that has an ARGB value of #FF9400D3. + /// + public static ColorRgba DarkViolet => new(System.Drawing.Color.DarkViolet); + + /// + /// Gets a system-defined color that has an ARGB value of #FFFF1493. + /// + public static ColorRgba DeepPink => new(System.Drawing.Color.DeepPink); + + /// + /// Gets a system-defined color that has an ARGB value of #FF00BFFF. + /// + public static ColorRgba DeepSkyBlue => new(System.Drawing.Color.DeepSkyBlue); + + /// + /// Gets a system-defined color that has an ARGB value of #FF696969. + /// + public static ColorRgba DimGray => new(System.Drawing.Color.DimGray); + + /// + /// Gets a system-defined color that has an ARGB value of #FF1E90FF. + /// + public static ColorRgba DodgerBlue => new(System.Drawing.Color.DodgerBlue); + + /// + /// Gets a system-defined color that has an ARGB value of #FFB22222. + /// + public static ColorRgba Firebrick => new(System.Drawing.Color.Firebrick); + + /// + /// Gets a system-defined color that has an ARGB value of #FFFFFAF0. + /// + public static ColorRgba FloralWhite => new(System.Drawing.Color.FloralWhite); + + /// + /// Gets a system-defined color that has an ARGB value of #FF228B22. + /// + public static ColorRgba ForestGreen => new(System.Drawing.Color.ForestGreen); + + /// + /// Gets a system-defined color that has an ARGB value of #FFFF00FF. + /// + public static ColorRgba Fuchsia => new(System.Drawing.Color.Fuchsia); + + /// + /// Gets a system-defined color that has an ARGB value of #FFDCDCDC. + /// + public static ColorRgba Gainsboro => new(System.Drawing.Color.Gainsboro); + + /// + /// Gets a system-defined color that has an ARGB value of #FFF8F8FF. + /// + public static ColorRgba GhostWhite => new(System.Drawing.Color.GhostWhite); + + /// + /// Gets a system-defined color that has an ARGB value of #FFFFD700. + /// + public static ColorRgba Gold => new(System.Drawing.Color.Gold); + + /// + /// Gets a system-defined color that has an ARGB value of #FFDAA520. + /// + public static ColorRgba Goldenrod => new(System.Drawing.Color.Goldenrod); + + /// + /// Gets a system-defined color that has an ARGB value of #FF808080. + /// + public static ColorRgba Gray => new(System.Drawing.Color.Gray); + + /// + /// Gets a system-defined color that has an ARGB value of #FF008000. + /// + public static ColorRgba Green => new(System.Drawing.Color.Green); + + /// + /// Gets a system-defined color that has an ARGB value of #FFADFF2F. + /// + public static ColorRgba GreenYellow => new(System.Drawing.Color.GreenYellow); + + /// + /// Gets a system-defined color that has an ARGB value of #FFF0FFF0. + /// + public static ColorRgba Honeydew => new(System.Drawing.Color.Honeydew); + + /// + /// Gets a system-defined color that has an ARGB value of #FFFF69B4. + /// + public static ColorRgba HotPink => new(System.Drawing.Color.HotPink); + + /// + /// Gets a system-defined color that has an ARGB value of #FFCD5C5C. + /// + public static ColorRgba IndianRed => new(System.Drawing.Color.IndianRed); + + /// + /// Gets a system-defined color that has an ARGB value of #FF4B0082. + /// + public static ColorRgba Indigo => new(System.Drawing.Color.Indigo); + + /// + /// Gets a system-defined color that has an ARGB value of #FFFFFFF0. + /// + public static ColorRgba Ivory => new(System.Drawing.Color.Ivory); + + /// + /// Gets a system-defined color that has an ARGB value of #FFF0E68C. + /// + public static ColorRgba Khaki => new(System.Drawing.Color.Khaki); + + /// + /// Gets a system-defined color that has an ARGB value of #FFE6E6FA. + /// + public static ColorRgba Lavender => new(System.Drawing.Color.Lavender); + + /// + /// Gets a system-defined color that has an ARGB value of #FFFFF0F5. + /// + public static ColorRgba LavenderBlush => new(System.Drawing.Color.LavenderBlush); + + /// + /// Gets a system-defined color that has an ARGB value of #FF7CFC00. + /// + public static ColorRgba LawnGreen => new(System.Drawing.Color.LawnGreen); + + /// + /// Gets a system-defined color that has an ARGB value of #FFFFFACD. + /// + public static ColorRgba LemonChiffon => new(System.Drawing.Color.LemonChiffon); + + /// + /// Gets a system-defined color that has an ARGB value of #FFADD8E6. + /// + public static ColorRgba LightBlue => new(System.Drawing.Color.LightBlue); + + /// + /// Gets a system-defined color that has an ARGB value of #FFF08080. + /// + public static ColorRgba LightCoral => new(System.Drawing.Color.LightCoral); + + /// + /// Gets a system-defined color that has an ARGB value of #FFE0FFFF. + /// + public static ColorRgba LightCyan => new(System.Drawing.Color.LightCyan); + + /// + /// Gets a system-defined color that has an ARGB value of #FFFAFAD2. + /// + public static ColorRgba LightGoldenrodYellow => new(System.Drawing.Color.LightGoldenrodYellow); + + /// + /// Gets a system-defined color that has an ARGB value of #FFD3D3D3. + /// + public static ColorRgba LightGray => new(System.Drawing.Color.LightGray); + + /// + /// Gets a system-defined color that has an ARGB value of #FF90EE90. + /// + public static ColorRgba LightGreen => new(System.Drawing.Color.LightGreen); + + /// + /// Gets a system-defined color that has an ARGB value of #FFFFB6C1. + /// + public static ColorRgba LightPink => new(System.Drawing.Color.LightPink); + + /// + /// Gets a system-defined color that has an ARGB value of #FFFFA07A. + /// + public static ColorRgba LightSalmon => new(System.Drawing.Color.LightSalmon); + + /// + /// Gets a system-defined color that has an ARGB value of #FF20B2AA. + /// + public static ColorRgba LightSeaGreen => new(System.Drawing.Color.LightSeaGreen); + + /// + /// Gets a system-defined color that has an ARGB value of #FF87CEFA. + /// + public static ColorRgba LightSkyBlue => new(System.Drawing.Color.LightSkyBlue); + + /// + /// Gets a system-defined color that has an ARGB value of #FF778899. + /// + public static ColorRgba LightSlateGray => new(System.Drawing.Color.LightSlateGray); + + /// + /// Gets a system-defined color that has an ARGB value of #FFB0C4DE. + /// + public static ColorRgba LightSteelBlue => new(System.Drawing.Color.LightSteelBlue); + + /// + /// Gets a system-defined color that has an ARGB value of #FFFFFFE0. + /// + public static ColorRgba LightYellow => new(System.Drawing.Color.LightYellow); + + /// + /// Gets a system-defined color that has an ARGB value of #FF00FF00. + /// + public static ColorRgba Lime => new(System.Drawing.Color.Lime); + + /// + /// Gets a system-defined color that has an ARGB value of #FF32CD32. + /// + public static ColorRgba LimeGreen => new(System.Drawing.Color.LimeGreen); + + /// + /// Gets a system-defined color that has an ARGB value of #FFFAF0E6. + /// + public static ColorRgba Linen => new(System.Drawing.Color.Linen); + + /// + /// Gets a system-defined color that has an ARGB value of #FFFF00FF. + /// + public static ColorRgba Magenta => new(System.Drawing.Color.Magenta); + + /// + /// Gets a system-defined color that has an ARGB value of #FF800000. + /// + public static ColorRgba Maroon => new(System.Drawing.Color.Maroon); + + /// + /// Gets a system-defined color that has an ARGB value of #FF66CDAA. + /// + public static ColorRgba MediumAquamarine => new(System.Drawing.Color.MediumAquamarine); + + /// + /// Gets a system-defined color that has an ARGB value of #FF0000CD. + /// + public static ColorRgba MediumBlue => new(System.Drawing.Color.MediumBlue); + + /// + /// Gets a system-defined color that has an ARGB value of #FFBA55D3. + /// + public static ColorRgba MediumOrchid => new(System.Drawing.Color.MediumOrchid); + + /// + /// Gets a system-defined color that has an ARGB value of #FF9370DB. + /// + public static ColorRgba MediumPurple => new(System.Drawing.Color.MediumPurple); + + /// + /// Gets a system-defined color that has an ARGB value of #FF3CB371. + /// + public static ColorRgba MediumSeaGreen => new(System.Drawing.Color.MediumSeaGreen); + + /// + /// Gets a system-defined color that has an ARGB value of #FF7B68EE. + /// + public static ColorRgba MediumSlateBlue => new(System.Drawing.Color.MediumSlateBlue); + + /// + /// Gets a system-defined color that has an ARGB value of #FF00FA9A. + /// + public static ColorRgba MediumSpringGreen => new(System.Drawing.Color.MediumSpringGreen); + + /// + /// Gets a system-defined color that has an ARGB value of #FF48D1CC. + /// + public static ColorRgba MediumTurquoise => new(System.Drawing.Color.MediumTurquoise); + + /// + /// Gets a system-defined color that has an ARGB value of #FFC71585. + /// + public static ColorRgba MediumVioletRed => new(System.Drawing.Color.MediumVioletRed); + + /// + /// Gets a system-defined color that has an ARGB value of #FF191970. + /// + public static ColorRgba MidnightBlue => new(System.Drawing.Color.MidnightBlue); + + /// + /// Gets a system-defined color that has an ARGB value of #FFF5FFFA. + /// + public static ColorRgba MintCream => new(System.Drawing.Color.MintCream); + + /// + /// Gets a system-defined color that has an ARGB value of #FFFFE4E1. + /// + public static ColorRgba MistyRose => new(System.Drawing.Color.MistyRose); + + /// + /// Gets a system-defined color that has an ARGB value of #FFFFE4B5. + /// + public static ColorRgba Moccasin => new(System.Drawing.Color.Moccasin); + + /// + /// Gets a system-defined color that has an ARGB value of #FFFFDEAD. + /// + public static ColorRgba NavajoWhite => new(System.Drawing.Color.NavajoWhite); + + /// + /// Gets a system-defined color that has an ARGB value of #FF000080. + /// + public static ColorRgba Navy => new(System.Drawing.Color.Navy); + + /// + /// Gets a system-defined color that has an ARGB value of #FFFDF5E6. + /// + public static ColorRgba OldLace => new(System.Drawing.Color.OldLace); + + /// + /// Gets a system-defined color that has an ARGB value of #FF808000. + /// + public static ColorRgba Olive => new(System.Drawing.Color.Olive); + + /// + /// Gets a system-defined color that has an ARGB value of #FF6B8E23. + /// + public static ColorRgba OliveDrab => new(System.Drawing.Color.OliveDrab); + + /// + /// Gets a system-defined color that has an ARGB value of #FFFFA500. + /// + public static ColorRgba Orange => new(System.Drawing.Color.Orange); + + /// + /// Gets a system-defined color that has an ARGB value of #FFFF4500. + /// + public static ColorRgba OrangeRed => new(System.Drawing.Color.OrangeRed); + + /// + /// Gets a system-defined color that has an ARGB value of #FFDA70D6. + /// + public static ColorRgba Orchid => new(System.Drawing.Color.Orchid); + + /// + /// Gets a system-defined color that has an ARGB value of #FFEEE8AA. + /// + public static ColorRgba PaleGoldenrod => new(System.Drawing.Color.PaleGoldenrod); + + /// + /// Gets a system-defined color that has an ARGB value of #FF98FB98. + /// + public static ColorRgba PaleGreen => new(System.Drawing.Color.PaleGreen); + + /// + /// Gets a system-defined color that has an ARGB value of #FFAFEEEE. + /// + public static ColorRgba PaleTurquoise => new(System.Drawing.Color.PaleTurquoise); + + /// + /// Gets a system-defined color that has an ARGB value of #FFDB7093. + /// + public static ColorRgba PaleVioletRed => new(System.Drawing.Color.PaleVioletRed); + + /// + /// Gets a system-defined color that has an ARGB value of #FFFFEFD5. + /// + public static ColorRgba PapayaWhip => new(System.Drawing.Color.PapayaWhip); + + /// + /// Gets a system-defined color that has an ARGB value of #FFFFDAB9. + /// + public static ColorRgba PeachPuff => new(System.Drawing.Color.PeachPuff); + + /// + /// Gets a system-defined color that has an ARGB value of #FFCD853F. + /// + public static ColorRgba Peru => new(System.Drawing.Color.Peru); + + /// + /// Gets a system-defined color that has an ARGB value of #FFFFC0CB. + /// + public static ColorRgba Pink => new(System.Drawing.Color.Pink); + + /// + /// Gets a system-defined color that has an ARGB value of #FFDDA0DD. + /// + public static ColorRgba Plum => new(System.Drawing.Color.Plum); + + /// + /// Gets a system-defined color that has an ARGB value of #FFB0E0E6. + /// + public static ColorRgba PowderBlue => new(System.Drawing.Color.PowderBlue); + + /// + /// Gets a system-defined color that has an ARGB value of #FF800080. + /// + public static ColorRgba Purple => new(System.Drawing.Color.Purple); + + /// + /// Gets a system-defined color that has an ARGB value of #FFFF0000. + /// + public static ColorRgba Red => new(System.Drawing.Color.Red); + + /// + /// Gets a system-defined color that has an ARGB value of #FFBC8F8F. + /// + public static ColorRgba RosyBrown => new(System.Drawing.Color.RosyBrown); + + /// + /// Gets a system-defined color that has an ARGB value of #FF4169E1. + /// + public static ColorRgba RoyalBlue => new(System.Drawing.Color.RoyalBlue); + + /// + /// Gets a system-defined color that has an ARGB value of #FF8B4513. + /// + public static ColorRgba SaddleBrown => new(System.Drawing.Color.SaddleBrown); + + /// + /// Gets a system-defined color that has an ARGB value of #FFFA8072. + /// + public static ColorRgba Salmon => new(System.Drawing.Color.Salmon); + + /// + /// Gets a system-defined color that has an ARGB value of #FFF4A460. + /// + public static ColorRgba SandyBrown => new(System.Drawing.Color.SandyBrown); + + /// + /// Gets a system-defined color that has an ARGB value of #FF2E8B57. + /// + public static ColorRgba SeaGreen => new(System.Drawing.Color.SeaGreen); + + /// + /// Gets a system-defined color that has an ARGB value of #FFFFF5EE. + /// + public static ColorRgba SeaShell => new(System.Drawing.Color.SeaShell); + + /// + /// Gets a system-defined color that has an ARGB value of #FFA0522D. + /// + public static ColorRgba Sienna => new(System.Drawing.Color.Sienna); + + /// + /// Gets a system-defined color that has an ARGB value of #FFC0C0C0. + /// + public static ColorRgba Silver => new(System.Drawing.Color.Silver); + + /// + /// Gets a system-defined color that has an ARGB value of #FF87CEEB. + /// + public static ColorRgba SkyBlue => new(System.Drawing.Color.SkyBlue); + + /// + /// Gets a system-defined color that has an ARGB value of #FF6A5ACD. + /// + public static ColorRgba SlateBlue => new(System.Drawing.Color.SlateBlue); + + /// + /// Gets a system-defined color that has an ARGB value of #FF708090. + /// + public static ColorRgba SlateGray => new(System.Drawing.Color.SlateGray); + + /// + /// Gets a system-defined color that has an ARGB value of #FFFFFAFA. + /// + public static ColorRgba Snow => new(System.Drawing.Color.Snow); + + /// + /// Gets a system-defined color that has an ARGB value of #FF00FF7F. + /// + public static ColorRgba SpringGreen => new(System.Drawing.Color.SpringGreen); + + /// + /// Gets a system-defined color that has an ARGB value of #FF4682B4. + /// + public static ColorRgba SteelBlue => new(System.Drawing.Color.SteelBlue); + + /// + /// Gets a system-defined color that has an ARGB value of #FFD2B48C. + /// + public static ColorRgba Tan => new(System.Drawing.Color.Tan); + + /// + /// Gets a system-defined color that has an ARGB value of #FF008080. + /// + public static ColorRgba Teal => new(System.Drawing.Color.Teal); + + /// + /// Gets a system-defined color that has an ARGB value of #FFD8BFD8. + /// + public static ColorRgba Thistle => new(System.Drawing.Color.Thistle); + + /// + /// Gets a system-defined color that has an ARGB value of #FFFF6347. + /// + public static ColorRgba Tomato => new(System.Drawing.Color.Tomato); + + /// + /// Gets a system-defined color that has an ARGB value of #FF40E0D0. + /// + public static ColorRgba Turquoise => new(System.Drawing.Color.Turquoise); + + /// + /// Gets a system-defined color that has an ARGB value of #FFEE82EE. + /// + public static ColorRgba Violet => new(System.Drawing.Color.Violet); + + /// + /// Gets a system-defined color that has an ARGB value of #FFF5DEB3. + /// + public static ColorRgba Wheat => new(System.Drawing.Color.Wheat); + + /// + /// Gets a system-defined color that has an ARGB value of #FFFFFFFF. + /// + public static ColorRgba White => new(System.Drawing.Color.White); + + /// + /// Gets a system-defined color that has an ARGB value of #FFF5F5F5. + /// + public static ColorRgba WhiteSmoke => new(System.Drawing.Color.WhiteSmoke); + + /// + /// Gets a system-defined color that has an ARGB value of #FFFFFF00. + /// + public static ColorRgba Yellow => new(System.Drawing.Color.Yellow); + + /// + /// Gets a system-defined color that has an ARGB value of #FF9ACD32. + /// + public static ColorRgba YellowGreen => new(System.Drawing.Color.YellowGreen); + #endregion + #region Constructors /// @@ -476,7 +1184,24 @@ public ColorRgba ExpDecayLerpComplex(ColorRgba to, float decay, float dt) public ColorRgba ChangeBlue(int amount) => new(R, G, Clamp(B + amount), A); #endregion + #region Implicit Conversion + + public static implicit operator System.Drawing.Color(ColorRgba c) + => System.Drawing.Color.FromArgb(c.A, c.R, c.G, c.B); + + public static implicit operator ColorRgba(System.Drawing.Color c) + => new ColorRgba(c.R, c.G, c.B, c.A); + + public static implicit operator Raylib_cs.Color(ColorRgba c) + => new Raylib_cs.Color(c.R, c.G, c.B, c.A); + + public static implicit operator ColorRgba(Raylib_cs.Color c) + => new ColorRgba(c.R, c.G, c.B, c.A); + + #endregion + #region Conversion + /// /// Converts this ColorRgba to a System.Drawing.Color. /// @@ -488,7 +1213,28 @@ public ColorRgba ExpDecayLerpComplex(ColorRgba to, float decay, float dt) /// /// A Raylib_cs.Color equivalent to this ColorRgba. public Raylib_cs.Color ToRayColor() => new (R, G, B, A); - + + /// + /// Creates a from a . + /// + /// The known color to convert. + /// A new instance representing the known color. + public static ColorRgba FromKnownColor(KnownColor c) => new(System.Drawing.Color.FromKnownColor(c)); + + /// + /// Creates a from a . + /// + /// The to convert. + /// A new instance representing the system color. + public static ColorRgba FromSystemColor(System.Drawing.Color c) => new(c); + + /// + /// Creates a from a . + /// + /// The to convert. + /// A new instance representing the Raylib color. + public static ColorRgba FromRayColor(Raylib_cs.Color c) => new(c); + /// /// Normalizes the color components from the range [0, 255] to [0.0, 1.0]. /// @@ -718,26 +1464,101 @@ public static ColorRgba[] ParseColors(params string[] hexColors) /// A hash code for the current ColorRgba, suitable for use in hashing algorithms and data structures like a hash table. public override int GetHashCode() => ToSysColor().GetHashCode(); #endregion - + + #region Clamp /// - /// Represents a predefined white color (R:255, G:255, B:255, A:255). + /// Clamps an integer value to the valid color component range of 0-255. /// - public static ColorRgba White => new(255, 255, 255, 255); - + /// The integer value to clamp. + /// A byte value clamped to the range [0, 255]. + public static byte Clamp(byte value) => ShapeMath.Clamp(value, byte.MinValue, byte.MaxValue); + /// - /// Represents a predefined black color (R:0, G:0, B:0, A:255). + /// Clamps an integer value to the valid color component range of 0-255 and returns it as a byte. /// - public static ColorRgba Black => new(0, 0, 0, 255); + /// The integer value to clamp. + /// A byte value clamped to the range [0, 255]. + public static byte Clamp(int value) => (byte)ShapeMath.Clamp(value, byte.MinValue, byte.MaxValue); /// - /// Represents a predefined fully transparent color (R:0, G:0, B:0, A:0). + /// Clamps a byte value to the specified minimum and maximum range. /// - public static ColorRgba Clear => new(0, 0, 0, 0); - + /// The byte value to clamp. + /// The minimum allowed value. + /// The maximum allowed value. + /// The clamped byte value within the specified range. + public static byte Clamp(byte value, byte min, byte max) => ShapeMath.Clamp(value, min, max); + /// - /// Clamps an integer value to the valid color component range of 0-255. + /// Clamps each RGBA component of this color to the valid range [0, 255]. /// - /// The integer value to clamp. - /// A byte value clamped to the range [0, 255]. - private static byte Clamp(int value) => (byte)ShapeMath.Clamp(value, 0, 255); + /// A new with all components clamped to [0, 255]. + public ColorRgba Clamp() + { + return new + ( + Clamp(R), + Clamp(G), + Clamp(B), + Clamp(A) + ); + } + /// + /// Clamps each RGBA component of this color to the specified minimum and maximum values. + /// + /// The minimum allowed value for each component. + /// The maximum allowed value for each component. + /// A new with all components clamped to the specified range. + public ColorRgba Clamp(byte min, byte max) + { + return new + ( + Clamp(R, min, max), + Clamp(G, min, max), + Clamp(B, min, max), + Clamp(A, min, max) + ); + } + /// + /// Clamps each RGBA component of this color to the range [0, maxX] for each respective component. + /// + /// The maximum allowed value for the red component. + /// The maximum allowed value for the green component. + /// The maximum allowed value for the blue component. + /// The maximum allowed value for the alpha component. + /// A new with each component clamped to its specified maximum value. + public ColorRgba Clamp(byte maxR, byte maxG, byte maxB, byte maxA) + { + return new + ( + Clamp(R, byte.MinValue, maxR), + Clamp(G, byte.MinValue, maxG), + Clamp(B, byte.MinValue, maxB), + Clamp(A, byte.MinValue, maxA) + ); + } + /// + /// Clamps each RGBA component of this color to the specified minimum and maximum values for each channel. + /// + /// The minimum allowed value for the red component. + /// The maximum allowed value for the red component. + /// The minimum allowed value for the green component. + /// The maximum allowed value for the green component. + /// The minimum allowed value for the blue component. + /// The maximum allowed value for the blue component. + /// The minimum allowed value for the alpha component. + /// The maximum allowed value for the alpha component. + /// A new with each component clamped to its specified range. + public ColorRgba Clamp(byte minR, byte maxR, byte minG, byte maxG, byte minB, byte maxB, byte minA, byte maxA) + { + return new + ( + Clamp(R, minR, maxR), + Clamp(G, minG, maxG), + Clamp(B, minB, maxB), + Clamp(A, minA, maxA) + ); + } + + #endregion } \ No newline at end of file diff --git a/ShapeEngine/Content/ContentPack.cs b/ShapeEngine/Content/ContentPack.cs index 47448eff..89a60679 100644 --- a/ShapeEngine/Content/ContentPack.cs +++ b/ShapeEngine/Content/ContentPack.cs @@ -137,7 +137,21 @@ public ContentPack(string sourceFilePath) public byte[]? GetFileData(string filePath) { if(!IsLoaded) return null; - if(!HasFile(filePath)) return null; + + if (!HasFile(filePath)) + { + string alternativePath = filePath.Contains('\\') ? filePath.Replace('\\', '/') : filePath.Replace('/', '\\'); + if (HasFile(alternativePath)) + { + Game.Instance.Logger.LogWarning($"File path '{filePath}' not found, but alternative path '{alternativePath}' exists. Returning data for alternative path."); + filePath = alternativePath; + } + else + { + Game.Instance.Logger.LogError($"Pack does not contain file: {filePath}."); + return null; + } + } if(CurrentUnpackMode == UnpackMode.Memory) return cache[filePath]; diff --git a/ShapeEngine/Core/CurveColor.cs b/ShapeEngine/Core/CurveColor.cs index fdc16289..3f717ac7 100644 --- a/ShapeEngine/Core/CurveColor.cs +++ b/ShapeEngine/Core/CurveColor.cs @@ -22,8 +22,8 @@ protected override ColorRgba Interpolate(ColorRgba a, ColorRgba b, float time) } /// - /// Gets the default value for the curve, which is . + /// Gets the default value for the curve, which is . /// /// The default value. - protected override ColorRgba GetDefaultValue() => ColorRgba.Clear; + protected override ColorRgba GetDefaultValue() => ColorRgba.Transparent; } \ No newline at end of file diff --git a/ShapeEngine/Core/PerformanceMeasureWatch.cs b/ShapeEngine/Core/PerformanceMeasureWatch.cs new file mode 100644 index 00000000..126d8363 --- /dev/null +++ b/ShapeEngine/Core/PerformanceMeasureWatch.cs @@ -0,0 +1,347 @@ +using System.Diagnostics; + +namespace ShapeEngine.Core; + +/// +/// Provides functionality for measuring and recording performance timings for code sections, supporting tagging and data retrieval. +/// +public class PerformanceMeasureWatch +{ + #region Structs + + /// + /// Represents a set of performance measurement statistics for a specific title and tag. + /// + /// The name of the measured code section. + /// A category or tag for grouping/filtering measurements. + /// The total measured time across all samples. + /// The most recent measured elapsed time. + /// The number of measurements taken. + /// The minimum measured time. + /// The maximum measured time. + /// The average measured time. + /// + /// Provides print and comparison utilities for different time units and statistics. + /// + public record MeasurementData(string Title, string Tag, TimeSpan Total, TimeSpan Elapsed, long Count, TimeSpan Min, TimeSpan Max, TimeSpan Average) + { + /// + /// Returns a string representation of the measurement data using the default TimeSpan format. + /// + public override string ToString() + { + return $"{Title} [{Tag}] -> Elapsed: {Elapsed}, Min: {Min}, Max: {Max}, Average: {Average} = {Total} / {Count}"; + } + /// + /// Returns a string representation of the measurement data in seconds with fixed precision. + /// + public string PrintSeconds() + { + return $"{Title} [{Tag}] -> Elapsed: {Elapsed.TotalSeconds:F6}s, Min: {Min.TotalSeconds:F6}s, Max: {Max.TotalSeconds:F6}s, Average: {Average.TotalSeconds:F6}s = {Total.TotalSeconds:F6}s / {Count}"; + } + /// + /// Returns a string representation of the measurement data in milliseconds with fixed precision. + /// + public string PrintMilliseconds() + { + return $"{Title} [{Tag}] -> Elapsed: {Elapsed.TotalMilliseconds:F3}ms, Min: {Min.TotalMilliseconds:F3}ms, Max: {Max.TotalMilliseconds:F3}ms, Average: {Average.TotalMilliseconds:F3}ms = {Total.TotalMilliseconds:F3}ms / {Count}"; + } + /// + /// Returns a string representation of the measurement data in nanoseconds, calculated using Stopwatch.Frequency. + /// + /// + /// Nanosecond values are calculated from ticks and Stopwatch.Frequency for accuracy. + /// + public string PrintNanoseconds() + { + // Convert ticks to nanoseconds using Stopwatch.Frequency + double ticksPerSecond = Stopwatch.Frequency; + double elapsedNs = Elapsed.Ticks * 1_000_000_000.0 / ticksPerSecond; + double minNs = Min.Ticks * 1_000_000_000.0 / ticksPerSecond; + double maxNs = Max.Ticks * 1_000_000_000.0 / ticksPerSecond; + double avgNs = Average.Ticks * 1_000_000_000.0 / ticksPerSecond; + double totalNs = Total.Ticks * 1_000_000_000.0 / ticksPerSecond; + return $"{Title} [{Tag}] -> Elapsed: {elapsedNs:F0}ns, Min: {minNs:F0}ns, Max: {maxNs:F0}ns, Average: {avgNs:F0}ns = {totalNs:F0}ns / {Count}"; + } + /// + /// Calculates the percentage difference in elapsed time between two measurement data entries. + /// + /// The baseline measurement data. + /// The measurement data to compare against the baseline. + /// The percentage difference in elapsed time. + public static double CompareElapsedPercent(MeasurementData a, MeasurementData b) + { + if (a.Elapsed == TimeSpan.Zero) return 0; + return ((b.Elapsed.TotalSeconds - a.Elapsed.TotalSeconds) / a.Elapsed.TotalSeconds) * 100.0; + } + /// + /// Calculates the percentage difference in total time between two measurement data entries. + /// + /// The baseline measurement data. + /// The measurement data to compare against the baseline. + /// The percentage difference in total time. + public static double CompareTotalPercent(MeasurementData a, MeasurementData b) + { + if (a.Total == TimeSpan.Zero) return 0; + return ((b.Total.TotalSeconds - a.Total.TotalSeconds) / a.Total.TotalSeconds) * 100.0; + } + /// + /// Calculates the percentage difference in minimum measured time between two measurement data entries. + /// + /// The baseline measurement data. + /// The measurement data to compare against the baseline. + /// The percentage difference in minimum measured time. + public static double CompareMinPercent(MeasurementData a, MeasurementData b) + { + if (a.Min == TimeSpan.Zero) return 0; + return ((b.Min.TotalSeconds - a.Min.TotalSeconds) / a.Min.TotalSeconds) * 100.0; + } + /// + /// Calculates the percentage difference in maximum measured time between two measurement data entries. + /// + /// The baseline measurement data. + /// The measurement data to compare against the baseline. + /// The percentage difference in maximum measured time. + public static double CompareMaxPercent(MeasurementData a, MeasurementData b) + { + if (a.Max == TimeSpan.Zero) return 0; + return ((b.Max.TotalSeconds - a.Max.TotalSeconds) / a.Max.TotalSeconds) * 100.0; + } + /// + /// Calculates the percentage difference in average measured time between two measurement data entries. + /// + /// The baseline measurement data. + /// The measurement data to compare against the baseline. + /// The percentage difference in average measured time. + public static double CompareAveragePercent(MeasurementData a, MeasurementData b) + { + if (a.Average == TimeSpan.Zero) return 0; + return ((b.Average.TotalSeconds - a.Average.TotalSeconds) / a.Average.TotalSeconds) * 100.0; + } + } + + /// + /// Internal struct for accumulating measurement statistics for a specific title and tag. + /// + /// + /// Not intended for public use. + /// + private struct Measurement + { + public TimeSpan Start; + public TimeSpan Total; + public long Count; + public TimeSpan Elapsed; + public TimeSpan Min; + public TimeSpan Max; + public string Tag; + + public Measurement(TimeSpan start, string tag) + { + Start = start; + Total = TimeSpan.Zero; + Count = 0; + Elapsed = TimeSpan.Zero; + Min = TimeSpan.MaxValue; + Max = TimeSpan.MinValue; + Tag = tag; + } + + private Measurement(TimeSpan start, TimeSpan total, TimeSpan elapsed, long count, TimeSpan min, TimeSpan max, string tag) + { + Start = start; + Total = total; + Elapsed = elapsed; + Count = count; + Min = min; + Max = max; + Tag = tag; + } + public Measurement SetStart(TimeSpan start) + { + return new Measurement(start, Total, Elapsed, Count, Min, Max, Tag); + } + public Measurement TakeMeasurement(TimeSpan end) + { + var elapsed = end - Start; + var newMin = Min < elapsed ? Min : elapsed; + var newMax = Max > elapsed ? Max : elapsed; + return new Measurement(TimeSpan.Zero, Total + elapsed, elapsed, Count + 1, newMin, newMax, Tag); + } + public TimeSpan Average => Count > 0 ? TimeSpan.FromTicks(Total.Ticks / Count) : TimeSpan.Zero; + } + + /// + /// Provides a disposable scope for automatic measurement of code execution time. + /// + /// + /// Use with a using statement to automatically begin and end a measurement. + /// + public struct MeasurementScope : IDisposable + { + /// + /// The parent PerformanceMeasureWatch instance. + /// + private readonly PerformanceMeasureWatch watch; + /// + /// The name of the measured code section. + /// + private readonly string title; + /// + /// A category or tag for grouping/filtering measurements. + /// + private readonly string tag; + + /// + /// Initializes a new measurement scope for the specified title and tag. + /// + /// The parent PerformanceMeasureWatch instance. + /// The name of the measured code section. + /// A category or tag for grouping/filtering measurements. + public MeasurementScope(PerformanceMeasureWatch watch, string title, string tag) + { + this.watch = watch; + this.title = title; + this.tag = tag; + watch.BeginMeasurement(title, tag); + } + + /// + /// Ends the measurement for the associated title and tag. + /// + public void Dispose() + { + watch.EndMeasurement(title, tag); + } + } + #endregion + + #region Fields + private readonly object _lock = new(); + private readonly Dictionary<(string Title, string Tag), Measurement> measurements = new(); + private readonly Stopwatch watch; + #endregion + + #region Constructor + /// + /// Initializes a new instance of the PerformanceMeasureWatch class. + /// + public PerformanceMeasureWatch() + { + watch = new Stopwatch(); + } + #endregion + + #region Functions + /// + /// Creates a disposable measurement scope for the specified title and tag. + /// + /// The name of the measured code section. + /// A category or tag for grouping/filtering measurements. + /// A disposable scope that automatically begins and ends the measurement. + /// + /// Use with a using statement for automatic measurement. + /// + public MeasurementScope Measure(string title, string tag = "") + { + return new MeasurementScope(this, title, tag); + } + + /// + /// Starts the performance watch and clears all previous measurements. + /// + public void Start() + { + lock (_lock) + { + watch.Restart(); + measurements.Clear(); + } + } + + /// + /// Stops and resets the performance watch. + /// + public void Stop() + { + lock (_lock) + { + watch.Stop(); + watch.Reset(); + } + } + + /// + /// Begins a new measurement for the specified title and tag. + /// + /// The name of the measured code section. + /// A category or tag for grouping/filtering measurements. + public void BeginMeasurement(string title, string tag = "") + { + lock (_lock) + { + var key = (title, tag); + if (measurements.ContainsKey(key)) + { + measurements[key] = measurements[key].SetStart(watch.Elapsed); + } + else + { + measurements.Add(key, new Measurement(watch.Elapsed, tag)); + } + } + } + + /// + /// Ends the measurement for the specified title and tag, recording the elapsed time. + /// + /// The name of the measured code section. + /// A category or tag for grouping/filtering measurements. + public void EndMeasurement(string title, string tag = "") + { + lock (_lock) + { + var key = (title, tag); + if (!measurements.TryGetValue(key, out var measurement)) return; //it was never started + if (measurement.Start == TimeSpan.Zero) return; //it was never started + measurements[key] = measurement.TakeMeasurement(watch.Elapsed); + } + } + /// + /// Clears all recorded measurement data. + /// + public void ClearData() + { + lock (_lock) + { + measurements.Clear(); + } + } + /// + /// Retrieves all recorded measurement data, optionally filtered by a predicate. + /// + /// An optional filter predicate to select specific measurement data. + /// An enumerable of MeasurementData records. + public IEnumerable GetData(Func? filter = null) + { + lock (_lock) + { + var data = measurements.Select(kvp => + new MeasurementData( + kvp.Key.Title, + kvp.Key.Tag, + kvp.Value.Total, + kvp.Value.Elapsed, + kvp.Value.Count, + kvp.Value.Min, + kvp.Value.Max, + kvp.Value.Average + ) + ); + return filter != null ? data.Where(filter).ToList() : data.ToList(); + } + } + + #endregion +} + diff --git a/ShapeEngine/Core/Structs/ScreenInfo.cs b/ShapeEngine/Core/Structs/ScreenInfo.cs index 1d75ee14..dbe7c566 100644 --- a/ShapeEngine/Core/Structs/ScreenInfo.cs +++ b/ShapeEngine/Core/Structs/ScreenInfo.cs @@ -56,8 +56,31 @@ public ScreenInfo SetFixedFramerateInterpolationFactor(double factor) /// 0,0 is the top-left corner of the area /// 1, 1 is the bottom right corner of the area /// - public Vector2 RelativeMousePosition => MousePos / Area.Size.ToVector2(); - + public Vector2 RelativeMousePosition + { + get + { + var size = Area.Size.ToVector2(); + var pos = MousePos + size / 2; + return pos / size; + } + } + /// + /// Returns the mouse position normalized and centered around the area's midpoint. + /// Values are in range approximately -1..1 where (0,0) is the center of . + /// + /// + /// This divides the mouse coordinates by half the area's size. If the area's width or + /// height is zero this will produce Infinity/NaN values; ensure has a valid size. + /// + public Vector2 RelativeMousePositionCentered + { + get + { + var size = Area.Size.ToVector2() / 2; + return MousePos / size; + } + } /// /// Initializes a new instance of the struct. /// diff --git a/ShapeEngine/Core/Structs/Size.cs b/ShapeEngine/Core/Structs/Size.cs index aaa9f25d..6c179f41 100644 --- a/ShapeEngine/Core/Structs/Size.cs +++ b/ShapeEngine/Core/Structs/Size.cs @@ -81,6 +81,13 @@ public Size(float w, float h) #endregion #region Public Functions + + /// + /// Returns a new where each component is the reciprocal of the original. + /// + /// A with Width = 1 / Width and Height = 1 / Height. + public Size Inverse() => new(Width == 0f ? 0f : 1f / Width, Height == 0f ? 0f : 1f / Height); + /// /// Returns the greater of the width or height. /// @@ -295,6 +302,21 @@ public Size Round(int decimals) ); } + /// + /// Returns a new with both width and height negated. + /// + public Size Negate() => new(-Width, -Height); + + /// + /// Returns a new with the width negated and the height unchanged. + /// + public Size NegateWidth() => new(-Width, Height); + + /// + /// Returns a new with the height negated and the width unchanged. + /// + public Size NegateHeight() => new(Width, -Height); + /// /// Returns a new with the width set to the specified value. /// @@ -346,6 +368,14 @@ public override string ToString() #region Operators + /// + /// Negates both the width and height of the specified . + /// + public static Size operator -(Size size) + { + return new Size(-size.Width, -size.Height); + } + /// /// Determines whether two instances are equal. /// diff --git a/ShapeEngine/Core/Structs/Transform2D.cs b/ShapeEngine/Core/Structs/Transform2D.cs index a0f42b28..19a40935 100644 --- a/ShapeEngine/Core/Structs/Transform2D.cs +++ b/ShapeEngine/Core/Structs/Transform2D.cs @@ -1,4 +1,6 @@ using System.Numerics; +using ShapeEngine.Geometry.CircleDef; +using ShapeEngine.Geometry.RectDef; using ShapeEngine.StaticLib; namespace ShapeEngine.Core.Structs; @@ -66,6 +68,22 @@ namespace ShapeEngine.Core.Structs; /// public bool IsEmpty() => Position == Vector2.Zero && RotationRad == 0f && BaseSize == Size.Zero; + /// + /// Returns a representing this transform, + /// using the current position and the radius of the scaled size. + /// + public Circle GetCircle() => new(Position, ScaledSize.Radius); + + /// + /// Returns a representing this transform, + /// using the current position, scaled size, and the specified alignment anchor. + /// + /// The anchor point for rectangle alignment. + /// A with the specified alignment. + public Rect GetRect(AnchorPoint alignment) + { + return new Rect(Position, ScaledSize, alignment); + } #endregion #region Constructors diff --git a/ShapeEngine/Core/Structs/ValueRange.cs b/ShapeEngine/Core/Structs/ValueRange.cs index e7ba9b9c..bfd6397e 100644 --- a/ShapeEngine/Core/Structs/ValueRange.cs +++ b/ShapeEngine/Core/Structs/ValueRange.cs @@ -149,6 +149,7 @@ public ValueRange UpdateRange(float newValue) public float GetFactor(float value) { if(value < Min) return 0.0f; + if(value > Max) return 1.0f; return (value - Min) / (Max - Min); } /// @@ -173,11 +174,22 @@ public float GetFactor(float value) /// The interpolated value. public float Lerp(float f) { return ShapeMath.LerpFloat(Min, Max, f); } /// + /// Linearly interpolates between Min and Max by the inverse of the given factor (1 - f). + /// + /// The interpolation factor (0-1). 0 returns Max and 1 returns Min. + /// The interpolated value. + public float LerpInverse(float f) { return ShapeMath.LerpFloat(Min, Max, 1f - f); } + /// /// Calculates the normalized position of a value within the range. /// /// The value to evaluate. /// The normalized position (0-1). - public float Inverse(float value) { return (value - Min) / (Max - Min); } + public float Inverse(float value) + { + if(value < Min) return 0.0f; + if(value > Max) return 1.0f; + return (value - Min) / (Max - Min); + } /// /// Remaps a value from this range to another . /// diff --git a/ShapeEngine/Core/TriangleBatch.cs b/ShapeEngine/Core/TriangleBatch.cs new file mode 100644 index 00000000..772fcfa6 --- /dev/null +++ b/ShapeEngine/Core/TriangleBatch.cs @@ -0,0 +1,722 @@ +using System.Numerics; +using Raylib_cs; +using ShapeEngine.Color; +using ShapeEngine.Core.GameDef; +using ShapeEngine.Geometry.TriangleDef; + +namespace ShapeEngine.Core; + +/// +/// A class for efficient triangle drawing using Raylib's RLGL, supporting both persistent batches and immediate-mode drawing. +/// +/// This class serves two main purposes: +/// +/// +/// +/// Pooled Instances: obtain an instance via to manage a persistent list of vertices. +/// This is ideal for static geometry that doesn't change often. The batch can be drawn repeatedly with . +/// Remember to return it to the pool with when finished. +/// +/// +/// Immediate Mode (Static): use static methods like , , +/// and for dynamic, per-frame triangle drawing without managing a specific object instance. +/// +/// +/// +/// Both approaches utilize , meaning every 3 consecutive vertices form a single triangle. +/// +/// +public class TriangleBatch +{ + #region Pooling Logic + /// + /// A stack used to pool instances for reuse. + /// + private static readonly Stack Pool = new(); + + /// + /// Retrieves a cleared, ready-to-use TriangleBatch from the pool or creates a new one. + /// + /// The color to be used for drawing the triangles in this batch. + /// The initial capacity of the vertex list (number of triangles * 3). + /// A TriangleBatch instance ready for use. + public static TriangleBatch Get(ColorRgba color, int initialCapacity = 64) + { + TriangleBatch batch; + + if (Pool.Count > 0) + { + batch = Pool.Pop(); + batch.color = color; + + if (batch.vertices.Capacity < initialCapacity * 3) + { + batch.vertices.Capacity = initialCapacity * 3; + } + } + else + { + batch = new TriangleBatch(color, initialCapacity); + } + + batch.InUse = true; + return batch; + } + + /// + /// Returns the batch to the pool for reuse. + /// DO NOT use the batch reference after calling this. + /// + /// The batch to return to the pool. + public static void Return(TriangleBatch batch) + { + if (!batch.InUse) + { + Game.Instance.Logger.LogWarning("Returning TriangleBatch to the pool is not possible! The TriangleBatch is already in the pool."); + return; + } + + batch.InUse = false; + batch.Clear(); // Clear data before storing + Pool.Push(batch); + } + + #endregion + + #region Instance Logic + + #region Members + /// + /// The list of vertices currently in the batch. + /// + private readonly List vertices; + /// + /// The color to be used when drawing the batch. + /// + private ColorRgba color; + + /// + /// Gets a value indicating whether this batch is currently in use. + /// + public bool InUse { get; private set; } + #endregion + + #region Constructor + /// + /// Initializes a new instance of the class. + /// + /// The initial color of the batch. + /// The initial capacity of the vertex list (number of triangles). + private TriangleBatch(ColorRgba color, int initialCapacity) + { + this.color = color; + this.vertices = new List(initialCapacity * 3); + this.InUse = false; + } + #endregion + + #region Methods + /// + /// Sets the color for the entire batch. This is mostly useful for changing color between frames or over time + /// without needing to create a new batch, but it can also be used to change the color of the batch before drawing if needed. + /// + /// The new color to apply. + /// + /// Can only be called if the batch is in use. + /// + public void SetColor(ColorRgba newColor) + { + if (!InUse) + { + Game.Instance.Logger.LogWarning("TriangleBatch color cannot be changed! The TriangleBatch is not in use. Call TriangleBatch.Get() to retrieve a batch from the pool before setting the color."); + return; + } + this.color = newColor; + } + + /// + /// Adds a triangle to the batch. + /// + /// The triangle to add. + /// + /// Can only be called if the batch is in use. + /// + public void AddTriangle(Triangle triangle) + { + if (!InUse) + { + Game.Instance.Logger.LogWarning("TriangleBatch cannot add triangle! The TriangleBatch is not in use. Call TriangleBatch.Get() to retrieve a batch from the pool before adding triangles."); + return; + } + vertices.Add(triangle.A); + vertices.Add(triangle.B); + vertices.Add(triangle.C); + } + + /// + /// Adds a triangle defined by three vertices to the batch. + /// + /// The first vertex of the triangle. + /// The second vertex of the triangle. + /// The third vertex of the triangle. + /// + /// Can only be called if the batch is in use. + /// + public void AddTriangle(Vector2 a, Vector2 b, Vector2 c) + { + if (!InUse) + { + Game.Instance.Logger.LogWarning("TriangleBatch cannot add triangle! The TriangleBatch is not in use. Call TriangleBatch.Get() to retrieve a batch from the pool before adding triangles."); + return; + } + vertices.Add(a); + vertices.Add(b); + vertices.Add(c); + } + + /// + /// Draws all triangles currently in the batch using the batch's color. + /// + /// + /// Uses (Raylib OpenGL wrapper) to draw vertices in mode. + /// Can only be called if the batch is in use. + /// + public void Draw() + { + if (!InUse) + { + Game.Instance.Logger.LogWarning("TriangleBatch cannot be drawn! The TriangleBatch is not in use. Call TriangleBatch.Get() to retrieve a batch from the pool before drawing."); + return; + } + + if (vertices.Count == 0) return; + + Rlgl.Begin(DrawMode.Triangles); + Rlgl.Color4f(color.R / 255f, color.G / 255f, color.B / 255f, color.A / 255f); + + var count = vertices.Count; + for (int i = 0; i < count; i++) + { + var v = vertices[i]; + Rlgl.Vertex2f(v.X, v.Y); + } + + Rlgl.End(); + } + + /// + /// Clears all vertices from the batch. + /// + public void Clear() + { + vertices.Clear(); + } + + /// + /// Clears all vertices from the batch and sets a new color. + /// + /// The new color to use for the batch. + public void Clear(ColorRgba newColor) + { + this.color = newColor; + vertices.Clear(); + } + #endregion + + #endregion + + #region Immediate Mode Logic + private static bool batchActive; + private static ColorRgba currentColor; + + #region Start/End Batch Logic + /// + /// Starts a new triangle batch drawing operation. + /// + /// The color to be applied to subsequent vertices. + /// + /// Calls with . + /// Must be paired with . + /// + public static void StartImmediateBatch(ColorRgba color) + { + if (batchActive) + { + Game.Instance.Logger.LogWarning("Cannot start triangle batch! A batch is already active. Call EndBatch() before starting a new batch."); + return; + } + + batchActive = true; + + currentColor = color; + + Rlgl.Begin(DrawMode.Triangles); + Rlgl.Color4f(color.R / 255f, color.G / 255f, color.B / 255f, color.A / 255f); + } + + /// + /// Ends the current triangle batch drawing operation. + /// + /// + /// Calls . + /// + public static void EndImmediateBatch() + { + if (!batchActive) + { + Game.Instance.Logger.LogWarning("Cannot end triangle batch! No batch is currently active. Call StartBatch() before calling EndBatch()."); + return; + } + + batchActive = false; + + Rlgl.End(); + } + #endregion + + #region Set Color + /// + /// Sets the current drawing color in RLGL. + /// This affects all subsequent vertices until the color is changed again or the batch ends. + /// This method can be used to change colors between vertices within the same batch. + /// + /// The color to set. + /// + /// Batch must be active before using this function. Use to start a batch before setting the color. + /// + public static void SetImmediateColor(ColorRgba color) + { + if (!batchActive) + { + Game.Instance.Logger.LogWarning("Cannot set triangle batch color! No batch is currently active. Call StartBatch() before setting the color."); + return; + } + + currentColor = color; + + Rlgl.Color4f(color.R / 255f, color.G / 255f, color.B / 255f, color.A / 255f); + } + #endregion + + #region Add Vertices + + /// + /// Adds a single vertex to the current batch using the current color. + /// TriangleBatcher uses DrawMode.Triangles, so every 3 consecutive vertices added will form a triangle when is called! + /// + /// The x-coordinate of the vertex. + /// The y-coordinate of the vertex. + /// + /// Batch must be active before using this function. Use to start a batch. + /// + public static void AddImmediateVertex(float x, float y) + { + if (!batchActive) + { + Game.Instance.Logger.LogWarning("Cannot add vertex to batch! No batch is currently active. Call StartBatch() before adding vertices."); + return; + } + + Rlgl.Vertex2f(x, y); + } + + /// + /// Sets the color and adds a single vertex to the current batch. + /// TriangleBatcher uses DrawMode.Triangles, so every 3 consecutive vertices added will form a triangle when is called! + /// + /// The x-coordinate of the vertex. + /// The y-coordinate of the vertex. + /// The color to apply for this vertex and subsequent vertices. + /// + /// Batch must be active before using this function. Use to start a batch. + /// Resets the color back to the previous color set by or after adding the vertex, so this will not affect subsequent vertices. + /// + public static void AddImmediateVertex(float x, float y, ColorRgba color) + { + if (!batchActive) + { + Game.Instance.Logger.LogWarning("Cannot add vertex to batch! No batch is currently active. Call StartBatch() before adding vertices."); + return; + } + + Rlgl.Color4f(color.R / 255f, color.G / 255f, color.B / 255f, color.A / 255f); + + Rlgl.Vertex2f(x, y); + + Rlgl.Color4f(currentColor.R / 255f, currentColor.G / 255f, currentColor.B / 255f, currentColor.A / 255f); + } + + /// + /// Adds a single vertex to the current batch using the current color. + /// TriangleBatcher uses DrawMode.Triangles, so every 3 consecutive vertices added will form a triangle when is called! + /// + /// The position of the vertex. + /// + /// Batch must be active before using this function. Use to start a batch. + /// + public static void AddImmediateVertex(Vector2 vertex) + { + if (!batchActive) + { + Game.Instance.Logger.LogWarning("Cannot add vertex to batch! No batch is currently active. Call StartBatch() before adding vertices."); + return; + } + + Rlgl.Vertex2f(vertex.X, vertex.Y); + } + + /// + /// Sets the color and adds a single vertex to the current batch. + /// The color used will affect all subsequent vertices until changed again or the batch ends. + /// TriangleBatcher uses DrawMode.Triangles, so every 3 consecutive vertices added will form a triangle when is called! + /// + /// The position of the vertex. + /// The color to apply for this vertex and subsequent vertices. + /// + /// Batch must be active before using this function. Use to start a batch. + /// Resets the color back to the previous color set by or after adding the vertex, so this will not affect subsequent vertices. + /// + public static void AddImmediateVertex(Vector2 vertex, ColorRgba color) + { + if (!batchActive) + { + Game.Instance.Logger.LogWarning("Cannot add vertex to batch! No batch is currently active. Call StartBatch() before adding vertices."); + return; + } + + Rlgl.Color4f(color.R / 255f, color.G / 255f, color.B / 255f, color.A / 255f); + + Rlgl.Vertex2f(vertex.X, vertex.Y); + + Rlgl.Color4f(currentColor.R / 255f, currentColor.G / 255f, currentColor.B / 255f, currentColor.A / 255f); + } + + /// + /// Adds a collection of vertices to the current batch using the current color. + /// TriangleBatcher uses DrawMode.Triangles, so every 3 consecutive vertices added will form a triangle when is called! + /// + /// The collection of vertices to add. + /// + /// Batch must be active before using this function. Use to start a batch. + /// + public static void AddImmediateVertices(IEnumerable vertices) + { + if (!batchActive) + { + Game.Instance.Logger.LogWarning("Cannot add vertices to batch! No batch is currently active. Call StartBatch() before adding vertices."); + return; + } + + foreach (var vertex in vertices) + { + Rlgl.Vertex2f(vertex.X, vertex.Y); + } + } + + /// + /// Sets the color and adds a collection of vertices to the current batch. + /// The color used will affect all subsequent vertices until changed again or the batch ends. + /// TriangleBatcher uses DrawMode.Triangles, so every 3 consecutive vertices added will form a triangle when is called! + /// + /// The collection of vertices to add. + /// The color to apply for these vertices and subsequent vertices. + /// + /// Batch must be active before using this function. Use to start a batch. + /// Resets the color back to the previous color set by or after adding the vertices, so this will not affect subsequent vertices. + /// + public static void AddImmediateVertices(IEnumerable vertices, ColorRgba color) + { + if (!batchActive) + { + Game.Instance.Logger.LogWarning("Cannot add vertices to batch! No batch is currently active. Call StartBatch() before adding vertices."); + return; + } + + Rlgl.Color4f(color.R / 255f, color.G / 255f, color.B / 255f, color.A / 255f); + + foreach (var vertex in vertices) + { + Rlgl.Vertex2f(vertex.X, vertex.Y); + } + + Rlgl.Color4f(currentColor.R / 255f, currentColor.G / 255f, currentColor.B / 255f, currentColor.A / 255f); + } + #endregion + + #region Add Triangles + /// + /// Adds a triangle's vertices to the current batch. + /// + /// The first vertex. + /// The second vertex. + /// The third vertex. + /// + /// Batch must be active before using this function. Use to start a batch before setting the color. + /// + public static void AddImmediateTriangle(Vector2 a, Vector2 b, Vector2 c) + { + if (!batchActive) + { + Game.Instance.Logger.LogWarning("Cannot add triangle to batch! No batch is currently active. Call StartBatch() before adding triangles."); + return; + } + + Rlgl.Vertex2f(a.X, a.Y); + Rlgl.Vertex2f(b.X, b.Y); + Rlgl.Vertex2f(c.X, c.Y); + } + + /// + /// Sets the color and adds a triangle's vertices to the current batch. + /// The color used will affect all subsequent vertices until changed again or the batch ends. + /// + /// The first vertex. + /// The second vertex. + /// The third vertex. + /// The color to use for this triangle. + /// + /// Batch must be active before using this function. Use to start a batch before setting the color. + /// Resets the color back to the previous color set by or after adding the triangle vertices, so this will not affect subsequent vertices. + /// + public static void AddImmediateTriangle(Vector2 a, Vector2 b, Vector2 c, ColorRgba color) + { + if (!batchActive) + { + Game.Instance.Logger.LogWarning("Cannot add triangle to batch! No batch is currently active. Call StartBatch() before adding triangles."); + return; + } + + Rlgl.Color4f(color.R / 255f, color.G / 255f, color.B / 255f, color.A / 255f); + + Rlgl.Vertex2f(a.X, a.Y); + Rlgl.Vertex2f(b.X, b.Y); + Rlgl.Vertex2f(c.X, c.Y); + + Rlgl.Color4f(currentColor.R / 255f, currentColor.G / 255f, currentColor.B / 255f, currentColor.A / 255f); + } + + /// + /// Adds a triangle's vertices to the current batch. + /// + /// The triangle to add. + /// + /// Batch must be active before using this function. Use to start a batch before setting the color. + /// + public static void AddImmediateTriangle(Triangle triangle) + { + if (!batchActive) + { + Game.Instance.Logger.LogWarning("Cannot add triangle to batch! No batch is currently active. Call StartBatch() before adding triangles."); + return; + } + + Rlgl.Vertex2f(triangle.A.X, triangle.A.Y); + Rlgl.Vertex2f(triangle.B.X, triangle.B.Y); + Rlgl.Vertex2f(triangle.C.X, triangle.C.Y); + } + + /// + /// Sets the color and adds a triangle's vertices to the current batch. + /// The color used will affect all subsequent vertices until changed again or the batch ends. + /// + /// The triangle to add. + /// The color to use for this triangle. + /// + /// Batch must be active before using this function. Use to start a batch before setting the color. + /// Resets the color back to the previous color set by or after adding the triangle, so this will not affect subsequent vertices. + /// + public static void AddImmediateTriangle(Triangle triangle, ColorRgba color) + { + if (!batchActive) + { + Game.Instance.Logger.LogWarning("Cannot add triangle to batch! No batch is currently active. Call StartBatch() before adding triangles."); + return; + } + + Rlgl.Color4f(color.R / 255f, color.G / 255f, color.B / 255f, color.A / 255f); + + Rlgl.Vertex2f(triangle.A.X, triangle.A.Y); + Rlgl.Vertex2f(triangle.B.X, triangle.B.Y); + Rlgl.Vertex2f(triangle.C.X, triangle.C.Y); + + Rlgl.Color4f(currentColor.R / 255f, currentColor.G / 255f, currentColor.B / 255f, currentColor.A / 255f); + } + + /// + /// Adds a collection of triangles to the current batch. + /// + /// The collection of triangles to add. + /// + /// Batch must be active before using this function. Use to start a batch before adding triangles. + /// + public static void AddImmediateTriangles(IEnumerable triangles) + { + if (!batchActive) + { + Game.Instance.Logger.LogWarning("Cannot add triangles to batch! No batch is currently active. Call StartBatch() before adding triangles."); + return; + } + + foreach (var triangle in triangles) + { + Rlgl.Vertex2f(triangle.A.X, triangle.A.Y); + Rlgl.Vertex2f(triangle.B.X, triangle.B.Y); + Rlgl.Vertex2f(triangle.C.X, triangle.C.Y); + } + } + + /// + /// Sets the color and adds a collection of triangles to the current batch. + /// The color used will affect all subsequent vertices until changed again or the batch ends. + /// + /// The collection of triangles to add. + /// The color to use for these triangles. + /// + /// Batch must be active before using this function. Use to start a batch before adding triangles. + /// Resets the color back to the previous color set by or after adding the triangles, so this will not affect subsequent vertices. + /// + public static void AddImmediateTriangles(IEnumerable triangles, ColorRgba color) + { + if (!batchActive) + { + Game.Instance.Logger.LogWarning("Cannot add triangles to batch! No batch is currently active. Call StartBatch() before adding triangles."); + return; + } + + Rlgl.Color4f(color.R / 255f, color.G / 255f, color.B / 255f, color.A / 255f); + + foreach (var triangle in triangles) + { + Rlgl.Vertex2f(triangle.A.X, triangle.A.Y); + Rlgl.Vertex2f(triangle.B.X, triangle.B.Y); + Rlgl.Vertex2f(triangle.C.X, triangle.C.Y); + } + + Rlgl.Color4f(currentColor.R / 255f, currentColor.G / 255f, currentColor.B / 255f, currentColor.A / 255f); + } + #endregion + + #region Immediate Mode Drawing + /// + /// Draws a single triangle immediately. + /// + /// The triangle to draw. + /// The color of the triangle. + /// + /// Batch must NOT be active when calling this function. This is for immediate-mode style drawing of individual triangles. + /// Use to end any active batch before calling this function. + /// + public static void DrawImmediateTriangle(Triangle triangle, ColorRgba color) + { + if (batchActive) + { + Game.Instance.Logger.LogWarning("Cannot draw triangle immediately! A batch is currently active. Call EndBatch() before drawing individual triangles."); + return; + } + + Rlgl.Begin(DrawMode.Triangles); + + Rlgl.Color4f(color.R / 255f, color.G / 255f, color.B / 255f, color.A / 255f); + + Rlgl.Vertex2f(triangle.A.X, triangle.A.Y); + Rlgl.Vertex2f(triangle.B.X, triangle.B.Y); + Rlgl.Vertex2f(triangle.C.X, triangle.C.Y); + + Rlgl.End(); + } + + /// + /// Draws a single triangle defined by three vertices immediately. + /// + /// The first vertex. + /// The second vertex. + /// The third vertex. + /// The color of the triangle. + /// + /// Batch must NOT be active when calling this function. This is for immediate-mode style drawing of individual triangles. + /// Use to end any active batch before calling this function. + /// + public static void DrawImmediateTriangle(Vector2 a, Vector2 b, Vector2 c, ColorRgba color) + { + if (batchActive) + { + Game.Instance.Logger.LogWarning("Cannot draw triangle immediately! A batch is currently active. Call EndBatch() before drawing individual triangles."); + return; + } + + Rlgl.Begin(DrawMode.Triangles); + + Rlgl.Color4f(color.R / 255f, color.G / 255f, color.B / 255f, color.A / 255f); + + Rlgl.Vertex2f(a.X, a.Y); + Rlgl.Vertex2f(b.X, b.Y); + Rlgl.Vertex2f(c.X, c.Y); + + Rlgl.End(); + } + + /// + /// Draws a collection of triangles immediately. + /// + /// The collection of triangles to draw. + /// The color to use for all triangles. + /// + /// Batch must NOT be active when calling this function. This is for immediate-mode style drawing of individual triangles. + /// Use to end any active batch before calling this function. + /// + public static void DrawImmediateTriangles(IEnumerable triangles, ColorRgba color) + { + if (batchActive) + { + Game.Instance.Logger.LogWarning("Cannot draw triangles immediately! A batch is currently active. Call EndBatch() before drawing individual triangles."); + return; + } + + Rlgl.Begin(DrawMode.Triangles); + + Rlgl.Color4f(color.R / 255f, color.G / 255f, color.B / 255f, color.A / 255f); + + foreach (var triangle in triangles) + { + Rlgl.Vertex2f(triangle.A.X, triangle.A.Y); + Rlgl.Vertex2f(triangle.B.X, triangle.B.Y); + Rlgl.Vertex2f(triangle.C.X, triangle.C.Y); + } + + Rlgl.End(); + } + + /// + /// Draws a series of triangles defined by a flat array of vertices immediately. + /// + /// An array of vertices where every 3 vertices define a triangle. + /// The color to use for all triangles. + /// + /// Batch must NOT be active when calling this function. This is for immediate-mode style drawing of individual triangles. + /// Use to end any active batch before calling this function. + /// + public static void DrawImmediateTriangles(Vector2[] trianglePoints, ColorRgba color) + { + if (batchActive) + { + Game.Instance.Logger.LogWarning("Cannot draw triangles immediately! A batch is currently active. Call EndBatch() before drawing individual triangles."); + return; + } + + if (trianglePoints.Length <= 0) return; + + Rlgl.Begin(DrawMode.Triangles); + + Rlgl.Color4f(color.R / 255f, color.G / 255f, color.B / 255f, color.A / 255f); + + foreach (var v in trianglePoints) + { + Rlgl.Vertex2f(v.X, v.Y); + } + + Rlgl.End(); + } + #endregion + + #endregion +} \ No newline at end of file diff --git a/ShapeEngine/Core/TweenType.cs b/ShapeEngine/Core/TweenType.cs index 19af6fde..bf6532db 100644 --- a/ShapeEngine/Core/TweenType.cs +++ b/ShapeEngine/Core/TweenType.cs @@ -54,5 +54,7 @@ public enum TweenType /// Elastic ease-out. ELASTIC_OUT = 26, /// Elastic ease-in-out. - ELASTIC_INOUT = 27 + ELASTIC_INOUT = 27, + /// Ping Pong between 0 to 1 and 1 to 0. + Ping_Pong = 28 } \ No newline at end of file diff --git a/ShapeEngine/Geometry/CircleDef/Circle.cs b/ShapeEngine/Geometry/CircleDef/Circle.cs index 8370644f..3f516847 100644 --- a/ShapeEngine/Geometry/CircleDef/Circle.cs +++ b/ShapeEngine/Geometry/CircleDef/Circle.cs @@ -6,11 +6,13 @@ using ShapeEngine.Geometry.RectDef; using ShapeEngine.Geometry.SegmentDef; using ShapeEngine.Geometry.SegmentsDef; +using ShapeEngine.Geometry.TriangleDef; using ShapeEngine.Geometry.TriangulationDef; using ShapeEngine.Random; using ShapeEngine.StaticLib; namespace ShapeEngine.Geometry.CircleDef; + /// /// Represents a 2D circle defined by a center point and a radius. /// @@ -19,6 +21,13 @@ namespace ShapeEngine.Geometry.CircleDef; /// public readonly partial struct Circle : IEquatable, IShapeTypeProvider, IClosedShapeTypeProvider { + #region Helper + + private static Points pointsBuffer = new(); + private static Segments segmentsBuffer = new(); + private static Polygon? circleSectorOutlineTriangulationPolyCache = null; + #endregion + #region Members /// /// The center position of the circle in 2D space. @@ -195,8 +204,16 @@ public bool Equals(Circle other) /// A hash code for the current circle. public readonly override int GetHashCode() => HashCode.Combine(Center, Radius); + /// + /// Gets the closed shape type represented by this shape. + /// + /// . public ClosedShapeType GetClosedShapeType() => ClosedShapeType.Circle; + /// + /// Gets the general shape type represented by this shape. + /// + /// . public ShapeType GetShapeType() => ShapeType.Circle; /// @@ -245,6 +262,7 @@ public Vector2 GetVertex(float angleRad, float angleStepRad, int index) { return Center + new Vector2(Radius, 0f).Rotate(angleRad + angleStepRad * index); } + /// /// Gets a point on the circle at a specified angle and scale factor. /// @@ -252,6 +270,7 @@ public Vector2 GetVertex(float angleRad, float angleStepRad, int index) /// The scale factor for the radius.(0 - 1) /// The point position as a . public Vector2 GetPoint(float angleRad, float f) { return Center + new Vector2(Radius * f, 0f).Rotate(angleRad); } + /// /// Gets a random point inside the circle. /// @@ -262,37 +281,59 @@ public Vector2 GetRandomPoint() var randDir = ShapeVec.VecFromAngleRad(randAngle); return Center + randDir * Rng.Instance.RandF(0, Radius); } + /// - /// Gets a collection of random points inside the circle. + /// Writes randomly generated points inside the circle into . /// /// The number of random points to generate. - /// A collection containing the random points. - public Points GetRandomPoints(int amount) + /// The destination collection that will be cleared and populated with the generated points. + /// + /// If is less than or equal to 0, the method returns immediately without modifying . + /// + public void GetRandomPoints(int amount, Points result) { - var points = new Points(); + if(amount <= 0) return; + result.Clear(); + result.EnsureCapacity(amount); for (int i = 0; i < amount; i++) { - points.Add(GetRandomPoint()); + result.Add(GetRandomPoint()); } - return points; } + /// - /// Gets a random vertex on the circle's edge. + /// Gets a random vertex from a polygonal approximation of the circle. /// - /// A random vertex as a . - public Vector2 GetRandomVertex() { return Rng.Instance.RandCollection(GetVertices()); } + /// The number of vertices to generate for the approximation before choosing one at random. + /// A randomly selected vertex, or if no vertices could be generated. + public Vector2 GetRandomVertex(int count = 16) + { + GetVertices(pointsBuffer, count); + if(pointsBuffer.Count <= 0) return Vector2.Zero; + return Rng.Instance.RandCollection(pointsBuffer); + } /// - /// Gets a random edge segment of the circle. + /// Gets a random edge segment from a polygonal approximation of the circle. /// - /// A random edge as a . - public Segment GetRandomEdge() { return Rng.Instance.RandCollection(GetEdges()); } + /// The number of vertices to generate for the approximation before choosing an edge at random. + /// A randomly selected edge segment, or the default if no edges could be generated. + public Segment GetRandomEdge(int count = 16) + { + GetEdges(segmentsBuffer, count); + if(segmentsBuffer.Count <= 0) return new Segment(); + return Rng.Instance.RandCollection(segmentsBuffer); + } /// /// Gets a random point on the circle's edge. /// /// A random point on the edge as a . - public Vector2 GetRandomPointOnEdge() { return GetRandomEdge().GetRandomPoint(); } + public Vector2 GetRandomPointOnEdge() + { + return GetRandomEdge().GetRandomPoint(); + } + /// /// Gets a collection of random points on the circle's edge. /// @@ -307,43 +348,63 @@ public Points GetRandomPointsOnEdge(int amount) } return points; } + + /// + /// Writes randomly generated points on the circle's polygonal edge approximation into . + /// + /// The destination collection that will be cleared and populated with the generated edge points. + /// The number of random edge points to generate. + public void GetRandomPointsOnEdge(Points result, int amount) + { + result.Clear(); + result.EnsureCapacity(amount); + for (int i = 0; i < amount; i++) + { + result.Add(GetRandomPointOnEdge()); + } + } #endregion #region Shapes /// - /// Gets the edges of the circle as a collection of segments. + /// Writes a polygonal edge approximation of the circle into . /// + /// The destination collection that will be cleared and populated with the generated edge segments. /// The number of points to use for generating the edges. Default is 16. - /// A collection representing the edges of the circle. - public Segments GetEdges(int pointCount = 16) + /// + /// The generated segments connect consecutive vertices around the circle, including the closing edge from the last vertex back to the first. + /// + public void GetEdges(Segments result, int pointCount = 16) { float angleStep = (MathF.PI * 2f) / pointCount; - Segments segments = new(); + result.Clear(); + result.EnsureCapacity(pointCount); for (int i = 0; i < pointCount; i++) { var start = Center + new Vector2(Radius, 0f).Rotate(-angleStep * i); var end = Center + new Vector2(Radius, 0f).Rotate(-angleStep * ((i + 1) % pointCount)); - segments.Add(new Segment(start, end)); + result.Add(new Segment(start, end)); } - return segments; } + /// - /// Gets the vertices of the circle as a collection of points. + /// Writes a polygonal vertex approximation of the circle into . /// + /// The destination collection that will be cleared and populated with the generated vertices. /// The number of vertices to generate. Default is 16. - /// A collection containing the vertices of the circle. - public Points GetVertices(int count = 16) + public void GetVertices(Points result, int count = 16) { float angleStep = (MathF.PI * 2f) / count; - Points points = new(); + result.Clear(); + result.EnsureCapacity(count); for (int i = 0; i < count; i++) { Vector2 p = Center + new Vector2(Radius, 0f).Rotate(angleStep * i); - points.Add(p); + result.Add(p); } - return points; } + /// /// Converts the circle into a polygon representation. /// @@ -360,17 +421,19 @@ public Polygon ToPolygon(int pointCount = 16) } return poly; } + /// /// Converts the circle into a polygon representation and stores the result in the provided . /// /// A reference to the to store the result. /// The number of points to use for the polygon. Default is 16. /// true if the conversion was successful; otherwise, false. - public bool ToPolygon(ref Polygon result, int pointCount = 16) + public bool ToPolygon(Polygon result, int pointCount = 16) { if (Radius <= 0f) return false; - if (result.Count > 0) result.Clear(); + result.Clear(); + result.EnsureCapacity(pointCount); float angleStep = (MathF.PI * 2f) / pointCount; for (var i = 0; i < pointCount; i++) @@ -381,6 +444,7 @@ public bool ToPolygon(ref Polygon result, int pointCount = 16) return true; } + /// /// Converts the circle into a polyline representation. /// @@ -397,16 +461,53 @@ public Polyline ToPolyline(int pointCount = 16) } return polyLine; } + + /// + /// Converts the circle into a polyline representation and stores the result in the provided . + /// + /// The destination polyline that will be cleared and populated with the generated points. + /// The number of points to use for the polyline. Default is 16. + /// true after the polyline points have been written to . + public bool ToPolyline(Polyline result, int pointCount = 16) + { + float angleStep = (MathF.PI * 2f) / pointCount; + result.Clear(); + result.EnsureCapacity(pointCount); + for (int i = 0; i < pointCount; i++) + { + Vector2 p = Center + new Vector2(Radius, 0f).Rotate(angleStep * i); + result.Add(p); + } + return true; + } + /// - /// Triangulates the circle into a set of triangles. + /// Triangulates the circle into a fan of triangles around the center. /// - /// A representing the triangulated circle. - public Triangulation Triangulate() { return ToPolygon().Triangulate(); } + /// The destination triangulation that will be cleared and populated with the generated triangles. + /// The number of outer points used to approximate the circle. Default is 16. + public void Triangulate(Triangulation result, int pointCount = 16) + { + float angleStep = (MathF.PI * 2f) / pointCount; + result.Clear(); + var cur = Center + new Vector2(Radius, 0f); + + for (int i = 0; i < pointCount; i++) + { + // Vector2 p1 = Center + new Vector2(Radius, 0f).Rotate(angleStep * i); + Vector2 next = Center + new Vector2(Radius, 0f).Rotate(angleStep * (i + 1)); + var t = new Triangle(Center, next, cur); + result.Add(t); + cur = next; + } + } + /// /// Gets the bounding box of the circle. /// /// A representing the bounding box of the circle. public Rect GetBoundingBox() { return new Rect(Center, new Size(Radius, Radius) * 2f, new(0.5f)); } + /// /// Combines the current circle with another circle. /// @@ -439,6 +540,7 @@ public Circle Combine(Circle other) var left = Center + new Vector2(-Radius, 0); return (top, right, bottom, left); } + /// /// Gets the top, right, bottom, and left points of the circle as a list. /// @@ -451,6 +553,25 @@ public List GetCornersList() var left = Center + new Vector2(-Radius, 0); return new() { top, right, bottom, left }; } + + /// + /// Gets the top, right, bottom, and left points of the circle and stores them in the provided list. + /// + /// The destination list that will be cleared and populated with the top, right, bottom, and left points. + public void GetCornersList(List result) + { + var top = Center + new Vector2(0, -Radius); + var right = Center + new Vector2(Radius, 0); + var bottom = Center + new Vector2(0, Radius); + var left = Center + new Vector2(-Radius, 0); + result.Clear(); + result.EnsureCapacity(4); + result.Add(top); + result.Add(right); + result.Add(bottom); + result.Add(left); + } + /// /// Gets the top-left, top-right, bottom-right, /// and bottom-left corners of the circle's bounding box. @@ -465,6 +586,7 @@ public List GetCornersList() var bl = Center + new Vector2(-Radius, Radius); return (tl, tr, br, bl); } + /// /// Gets the top-left, top-right, bottom-right, /// and bottom-left corners of the circle's bounding box as a list. @@ -479,6 +601,26 @@ public List GetRectCornersList() var bl = Center + new Vector2(-Radius, Radius); return new() {tl, tr, br, bl}; } + + /// + /// Gets the top-left, top-right, bottom-right, and bottom-left corners of the circle's bounding box + /// and stores them in the provided list. + /// + /// The destination list that will be cleared and populated with the bounding box corners. + public void GetRectCornersList(List result) + { + var tl = Center + new Vector2(-Radius, -Radius); + var tr = Center + new Vector2(Radius, -Radius); + var br = Center + new Vector2(Radius, Radius); + var bl = Center + new Vector2(-Radius, Radius); + + result.Clear(); + result.EnsureCapacity(4); + result.Add(tl); + result.Add(tr); + result.Add(br); + result.Add(bl); + } #endregion #region Operators @@ -496,6 +638,7 @@ public List GetRectCornersList() left.Radius + right.Radius ); } + /// /// Subtracts the center and radius of one circle from another. /// @@ -510,6 +653,7 @@ public List GetRectCornersList() left.Radius - right.Radius ); } + /// /// Multiplies the center and radii of two circles. /// @@ -524,6 +668,7 @@ public List GetRectCornersList() left.Radius * right.Radius ); } + /// /// Divides the center and radius of one circle by another. /// @@ -538,6 +683,7 @@ public List GetRectCornersList() left.Radius / right.Radius ); } + /// /// Adds a vector offset to the circle's center. /// @@ -552,6 +698,7 @@ public List GetRectCornersList() left.Radius ); } + /// /// Subtracts a vector offset from the circle's center. /// @@ -566,12 +713,13 @@ public List GetRectCornersList() left.Radius ); } + /// /// Multiplies the circle center by a vector. /// /// The circle. /// The vector. - /// A new with the scaled radius. + /// A new with the scaled center. public static Circle operator *(Circle left, Vector2 right) { return new @@ -580,12 +728,13 @@ public List GetRectCornersList() left.Radius ); } + /// /// Divides the circle center by a vector. /// /// The circle. /// The vector. - /// A new with the scaled radius. + /// A new with the scaled center. public static Circle operator /(Circle left, Vector2 right) { return new @@ -594,6 +743,7 @@ public List GetRectCornersList() left.Radius ); } + /// /// Adds a scalar value to the circle's radius. /// @@ -608,6 +758,7 @@ public List GetRectCornersList() left.Radius + right ); } + /// /// Subtracts a scalar value from the circle's radius. /// @@ -622,6 +773,7 @@ public List GetRectCornersList() left.Radius - right ); } + /// /// Multiplies the circle's radius by a scalar value. /// @@ -636,6 +788,7 @@ public List GetRectCornersList() left.Radius * right ); } + /// /// Divides the circle's radius by a scalar value. /// @@ -650,6 +803,7 @@ public List GetRectCornersList() left.Radius / right ); } + #endregion #region Static @@ -679,55 +833,85 @@ public static Circle Combine(params Circle[] circles) #region Interpolated Edge Points /// - /// Returns a set of interpolated edge points on the circle's circumference. + /// Generates interpolated edge points from a polygonal approximation of the circle and writes them into . /// - /// - /// The interpolation parameter, typically in the range [0, 1], used to offset the starting angle of the points. - /// - /// - /// The number of edge points to generate along the circle's circumference. - /// - /// - /// A collection containing the interpolated edge points, or null if the input is invalid. - /// - public Points? GetInterpolatedEdgePoints(float t, int vertexCount) + /// The interpolation factor used between each generated vertex and the next vertex. + /// The number of vertices to generate for the circle approximation before interpolation. + /// The destination collection that will receive the interpolated edge points. + /// true if a valid approximation was generated and was populated; otherwise, false. + public bool GetInterpolatedEdgePoints(float t, int vertexCount, Points result) { - if(vertexCount < 3) return null; + if(vertexCount < 3) return false; - var points = GetVertices(vertexCount); - if (points.Count <= 3) return null; + GetVertices(pointsBuffer, vertexCount); + if (pointsBuffer.Count <= 3) return false; - return points.GetInterpolatedEdgePoints(t); - } - /// - /// Returns a set of interpolated edge points on the circle's circumference, - /// using a specified number of interpolation steps and vertices. - /// - /// - /// The interpolation parameter, in the range [0, 1], - /// used to offset the starting angle of the points. - /// - /// - /// The number of interpolation steps to use between vertices. - /// - /// - /// The number of edge points (vertices) to generate along the circle's circumference. - /// - /// - /// A collection containing the interpolated edge points, - /// or null if the input is invalid. - /// - public Points? GetInterpolatedEdgePoints(float t, int steps, int vertexCount) - { - if(vertexCount < 3) return null; + pointsBuffer.GetInterpolatedEdgePoints(t, result); + return true; + } + + /// + /// Generates interpolated edge points from a polygonal approximation of the circle using multiple interpolation passes and writes them into . + /// + /// The interpolation factor used between each generated vertex and the next vertex on each pass. + /// The number of interpolation passes to perform. + /// The number of vertices to generate for the circle approximation before interpolation. + /// The destination collection that will receive the interpolated edge points. + /// true if a valid approximation was generated and was populated; otherwise, false. + public bool GetInterpolatedEdgePoints(float t, int steps, int vertexCount, Points result) + { + if(vertexCount < 3) return false; - var points = GetVertices(vertexCount); - if (points.Count <= 3) return null; + GetVertices(pointsBuffer, vertexCount); + if (pointsBuffer.Count <= 3) return false; - return points.GetInterpolatedEdgePoints(t, steps); + pointsBuffer.GetInterpolatedEdgePoints(t, steps, result); + return true; } #endregion + #region Arc Length + + /// + /// Converts an angle in radians to the corresponding arc length on this circle using its . + /// + /// Angle in radians. + /// true to normalize into the range [0, 2π) before calculating arc length; otherwise, uses the raw angle value. + /// The arc length along the circle corresponding to . + /// Thrown when this circle's is less than or equal to zero. + public float GetArcLengthFromAngle(float angleRad, bool normalize = true) + { + if (Radius <= 0f) throw new ArgumentException("radius must be > 0", nameof(Radius)); + float theta = angleRad; + if (normalize) + { + float twoPi = 2f * MathF.PI; + theta %= twoPi; + if (theta < 0f) theta += twoPi; + } + return Radius * theta; + } + + /// + /// Converts an arc length along this circle to the corresponding central angle in radians. + /// + /// The length of the arc along the circle. + /// true to normalize the computed angle into the range [0, 2π); otherwise, returns the raw angle value. + /// The angle in radians corresponding to the provided arc length. + /// Thrown when this circle's is less than or equal to zero. + public float GetAngleFromArcLength(float arcLength, bool normalize = true) + { + if (Radius <= 0f) throw new ArgumentException("radius must be > 0", nameof(Radius)); + float theta = arcLength / Radius; + if (normalize) + { + float twoPi = 2f * MathF.PI; + theta %= twoPi; + if (theta < 0f) theta += twoPi; + } + return theta; + } + + #endregion } - diff --git a/ShapeEngine/Geometry/CircleDef/CircleDrawing.cs b/ShapeEngine/Geometry/CircleDef/CircleDrawing.cs index 8a2b2173..bb1f9813 100644 --- a/ShapeEngine/Geometry/CircleDef/CircleDrawing.cs +++ b/ShapeEngine/Geometry/CircleDef/CircleDrawing.cs @@ -11,417 +11,276 @@ namespace ShapeEngine.Geometry.CircleDef; -/// -/// Provides static methods for drawing circles and circle-related shapes with various options, -/// including filled circles, outlines, sectors, and advanced line drawing with scaling and percentage. -/// -/// -/// This class is intended for use with Raylib and ShapeEngine types. -/// It offers both simple and advanced -/// circle drawing utilities, including performance-optimized methods for small circles. -/// public static class CircleDrawing { - #region Draw Masked - /// - /// Draws the circle outline as individual line segments clipped by a mask. - /// - /// The source whose circumference will be approximated by segments. - /// The used to mask (clip) each generated segment. - /// Parameters controlling line drawing (thickness, color, cap type, etc.). - /// Rotation offset in degrees applied to the entire approximated polygon. - /// Number of sides used to approximate the circle. Values below 3 will be clamped to 3. - /// If true, draws the parts inside the mask instead of outside. - public static void DrawLinesMasked(this Circle circle, Triangle mask, LineDrawingInfo lineInfo, float rotDeg, int sides, bool reversedMask = false) - { - if (sides < 3) sides = 3; - var angleStep = (2f * ShapeMath.PI) / sides; - var rotRad = rotDeg * ShapeMath.DEGTORAD; - var radius = circle.Radius; - var center = circle.Center; - for (int i = 0; i < sides; i++) - { - var nextIndex = (i + 1) % sides; - var curP = center + new Vector2(radius, 0f).Rotate(rotRad + angleStep * i); - var nextP = center + new Vector2(radius, 0f).Rotate(rotRad + angleStep * nextIndex); - var segment = new Segment(curP, nextP); - segment.DrawMasked(mask, lineInfo, reversedMask); - } - } - /// - /// Draws the circle outline as individual line segments clipped by a mask. - /// - /// The source whose circumference will be approximated by segments. - /// The used to mask (clip) each generated segment. - /// Parameters controlling line drawing (thickness, color, cap type, etc.). - /// Rotation offset in degrees applied to the entire approximated polygon. - /// Number of sides used to approximate the circle. Values below 3 will be clamped to 3. - /// If true, draws the parts inside the mask instead of outside. - public static void DrawLinesMasked(this Circle circle, Circle mask, LineDrawingInfo lineInfo, float rotDeg, int sides, bool reversedMask = false) - { - if (sides < 3) sides = 3; - var angleStep = (2f * ShapeMath.PI) / sides; - var rotRad = rotDeg * ShapeMath.DEGTORAD; - var radius = circle.Radius; - var center = circle.Center; - for (int i = 0; i < sides; i++) - { - var nextIndex = (i + 1) % sides; - var curP = center + new Vector2(radius, 0f).Rotate(rotRad + angleStep * i); - var nextP = center + new Vector2(radius, 0f).Rotate(rotRad + angleStep * nextIndex); - var segment = new Segment(curP, nextP); - segment.DrawMasked(mask, lineInfo, reversedMask); - } - } - /// - /// Draws the circle outline as individual line segments clipped by a mask. - /// - /// The source whose circumference will be approximated by segments. - /// The used to mask (clip) each generated segment. - /// Parameters controlling line drawing (thickness, color, cap type, etc.). - /// Rotation offset in degrees applied to the entire approximated polygon. - /// Number of sides used to approximate the circle. Values below 3 will be clamped to 3. - /// If true, draws the parts inside the mask instead of outside. - public static void DrawLinesMasked(this Circle circle, Rect mask, LineDrawingInfo lineInfo, float rotDeg, int sides, bool reversedMask = false) - { - if (sides < 3) sides = 3; - var angleStep = (2f * ShapeMath.PI) / sides; - var rotRad = rotDeg * ShapeMath.DEGTORAD; - var radius = circle.Radius; - var center = circle.Center; - for (int i = 0; i < sides; i++) - { - var nextIndex = (i + 1) % sides; - var curP = center + new Vector2(radius, 0f).Rotate(rotRad + angleStep * i); - var nextP = center + new Vector2(radius, 0f).Rotate(rotRad + angleStep * nextIndex); - var segment = new Segment(curP, nextP); - segment.DrawMasked(mask, lineInfo, reversedMask); - } - } - /// - /// Draws the circle outline as individual line segments clipped by a mask. - /// - /// The source whose circumference will be approximated by segments. - /// The used to mask (clip) each generated segment. - /// Parameters controlling line drawing (thickness, color, cap type, etc.). - /// Rotation offset in degrees applied to the entire approximated polygon. - /// Number of sides used to approximate the circle. Values below 3 will be clamped to 3. - /// If true, draws the parts inside the mask instead of outside. - public static void DrawLinesMasked(this Circle circle, Quad mask, LineDrawingInfo lineInfo, float rotDeg, int sides, bool reversedMask = false) - { - if (sides < 3) sides = 3; - var angleStep = (2f * ShapeMath.PI) / sides; - var rotRad = rotDeg * ShapeMath.DEGTORAD; - var radius = circle.Radius; - var center = circle.Center; - for (int i = 0; i < sides; i++) - { - var nextIndex = (i + 1) % sides; - var curP = center + new Vector2(radius, 0f).Rotate(rotRad + angleStep * i); - var nextP = center + new Vector2(radius, 0f).Rotate(rotRad + angleStep * nextIndex); - var segment = new Segment(curP, nextP); - segment.DrawMasked(mask, lineInfo, reversedMask); - } - } - /// - /// Draws the circle outline as individual line segments clipped by a mask. - /// - /// The source whose circumference will be approximated by segments. - /// The used to mask (clip) each generated segment. - /// Parameters controlling line drawing (thickness, color, cap type, etc.). - /// Rotation offset in degrees applied to the entire approximated polygon. - /// Number of sides used to approximate the circle. Values below 3 will be clamped to 3. - /// If true, draws the parts inside the mask instead of outside. - public static void DrawLinesMasked(this Circle circle, Polygon mask, LineDrawingInfo lineInfo, float rotDeg, int sides, bool reversedMask = false) - { - if (sides < 3) sides = 3; - var angleStep = (2f * ShapeMath.PI) / sides; - var rotRad = rotDeg * ShapeMath.DEGTORAD; - var radius = circle.Radius; - var center = circle.Center; - for (int i = 0; i < sides; i++) - { - var nextIndex = (i + 1) % sides; - var curP = center + new Vector2(radius, 0f).Rotate(rotRad + angleStep * i); - var nextP = center + new Vector2(radius, 0f).Rotate(rotRad + angleStep * nextIndex); - var segment = new Segment(curP, nextP); - segment.DrawMasked(mask, lineInfo, reversedMask); - } - } - /// - /// Draws the circle outline as individual line segments clipped by a mask of any closed shape type. - /// - /// The mask type implementing (for example: , , , , ). - /// The source whose circumference will be approximated by segments. - /// The mask used to clip each generated segment. - /// Parameters controlling line drawing (thickness, color, cap type, etc.). - /// Rotation offset in degrees applied to the entire approximated polygon. - /// Number of sides used to approximate the circle. Values below 3 will be clamped to 3. - /// If true, draws the parts inside the mask instead of outside. - public static void DrawLinesMasked(this Circle circle, T mask, LineDrawingInfo lineInfo, float rotDeg, int sides, bool reversedMask = false) where T : IClosedShapeTypeProvider - { - if (sides < 3) sides = 3; - var angleStep = (2f * ShapeMath.PI) / sides; - var rotRad = rotDeg * ShapeMath.DEGTORAD; - var radius = circle.Radius; - var center = circle.Center; - for (int i = 0; i < sides; i++) - { - var nextIndex = (i + 1) % sides; - var curP = center + new Vector2(radius, 0f).Rotate(rotRad + angleStep * i); - var nextP = center + new Vector2(radius, 0f).Rotate(rotRad + angleStep * nextIndex); - var segment = new Segment(curP, nextP); - segment.DrawMasked(mask, lineInfo, reversedMask); - } - } - #endregion + #region Draw /// - /// Draws a filled circle at the specified center with the given radius and color. + /// Draws the filled circle. /// - /// The center position of the circle. - /// The radius of the circle. + /// The circle to draw. + /// The rotation of the circle in degrees. /// The color of the circle. - /// The number of segments used to approximate the circle. Minimum is 3. - public static void DrawCircle(Vector2 center, float radius, ColorRgba color, int segments = 16) + /// + /// The smoothness value (0-1). This controls the visual quality of the circle by inversely interpolating the current . + /// A value of 0 uses the maximum side length (fewer sides, less smooth), while 1 uses the minimum side length (more sides, smoother). + /// The resulting side length determines the number of polygon sides used to approximate the circle. + /// + public static void Draw(this Circle c, float rotDeg, ColorRgba color, float smoothness = 0.5f) { - if (segments < 3) segments = 3; - Raylib.DrawCircleSector(center, radius, 0, 360, segments, color.ToRayColor()); + if (!CalculateCircleDrawingParameters(c.Radius, smoothness, out int sides)) return; + Raylib.DrawCircleSector(c.Center, c.Radius, rotDeg, 360 + rotDeg, sides, color.ToRayColor()); } /// - /// Draws a filled circle at the specified center with the given radius, color, and rotation. + /// Draws the filled circle. /// - /// The center position of the circle. - /// The radius of the circle. + /// The circle to draw. /// The color of the circle. - /// The rotation of the circle in degrees. - /// The number of segments used to approximate the circle. Minimum is 3. - public static void DrawCircle(Vector2 center, float radius, ColorRgba color, float rotDeg, int segments = 16) + /// + /// The smoothness value (0-1). This controls the visual quality of the circle by inversely interpolating the current . + /// A value of 0 uses the maximum side length (fewer sides, less smooth), while 1 uses the minimum side length (more sides, smoother). + /// The resulting side length determines the number of polygon sides used to approximate the circle. + /// + public static void Draw(this Circle c, ColorRgba color, float smoothness = 0.5f) { - if (segments < 3) segments = 3; - Raylib.DrawCircleSector(center, radius, rotDeg, 360 + rotDeg, segments, color.ToRayColor()); + if (!CalculateCircleDrawingParameters(c.Radius, smoothness, out int sides)) return; + Raylib.DrawCircleSector(c.Center, c.Radius, 0, 360, sides, color.ToRayColor()); } - + /// - /// Draws a filled circle using the specified instance, color, rotation, and segment count. + /// Draws the circle as a square (fastest). Usefull for very small circles to save performance and still look good. /// /// The circle to draw. /// The color of the circle. - /// The rotation of the circle in degrees. - /// The number of segments used to approximate the circle. Minimum is 3. - public static void Draw(this Circle c, ColorRgba color, float rotDeg, int segments = 16) + public static void DrawFast(this Circle c, ColorRgba color) { - if (segments < 3) segments = 3; - Raylib.DrawCircleSector(c.Center, c.Radius, rotDeg, 360 + rotDeg, segments, color.ToRayColor()); + RectDrawing.DrawRect(c.Center - new Vector2(c.Radius, c.Radius), c.Center + new Vector2(c.Radius, c.Radius), color); } - /// - /// Draws a filled circle using the specified instance and color. - /// - /// The circle to draw. - /// The color of the circle. - public static void Draw(this Circle c, ColorRgba color) => DrawCircle(c.Center, c.Radius, color); - - /// - /// Draws a filled circle using the specified instance, color, and segment count. - /// - /// The circle to draw. - /// The color of the circle. - /// The number of segments used to approximate the circle. - public static void Draw(this Circle c, ColorRgba color, int segments) => DrawCircle(c.Center, c.Radius, color, segments); - - /// - /// Draws the outline of a circle using the specified line thickness, number of sides, and color. - /// - /// The circle to draw. - /// The thickness of the outline. - /// The number of sides used to approximate the circle. - /// The color of the outline. - public static void DrawLines(this Circle c, float lineThickness, int sides, ColorRgba color) => Raylib.DrawPolyLinesEx(c.Center, sides, c.Radius, 0f, lineThickness * 2, color.ToRayColor()); - - /// - /// Draws the outline of a circle using the specified line drawing info and number of sides. - /// - /// The circle to draw. - /// The line drawing parameters. - /// The number of sides used to approximate the circle. - public static void DrawLines(this Circle c, LineDrawingInfo lineInfo, int sides) => DrawLines(c, lineInfo, 0f, sides); + #endregion + + #region Draw Scaled /// - /// Draws the outline of a circle using the specified line drawing info, rotation, and number of sides. + /// Draws a filled circle with each side scaled towards the origin of the side. /// /// The circle to draw. - /// The line drawing parameters. - /// The rotation of the circle in degrees. - /// The number of sides used to approximate the circle. - public static void DrawLines(this Circle c, LineDrawingInfo lineInfo, float rotDeg, int sides) + /// Rotation of the circle in degrees. + /// Color of the circle. + /// + /// The smoothness value (0-1). This controls the visual quality of the circle by inversely interpolating the current . + /// A value of 0 uses the maximum side length (fewer sides, less smooth), while 1 uses the minimum side length (more sides, smoother). + /// The resulting side length determines the number of polygon sides used to approximate the circle. + /// + /// Scale factor for each side (0 = no circle, 1 = normal circle). + /// Point along each segment to scale from (0-1, default 0.5). + public static void DrawScaled(this Circle c, float rotDeg, ColorRgba color, float smoothness, float sideScaleFactor, float sideScaleOrigin = 0.5f) { - if (sides < 3) sides = 3; - var angleStep = (2f * ShapeMath.PI) / sides; + if (c.Radius <= 0f || sideScaleFactor <= 0f) return; + if (sideScaleFactor >= 1f) + { + c.Draw(color, smoothness); + return; + } + + if (!CalculateCircleDrawingParameters(c.Radius, smoothness, out float angleStep, out int sides)) return; + var rotRad = rotDeg * ShapeMath.DEGTORAD; + var rayColor = color.ToRayColor(); for (int i = 0; i < sides; i++) { var nextIndex = (i + 1) % sides; - var curP = c.Center + new Vector2(c.Radius, 0f).Rotate(rotRad + angleStep * i); - var nextP = c.Center + new Vector2(c.Radius, 0f).Rotate(rotRad + angleStep * nextIndex); + var start = c.Center + new Vector2(c.Radius, 0f).Rotate(rotRad + angleStep * i); + var end = c.Center + new Vector2(c.Radius, 0f).Rotate(rotRad + angleStep * nextIndex); - SegmentDrawing.DrawSegment(curP, nextP, lineInfo); + var scaledSegment = new Segment(start, end).ScaleSegment(sideScaleFactor, sideScaleOrigin); + + Raylib.DrawTriangle(c.Center, scaledSegment.End, scaledSegment.Start, rayColor); } } - + #endregion + + #region Draw Percentage + /// - /// Draws part of a circle outline depending on f. + /// Draws a filled circle sector based on a percentage value. /// - /// The circle parameters. - /// The percentage of the outline to draw. A positive value goes counter-clockwise - /// and a negative value goes clockwise. - /// The line drawing parameters. - /// The rotation of the circle. The lower the resolution of the circle the more visible is rotation - /// The resolution of the circle. The more sides are used the closer it represents a circle. - /// The color of the line. - /// The end cap type of the line. - /// How many points are used to draw the end cap. + /// The circle to draw. + /// The fill percentage (0 to 1 or 0 to -1). Positive values are clockwise, negative are counter-clockwise. + /// The rotation offset in degrees. + /// The color of the circle sector. + /// + /// The smoothness value (0-1). This controls the visual quality of the circle by inversely interpolating the current . + /// A value of 0 uses the maximum side length (fewer sides, less smooth), while 1 uses the minimum side length (more sides, smoother). + /// The resulting side length determines the number of polygon sides used to approximate the circle. + /// /// - /// Useful for drawing progress arcs or partial circles. + /// This function does not clamp the resulting side count with . /// - public static void DrawLinesPercentage(this Circle c, float f, float lineThickness, float rotDeg, int sides, ColorRgba color, LineCapType lineCapType, int capPoints) + public static void DrawPercentage(this Circle c, float f, float rotDeg, ColorRgba color, float smoothness = 0.5f) { - if (sides < 3 || f == 0) return; - - DrawCircleLinesPercentage(c.Center, c.Radius, f, lineThickness, rotDeg, sides, color, lineCapType, capPoints); - } + if (f == 0) return; - /// - /// Draws part of a circle outline depending on f. - /// - /// The circle parameters. - /// The percentage of the outline to draw. A positive value goes counter-clockwise - /// and a negative value goes clockwise. - /// The line drawing parameters. - /// The rotation of the circle. The lower the resolution of the circle the more visible is rotation - /// The resolution of the circle. The more sides are used the closer it represents a circle. - public static void DrawLinesPercentage(this Circle c, float f, LineDrawingInfo lineInfo, float rotDeg, int sides) - { - DrawLinesPercentage(c, f, lineInfo.Thickness, rotDeg, sides, lineInfo.Color, lineInfo.CapType, lineInfo.CapPoints); + if (MathF.Abs(f) >= 1f) + { + c.Draw(rotDeg, color, smoothness); + return; + } + + if (TransformPercentageToAngles(f, out float startAngleDeg, out float endAngleDeg)) + { + c.DrawSector(startAngleDeg + rotDeg, endAngleDeg + rotDeg, color, smoothness); + } } - - /// - /// Draws the outline of a circle using the specified line thickness, rotation, number of sides, and color. - /// - /// The circle to draw. - /// The thickness of the outline. - /// The rotation of the circle in degrees. - /// The number of sides used to approximate the circle. - /// The color of the outline. - public static void DrawLines(this Circle c, float lineThickness, float rotDeg, int sides, ColorRgba color) => Raylib.DrawPolyLinesEx(c.Center, sides, c.Radius, rotDeg, lineThickness * 2, color.ToRayColor()); - + #endregion + + #region Draw Lines + /// - /// Draws the outline of a circle using the specified line thickness and color, automatically determining the number of sides based on side length. + /// Draws the outline of the circle. /// /// The circle to draw. /// The thickness of the outline. /// The color of the outline. - /// The maximum length of each side. Default is 8. - public static void DrawLines(this Circle c, float lineThickness, ColorRgba color, float sideLength = 8f) + /// + /// The smoothness value (0-1). This controls the visual quality of the circle by inversely interpolating the current . + /// A value of 0 uses the maximum side length (fewer sides, less smooth), while 1 uses the minimum side length (more sides, smoother). + /// The resulting side length determines the number of polygon sides used to approximate the circle. + /// + public static void DrawLines(this Circle c, float lineThickness, ColorRgba color, float smoothness = 0.5f) { - int sides = GetCircleSideCount(c.Radius, sideLength); - Raylib.DrawPolyLinesEx(c.Center, sides, c.Radius, 0f, lineThickness * 2, color.ToRayColor()); + if (c.Radius < lineThickness) + { + var circle = c.SetRadius(lineThickness * 2); + circle.Draw(color, smoothness); + return; + } + if (!CalculateCircleDrawingParameters(c.Radius, smoothness, out int sides)) return; + Raylib.DrawPolyLinesEx(c.Center, sides, c.Radius + lineThickness, 0f, lineThickness * 2, color.ToRayColor()); } /// - /// Draws the outline of a circle using the specified line drawing info and rotation, automatically determining the number of sides based on side length. + /// Draws the outline of the circle. /// /// The circle to draw. - /// The line drawing parameters. /// The rotation of the circle in degrees. - /// The maximum length of each side. Default is 8. - public static void DrawLines(this Circle c, LineDrawingInfo lineInfo, float rotDeg, float sideLength = 8f) - { - int sides = GetCircleSideCount(c.Radius, sideLength); - DrawLines(c, lineInfo, rotDeg, sides); - } - - /// - /// Draws a partial outline of a circle based on the specified percentage, line thickness, and color, automatically determining the number of sides. - /// - /// The circle to draw. - /// The percentage of the outline to draw. /// The thickness of the outline. /// The color of the outline. - /// The maximum length of each side. Default is 8. - public static void DrawLinesPercentage(this Circle c, float f, float lineThickness, ColorRgba color, float sideLength = 8f) + /// + /// The smoothness value (0-1). This controls the visual quality of the circle by inversely interpolating the current . + /// A value of 0 uses the maximum side length (fewer sides, less smooth), while 1 uses the minimum side length (more sides, smoother). + /// The resulting side length determines the number of polygon sides used to approximate the circle. + /// + public static void DrawLines(this Circle c, float rotDeg, float lineThickness, ColorRgba color, float smoothness = 0.5f) { - if (f == 0) return; - int sides = GetCircleSideCount(c.Radius, sideLength); - DrawLinesPercentage(c, f, lineThickness, 0f, sides, color, LineCapType.None, 0); + if (c.Radius < lineThickness) + { + var circle = c.SetRadius(lineThickness * 2); + circle.Draw(color, smoothness); + return; + } + if (!CalculateCircleDrawingParameters(c.Radius, smoothness, out int sides)) return; + Raylib.DrawPolyLinesEx(c.Center, sides, c.Radius + lineThickness, rotDeg, lineThickness * 2, color.ToRayColor()); } /// - /// Draws a partial outline of a circle based on the specified percentage, line thickness, rotation, color, cap type, and cap points, automatically determining the number of sides. + /// Draws the outline of the circle with detailed line drawing options. /// /// The circle to draw. - /// The percentage of the outline to draw. - /// The thickness of the outline. - /// The rotation of the circle in degrees. - /// The color of the outline. - /// The type of line cap to use at the ends. - /// The number of points used to draw the end cap. - /// The maximum length of each side. Default is 8. - public static void DrawLinesPercentage(this Circle c, float f, float lineThickness, float rotDeg, ColorRgba color, LineCapType capType, int capPoints, float sideLength = 8f) + /// Contains line drawing parameters (thickness, color, caps, etc.). + /// + /// The smoothness value (0-1). This controls the visual quality of the circle by inversely interpolating the current . + /// A value of 0 uses the maximum side length (fewer sides, less smooth), while 1 uses the minimum side length (more sides, smoother). + /// The resulting side length determines the number of polygon sides used to approximate the circle. + /// + public static void DrawLines(this Circle c, LineDrawingInfo lineInfo, float smoothness = 0.5f) { - if (f == 0) return; - int sides = GetCircleSideCount(c.Radius, sideLength); - DrawLinesPercentage(c, f, lineThickness, rotDeg, sides, color, capType, capPoints); + if (c.Radius < lineInfo.Thickness) + { + var circle = c.SetRadius(lineInfo.Thickness * 2); + circle.Draw(lineInfo.Color, smoothness); + return; + } + if (!CalculateCircleDrawingParameters(c.Radius, smoothness, out int sides)) return; + var lineThickness = lineInfo.Thickness; + Raylib.DrawPolyLinesEx(c.Center, sides, c.Radius + lineThickness, 0f, lineThickness * 2, lineInfo.Color.ToRayColor()); } /// - /// Draws a partial outline of a circle based on the specified percentage, line drawing info, and rotation, automatically determining the number of sides. + /// Draws the outline of the circle with detailed line drawing options. /// /// The circle to draw. - /// The percentage of the outline to draw. - /// The line drawing parameters. /// The rotation of the circle in degrees. - /// The maximum length of each side. Default is 8. - public static void DrawLinesPercentage(this Circle c, float f, LineDrawingInfo lineInfo, float rotDeg, float sideLength = 8f) + /// Contains line drawing parameters (thickness, color, caps, etc.). + /// + /// The smoothness value (0-1). This controls the visual quality of the circle by inversely interpolating the current . + /// A value of 0 uses the maximum side length (fewer sides, less smooth), while 1 uses the minimum side length (more sides, smoother). + /// The resulting side length determines the number of polygon sides used to approximate the circle. + /// + public static void DrawLines(this Circle c, float rotDeg, LineDrawingInfo lineInfo, float smoothness = 0.5f) { - if (f == 0) return; - int sides = GetCircleSideCount(c.Radius, sideLength); - DrawLinesPercentage(c, f, lineInfo, rotDeg, sides); + if (c.Radius < lineInfo.Thickness) + { + var circle = c.SetRadius(lineInfo.Thickness * 2); + circle.Draw(lineInfo.Color, smoothness); + return; + } + if (!CalculateCircleDrawingParameters(c.Radius, smoothness, out int sides)) return; + var lineThickness = lineInfo.Thickness; + Raylib.DrawPolyLinesEx(c.Center, sides, c.Radius + lineThickness, rotDeg, lineThickness * 2, lineInfo.Color.ToRayColor()); } + #endregion + + #region Draw Lines Scaled /// - /// Draws a circle outline where each side can be scaled towards the origin of the side. + /// Draws the outline of the circle where each segment is scaled towards the segment center. /// /// The circle to draw. - /// The line drawing parameters. - /// The number of sides used to approximate the circle. - /// The scale factor for each side (0 = no circle, 1 = normal circle). - /// The point along each circle segment to scale from in both directions (0-1, default 0.5). - public static void DrawLinesScaled(this Circle c, LineDrawingInfo lineInfo, int sides, float sideScaleFactor, float sideScaleOrigin = 0.5f) + /// Contains line drawing parameters (thickness, color, caps, etc.). + /// + /// The smoothness value (0-1). This controls the visual quality of the circle by inversely interpolating the current . + /// A value of 0 uses the maximum side length (fewer sides, less smooth), while 1 uses the minimum side length (more sides, smoother). + /// The resulting side length determines the number of polygon sides used to approximate the circle. + /// + /// Scale factor for each side segment (0 = invisible, 1 = full connected circle). + /// The point on the segment to scale from (0-1, default 0.5 is center). + public static void DrawLinesScaled(this Circle c, LineDrawingInfo lineInfo, float smoothness, float sideScaleFactor, float sideScaleOrigin = 0.5f) { - DrawLinesScaled(c, lineInfo, 0f, sides, sideScaleFactor, sideScaleOrigin); + DrawLinesScaled(c, 0f, lineInfo, smoothness, sideScaleFactor, sideScaleOrigin); } - + /// - /// Draws a circle outline where each side can be scaled towards the origin of the side, with rotation. + /// Draws the outline of the circle where each segment is scaled towards the segment center. /// /// The circle to draw. - /// The line drawing parameters. /// The rotation of the circle in degrees. - /// The number of sides used to approximate the circle. - /// The scale factor for each side (0 = no circle, 1 = normal circle). - /// The point along each circle segment to scale from in both directions (0-1, default 0.5). - public static void DrawLinesScaled(this Circle c, LineDrawingInfo lineInfo, float rotDeg, int sides, float sideScaleFactor, float sideScaleOrigin = 0.5f) + /// Contains line drawing parameters (thickness, color, caps, etc.). + /// + /// The smoothness value (0-1). This controls the visual quality of the circle by inversely interpolating the current . + /// A value of 0 uses the maximum side length (fewer sides, less smooth), while 1 uses the minimum side length (more sides, smoother). + /// The resulting side length determines the number of polygon sides used to approximate the circle. + /// + /// Scale factor for each side segment (0 = invisible, 1 = full connected circle). + /// The point on the segment to scale from (0-1, default 0.5 is center). + public static void DrawLinesScaled(this Circle c, float rotDeg, LineDrawingInfo lineInfo, float smoothness, float sideScaleFactor, float sideScaleOrigin = 0.5f) { + if (c.Radius < lineInfo.Thickness) + { + var circle = c.SetRadius(lineInfo.Thickness * 2); + circle.DrawScaled(rotDeg, lineInfo.Color, smoothness, sideScaleFactor, sideScaleOrigin); + return; + } if (sideScaleFactor <= 0f) return; if (sideScaleFactor >= 1f) { - DrawLines(c, lineInfo, sides); + c.DrawLines(lineInfo, smoothness); return; } - - var angleStep = (2f * ShapeMath.PI) / sides; + + if (!CalculateCircleDrawingParameters(c.Radius, smoothness, out float angleStep, out int sides)) return; + var rotRad = rotDeg * ShapeMath.DEGTORAD; for (int i = 0; i < sides; i++) @@ -433,516 +292,632 @@ public static void DrawLinesScaled(this Circle c, LineDrawingInfo lineInfo, floa SegmentDrawing.DrawSegment(start, end, lineInfo, sideScaleFactor, sideScaleOrigin); } } + + #endregion + + #region Draw Lines Percentage /// - /// Draws part of a circle outline depending on f. + /// Draws the outline of a circle sector based on a percentage value. /// - /// The center of the circle. - /// The radius of the circle. - /// The percentage of the outline to draw. A positive value goes counter-clockwise - /// and a negative value goes clockwise. - /// The line drawing parameters. - /// The rotation of the circle. The lower the resolution of the circle the more visible is rotation - /// The resolution of the circle. The more sides are used the closer it represents a circle. - /// The color of the line. - /// The end cap type of the line. - /// How many points are used to draw the end cap. + /// The circle to draw. + /// The fill percentage (0 to 1 or 0 to -1). Positive values are clockwise, negative are counter-clockwise. + /// The rotation offset in degrees. + /// The thickness of the outline. + /// The color of the outline. + /// + /// The smoothness value (0-1). This controls the visual quality of the circle by inversely interpolating the current . + /// A value of 0 uses the maximum side length (fewer sides, less smooth), while 1 uses the minimum side length (more sides, smoother). + /// The resulting side length determines the number of polygon sides used to approximate the circle. + /// /// - /// Useful for drawing progress arcs or partial circles. + /// This function does not clamp the resulting side count with . /// - public static void DrawCircleLinesPercentage(Vector2 center, float radius, float f, float lineThickness, float rotDeg, int sides, ColorRgba color, LineCapType lineCapType, int capPoints) + public static void DrawLinesPercentage(this Circle c, float f, float rotDeg, float lineThickness, ColorRgba color, float smoothness = 0.5f) { - if (sides < 3 || f == 0 || radius <= 0) return; + if (f == 0) return; - float angleStep; // = (2f * ShapeMath.PI) / sides; - float percentage; // = ShapeMath.Clamp(negative ? f * -1 : f, 0f, 1f); - if (f < 0) - { - angleStep = (-2f * ShapeMath.PI) / sides; - percentage = ShapeMath.Clamp(-f, 0f, 1f); - } - else + if (MathF.Abs(f) >= 1f) { - angleStep = (2f * ShapeMath.PI) / sides; - percentage = ShapeMath.Clamp(f, 0f, 1f); + c.DrawLines(rotDeg, lineThickness, color, smoothness); + return; } - - var rotRad = rotDeg * ShapeMath.DEGTORAD; - var perimeter = Circle.GetCircumference(radius); - var sideLength = perimeter / sides; - var perimeterToDraw = perimeter * percentage; - for (int i = 0; i < sides; i++) + + if (TransformPercentageToAngles(f, out float startAngleDeg, out float endAngleDeg)) { - var nextIndex = (i + 1) % sides; - var curP = center + new Vector2(radius, 0f).Rotate(rotRad + angleStep * i); - var nextP = center + new Vector2(radius, 0f).Rotate(rotRad + angleStep * nextIndex); - - if (sideLength > perimeterToDraw) - { - nextP = curP.Lerp(nextP, perimeterToDraw / sideLength); - SegmentDrawing.DrawSegment(curP, nextP, lineThickness, color, lineCapType, capPoints); - return; - } - else - { - SegmentDrawing.DrawSegment(curP, nextP, lineThickness, color, lineCapType, capPoints); - perimeterToDraw -= sideLength; - } + c.DrawSectorLines(startAngleDeg, endAngleDeg, rotDeg, lineThickness, color, smoothness); } } - + /// - /// Very usefull for drawing small/tiny circles. Drawing the circle as rect increases performance a lot. + /// Draws the outline of a circle sector based on a percentage value. /// - /// The center of the circle. - /// The radius of the circle. - /// The color of the circle. + /// The circle to draw. + /// The fill percentage (0 to 1 or 0 to -1). Positive values are clockwise, negative are counter-clockwise. + /// The rotation offset in degrees. + /// Contains line drawing parameters (thickness, color, caps, etc.). + /// + /// The smoothness value (0-1). This controls the visual quality of the circle by inversely interpolating the current . + /// A value of 0 uses the maximum side length (fewer sides, less smooth), while 1 uses the minimum side length (more sides, smoother). + /// The resulting side length determines the number of polygon sides used to approximate the circle. + /// /// - /// This method is optimized for performance and is useful for drawing tiny circles. Draws a rect! + /// This function does not clamp the resulting side count with . /// - public static void DrawCircleFast(Vector2 center, float radius, ColorRgba color) + public static void DrawLinesPercentage(this Circle c, float f, float rotDeg, LineDrawingInfo lineInfo, float smoothness = 0.5f) { - RectDrawing.DrawRect(center - new Vector2(radius, radius), center + new Vector2(radius, radius), color); - } + // DrawLinesPercentage(c, f, lineInfo.Thickness, rotDeg, lineInfo.Color, lineInfo.CapType, lineInfo.CapPoints, smoothness); + if (f == 0) return; - /// - /// Draws the outline of a circle at the specified center and radius using the given line thickness, number of sides, and color. - /// - /// The center of the circle. - /// The radius of the circle. - /// The thickness of the outline. - /// The number of sides used to approximate the circle. - /// The color of the outline. - public static void DrawCircleLines(Vector2 center, float radius, float lineThickness, int sides, ColorRgba color) - => Raylib.DrawPolyLinesEx(center, sides, radius, 0f, lineThickness * 2, color.ToRayColor()); - - /// - /// Draws the outline of a circle at the specified center and radius using the given line drawing info and number of sides. - /// - /// The center of the circle. - /// The radius of the circle. - /// The line drawing parameters. - /// The number of sides used to approximate the circle. - public static void DrawCircleLines(Vector2 center, float radius, LineDrawingInfo lineInfo, int sides) - => DrawCircleLines(center, radius, lineInfo, 0f, sides); - - /// - /// Draws a partial outline of a circle at the specified center and radius based on the given percentage and line drawing info. - /// - /// The center of the circle. - /// The radius of the circle. - /// The percentage of the outline to draw. - /// The line drawing parameters. - /// The rotation of the circle in degrees. - /// The number of sides used to approximate the circle. - public static void DrawCircleLinesPercentage(Vector2 center, float radius, float f, LineDrawingInfo lineInfo, float rotDeg, int sides) - => DrawCircleLinesPercentage(center, radius, f, lineInfo.Thickness, rotDeg, sides, lineInfo.Color, lineInfo.CapType, lineInfo.CapPoints); - - /// - /// Draws the outline of a circle at the specified center and radius using the given line thickness, rotation, number of sides, and color. - /// - /// The center of the circle. - /// The radius of the circle. - /// The thickness of the outline. - /// The rotation of the circle in degrees. - /// The number of sides used to approximate the circle. - /// The color of the outline. - public static void DrawCircleLines(Vector2 center, float radius, float lineThickness, float rotDeg, int sides, ColorRgba color) - => Raylib.DrawPolyLinesEx(center, sides, radius, rotDeg, lineThickness * 2, color.ToRayColor()); - - /// - /// Draws the outline of a circle at the specified center and radius using the given line drawing info, rotation, and number of sides. - /// - /// The center of the circle. - /// The radius of the circle. - /// The line drawing parameters. - /// The rotation of the circle in degrees. - /// The number of sides used to approximate the circle. - public static void DrawCircleLines(Vector2 center, float radius, LineDrawingInfo lineInfo, float rotDeg, int sides) - { - if (sides < 3) return; - var angleStep = (2f * ShapeMath.PI) / sides; - var rotRad = rotDeg * ShapeMath.DEGTORAD; - - for (int i = 0; i < sides; i++) + if (MathF.Abs(f) >= 1f) { - var nextIndex = (i + 1) % sides; - var curP = center + new Vector2(radius, 0f).Rotate(rotRad + angleStep * i); - var nextP = center + new Vector2(radius, 0f).Rotate(rotRad + angleStep * nextIndex); - - SegmentDrawing.DrawSegment(curP, nextP, lineInfo); + c.DrawLines(rotDeg, lineInfo, smoothness); + return; + } + + if (TransformPercentageToAngles(f, out float startAngleDeg, out float endAngleDeg)) + { + c.DrawSectorLines(startAngleDeg, endAngleDeg, rotDeg, lineInfo, smoothness); } } + #endregion + + #region Draw Sector /// - /// Draws the outline of a circle at the specified center and radius using the given line thickness and color, automatically determining the number of sides. - /// - /// The center of the circle. - /// The radius of the circle. - /// The thickness of the outline. - /// The color of the outline. - /// The maximum length of each side. Default is 8. - public static void DrawCircleLines(Vector2 center, float radius, float lineThickness, ColorRgba color, float sideLength = 8f) - { - int sides = GetCircleSideCount(radius, sideLength); - Raylib.DrawPolyLinesEx(center, sides, radius, 0f, lineThickness * 2, color.ToRayColor()); - } - - /// - /// Draws the outline of a circle at the specified center and radius using the given line drawing info and rotation, automatically determining the number of sides. - /// - /// The center of the circle. - /// The radius of the circle. - /// The line drawing parameters. - /// The rotation of the circle in degrees. - /// The maximum length of each side. Default is 8. - public static void DrawCircleLines(Vector2 center, float radius, LineDrawingInfo lineInfo, float rotDeg, float sideLength = 8f) - { - int sides = GetCircleSideCount(radius, sideLength); - DrawCircleLines(center, radius, lineInfo, rotDeg, sides); - } - - /// - /// Draws a partial outline of a circle at the specified center and radius based on the given percentage, line drawing info, and rotation, automatically determining the number of sides. - /// - /// The center of the circle. - /// The radius of the circle. - /// The percentage of the outline to draw. - /// The line drawing parameters. - /// The rotation of the circle in degrees. - /// The maximum length of each side. Default is 8. - public static void DrawCircleLinesPercentage(Vector2 center, float radius, float f, LineDrawingInfo lineInfo, float rotDeg, float sideLength = 8f) - { - int sides = GetCircleSideCount(radius, sideLength); - DrawCircleLinesPercentage(center, radius, f, lineInfo, rotDeg, sides); - } - - /// - /// Draws a filled sector of a circle using the specified instance, start and end angles, segment count, and color. + /// Draws a filled sector of the circle. /// /// The circle to draw. /// The starting angle of the sector in degrees. /// The ending angle of the sector in degrees. - /// The number of segments used to approximate the sector. /// The color of the sector. - public static void DrawSector(this Circle c, float startAngleDeg, float endAngleDeg, int segments, ColorRgba color) + /// + /// The smoothness value (0-1). This controls the visual quality of the circle by inversely interpolating the current . + /// A value of 0 uses the maximum side length (fewer sides, less smooth), while 1 uses the minimum side length (more sides, smoother). + /// The resulting side length determines the number of polygon sides used to approximate the circle. + /// + /// + /// This function does not clamp the resulting side count with . + /// + public static void DrawSector(this Circle c, float startAngleDeg, float endAngleDeg, ColorRgba color, float smoothness = 0.5f) { - Raylib.DrawCircleSector(c.Center, c.Radius, startAngleDeg, endAngleDeg, segments, color.ToRayColor()); + if (!CalculateCircleDrawingParameters(c.Radius, startAngleDeg, endAngleDeg, smoothness, out float angleDifRad, out float _, out int sides, false)) return; + Raylib.DrawCircleSector(c.Center, c.Radius, startAngleDeg, startAngleDeg + (angleDifRad * ShapeMath.RADTODEG), sides, color.ToRayColor()); } - + /// - /// Draws a filled sector of a circle at the specified center and radius. + /// Draws a filled sector of the circle. /// - /// The center of the circle. - /// The radius of the circle. + /// The circle to draw. /// The starting angle of the sector in degrees. /// The ending angle of the sector in degrees. - /// The number of segments used to approximate the sector. + /// The rotation offset in degrees. /// The color of the sector. - public static void DrawCircleSector(Vector2 center, float radius, float startAngleDeg, float endAngleDeg, int segments, ColorRgba color) + /// + /// The smoothness value (0-1). This controls the visual quality of the circle by inversely interpolating the current . + /// A value of 0 uses the maximum side length (fewer sides, less smooth), while 1 uses the minimum side length (more sides, smoother). + /// The resulting side length determines the number of polygon sides used to approximate the circle. + /// + /// + /// This function does not clamp the resulting side count with . + /// + public static void DrawSector(this Circle c, float startAngleDeg, float endAngleDeg, float rotDeg, ColorRgba color, float smoothness = 0.5f) { - Raylib.DrawCircleSector(center, radius, startAngleDeg, endAngleDeg, segments, color.ToRayColor()); + c.DrawSector(startAngleDeg + rotDeg, endAngleDeg + rotDeg, color, smoothness); } + #endregion + + #region Draw Sector Scaled /// - /// Draws the outline of a sector of a circle using the specified instance, start and end angles, line drawing info, and optional closure and side length. + /// Draws a filled sector of the circle where the outside edge of each (pie slice) segment is scaled towards the point determined by . /// /// The circle to draw. /// The starting angle of the sector in degrees. /// The ending angle of the sector in degrees. - /// The line drawing parameters. - /// Whether the sector should be closed (connect to the center). - /// The maximum length of each side. Default is 8. - public static void DrawSectorLines(this Circle c, float startAngleDeg, float endAngleDeg, LineDrawingInfo lineInfo, bool closed = true, float sideLength = 8f) + /// The rotation offset in degrees. + /// The color of the sector. + /// + /// The smoothness value (0-1). This controls the visual quality of the circle by inversely interpolating the current . + /// A value of 0 uses the maximum side length (fewer sides, less smooth), while 1 uses the minimum side length (more sides, smoother). + /// The resulting side length determines the number of polygon sides used to approximate the circle. + /// + /// Scale factor for each side segment (0 = invisible, 1 = full connected circle). + /// The point on the segment to scale from (0-1, default 0.5 is center). + /// + /// This function does not clamp the resulting side count with . + /// + public static void DrawSectorScaled(this Circle c, float startAngleDeg, float endAngleDeg, float rotDeg, ColorRgba color, float smoothness, float sideScaleFactor, float sideScaleOrigin = 0.5f) { - DrawCircleSectorLines(c.Center, c.Radius, startAngleDeg, endAngleDeg, lineInfo, closed, sideLength); - } + if (c.Radius <= 0f || sideScaleFactor <= 0f) return; + + if (sideScaleFactor >= 1f) + { + c.DrawSector(startAngleDeg, endAngleDeg, rotDeg, color, smoothness); + return; + } + + startAngleDeg = startAngleDeg + rotDeg; + if (!CalculateCircleDrawingParameters(c.Radius, startAngleDeg, endAngleDeg + rotDeg, smoothness, out float angleDifRad, out float angleStep, out int sides, false)) return; + + var absAngleDifRad = MathF.Abs(angleDifRad); + if (absAngleDifRad < 0.00001f) return; + + if (absAngleDifRad >= MathF.Tau) + { + c.DrawScaled(rotDeg, color, smoothness, sideScaleFactor, sideScaleOrigin); + return; + } + + float startAngleRad = startAngleDeg * ShapeMath.DEGTORAD; + var rayColor = color.ToRayColor(); + for (int i = 0; i < sides; i++) + { + var nextIndex = i + 1; + var start = c.Center + new Vector2(c.Radius, 0f).Rotate(startAngleRad + angleStep * i); + var end = c.Center + new Vector2(c.Radius, 0f).Rotate(startAngleRad + angleStep * nextIndex); + var scaledSegment = new Segment(start, end).ScaleSegment(sideScaleFactor, sideScaleOrigin); + + if (angleDifRad < 0) + { + Raylib.DrawTriangle(scaledSegment.Start, scaledSegment.End,c.Center, rayColor); + } + else + { + Raylib.DrawTriangle(c.Center, scaledSegment.End, scaledSegment.Start, rayColor); + } + + } + } + #endregion + + #region Draw Sector Lines + /// - /// Draws the outline of a sector of a circle using the specified instance, start and end angles, rotation offset, line drawing info, and optional closure and side length. + /// Draws the outline framework of a circle sector (perimeter lines including the radii). /// /// The circle to draw. /// The starting angle of the sector in degrees. /// The ending angle of the sector in degrees. /// The rotation offset in degrees. - /// The line drawing parameters. - /// Whether the sector should be closed (connect to the center). - /// The maximum length of each side. Default is 8. - public static void DrawSectorLines(this Circle c, float startAngleDeg, float endAngleDeg, float rotOffsetDeg, LineDrawingInfo lineInfo, bool closed = true, float sideLength = 8f) + /// Contains line drawing parameters (thickness, color, caps, etc.). + /// + /// The smoothness value (0-1). This controls the visual quality of the circle by inversely interpolating the current . + /// A value of 0 uses the maximum side length (fewer sides, less smooth), while 1 uses the minimum side length (more sides, smoother). + /// The resulting side length determines the number of polygon sides used to approximate the circle. + /// + /// + /// This function does not clamp the resulting side count with . + /// + public static void DrawSectorLinesClosed(this Circle c, float startAngleDeg, float endAngleDeg, float rotOffsetDeg, LineDrawingInfo lineInfo, float smoothness = 0.5f) { - DrawCircleSectorLines(c.Center, c.Radius, startAngleDeg + rotOffsetDeg, endAngleDeg + rotOffsetDeg, lineInfo, closed, sideLength); + var color = lineInfo.Color.SetAlpha(255); + if (c.Radius < lineInfo.Thickness) + { + var circle = c.SetRadius(lineInfo.Thickness * 2); + circle.DrawSector(startAngleDeg, endAngleDeg, rotOffsetDeg, lineInfo.Color, smoothness); + return; + } + + startAngleDeg = startAngleDeg + rotOffsetDeg; + if (!CalculateCircleDrawingParameters(c.Radius, startAngleDeg, endAngleDeg + rotOffsetDeg, smoothness, out float angleDifRad, out float _, out int sides, false)) return; + endAngleDeg = startAngleDeg + angleDifRad * ShapeMath.RADTODEG; + + var absAngleDifRad = MathF.Abs(angleDifRad); + if (absAngleDifRad < 0.00001f) return; + + var start = c.Center + (ShapeVec.Right() * c.Radius).Rotate(startAngleDeg * ShapeMath.DEGTORAD); + SegmentDrawing.DrawSegment(c.Center, start, lineInfo.Thickness, color, LineCapType.CappedExtended, lineInfo.CapPoints); + + if (absAngleDifRad >= MathF.Tau) + { + c.DrawLines(rotOffsetDeg, lineInfo.Thickness, color, smoothness); + return; + } + + var end = c.Center + (ShapeVec.Right() * c.Radius).Rotate(endAngleDeg * ShapeMath.DEGTORAD); + SegmentDrawing.DrawSegment(c.Center, end, lineInfo.Thickness, color, LineCapType.CappedExtended, lineInfo.CapPoints); + + Raylib.DrawRing(c.Center, c.Radius - lineInfo.Thickness, c.Radius + lineInfo.Thickness, startAngleDeg, endAngleDeg, sides, color.ToRayColor()); } - + /// - /// Draws the outline of a sector of a circle using the specified instance, start and end angles, number of sides, line drawing info, and optional closure. + /// Draws the outline arc of a circle sector (only the curved part). /// /// The circle to draw. /// The starting angle of the sector in degrees. /// The ending angle of the sector in degrees. - /// The number of sides used to approximate the sector. - /// The line drawing parameters. - /// Whether the sector should be closed (connect to the center). - public static void DrawSectorLines(this Circle c, float startAngleDeg, float endAngleDeg, int sides, LineDrawingInfo lineInfo, bool closed = true) + /// The rotation offset in degrees. + /// Contains line drawing parameters (thickness, color, caps, etc.). + /// + /// The smoothness value (0-1). This controls the visual quality of the circle by inversely interpolating the current . + /// A value of 0 uses the maximum side length (fewer sides, less smooth), while 1 uses the minimum side length (more sides, smoother). + /// The resulting side length determines the number of polygon sides used to approximate the circle. + /// + /// + /// This function does not clamp the resulting side count with . + /// + public static void DrawSectorLines(this Circle c, float startAngleDeg, float endAngleDeg, float rotOffsetDeg, LineDrawingInfo lineInfo, float smoothness = 0.5f) { - DrawCircleSectorLines(c.Center, c.Radius, startAngleDeg, endAngleDeg, sides, lineInfo, closed); - } + if (c.Radius < lineInfo.Thickness) + { + var circle = c.SetRadius(lineInfo.Thickness * 2); + circle.DrawSector(startAngleDeg, endAngleDeg, rotOffsetDeg, lineInfo.Color, smoothness); + return; + } + + startAngleDeg = startAngleDeg + rotOffsetDeg; + if (!CalculateCircleDrawingParameters(c.Radius, startAngleDeg, endAngleDeg + rotOffsetDeg, smoothness, out float angleDifRad, out float _, out int sides, false)) return; + endAngleDeg = startAngleDeg + angleDifRad * ShapeMath.RADTODEG; + + var absAngleDifRad = MathF.Abs(angleDifRad); + if (absAngleDifRad < 0.00001f) return; + if (absAngleDifRad >= MathF.Tau) + { + c.DrawLines(rotOffsetDeg, lineInfo, smoothness); + return; + } + + var lineCapType = lineInfo.CapType; + var capPoints = lineInfo.CapPoints; + var drawCap = (lineCapType is LineCapType.Capped or LineCapType.CappedExtended) && capPoints > 0; + if (drawCap) + { + var startAngleRad = startAngleDeg * ShapeMath.DEGTORAD; + var endAngleRad = endAngleDeg * ShapeMath.DEGTORAD; + + var p1Inner = c.Center + (ShapeVec.Right() * (c.Radius - lineInfo.Thickness)).Rotate(startAngleRad); + var p1Outer = c.Center + (ShapeVec.Right() * (c.Radius + lineInfo.Thickness)).Rotate(startAngleRad); + + var p2Inner = c.Center + (ShapeVec.Right() * (c.Radius - lineInfo.Thickness)).Rotate(endAngleRad); + var p2Outer = c.Center + (ShapeVec.Right() * (c.Radius + lineInfo.Thickness)).Rotate(endAngleRad); + + if (angleDifRad > 0) + { + SegmentDrawing.DrawRoundCap(p2Inner, p2Outer, lineInfo.CapPoints, lineInfo.Color); + SegmentDrawing.DrawRoundCap(p1Outer, p1Inner, lineInfo.CapPoints, lineInfo.Color); + } + else + { + SegmentDrawing.DrawRoundCap(p2Outer, p2Inner, lineInfo.CapPoints, lineInfo.Color); + SegmentDrawing.DrawRoundCap(p1Inner, p1Outer, lineInfo.CapPoints, lineInfo.Color); + } + } + + Raylib.DrawRing(c.Center, c.Radius - lineInfo.Thickness, c.Radius + lineInfo.Thickness, startAngleDeg, endAngleDeg, sides, lineInfo.Color.ToRayColor()); + + } + /// - /// Draws the outline of a sector of a circle using the specified instance, start and end angles, rotation offset, number of sides, line drawing info, and optional closure. + /// Draws the outline arc of a circle sector (only the curved part). /// /// The circle to draw. /// The starting angle of the sector in degrees. /// The ending angle of the sector in degrees. /// The rotation offset in degrees. - /// The number of sides used to approximate the sector. - /// The line drawing parameters. - /// Whether the sector should be closed (connect to the center). - public static void DrawSectorLines(this Circle c, float startAngleDeg, float endAngleDeg, float rotOffsetDeg, int sides, LineDrawingInfo lineInfo, bool closed = true) + /// The thickness of the outline. + /// The color of the outline. + /// + /// The smoothness value (0-1). This controls the visual quality of the circle by inversely interpolating the current . + /// A value of 0 uses the maximum side length (fewer sides, less smooth), while 1 uses the minimum side length (more sides, smoother). + /// The resulting side length determines the number of polygon sides used to approximate the circle. + /// + /// + /// This function does not clamp the resulting side count with . + /// + public static void DrawSectorLines(this Circle c, float startAngleDeg, float endAngleDeg, float rotOffsetDeg, float lineThickness, ColorRgba color, float smoothness = 0.5f) { - DrawCircleSectorLines(c.Center, c.Radius, startAngleDeg + rotOffsetDeg, endAngleDeg + rotOffsetDeg, sides, lineInfo, closed); + if (c.Radius < lineThickness) + { + var circle = c.SetRadius(lineThickness * 2); + circle.DrawSector(startAngleDeg, endAngleDeg, rotOffsetDeg, color, smoothness); + return; + } + if (!CalculateCircleDrawingParameters(c.Radius, startAngleDeg + rotOffsetDeg, endAngleDeg + rotOffsetDeg, smoothness, out float angleDifRad, out float _, out int sides, false)) return; + Raylib.DrawRing(c.Center, c.Radius - lineThickness, c.Radius + lineThickness, startAngleDeg + rotOffsetDeg, startAngleDeg + rotOffsetDeg + (angleDifRad * ShapeMath.RADTODEG), sides, color.ToRayColor()); } - + #endregion + + #region Draw Sector Lines Scaled + /// - /// Draws a sector outline where each side can be scaled towards the origin of the side. + /// Draws the outline of a circle sector where each segment is scaled. /// /// The circle to draw. - /// The line drawing parameters. /// The starting angle of the sector in degrees. /// The ending angle of the sector in degrees. /// The rotation offset in degrees. - /// The number of sides used to approximate the sector. - /// The scale factor for each side (0 = no sector, 1 = normal sector). - /// The point along each circle segment to scale from in both directions (0-1, default 0.5). - /// Whether the sector should be closed (connect to the center). - public static void DrawSectorLinesScaled(this Circle c, LineDrawingInfo lineInfo, float startAngleDeg, float endAngleDeg, float rotOffsetDeg, int sides, float sideScaleFactor, float sideScaleOrigin = 0.5f, bool closed = true) + /// Contains line drawing parameters (thickness, color, caps, etc.). + /// + /// The smoothness value (0-1). This controls the visual quality of the circle by inversely interpolating the current . + /// A value of 0 uses the maximum side length (fewer sides, less smooth), while 1 uses the minimum side length (more sides, smoother). + /// The resulting side length determines the number of polygon sides used to approximate the circle. + /// + /// Scale factor for each side segment (0 = invisible, 1 = full connected circle). + /// The point on the segment to scale from (0-1, default 0.5 is center). + /// + /// This function does not clamp the resulting side count with . + /// + public static void DrawSectorLinesScaledClosed(this Circle c, float startAngleDeg, float endAngleDeg, float rotOffsetDeg, LineDrawingInfo lineInfo, float smoothness, float sideScaleFactor, float sideScaleOrigin = 0.5f) { + if (c.Radius < lineInfo.Thickness) + { + var circle = c.SetRadius(lineInfo.Thickness * 2); + circle.DrawSectorScaled(startAngleDeg, endAngleDeg, rotOffsetDeg, lineInfo.Color, smoothness, sideScaleFactor, sideScaleOrigin); + return; + } if (sideScaleFactor <= 0f) return; + if (sideScaleFactor >= 1f) { - DrawSectorLines(c, startAngleDeg, endAngleDeg, rotOffsetDeg, sides, lineInfo, closed); + DrawSectorLinesClosed(c, startAngleDeg, endAngleDeg, rotOffsetDeg, lineInfo, smoothness); return; } - - float startAngleRad = (startAngleDeg + rotOffsetDeg) * ShapeMath.DEGTORAD; - float endAngleRad = (endAngleDeg + rotOffsetDeg) * ShapeMath.DEGTORAD; - float anglePiece = endAngleRad - startAngleRad; - float angleStep = anglePiece / sides; - if (closed) + startAngleDeg = startAngleDeg + rotOffsetDeg; + if (!CalculateCircleDrawingParameters(c.Radius, startAngleDeg, endAngleDeg + rotOffsetDeg, smoothness, out float angleDifRad, out float angleStep, out int sides, false)) return; + endAngleDeg = startAngleDeg + angleDifRad * ShapeMath.RADTODEG; + + var absAngleDifRad = MathF.Abs(angleDifRad); + if (absAngleDifRad < 0.00001f) return; + + float startAngleRad = startAngleDeg * ShapeMath.DEGTORAD; + float endAngleRad = endAngleDeg * ShapeMath.DEGTORAD; + + var sectorStart = c.Center + (ShapeVec.Right() * c.Radius).Rotate(startAngleRad); + SegmentDrawing.DrawSegment(c.Center, sectorStart, lineInfo, sideScaleFactor, sideScaleOrigin); + + if (absAngleDifRad >= MathF.Tau) { - var sectorStart = c.Center + (ShapeVec.Right() * c.Radius).Rotate(startAngleRad); - SegmentDrawing.DrawSegment(c.Center, sectorStart, lineInfo, sideScaleFactor, sideScaleOrigin); - - var sectorEnd = c.Center + (ShapeVec.Right() * c.Radius).Rotate(endAngleRad); - SegmentDrawing.DrawSegment(c.Center, sectorEnd, lineInfo, sideScaleFactor, sideScaleOrigin); + c.DrawLinesScaled(rotOffsetDeg, lineInfo, smoothness, sideScaleFactor, sideScaleOrigin); + return; } + + var sectorEnd = c.Center + (ShapeVec.Right() * c.Radius).Rotate(endAngleRad); + SegmentDrawing.DrawSegment(c.Center, sectorEnd, lineInfo, sideScaleFactor, sideScaleOrigin); + for (int i = 0; i < sides; i++) { - var nextIndex = (i + 1) % sides; + var nextIndex = i + 1; var start = c.Center + new Vector2(c.Radius, 0f).Rotate(startAngleRad + angleStep * i); var end = c.Center + new Vector2(c.Radius, 0f).Rotate(startAngleRad + angleStep * nextIndex); SegmentDrawing.DrawSegment(start, end, lineInfo, sideScaleFactor, sideScaleOrigin); } } - + /// - /// Draws the outline of a sector of a circle at the specified center and radius using the given line drawing info and optional closure and side length. + /// Draws the outline arc of a circle sector where each segment is scaled (only the curved part). /// - /// The center of the circle. - /// The radius of the circle. + /// The circle to draw. /// The starting angle of the sector in degrees. /// The ending angle of the sector in degrees. - /// The line drawing parameters. - /// Whether the sector should be closed (connect to the center). - /// The maximum length of each side. Default is 8. - public static void DrawCircleSectorLines(Vector2 center, float radius, float startAngleDeg, float endAngleDeg, LineDrawingInfo lineInfo, bool closed = true, float sideLength = 8f) + /// The rotation offset in degrees. + /// Contains line drawing parameters (thickness, color, caps, etc.). + /// + /// The smoothness value (0-1). This controls the visual quality of the circle by inversely interpolating the current . + /// A value of 0 uses the maximum side length (fewer sides, less smooth), while 1 uses the minimum side length (more sides, smoother). + /// The resulting side length determines the number of polygon sides used to approximate the circle. + /// + /// Scale factor for each side segment (0 = invisible, 1 = full connected circle). + /// The point on the segment to scale from (0-1, default 0.5 is center). + /// + /// This function does not clamp the resulting side count with . + /// + public static void DrawSectorLinesScaled(this Circle c, float startAngleDeg, float endAngleDeg, float rotOffsetDeg, LineDrawingInfo lineInfo, float smoothness, float sideScaleFactor, float sideScaleOrigin = 0.5f) { - float startAngleRad = startAngleDeg * ShapeMath.DEGTORAD; - float endAngleRad = endAngleDeg * ShapeMath.DEGTORAD; - float anglePiece = endAngleRad - startAngleRad; - int sides = GetCircleArcSideCount(radius, MathF.Abs(anglePiece * ShapeMath.RADTODEG), sideLength); - float angleStep = anglePiece / sides; - if (closed) + if (c.Radius < lineInfo.Thickness) { - var sectorStart = center + (ShapeVec.Right() * radius + new Vector2(lineInfo.Thickness / 2, 0)).Rotate(startAngleRad); - SegmentDrawing.DrawSegment(center, sectorStart, lineInfo); - - var sectorEnd = center + (ShapeVec.Right() * radius + new Vector2(lineInfo.Thickness / 2, 0)).Rotate(endAngleRad); - SegmentDrawing.DrawSegment(center, sectorEnd, lineInfo); + var circle = c.SetRadius(lineInfo.Thickness * 2); + circle.DrawSectorScaled(startAngleDeg, endAngleDeg, rotOffsetDeg, lineInfo.Color, smoothness, sideScaleFactor, sideScaleOrigin); + return; } - for (var i = 0; i < sides; i++) + if (sideScaleFactor <= 0f) return; + + if (sideScaleFactor >= 1f) { - var start = center + (ShapeVec.Right() * radius).Rotate(startAngleRad + angleStep * i); - var end = center + (ShapeVec.Right() * radius).Rotate(startAngleRad + angleStep * (i + 1)); - SegmentDrawing.DrawSegment(start, end, lineInfo); + c.DrawSectorLines(startAngleDeg, endAngleDeg, rotOffsetDeg, lineInfo, smoothness); + return; } - } - - /// - /// Draws the outline of a sector of a circle at the specified center and radius using the given rotation offset, line drawing info, and optional closure and side length. - /// - /// The center of the circle. - /// The radius of the circle. - /// The starting angle of the sector in degrees. - /// The ending angle of the sector in degrees. - /// The rotation offset in degrees. - /// The line drawing parameters. - /// Whether the sector should be closed (connect to the center). - /// The maximum length of each side. Default is 8. - public static void DrawCircleSectorLines(Vector2 center, float radius, float startAngleDeg, float endAngleDeg, float rotOffsetDeg, LineDrawingInfo lineInfo, bool closed = true, float sideLength = 8f) - { - DrawCircleSectorLines(center, radius, startAngleDeg + rotOffsetDeg, endAngleDeg + rotOffsetDeg, lineInfo, closed, sideLength); - } - - /// - /// Draws the outline of a sector of a circle at the specified center and radius using the given number of sides, line drawing info, and optional closure. - /// - /// The center of the circle. - /// The radius of the circle. - /// The starting angle of the sector in degrees. - /// The ending angle of the sector in degrees. - /// The number of sides used to approximate the sector. - /// The line drawing parameters. - /// Whether the sector should be closed (connect to the center). - public static void DrawCircleSectorLines(Vector2 center, float radius, float startAngleDeg, float endAngleDeg, int sides, LineDrawingInfo lineInfo, bool closed = true) - { - float startAngleRad = startAngleDeg * ShapeMath.DEGTORAD; - float endAngleRad = endAngleDeg * ShapeMath.DEGTORAD; - float anglePiece = endAngleDeg - startAngleRad; - float angleStep = MathF.Abs(anglePiece) / sides; - if (closed) + + startAngleDeg = startAngleDeg + rotOffsetDeg; + if (!CalculateCircleDrawingParameters(c.Radius, startAngleDeg, endAngleDeg + rotOffsetDeg, smoothness, out float angleDifRad, out float angleStep, out int sides, false)) return; + + var absAngleDifRad = MathF.Abs(angleDifRad); + if (absAngleDifRad < 0.00001f) return; + + if (absAngleDifRad >= MathF.Tau) { - var sectorStart = center + (ShapeVec.Right() * radius + new Vector2(lineInfo.Thickness / 2, 0)).Rotate(startAngleRad); - SegmentDrawing.DrawSegment(center, sectorStart, lineInfo); - - var sectorEnd = center + (ShapeVec.Right() * radius + new Vector2(lineInfo.Thickness / 2, 0)).Rotate(endAngleRad); - SegmentDrawing.DrawSegment(center, sectorEnd, lineInfo); + c.DrawLinesScaled(rotOffsetDeg, lineInfo, smoothness, sideScaleFactor, sideScaleOrigin); + return; } - for (var i = 0; i < sides; i++) + + float startAngleRad = startAngleDeg * ShapeMath.DEGTORAD; + for (int i = 0; i < sides; i++) { - var start = center + (ShapeVec.Right() * radius).Rotate(startAngleRad + angleStep * i); - var end = center + (ShapeVec.Right() * radius).Rotate(startAngleRad + angleStep * (i + 1)); - SegmentDrawing.DrawSegment(start, end, lineInfo); + var nextIndex = i + 1; + var start = c.Center + new Vector2(c.Radius, 0f).Rotate(startAngleRad + angleStep * i); + var end = c.Center + new Vector2(c.Radius, 0f).Rotate(startAngleRad + angleStep * nextIndex); + + SegmentDrawing.DrawSegment(start, end, lineInfo, sideScaleFactor, sideScaleOrigin); } } - + #endregion + + #region Draw Masked /// - /// Draws the outline of a sector of a circle at the specified center and radius using the given rotation offset, number of sides, line drawing info, and optional closure. + /// Draws the circle outline as individual line segments clipped by a mask. /// - /// The center of the circle. - /// The radius of the circle. - /// The starting angle of the sector in degrees. - /// The ending angle of the sector in degrees. - /// The rotation offset in degrees. - /// The number of sides used to approximate the sector. - /// The line drawing parameters. - /// Whether the sector should be closed (connect to the center). - public static void DrawCircleSectorLines(Vector2 center, float radius, float startAngleDeg, float endAngleDeg, float rotOffsetDeg, int sides, LineDrawingInfo lineInfo, bool closed = true) + /// The source whose circumference will be approximated by segments. + /// The used to mask (clip) each generated segment. + /// Parameters controlling line drawing (thickness, color, cap type, etc.). + /// Rotation offset in degrees applied to the entire approximated polygon. + /// + /// The smoothness value (0-1). This controls the visual quality of the circle by inversely interpolating the current . + /// A value of 0 uses the maximum side length (fewer sides, less smooth), while 1 uses the minimum side length (more sides, smoother). + /// The resulting side length determines the number of polygon sides used to approximate the circle. + /// + /// If true, draws the parts inside the mask instead of outside. + public static void DrawLinesMasked(this Circle circle, Triangle mask, LineDrawingInfo lineInfo, float rotDeg, float smoothness, bool reversedMask = false) { - DrawCircleSectorLines(center, radius, startAngleDeg + rotOffsetDeg, endAngleDeg + rotOffsetDeg, sides, lineInfo, closed); + if (!CalculateCircleDrawingParameters(circle.Radius, smoothness, out float angleStep, out int sides)) return; + var rotRad = rotDeg * ShapeMath.DEGTORAD; + var radius = circle.Radius; + var center = circle.Center; + for (int i = 0; i < sides; i++) + { + var nextIndex = (i + 1) % sides; + var curP = center + new Vector2(radius, 0f).Rotate(rotRad + angleStep * i); + var nextP = center + new Vector2(radius, 0f).Rotate(rotRad + angleStep * nextIndex); + var segment = new Segment(curP, nextP); + segment.DrawMasked(mask, lineInfo, reversedMask); + } } - /// - /// Draws the outline of a sector of a circle at the specified center and radius using the given line thickness, color, and optional closure and side length. + /// Draws the circle outline as individual line segments clipped by a mask. /// - /// The center of the circle. - /// The radius of the circle. - /// The starting angle of the sector in degrees. - /// The ending angle of the sector in degrees. - /// The thickness of the outline. - /// The color of the outline. - /// Whether the sector should be closed (connect to the center). - /// The maximum length of each side. Default is 8. - public static void DrawCircleSectorLines(Vector2 center, float radius, float startAngleDeg, float endAngleDeg, float lineThickness, ColorRgba color, bool closed = true, float sideLength = 8f) + /// The source whose circumference will be approximated by segments. + /// The used to mask (clip) each generated segment. + /// Parameters controlling line drawing (thickness, color, cap type, etc.). + /// Rotation offset in degrees applied to the entire approximated polygon. + /// + /// The smoothness value (0-1). This controls the visual quality of the circle by inversely interpolating the current . + /// A value of 0 uses the maximum side length (fewer sides, less smooth), while 1 uses the minimum side length (more sides, smoother). + /// The resulting side length determines the number of polygon sides used to approximate the circle. + /// + /// If true, draws the parts inside the mask instead of outside. + public static void DrawLinesMasked(this Circle circle, Circle mask, LineDrawingInfo lineInfo, float rotDeg, float smoothness, bool reversedMask = false) { - float startAngleRad = startAngleDeg * ShapeMath.DEGTORAD; - float endAngleRad = endAngleDeg * ShapeMath.DEGTORAD; - float anglePiece = endAngleRad - startAngleRad; - int sides = GetCircleArcSideCount(radius, MathF.Abs(anglePiece * ShapeMath.RADTODEG), sideLength); - float angleStep = anglePiece / sides; - if (closed) - { - var sectorStart = center + (ShapeVec.Right() * radius + new Vector2(lineThickness / 2, 0)).Rotate(startAngleRad); - SegmentDrawing.DrawSegment(center, sectorStart, lineThickness, color, LineCapType.CappedExtended, 4); - - var sectorEnd = center + (ShapeVec.Right() * radius + new Vector2(lineThickness / 2, 0)).Rotate(endAngleRad); - SegmentDrawing.DrawSegment(center, sectorEnd, lineThickness, color, LineCapType.CappedExtended, 4); - } - for (var i = 0; i < sides; i++) + if (!CalculateCircleDrawingParameters(circle.Radius, smoothness, out float angleStep, out int sides)) return; + var rotRad = rotDeg * ShapeMath.DEGTORAD; + var radius = circle.Radius; + var center = circle.Center; + for (int i = 0; i < sides; i++) { - var start = center + (ShapeVec.Right() * radius).Rotate(startAngleRad + angleStep * i); - var end = center + (ShapeVec.Right() * radius).Rotate(startAngleRad + angleStep * (i + 1)); - SegmentDrawing.DrawSegment(start, end, lineThickness, color, LineCapType.CappedExtended, 4); + var nextIndex = (i + 1) % sides; + var curP = center + new Vector2(radius, 0f).Rotate(rotRad + angleStep * i); + var nextP = center + new Vector2(radius, 0f).Rotate(rotRad + angleStep * nextIndex); + var segment = new Segment(curP, nextP); + segment.DrawMasked(mask, lineInfo, reversedMask); } } - /// - /// Draws the outline of a sector of a circle at the specified center and radius using the given rotation offset, line thickness, color, and optional closure and side length. + /// Draws the circle outline as individual line segments clipped by a mask. /// - /// The center of the circle. - /// The radius of the circle. - /// The starting angle of the sector in degrees. - /// The ending angle of the sector in degrees. - /// The rotation offset in degrees. - /// The thickness of the outline. - /// The color of the outline. - /// Whether the sector should be closed (connect to the center). - /// The maximum length of each side. Default is 8. - public static void DrawCircleSectorLines(Vector2 center, float radius, float startAngleDeg, float endAngleDeg, float rotOffsetDeg, float lineThickness, ColorRgba color, bool closed = true, float sideLength = 8f) + /// The source whose circumference will be approximated by segments. + /// The used to mask (clip) each generated segment. + /// Parameters controlling line drawing (thickness, color, cap type, etc.). + /// Rotation offset in degrees applied to the entire approximated polygon. + /// + /// The smoothness value (0-1). This controls the visual quality of the circle by inversely interpolating the current . + /// A value of 0 uses the maximum side length (fewer sides, less smooth), while 1 uses the minimum side length (more sides, smoother). + /// The resulting side length determines the number of polygon sides used to approximate the circle. + /// + /// If true, draws the parts inside the mask instead of outside. + public static void DrawLinesMasked(this Circle circle, Rect mask, LineDrawingInfo lineInfo, float rotDeg, float smoothness, bool reversedMask = false) { - DrawCircleSectorLines(center, radius, startAngleDeg + rotOffsetDeg, endAngleDeg + rotOffsetDeg, lineThickness, color, closed, sideLength); + if (!CalculateCircleDrawingParameters(circle.Radius, smoothness, out float angleStep, out int sides)) return; + var rotRad = rotDeg * ShapeMath.DEGTORAD; + var radius = circle.Radius; + var center = circle.Center; + for (int i = 0; i < sides; i++) + { + var nextIndex = (i + 1) % sides; + var curP = center + new Vector2(radius, 0f).Rotate(rotRad + angleStep * i); + var nextP = center + new Vector2(radius, 0f).Rotate(rotRad + angleStep * nextIndex); + var segment = new Segment(curP, nextP); + segment.DrawMasked(mask, lineInfo, reversedMask); + } } - /// - /// Draws the outline of a sector of a circle at the specified center and radius using the given number of sides, line thickness, color, and optional closure. + /// Draws the circle outline as individual line segments clipped by a mask. /// - /// The center of the circle. - /// The radius of the circle. - /// The starting angle of the sector in degrees. - /// The ending angle of the sector in degrees. - /// The number of sides used to approximate the sector. - /// The thickness of the outline. - /// The color of the outline. - /// Whether the sector should be closed (connect to the center). - public static void DrawCircleSectorLines(Vector2 center, float radius, float startAngleDeg, float endAngleDeg, int sides, float lineThickness, ColorRgba color, bool closed = true) + /// The source whose circumference will be approximated by segments. + /// The used to mask (clip) each generated segment. + /// Parameters controlling line drawing (thickness, color, cap type, etc.). + /// Rotation offset in degrees applied to the entire approximated polygon. + /// + /// The smoothness value (0-1). This controls the visual quality of the circle by inversely interpolating the current . + /// A value of 0 uses the maximum side length (fewer sides, less smooth), while 1 uses the minimum side length (more sides, smoother). + /// The resulting side length determines the number of polygon sides used to approximate the circle. + /// + /// If true, draws the parts inside the mask instead of outside. + public static void DrawLinesMasked(this Circle circle, Quad mask, LineDrawingInfo lineInfo, float rotDeg, float smoothness, bool reversedMask = false) { - float startAngleRad = startAngleDeg * ShapeMath.DEGTORAD; - float endAngleRad = endAngleDeg * ShapeMath.DEGTORAD; - float anglePiece = endAngleDeg - startAngleRad; - float angleStep = MathF.Abs(anglePiece) / sides; - if (closed) + if (!CalculateCircleDrawingParameters(circle.Radius, smoothness, out float angleStep, out int sides)) return; + var rotRad = rotDeg * ShapeMath.DEGTORAD; + var radius = circle.Radius; + var center = circle.Center; + for (int i = 0; i < sides; i++) { - var sectorStart = center + (ShapeVec.Right() * radius + new Vector2(lineThickness / 2, 0)).Rotate(startAngleRad); - SegmentDrawing.DrawSegment(center, sectorStart, lineThickness, color, LineCapType.CappedExtended, 2); - - var sectorEnd = center + (ShapeVec.Right() * radius + new Vector2(lineThickness / 2, 0)).Rotate(endAngleRad); - SegmentDrawing.DrawSegment(center, sectorEnd, lineThickness, color, LineCapType.CappedExtended, 2); + var nextIndex = (i + 1) % sides; + var curP = center + new Vector2(radius, 0f).Rotate(rotRad + angleStep * i); + var nextP = center + new Vector2(radius, 0f).Rotate(rotRad + angleStep * nextIndex); + var segment = new Segment(curP, nextP); + segment.DrawMasked(mask, lineInfo, reversedMask); } - for (var i = 0; i < sides; i++) + } + /// + /// Draws the circle outline as individual line segments clipped by a mask. + /// + /// The source whose circumference will be approximated by segments. + /// The used to mask (clip) each generated segment. + /// Parameters controlling line drawing (thickness, color, cap type, etc.). + /// Rotation offset in degrees applied to the entire approximated polygon. + /// + /// The smoothness value (0-1). This controls the visual quality of the circle by inversely interpolating the current . + /// A value of 0 uses the maximum side length (fewer sides, less smooth), while 1 uses the minimum side length (more sides, smoother). + /// The resulting side length determines the number of polygon sides used to approximate the circle. + /// + /// If true, draws the parts inside the mask instead of outside. + public static void DrawLinesMasked(this Circle circle, Polygon mask, LineDrawingInfo lineInfo, float rotDeg, float smoothness, bool reversedMask = false) + { + if (!CalculateCircleDrawingParameters(circle.Radius, smoothness, out float angleStep, out int sides)) return; + var rotRad = rotDeg * ShapeMath.DEGTORAD; + var radius = circle.Radius; + var center = circle.Center; + for (int i = 0; i < sides; i++) { - var start = center + (ShapeVec.Right() * radius).Rotate(startAngleRad + angleStep * i); - var end = center + (ShapeVec.Right() * radius).Rotate(startAngleRad + angleStep * (i + 1)); - SegmentDrawing.DrawSegment(start, end, lineThickness, color, LineCapType.CappedExtended, 2); + var nextIndex = (i + 1) % sides; + var curP = center + new Vector2(radius, 0f).Rotate(rotRad + angleStep * i); + var nextP = center + new Vector2(radius, 0f).Rotate(rotRad + angleStep * nextIndex); + var segment = new Segment(curP, nextP); + segment.DrawMasked(mask, lineInfo, reversedMask); } } - /// - /// Draws the outline of a sector of a circle at the specified center and radius using the given rotation offset, number of sides, line thickness, color, and optional closure. + /// Draws the circle outline as individual line segments clipped by a mask of any closed shape type. /// - /// The center of the circle. - /// The radius of the circle. - /// The starting angle of the sector in degrees. - /// The ending angle of the sector in degrees. - /// The rotation offset in degrees. - /// The number of sides used to approximate the sector. - /// The thickness of the outline. - /// The color of the outline. - /// Whether the sector should be closed (connect to the center). - public static void DrawCircleSectorLines(Vector2 center, float radius, float startAngleDeg, float endAngleDeg, float rotOffsetDeg, int sides, float lineThickness, ColorRgba color, bool closed = true) + /// The mask type implementing (for example: , , , , ). + /// The source whose circumference will be approximated by segments. + /// The mask used to clip each generated segment. + /// Parameters controlling line drawing (thickness, color, cap type, etc.). + /// Rotation offset in degrees applied to the entire approximated polygon. + /// + /// The smoothness value (0-1). This controls the visual quality of the circle by inversely interpolating the current . + /// A value of 0 uses the maximum side length (fewer sides, less smooth), while 1 uses the minimum side length (more sides, smoother). + /// The resulting side length determines the number of polygon sides used to approximate the circle. + /// + /// If true, draws the parts inside the mask instead of outside. + public static void DrawLinesMasked(this Circle circle, T mask, LineDrawingInfo lineInfo, float rotDeg, float smoothness, bool reversedMask = false) where T : IClosedShapeTypeProvider { - DrawCircleSectorLines(center, radius, startAngleDeg + rotOffsetDeg, endAngleDeg + rotOffsetDeg, sides, lineThickness, color, closed); + if (!CalculateCircleDrawingParameters(circle.Radius, smoothness, out float angleStep, out int sides)) return; + var rotRad = rotDeg * ShapeMath.DEGTORAD; + var radius = circle.Radius; + var center = circle.Center; + for (int i = 0; i < sides; i++) + { + var nextIndex = (i + 1) % sides; + var curP = center + new Vector2(radius, 0f).Rotate(rotRad + angleStep * i); + var nextP = center + new Vector2(radius, 0f).Rotate(rotRad + angleStep * nextIndex); + var segment = new Segment(curP, nextP); + segment.DrawMasked(mask, lineInfo, reversedMask); + } } - + #endregion + + #region Draw Checkered /// /// Draws a checkered pattern of lines inside a circle, optionally with a background color. /// @@ -954,11 +929,16 @@ public static void DrawCircleSectorLines(Vector2 center, float radius, float sta /// The rotation of the checkered pattern in degrees. /// The color of the checkered lines. /// The background color of the circle (drawn first if alpha > 0). - /// The number of segments used to approximate the circle. + /// + /// The smoothness value (0-1). This controls the visual quality of the circle by inversely interpolating the current . + /// A value of 0 uses the maximum side length (fewer sides, less smooth), while 1 uses the minimum side length (more sides, smoother). + /// The resulting side length determines the number of polygon sides used to approximate the circle. + /// /// /// Useful for visualizing grid or checkered overlays on circular shapes. /// - public static void DrawCircleCheckeredLines(Vector2 pos, AnchorPoint alignment, float radius, float spacing, float lineThickness, float angleDeg, ColorRgba lineColorRgba, ColorRgba bgColorRgba, int circleSegments) + public static void DrawCircleCheckeredLines(Vector2 pos, AnchorPoint alignment, float radius, float spacing, + float lineThickness, float angleDeg, ColorRgba lineColorRgba, ColorRgba bgColorRgba, float smoothness = 0.5f) { float maxDimension = radius; @@ -967,7 +947,11 @@ public static void DrawCircleCheckeredLines(Vector2 pos, AnchorPoint alignment, var center = pos - aVector + size / 2; float rotRad = angleDeg * ShapeMath.DEGTORAD; - if (bgColorRgba.A > 0) DrawCircle(center, radius, bgColorRgba, circleSegments); + if (bgColorRgba.A > 0) + { + var circle = new Circle(center, radius); + circle.Draw(bgColorRgba, smoothness); + } var cur = new Vector2(-spacing / 2, 0f); while (cur.X > -maxDimension) @@ -999,32 +983,971 @@ public static void DrawCircleCheckeredLines(Vector2 pos, AnchorPoint alignment, var start = p + up.Rotate(rotRad); var end = p + down.Rotate(rotRad); SegmentDrawing.DrawSegment(start, end, lineThickness, lineColorRgba); - cur.X += spacing; + cur.X += spacing; + } + } + #endregion + + #region Draw Ring Lines + + /// + /// Draws a ring (annulus) outline. + /// + /// The circle defining the center and outer radius of the ring. + /// The thickness of the ring (inwards from the circle radius). + /// The rotation of the ring in degrees. + /// The thickness of the outline lines. + /// The color of the outline. + /// + /// The smoothness value (0-1). This controls the visual quality of the circle by inversely interpolating the current . + /// A value of 0 uses the maximum side length (fewer sides, less smooth), while 1 uses the minimum side length (more sides, smoother). + /// The resulting side length determines the number of polygon sides used to approximate the circle. + /// + public static void DrawRingLines(this Circle ring, float ringThickness, float rotDeg, float lineThickness, ColorRgba color, float smoothness) + { + if(ringThickness <= 0 && lineThickness <= 0) + { + ring.Draw(rotDeg, color, smoothness); + return; + } + + if (lineThickness <= 0) + { + ring.DrawLines(rotDeg, ringThickness, color, smoothness); + return; + } + + if (ringThickness <= 0) + { + ring.DrawLines(rotDeg, lineThickness, color, smoothness); + return; } + + // 1. Calculate the effective radius of the inner ring, applying the clamp constraint. + // The inner ring represents the centerline of the inner stroke. + float effectiveInnerRadius = MathF.Max(ring.Radius - ringThickness, lineThickness * 2f); + + // 2. Calculate the outer edge of the inner stroke. + // Stroke is centered on effectiveInnerRadius with total width (lineThickness * 2). + // It extends outwards by lineThickness. + float innerStrokeOuterEdge = effectiveInnerRadius + lineThickness; + + // 3. Calculate the inner edge of the outer stroke. + // Outer ring is at Radius + ringThickness. + // Stroke extends inwards by lineThickness. + float outerStrokeInnerEdge = (ring.Radius + ringThickness) - lineThickness; + + if (innerStrokeOuterEdge >= outerStrokeInnerEdge) + { + // Calculating the total bounds of the merged shape + float totalInnerRadius = effectiveInnerRadius - lineThickness; + float totalOuterRadius = (ring.Radius + ringThickness) + lineThickness; + + // Calculate the new center radius and thickness for the merged ring + float totalThickness = (totalOuterRadius - totalInnerRadius) * 0.5f; + float newRadius = totalInnerRadius + totalThickness; + + // Draw the merged result using the calculated geometric center and thickness + var mergedRing = ring.SetRadius(newRadius); + mergedRing.DrawLines(rotDeg, lineThickness, color, smoothness); + return; + } + + var innerRing = ring.SetRadius(effectiveInnerRadius); + var outerRing = ring.SetRadius(ring.Radius + ringThickness); + + innerRing.DrawLines(rotDeg, lineThickness, color, smoothness); + outerRing.DrawLines(rotDeg, lineThickness, color, smoothness); } /// - /// Calculates the number of sides needed to approximate a circle with the given radius and maximum side length. + /// Draws a ring (annulus) outline using line drawing info. + /// + /// The circle defining the center and outer radius of the ring. + /// The thickness of the ring (inwards from the circle radius). + /// The rotation of the ring in degrees. + /// Contains line drawing parameters (thickness, color, caps, etc.). + /// + /// The smoothness value (0-1). This controls the visual quality of the circle by inversely interpolating the current . + /// A value of 0 uses the maximum side length (fewer sides, less smooth), while 1 uses the minimum side length (more sides, smoother). + /// The resulting side length determines the number of polygon sides used to approximate the circle. + /// + public static void DrawRingLines(this Circle ring, float ringThickness, float rotDeg, LineDrawingInfo lineInfo, float smoothness) + { + ring.DrawRingLines(ringThickness, rotDeg, lineInfo.Thickness, lineInfo.Color, smoothness); + } + + #endregion + + #region Draw Ring Lines Scaled + + /// + /// Draws a ring (annulus) outline where each segment is scaled. + /// + /// The circle defining the center and outer radius of the ring. + /// The thickness of the ring (inwards from the circle radius). + /// The rotation of the ring in degrees. + /// Contains line drawing parameters (thickness, color, caps, etc.). + /// + /// The smoothness value (0-1). This controls the visual quality of the circle by inversely interpolating the current . + /// A value of 0 uses the maximum side length (fewer sides, less smooth), while 1 uses the minimum side length (more sides, smoother). + /// The resulting side length determines the number of polygon sides used to approximate the circle. + /// + /// Scale factor for each side segment (0 = invisible, 1 = full connected circle). + /// The point on the segment to scale from (0-1, default 0.5 is center). + public static void DrawRingLinesScaled(this Circle ring, float ringThickness, float rotDeg, LineDrawingInfo lineInfo, float smoothness, float sideScaleFactor, float sideScaleOrigin = 0.5f) + { + if (sideScaleFactor <= 0f) + { + return; + } + + var lineThickness = lineInfo.Thickness; + var color = lineInfo.Color; + + if (sideScaleFactor >= 1f) + { + ring.DrawRingLines(ringThickness, rotDeg, lineThickness, color, smoothness); + return; + } + + if(ringThickness <= 0 && lineThickness <= 0) + { + ring.DrawScaled(rotDeg, color, smoothness, sideScaleFactor, sideScaleOrigin); + return; + } + + if (lineThickness <= 0) + { + var newLineInfo = lineInfo.SetThickness(ringThickness); + ring.DrawLinesScaled(rotDeg, newLineInfo, smoothness, sideScaleFactor, sideScaleOrigin); + return; + } + + if (ringThickness <= 0) + { + ring.DrawLinesScaled(rotDeg, lineInfo, smoothness, sideScaleFactor, sideScaleOrigin); + return; + } + + + // 1. Calculate the effective radius of the inner ring, applying the clamp constraint. + // The inner ring represents the centerline of the inner stroke. + float effectiveInnerRadius = MathF.Max(ring.Radius - ringThickness, lineThickness * 2f); + + // 2. Calculate the outer edge of the inner stroke. + // Stroke is centered on effectiveInnerRadius with total width (lineThickness * 2). + // It extends outwards by lineThickness. + float innerStrokeOuterEdge = effectiveInnerRadius + lineThickness; + + // 3. Calculate the inner edge of the outer stroke. + // Outer ring is at Radius + ringThickness. + // Stroke extends inwards by lineThickness. + float outerStrokeInnerEdge = (ring.Radius + ringThickness) - lineThickness; + + if (innerStrokeOuterEdge >= outerStrokeInnerEdge) + { + // Calculating the total bounds of the merged shape + float totalInnerRadius = effectiveInnerRadius - lineThickness; + float totalOuterRadius = (ring.Radius + ringThickness) + lineThickness; + + // Calculate the new center radius and thickness for the merged ring + float totalThickness = (totalOuterRadius - totalInnerRadius) * 0.5f; + float newRadius = totalInnerRadius + totalThickness; + + // Draw the merged result using the calculated geometric center and thickness + var mergedRing = ring.SetRadius(newRadius); + var newLineInfo = lineInfo.SetThickness(totalThickness); + mergedRing.DrawLinesScaled(rotDeg, newLineInfo, smoothness, sideScaleFactor, sideScaleOrigin); + return; + } + + var innerRing = ring.SetRadius(effectiveInnerRadius); + var outerRing = ring.SetRadius(ring.Radius + ringThickness); + + innerRing.DrawLinesScaled(rotDeg, lineInfo, smoothness, sideScaleFactor, sideScaleOrigin); + outerRing.DrawLinesScaled(rotDeg, lineInfo, smoothness, sideScaleFactor, sideScaleOrigin); + } + + #endregion + + #region Draw Ring Lines Percentage + + /// + /// Draws a ring (annulus) sector outline based on a percentage value. + /// + /// The circle defining the center and outer radius of the ring. + /// The thickness of the ring (inwards from the circle radius). + /// The fill percentage (0 to 1 or 0 to -1). Positive values are clockwise, negative are counter-clockwise. + /// The rotation offset in degrees. + /// The thickness of the outline. + /// The color of the outline. + /// + /// The smoothness value (0-1). This controls the visual quality of the circle by inversely interpolating the current . + /// A value of 0 uses the maximum side length (fewer sides, less smooth), while 1 uses the minimum side length (more sides, smoother). + /// The resulting side length determines the number of polygon sides used to approximate the circle. + /// + /// If true, draws the start and end caps of the sector. + /// + /// This function does not clamp the resulting side count with . + /// + public static void DrawRingLinesPercentage(this Circle ring, float ringThickness, float f, float rotDeg, float lineThickness, ColorRgba color, float smoothness, bool closed = true) + { + if (!TransformPercentageToAngles(f, out var startAngleDeg, out var endAngleDeg)) return; + ring.DrawRingSectorLines(ringThickness, startAngleDeg, endAngleDeg, rotDeg, lineThickness, color, smoothness, closed); + + } + + /// + /// Draws a ring (annulus) sector outline based on a percentage value. + /// + /// The circle defining the center and outer radius of the ring. + /// The thickness of the ring (inwards from the circle radius). + /// The fill percentage (0 to 1 or 0 to -1). Positive values are clockwise, negative are counter-clockwise. + /// The rotation offset in degrees. + /// Contains line drawing parameters (thickness, color, caps, etc.). + /// + /// The smoothness value (0-1). This controls the visual quality of the circle by inversely interpolating the current . + /// A value of 0 uses the maximum side length (fewer sides, less smooth), while 1 uses the minimum side length (more sides, smoother). + /// The resulting side length determines the number of polygon sides used to approximate the circle. + /// + /// + /// This function does not clamp the resulting side count with . + /// + public static void DrawRingLinesPercentage(this Circle ring, float ringThickness, float f, float rotDeg, LineDrawingInfo lineInfo, float smoothness) + { + if (!TransformPercentageToAngles(f, out var startAngleDeg, out var endAngleDeg)) return; + ring.DrawRingSectorLines(ringThickness, startAngleDeg, endAngleDeg, rotDeg, lineInfo, smoothness); + + } + + #endregion + + #region Draw Ring Sector Lines + + /// + /// Draws a ring (annulus) sector outline. + /// + /// The circle defining the center and outer radius of the ring. + /// The thickness of the ring (inwards from the circle radius). + /// The starting angle of the sector in degrees. + /// The ending angle of the sector in degrees. + /// The rotation offset in degrees. + /// The thickness of the outline. + /// The color of the outline. + /// + /// The smoothness value (0-1). This controls the visual quality of the circle by inversely interpolating the current . + /// A value of 0 uses the maximum side length (fewer sides, less smooth), while 1 uses the minimum side length (more sides, smoother). + /// The resulting side length determines the number of polygon sides used to approximate the circle. + /// + /// If true, draws the start and end caps of the sector. + /// + /// This function does not clamp the resulting side count with . + /// + public static void DrawRingSectorLines(this Circle ring, float ringThickness, float startAngleDeg, float endAngleDeg, float rotDeg, float lineThickness, ColorRgba color, float smoothness, bool closed = true) + { + if(ringThickness <= 0 && lineThickness <= 0) + { + ring.DrawSector(startAngleDeg, endAngleDeg, rotDeg, color, smoothness); + return; + } + + if (lineThickness <= 0) + { + ring.DrawSectorLines(startAngleDeg, endAngleDeg, rotDeg, ringThickness, color, smoothness); + return; + } + + if (ringThickness <= 0) + { + ring.DrawSectorLines(startAngleDeg, endAngleDeg, rotDeg, lineThickness, color, smoothness); + return; + } + + // 1. Calculate the effective radius of the inner ring, applying the clamp constraint. + // The inner ring represents the centerline of the inner stroke. + float effectiveInnerRadius = MathF.Max(ring.Radius - ringThickness, lineThickness * 2f); + + // 2. Calculate the outer edge of the inner stroke. + // Stroke is centered on effectiveInnerRadius with total width (lineThickness * 2). + // It extends outwards by lineThickness. + float innerStrokeOuterEdge = effectiveInnerRadius + lineThickness; + + // 3. Calculate the inner edge of the outer stroke. + // Outer ring is at Radius + ringThickness. + // Stroke extends inwards by lineThickness. + float outerStrokeInnerEdge = (ring.Radius + ringThickness) - lineThickness; + + if (innerStrokeOuterEdge >= outerStrokeInnerEdge) + { + // Calculating the total bounds of the merged shape + float totalInnerRadius = effectiveInnerRadius - lineThickness; + float totalOuterRadius = (ring.Radius + ringThickness) + lineThickness; + + // Calculate the new center radius and thickness for the merged ring + float totalThickness = (totalOuterRadius - totalInnerRadius) * 0.5f; + float newRadius = totalInnerRadius + totalThickness; + + // Draw the merged result using the calculated geometric center and thickness + var mergedRing = ring.SetRadius(newRadius); + mergedRing.DrawSectorLines(startAngleDeg, endAngleDeg, rotDeg, totalThickness, color, smoothness); + return; + } + + var innerRing = ring.SetRadius(effectiveInnerRadius); + var outerRing = ring.SetRadius(ring.Radius + ringThickness); + + var innerParametersValid = CalculateCircleDrawingParameters(innerRing.Radius, startAngleDeg + rotDeg, endAngleDeg + rotDeg, smoothness, + out float innerAngleDifRad, out float _, out int innerSides, false); + if (!innerParametersValid) return; + + var outerParametersValid = CalculateCircleDrawingParameters(outerRing.Radius, startAngleDeg + rotDeg, endAngleDeg + rotDeg, smoothness, + out float outerAngleDifRad, out float _, out int outerSides, false); + if (!outerParametersValid) return; + + var innerStartAngleDeg = startAngleDeg + rotDeg; + var outerStartAngleDeg = startAngleDeg + rotDeg; + var innerEndAngleDeg = innerStartAngleDeg + (innerAngleDifRad * ShapeMath.RADTODEG); + var outerEndAngleDeg = outerStartAngleDeg + (outerAngleDifRad * ShapeMath.RADTODEG); + var rayColor = color.ToRayColor(); + + if (closed) + { + var innerStartAngleRad = innerStartAngleDeg * ShapeMath.DEGTORAD; + var outerStartAngleRad = outerStartAngleDeg * ShapeMath.DEGTORAD; + var innerEndAngleRad = innerEndAngleDeg * ShapeMath.DEGTORAD; + var outerEndAngleRad = outerEndAngleDeg * ShapeMath.DEGTORAD; + + var ccw = innerAngleDifRad < 0; + var innerStartPoint = innerRing.Center + new Vector2(innerRing.Radius - lineThickness, 0f).Rotate(innerStartAngleRad); + var outerStartPoint = outerRing.Center + new Vector2(outerRing.Radius + lineThickness, 0f).Rotate(outerStartAngleRad); + var startDir = (outerStartPoint - innerStartPoint).Normalize(); + var startPerp = ccw ? startDir.GetPerpendicularRight() : startDir.GetPerpendicularLeft(); + var innerStartOffsetPoint = innerStartPoint + startPerp * lineThickness * 2; + var outerStartOffsetPoint = outerStartPoint + startPerp * lineThickness * 2; + + var innerEndPoint = innerRing.Center + new Vector2(innerRing.Radius - lineThickness, 0f).Rotate(innerEndAngleRad); + var outerEndPoint = outerRing.Center + new Vector2(outerRing.Radius + lineThickness, 0f).Rotate(outerEndAngleRad); + var endDir = (outerEndPoint - innerEndPoint).Normalize(); + var endPerp = ccw ? endDir.GetPerpendicularLeft() : endDir.GetPerpendicularRight(); + var innerEndOffsetPoint = innerEndPoint + endPerp * lineThickness * 2; + var outerEndOffsetPoint = outerEndPoint + endPerp * lineThickness * 2; + + if (ccw) + { + Raylib.DrawTriangle(outerStartOffsetPoint, innerStartPoint, innerStartOffsetPoint, rayColor); + Raylib.DrawTriangle(outerStartOffsetPoint, outerStartPoint, innerStartPoint, rayColor); + + Raylib.DrawTriangle(outerEndPoint, innerEndOffsetPoint, innerEndPoint, rayColor); + Raylib.DrawTriangle(outerEndPoint, outerEndOffsetPoint, innerEndOffsetPoint, rayColor); + } + else + { + Raylib.DrawTriangle(innerStartOffsetPoint, innerStartPoint, outerStartPoint, rayColor); + Raylib.DrawTriangle(innerStartOffsetPoint, outerStartPoint, outerStartOffsetPoint, rayColor); + + Raylib.DrawTriangle(innerEndPoint, innerEndOffsetPoint, outerEndOffsetPoint, rayColor); + Raylib.DrawTriangle(innerEndPoint, outerEndOffsetPoint, outerEndPoint, rayColor); + } + } + + Raylib.DrawRing(innerRing.Center, innerRing.Radius - lineThickness, innerRing.Radius + lineThickness, innerStartAngleDeg, innerEndAngleDeg, innerSides, rayColor); + Raylib.DrawRing(outerRing.Center, outerRing.Radius - lineThickness, outerRing.Radius + lineThickness, outerStartAngleDeg, outerEndAngleDeg, outerSides, rayColor); + } + + /// + /// Draws a ring (annulus) sector outline. + /// + /// The circle defining the center and outer radius of the ring. + /// The thickness of the ring (inwards from the circle radius). + /// The starting angle of the sector in degrees. + /// The ending angle of the sector in degrees. + /// The rotation offset in degrees. + /// Contains line drawing parameters (thickness, color, caps, etc.). + /// + /// The smoothness value (0-1). This controls the visual quality of the circle by inversely interpolating the current . + /// A value of 0 uses the maximum side length (fewer sides, less smooth), while 1 uses the minimum side length (more sides, smoother). + /// The resulting side length determines the number of polygon sides used to approximate the circle. + /// + /// + /// This function does not clamp the resulting side count with . + /// + public static void DrawRingSectorLines(this Circle ring, float ringThickness, float startAngleDeg, float endAngleDeg, float rotDeg, LineDrawingInfo lineInfo, float smoothness) + { + var lineThickness = lineInfo.Thickness; + var color = lineInfo.Color; + if(ringThickness <= 0 && lineThickness <= 0) + { + ring.DrawSector(startAngleDeg, endAngleDeg, rotDeg, color, smoothness); + return; + } + + if (lineThickness <= 0) + { + var newLineInfo = lineInfo.SetThickness(ringThickness); + ring.DrawSectorLines(startAngleDeg, endAngleDeg, rotDeg, newLineInfo, smoothness); + return; + } + + if (ringThickness <= 0) + { + ring.DrawSectorLines(startAngleDeg, endAngleDeg, rotDeg, lineInfo, smoothness); + return; + } + + // 1. Calculate the effective radius of the inner ring, applying the clamp constraint. + // The inner ring represents the centerline of the inner stroke. + float effectiveInnerRadius = MathF.Max(ring.Radius - ringThickness, lineThickness * 2f); + + // 2. Calculate the outer edge of the inner stroke. + // Stroke is centered on effectiveInnerRadius with total width (lineThickness * 2). + // It extends outwards by lineThickness. + float innerStrokeOuterEdge = effectiveInnerRadius + lineThickness; + + // 3. Calculate the inner edge of the outer stroke. + // Outer ring is at Radius + ringThickness. + // Stroke extends inwards by lineThickness. + float outerStrokeInnerEdge = (ring.Radius + ringThickness) - lineThickness; + + if (innerStrokeOuterEdge >= outerStrokeInnerEdge) + { + // Calculating the total bounds of the merged shape + float totalInnerRadius = effectiveInnerRadius - lineThickness; + float totalOuterRadius = (ring.Radius + ringThickness) + lineThickness; + + // Calculate the new center radius and thickness for the merged ring + float totalThickness = (totalOuterRadius - totalInnerRadius) * 0.5f; + float newRadius = totalInnerRadius + totalThickness; + + // Draw the merged result using the calculated geometric center and thickness + var mergedRing = ring.SetRadius(newRadius); + var newLineInfo = lineInfo.SetThickness(totalThickness); + mergedRing.DrawSectorLines(startAngleDeg, endAngleDeg, rotDeg, newLineInfo, smoothness); + return; + } + + var innerRing = ring.SetRadius(effectiveInnerRadius); + var outerRing = ring.SetRadius(ring.Radius + ringThickness); + + innerRing.DrawSectorLines(startAngleDeg, endAngleDeg, rotDeg, lineInfo, smoothness); + outerRing.DrawSectorLines(startAngleDeg, endAngleDeg, rotDeg, lineInfo, smoothness); + } + + #endregion + + #region Gapped + /// + /// Draws a gapped outline for a circle, creating a dashed or segmented circular outline. + /// + /// The circle to draw. + /// Parameters describing how to draw the outline. + /// Parameters describing the gap configuration. + /// The rotation of the circle in degrees. + /// + /// The smoothness value (0-1). This controls the visual quality of the circle by inversely interpolating the current . + /// A value of 0 uses the maximum side length (fewer sides, less smooth), while 1 uses the minimum side length (more sides, smoother). + /// The resulting side length determines the number of polygon sides used to approximate the circle. + /// + /// + /// - If is 0 or is 0, the outline is drawn solid. + /// - If is 1 or greater, no outline is drawn. + /// + public static void DrawGappedOutline(this Circle circle, LineDrawingInfo lineInfo, GappedOutlineDrawingInfo gapDrawingInfo, float rotDeg, float smoothness) + { + if (!CircleDrawing.CalculateCircleDrawingParameters(circle.Radius, smoothness, out float angleStep, out int sides)) return; + if (gapDrawingInfo.Gaps <= 0 || gapDrawingInfo.GapPerimeterPercentage <= 0f) + { + circle.DrawLines(rotDeg, lineInfo, smoothness); + return; + } + + if (gapDrawingInfo.GapPerimeterPercentage >= 1f) return; + + var nonGapPercentage = 1f - gapDrawingInfo.GapPerimeterPercentage; + + var gapPercentageRange = gapDrawingInfo.GapPerimeterPercentage / gapDrawingInfo.Gaps; + var nonGapPercentageRange = nonGapPercentage / gapDrawingInfo.Gaps; + + float angleRad = rotDeg * ShapeMath.DEGTORAD; + Vector2[] circlePoints = new Vector2[sides]; + + float circumference = 0f; + for (int i = 0; i < sides; i++) + { + var curP = circle.GetVertex(angleRad, angleStep, i); + circlePoints[i] = curP; + var nextP = circle.GetVertex(angleRad, angleStep, (i + 1) % sides); + circumference += (nextP - curP).Length(); + } + + var startDistance = circumference * gapDrawingInfo.StartOffset; + var curDistance = 0f; + var nextDistance = startDistance; + + var curIndex = 0; + var curPoint = circlePoints[0]; + var nextPoint= circlePoints[1]; + var curW = nextPoint - curPoint; + var curDis = curW.Length(); + + var points = new List(3); + + int whileCounter = gapDrawingInfo.Gaps; + + while (whileCounter > 0) + { + if (curDistance + curDis >= nextDistance) + { + var p = curPoint + (curW / curDis) * (nextDistance - curDistance); + + + if (points.Count == 0) + { + nextDistance += nonGapPercentageRange * circumference; + points.Add(p); + + } + else + { + nextDistance += gapPercentageRange * circumference; + points.Add(p); + if (points.Count == 2) + { + SegmentDrawing.DrawSegment(points[0], points[1], lineInfo); + } + else + { + for (var i = 0; i < points.Count - 1; i++) + { + var p1 = points[i]; + var p2 = points[(i + 1) % points.Count]; + SegmentDrawing.DrawSegment(p1, p2, lineInfo); + } + } + + points.Clear(); + whileCounter--; + } + + } + else + { + + if(points.Count > 0) points.Add(nextPoint); + + curDistance += curDis; + curIndex = (curIndex + 1) % sides; + curPoint = circlePoints[curIndex]; + nextPoint = circlePoints[(curIndex + 1) % sides]; + curW = nextPoint - curPoint; + curDis = curW.Length(); + } + + } + } + + /// + /// Draws a gapped outline for a ring (annulus), creating dashed or segmented outlines for both inner and outer circles. + /// + /// The center of the ring. + /// The radius of the inner circle. If zero or negative, only the outer circle is drawn. + /// The radius of the outer circle. If zero or negative, only the inner circle is drawn. + /// Parameters describing how to draw the outlines. + /// Parameters describing the gap configuration. + /// The rotation of the ring in degrees. + /// The approximate length of each side used to approximate the circles. + /// + /// - If both radii are zero or negative, nothing is drawn. + /// - The number of sides for each circle is determined by the radius and . + /// + public static void DrawGappedRing(Vector2 center, float innerRadius, float outerRadius, LineDrawingInfo lineInfo, GappedOutlineDrawingInfo gapDrawingInfo, float rotDeg, float sideLength = 8f) + { + if (innerRadius <= 0 && outerRadius <= 0) return; + + + int outerSides = CircleDrawing.GetCircleSideCount(outerRadius, sideLength); + if (innerRadius <= 0) + { + DrawGappedOutline(new Circle(center, outerRadius), lineInfo, gapDrawingInfo, rotDeg, outerSides); + return; + } + + int innerSides = CircleDrawing.GetCircleSideCount(innerRadius, sideLength); + if (outerRadius <= 0) + { + DrawGappedOutline(new Circle(center, innerRadius), lineInfo, gapDrawingInfo, rotDeg, innerSides); + return; + } + + DrawGappedOutline(new Circle(center, innerRadius), lineInfo, gapDrawingInfo, rotDeg, innerSides); + DrawGappedOutline(new Circle(center, outerRadius), lineInfo, gapDrawingInfo, rotDeg, outerSides); + } + + #endregion + + #region UI + /// + /// Draws an outline bar along the circumference of a circle, filling the outline based on the specified progress value. + /// + /// The circle to draw the outline on. + /// The thickness of the outline. + /// The progress value (0 to 1) indicating how much of the outline to draw (as a fraction of the circle). + /// The color of the outline. + /// + /// The smoothness value (0-1). This controls the visual quality of the circle by inversely interpolating the current . + /// A value of 0 uses the maximum side length (fewer sides, less smooth), while 1 uses the minimum side length (more sides, smoother). + /// The resulting side length determines the number of polygon sides used to approximate the circle. + /// + /// + /// The outline is drawn as a sector of the circle, starting from 0 degrees. + /// + public static void DrawOutlineBar(this Circle c, float thickness, float f, ColorRgba color, float smoothness = 0.5f) + { + c.DrawSectorLines(0, 360 * f, 0f, thickness, color, smoothness); + } + + /// + /// Draws an outline bar along the circumference of a circle, starting at a specified angle offset, and filling based on the progress value. + /// + /// The circle to draw the outline on. + /// The starting angle offset in degrees. + /// The thickness of the outline. + /// The progress value (0 to 1) indicating how much of the outline to draw (as a fraction of the circle). + /// The color of the outline. + /// + /// The smoothness value (0-1). This controls the visual quality of the circle by inversely interpolating the current . + /// A value of 0 uses the maximum side length (fewer sides, less smooth), while 1 uses the minimum side length (more sides, smoother). + /// The resulting side length determines the number of polygon sides used to approximate the circle. + /// + /// + /// The outline is drawn as a sector of the circle, starting from the specified angle offset. + /// + public static void DrawOutlineBar(this Circle c, float startOffsetDeg, float thickness, float f, ColorRgba color, float smoothness = 0.5f) + { + c.DrawSectorLines(0, 360 * f, startOffsetDeg, thickness, color, smoothness); + } + + #endregion + + #region Math + /// + /// Defines the minimum and maximum allowed side lengths for circle approximation. + /// Used to control the smoothness and performance of circle drawing by limiting the number of polygon sides. + /// + /// + /// The smaller a side length is the more sides are used to approximate the circle, resulting in a smoother appearance but potentially worse performance. + /// The smoothness value in the CircleDrawing methods is used to inversly interpolate between the minimum and maximum side lengths. (Lerp(min, max, 1 - smoothness)) + /// + /// + /// A smoothness of 0 will use the maximum side length (fewer sides, less smooth), while a smoothness of 1 will use the minimum side length (more sides, smoother). + /// + public static ValueRange CircleSideLengthRange = new ValueRange(2f, 75f); + + /// + /// Defines the minimum and maximum allowed number of sides for circle approximation. + /// Used to control the smoothness and performance of circle drawing by limiting the number of polygon sides. + /// + /// + /// This is the most useful for constraining the minimum number of sides to a value that still resembles a circle (for example, 3 or 4 sides would look like a triangle or square, not a circle). + /// Any function that uses an arc length (Sector / Percentage variations) do not clamp the side count. + /// + public static ValueRangeInt CircleSideCountRange = new ValueRangeInt(8, 128); + + /// + /// Calculates the smoothness value required to match a given number of sides for a circle. + /// + /// The radius of the circle. + /// The target number of sides. + /// Output smoothness value (0-1). + /// True if calculation was successful, false if radius is invalid. + public static bool GetCircleSmoothness(float radius, int sides, out float smoothness) + { + smoothness = 0f; + if (radius <= 0f) return false; + sides = CircleSideCountRange.Clamp(sides); + float circumference = 2.0f * ShapeMath.PI * radius; + float sideLength = circumference / sides; + float f = CircleSideLengthRange.Inverse(sideLength); + smoothness = ShapeMath.Clamp(f, 0f, 1f); + return true; + } + + /// + /// Calculates the smoothness value required to match a given number of sides for a circle arc. + /// + /// The radius of the circle. + /// The starting angle of the arc in degrees. + /// The ending angle of the arc in degrees. + /// The target number of sides. + /// Output smoothness value (0-1). + /// True if calculation was successful, false if inputs are invalid. + public static bool GetCircleSmoothness(float radius, float startAngleDeg, float endAngleDeg, int sides, out float smoothness) + { + smoothness = 0f; + if (radius <= 0f) return false; + + // Normalize angle difference to [-360, 360], preserving sign + float angleDiffDeg = endAngleDeg - startAngleDeg; + if (angleDiffDeg > 360f) angleDiffDeg %= 360f; + if (angleDiffDeg < -360f) angleDiffDeg %= 360f; + float absAngleDiffDeg = MathF.Abs(angleDiffDeg); + if (absAngleDiffDeg < 0.00001f) return false; + + sides = CircleSideCountRange.Clamp(sides); + float arcLength = 2f * ShapeMath.PI * radius * (absAngleDiffDeg / 360f); + float sideLength = arcLength / sides; + float f = CircleSideLengthRange.Inverse(sideLength); + smoothness = ShapeMath.Clamp(f, 0f, 1f); + return true; + } + + /// + /// Calculates drawing parameters for a circle sector (arc) based on smoothness. + /// + /// The radius of the circle. + /// The starting angle in degrees. + /// The ending angle in degrees. + /// + /// The smoothness value (0-1). This controls the visual quality of the circle by inversely interpolating the current . + /// A value of 0 uses the maximum side length (fewer sides, less smooth), while 1 uses the minimum side length (more sides, smoother). + /// The resulting side length determines the number of polygon sides used to approximate the circle. + /// + /// Output total angular difference in radians. + /// Output angle step per side in radians. + /// Output number of sides. + /// If true, clamps the number of sides to a min/max range set in . + /// True if parameters were calculated successfully. + public static bool CalculateCircleDrawingParameters(float radius, float startAngleDeg, float endAngleDeg, float smoothness, out float angleDifRad, out float angleStepRad, out int sides, bool clampSides = true) + { + angleStepRad = 0f; + sides = 0; + angleDifRad = 0f; + if (radius <= 0f) return false; + + // Normalize angle difference to [-360, 360], preserving sign + float angleDiffDeg = endAngleDeg - startAngleDeg; + if (angleDiffDeg > 360f) angleDiffDeg %= 360f; + if (angleDiffDeg < -360f) angleDiffDeg %= 360f; + + float absAngleDiffDeg = MathF.Abs(angleDiffDeg); + if (absAngleDiffDeg < 0.00001f) return false; + + angleDifRad = angleDiffDeg * ShapeMath.DEGTORAD; + + smoothness = ShapeMath.Clamp(smoothness, 0f, 1f); + float sideLength = CircleSideLengthRange.LerpInverse(smoothness); + if(sideLength <= 0f) return false; + + // Arc length for the given angle + float arcLength = 2f * ShapeMath.PI * radius * (absAngleDiffDeg / 360f); + + sides = (int)MathF.Ceiling(arcLength / sideLength); + if(clampSides) sides = CircleSideCountRange.Clamp(sides); + + // Angle step in radians (preserve direction) + angleStepRad = angleDifRad / sides; + + return true; + } + + /// + /// Calculates drawing parameters for a circle sector (arc) based on smoothness. + /// + /// The radius of the circle. + /// The starting angle in degrees. + /// The ending angle in degrees. + /// + /// The smoothness value (0-1). This controls the visual quality of the circle by inversely interpolating the current . + /// A value of 0 uses the maximum side length (fewer sides, less smooth), while 1 uses the minimum side length (more sides, smoother). + /// The resulting side length determines the number of polygon sides used to approximate the circle. + /// + /// Output angle step per side in radians. + /// Output number of sides. + /// If true, clamps the number of sides to a min/max range set in . + /// True if parameters were calculated successfully. + public static bool CalculateCircleDrawingParameters(float radius, float startAngleDeg, float endAngleDeg, float smoothness, out float angleStepRad, out int sides, bool clampSides = true) + { + angleStepRad = 0f; + sides = 0; + if (radius <= 0f) return false; + + // Normalize angle difference to [-360, 360], preserving sign + float angleDiffDeg = endAngleDeg - startAngleDeg; + if (angleDiffDeg > 360f) angleDiffDeg %= 360f; + if (angleDiffDeg < -360f) angleDiffDeg %= 360f; + + float absAngleDiffDeg = MathF.Abs(angleDiffDeg); + if (absAngleDiffDeg < 0.00001f) return false; + + smoothness = ShapeMath.Clamp(smoothness, 0f, 1f); + float sideLength = CircleSideLengthRange.LerpInverse(smoothness); + if(sideLength <= 0f) return false; + + // Arc length for the given angle + float arcLength = 2f * ShapeMath.PI * radius * (absAngleDiffDeg / 360f); + + sides = (int)MathF.Ceiling(arcLength / sideLength); + if(clampSides) sides = CircleSideCountRange.Clamp(sides); + + // Angle step in radians (preserve direction) + angleStepRad = (angleDiffDeg * ShapeMath.DEGTORAD) / sides; + + return true; + } + + /// + /// Calculates the number of sides for a circle sector (arc) based on smoothness. /// /// The radius of the circle. - /// The maximum length of each side. Default is 10. - /// The number of sides to use for the circle. - public static int GetCircleSideCount(float radius, float maxLength = 10f) + /// The starting angle in degrees. + /// The ending angle in degrees. + /// + /// The smoothness value (0-1). This controls the visual quality of the circle by inversely interpolating the current . + /// A value of 0 uses the maximum side length (fewer sides, less smooth), while 1 uses the minimum side length (more sides, smoother). + /// The resulting side length determines the number of polygon sides used to approximate the circle. + /// + /// Output number of sides. + /// If true, clamps the number of sides to a min/max range set in . + /// True if parameters were calculated successfully. + public static bool CalculateCircleDrawingParameters(float radius, float startAngleDeg, float endAngleDeg, float smoothness, out int sides, bool clampSides = true) { + sides = 0; + if (radius <= 0f) return false; + + // Normalize angle difference to [-360, 360], preserving sign + float angleDiffDeg = endAngleDeg - startAngleDeg; + if (angleDiffDeg > 360f) angleDiffDeg %= 360f; + if (angleDiffDeg < -360f) angleDiffDeg %= 360f; + + float absAngleDiffDeg = MathF.Abs(angleDiffDeg); + if (absAngleDiffDeg < 0.00001f) return false; + + smoothness = ShapeMath.Clamp(smoothness, 0f, 1f); + float sideLength = CircleSideLengthRange.LerpInverse(smoothness); + if(sideLength <= 0f) return false; + + // Arc length for the given angle + float arcLength = 2f * ShapeMath.PI * radius * (absAngleDiffDeg / 360f); + + sides = (int)MathF.Ceiling(arcLength / sideLength); + if(clampSides) sides = CircleSideCountRange.Clamp(sides); + + return true; + } + + /// + /// Calculates drawing parameters for a full circle based on smoothness. + /// + /// The radius of the circle. + /// + /// The smoothness value (0-1). This controls the visual quality of the circle by inversely interpolating the current . + /// A value of 0 uses the maximum side length (fewer sides, less smooth), while 1 uses the minimum side length (more sides, smoother). + /// The resulting side length determines the number of polygon sides used to approximate the circle. + /// + /// Output angle step per side in radians. + /// Output number of sides. + /// If true, clamps the number of sides to a min/max range set in . + /// True if parameters were calculated successfully. + public static bool CalculateCircleDrawingParameters(float radius, float smoothness, out float angleStepRad, out int sides, bool clampSides = true) + { + sides = 0; + angleStepRad = 0f; + if(radius <= 0f) return false; + + smoothness = ShapeMath.Clamp(smoothness, 0f, 1f); + float sideLength = CircleSideLengthRange.LerpInverse(smoothness); + if(sideLength <= 0f) return false; + float circumference = 2.0f * ShapeMath.PI * radius; - return (int)MathF.Max(circumference / maxLength, 5); + sides = (int)MathF.Ceiling(circumference / sideLength); + if(clampSides) sides = CircleSideCountRange.Clamp(sides); + + angleStepRad = MathF.Tau / sides; + return true; + } + + /// + /// Calculates the number of sides for a full circle based on smoothness. + /// + /// The radius of the circle. + /// + /// The smoothness value (0-1). This controls the visual quality of the circle by inversely interpolating the current . + /// A value of 0 uses the maximum side length (fewer sides, less smooth), while 1 uses the minimum side length (more sides, smoother). + /// The resulting side length determines the number of polygon sides used to approximate the circle. + /// + /// Output number of sides. + /// If true, clamps the number of sides to a min/max range set in . + /// True if parameters were calculated successfully. + public static bool CalculateCircleDrawingParameters(float radius, float smoothness, out int sides, bool clampSides = true) + { + sides = 0; + if(radius <= 0f) return false; + + smoothness = ShapeMath.Clamp(smoothness, 0f, 1f); + float sideLength = CircleSideLengthRange.LerpInverse(smoothness); + if(sideLength <= 0f) return false; + + float circumference = 2.0f * ShapeMath.PI * radius; + sides = (int)MathF.Ceiling(circumference / sideLength); + if(clampSides) sides = CircleSideCountRange.Clamp(sides); + + return true; + } + + /// + /// Calculates the number of sides for a circle based on a target side length. + /// + /// The radius of the circle. + /// The desired length of each side segment. + /// If true, clamps the number of sides to a min/max range set in . + /// The calculated number of sides. + public static int GetCircleSideCount(float radius, float sideLength = 10f, bool clampSides = true) + { + if (radius <= 0f || sideLength <= 0f) return 0; + float circumference = 2.0f * ShapeMath.PI * radius; + var sides = (int)MathF.Ceiling(circumference / sideLength); + if(clampSides) sides = CircleSideCountRange.Clamp(sides); + return sides; } /// - /// Calculates the number of sides needed to approximate an arc of a circle with the given radius, angle, and maximum side length. + /// Calculates the number of sides for a circle arc based on a target side length. /// /// The radius of the circle. - /// The angle of the arc in degrees. - /// The maximum length of each side. Default is 10. - /// The number of sides to use for the arc. - public static int GetCircleArcSideCount(float radius, float angleDeg, float maxLength = 10f) + /// The angular span of the arc in degrees. + /// The desired length of each side segment. + /// If true, clamps the number of sides to a min/max range set in . + /// The calculated number of sides. + public static int GetCircleArcSideCount(float radius, float angleDeg, float sideLength = 10f, bool clampSides = true) { + if (radius <= 0f || angleDeg <= 0f || sideLength <= 0f) return 0; + float circumference = 2.0f * ShapeMath.PI * radius * (angleDeg / 360f); - return (int)MathF.Max(circumference / maxLength, 1); + var sides = (int)MathF.Ceiling(circumference / sideLength); + if(clampSides) sides = CircleSideCountRange.Clamp(sides); + return sides; + } + + #endregion + + #region Helper + private static bool TransformPercentageToAngles(float f, out float startAngleDeg, out float endAngleDeg) + { + startAngleDeg = 0f; + endAngleDeg = 0f; + + if(f == 0) return false; + + if (f >= 0f) + { + startAngleDeg = 0f; + endAngleDeg = 360f * ShapeMath.Clamp(f, 0f, 1f); + return true; + } + + startAngleDeg = 0f; + endAngleDeg = 360f * ShapeMath.Clamp(f, -1f, 0f); + return true; } -} \ No newline at end of file + #endregion +} diff --git a/ShapeEngine/Geometry/CircleDef/CircleMath.cs b/ShapeEngine/Geometry/CircleDef/CircleMath.cs index 4789b979..8588f95e 100644 --- a/ShapeEngine/Geometry/CircleDef/CircleMath.cs +++ b/ShapeEngine/Geometry/CircleDef/CircleMath.cs @@ -2,6 +2,8 @@ using ShapeEngine.Core.Structs; using ShapeEngine.Geometry.PointsDef; using ShapeEngine.Geometry.PolygonDef; +using ShapeEngine.Geometry.TriangulationDef; +using ShapeEngine.ShapeClipper; using ShapeEngine.StaticLib; namespace ShapeEngine.Geometry.CircleDef; @@ -94,7 +96,7 @@ public Circle SetTransform(Transform2D transform) #endregion #region Math - + /// /// Projects the circle's shape points along a given vector. /// @@ -105,18 +107,39 @@ public Circle SetTransform(Transform2D transform) public Points? GetProjectedShapePoints(Vector2 v, int pointCount = 8) { if (pointCount < 4 || v.LengthSquared() <= 0f) return null; - float angleStep = (MathF.PI * 2f) / pointCount; + Points points = new(pointCount * 2); + + GetProjectedShapePoints(points, v, pointCount); + + return points; + } + + /// + /// Writes the circle's projected shape points into by sampling the circle and duplicating each sample offset by . + /// + /// The destination collection that will be cleared and populated with the projected shape points. + /// The vector along which the sampled circle points are projected. + /// The number of circle sample points to generate before projection. Must be at least 4. + /// true if valid parameters were provided and was populated; otherwise, false. + public bool GetProjectedShapePoints(Points result, Vector2 v, int pointCount = 8) + { + if (pointCount < 4 || v.LengthSquared() <= 0f) return false; + float angleStep = (MathF.PI * 2f) / pointCount; + + result.Clear(); + result.EnsureCapacity(pointCount * 2); + for (var i = 0; i < pointCount; i++) { var p = Center + new Vector2(Radius, 0f).Rotate(angleStep * i); - points.Add(p); - points.Add(p + v); + result.Add(p); + result.Add(p + v); } - return points; + return true; } - + /// /// Projects the circle's shape into a polygon along a given vector. /// @@ -128,17 +151,47 @@ public Circle SetTransform(Transform2D transform) { if (pointCount < 4 || v.LengthSquared() <= 0f) return null; float angleStep = (MathF.PI * 2f) / pointCount; - Points points = new(pointCount * 2); + pointsBuffer.Clear(); + pointsBuffer.EnsureCapacity(pointCount * 2); for (var i = 0; i < pointCount; i++) { var p = Center + new Vector2(Radius, 0f).Rotate(angleStep * i); - points.Add(p); - points.Add(p + v); + pointsBuffer.Add(p); + pointsBuffer.Add(p + v); } - return Polygon.FindConvexHull(points); + Polygon result = new Polygon(); + pointsBuffer.FindConvexHull(result); + return result; } + /// + /// Projects the circle's shape into a polygon along the specified vector and writes the convex hull into . + /// + /// The destination polygon that will receive the projected shape. + /// The vector along which the circle shape is projected. + /// The number of circle sample points to generate before projection. Must be at least 4. + /// true if valid parameters were provided and was populated; otherwise, false. + /// + /// This method samples the circle, adds a projected copy of each sample offset by , and computes the convex hull of those points. + /// + public bool ProjectShape(Polygon result, Vector2 v, int pointCount = 8) + { + if (pointCount < 4 || v.LengthSquared() <= 0f) return false; + float angleStep = (MathF.PI * 2f) / pointCount; + pointsBuffer.Clear(); + pointsBuffer.EnsureCapacity(pointCount * 2); + for (var i = 0; i < pointCount; i++) + { + var p = Center + new Vector2(Radius, 0f).Rotate(angleStep * i); + pointsBuffer.Add(p); + pointsBuffer.Add(p + v); + } + + pointsBuffer.FindConvexHull(result); + return true; + } + /// /// Floors the circle's center and radius values to the nearest lower integer. /// @@ -213,4 +266,56 @@ public float GetCircumferenceSquared() } #endregion + + #region Generate Circle Sector Outline Triangulation + + /// + /// Generates a triangulated outline for a circle sector and writes it into . + /// + /// The destination triangulation that will receive the generated outline triangles. + /// The starting sector angle in degrees. + /// The ending sector angle in degrees. + /// The number of sides used to approximate the curved arc of the sector. + /// The thickness of the generated outline. + /// The maximum miter length used when generating the outline joins. + /// true to use beveled joins; otherwise, mitered joins are used. + /// true to use Delaunay triangulation when generating the outline; otherwise, the default triangulation is used. + /// + /// The method returns immediately without modifying if the circle radius is not positive, is less than 3, the sector angle span is effectively zero, or the absolute angle span is 360 degrees or greater. + /// + public void GenerateCircleSectorOutlineTriangulation(Triangulation result, float startAngleDeg, float endAngleDeg, int sides, float lineThickness, + float miterLimit = 2f, bool beveled = false, bool useDelaunay = false) + { + if (sides < 3 || Radius <= 0) return; + float angleDifDeg = endAngleDeg - startAngleDeg; + float angleDifDegAbs = MathF.Abs(angleDifDeg); + if (angleDifDegAbs < 0.0001f) return; + + if (angleDifDegAbs >= 360f) + { + return; + } + + float startAngleRad = startAngleDeg * ShapeMath.DEGTORAD; + float anglePieceRad = angleDifDeg * ShapeMath.DEGTORAD; + float angleStepRad = anglePieceRad / sides; + + if (circleSectorOutlineTriangulationPolyCache == null) + { + circleSectorOutlineTriangulationPolyCache = new Polygon(sides + 1); + } + else + { + circleSectorOutlineTriangulationPolyCache.Clear(); + } + + circleSectorOutlineTriangulationPolyCache.Add(Center); + for (var i = 0; i < sides; i++) + { + var p = Center + new Vector2(Radius, 0f).Rotate(startAngleRad + angleStepRad * i); + circleSectorOutlineTriangulationPolyCache.Add(p); + } + ClipperImmediate2D.CreatePolygonOutlineTriangulation(circleSectorOutlineTriangulationPolyCache, lineThickness, miterLimit, beveled, useDelaunay, result); + } + #endregion } \ No newline at end of file diff --git a/ShapeEngine/Geometry/CircleDef/CircleSector.cs b/ShapeEngine/Geometry/CircleDef/CircleSector.cs index 0e25b58d..878004a8 100644 --- a/ShapeEngine/Geometry/CircleDef/CircleSector.cs +++ b/ShapeEngine/Geometry/CircleDef/CircleSector.cs @@ -43,9 +43,9 @@ public Vector2 Center set => Transform = Transform.SetPosition(value); } - /// - /// Gets or sets the radius of the sector in world units by changing .BaseSize. - /// + /// + /// Gets or sets the radius of the sector in world units by changing .BaseSize. + /// public float Radius { get => Transform.ScaledSize.Radius; diff --git a/ShapeEngine/Geometry/CircleDef/RingDrawing.cs b/ShapeEngine/Geometry/CircleDef/RingDrawing.cs deleted file mode 100644 index 86fa35bc..00000000 --- a/ShapeEngine/Geometry/CircleDef/RingDrawing.cs +++ /dev/null @@ -1,781 +0,0 @@ -using System.Numerics; -using Raylib_cs; -using ShapeEngine.Color; -using ShapeEngine.Geometry.SegmentDef; -using ShapeEngine.StaticLib; - -namespace ShapeEngine.Geometry.CircleDef; - -/// -/// Provides static methods for drawing ring shapes and ring outlines with various customization options. -/// -/// -/// This class contains utility methods for drawing rings, sector rings, and their outlines with support for line thickness, color, rotation, scaling, and side count. -/// -public static class RingDrawing -{ - /// - /// Draws the outlines of a ring by drawing the inner and outer circles as lines. - /// - /// The center position of the ring. - /// The radius of the inner circle. - /// The radius of the outer circle. - /// The thickness of the ring lines. - /// The color of the ring lines. - /// The length of each side segment. Default is 8. - /// - /// Both the inner and outer circles are drawn as lines to represent the ring's outline. - /// - public static void DrawRingLines(Vector2 center, float innerRadius, float outerRadius, float lineThickness, ColorRgba color, float sideLength = 8f) - { - CircleDrawing.DrawCircleLines(center, innerRadius, lineThickness, color, sideLength); - CircleDrawing.DrawCircleLines(center, outerRadius, lineThickness, color, sideLength); - } - - /// - /// Draws the outlines of a ring using line drawing information for style. - /// - /// The center position of the ring. - /// The radius of the inner circle. - /// The radius of the outer circle. - /// The line drawing style information. - /// The length of each side segment. Default is 8. - public static void DrawRingLines(Vector2 center, float innerRadius, float outerRadius, LineDrawingInfo lineInfo, float sideLength = 8f) - { - CircleDrawing.DrawCircleLines(center, innerRadius, lineInfo, 0f, sideLength); - CircleDrawing.DrawCircleLines(center, outerRadius, lineInfo, 0f, sideLength); - } - - /// - /// Draws the outlines of a ring with separate rotation for inner and outer circles. - /// - /// The center position of the ring. - /// The radius of the inner circle. - /// The radius of the outer circle. - /// Rotation of the inner circle in degrees. - /// Rotation of the outer circle in degrees. - /// The line drawing style information. - /// The length of each side segment. Default is 8. - public static void DrawRingLines(Vector2 center, float innerRadius, float outerRadius, float innerRotDeg, float outerRotDeg, LineDrawingInfo lineInfo, float sideLength = 8f) - { - CircleDrawing.DrawCircleLines(center, innerRadius, lineInfo, innerRotDeg, sideLength); - CircleDrawing.DrawCircleLines(center, outerRadius, lineInfo, outerRotDeg, sideLength); - } - - /// - /// Draws a percentage of the outlines of a ring with separate rotation for inner and outer circles. - /// - /// The center position of the ring. - /// The radius of the inner circle. - /// The radius of the outer circle. - /// The percentage of the ring to draw (0 to 1). - /// Rotation of the inner circle in degrees. - /// Rotation of the outer circle in degrees. - /// The line drawing style information. - /// The length of each side segment. Default is 8. - public static void DrawRingLinesPercentage(Vector2 center, float innerRadius, float outerRadius, float f, float innerRotDeg, float outerRotDeg, LineDrawingInfo lineInfo, float sideLength = 8f) - { - CircleDrawing.DrawCircleLinesPercentage(center, innerRadius, f, lineInfo, innerRotDeg, sideLength); - CircleDrawing.DrawCircleLinesPercentage(center, outerRadius, f, lineInfo, outerRotDeg, sideLength); - } - - /// - /// Draws the outlines of a ring with separate rotation and side length for inner and outer circles. - /// - /// The center position of the ring. - /// The radius of the inner circle. - /// The radius of the outer circle. - /// Rotation of the inner circle in degrees. - /// Rotation of the outer circle in degrees. - /// Side length for the inner circle. - /// Side length for the outer circle. - /// The line drawing style information. - public static void DrawRingLines(Vector2 center, float innerRadius, float outerRadius, float innerRotDeg, float outerRotDeg, float innerSideLength, float outerSideLength, LineDrawingInfo lineInfo) - { - CircleDrawing.DrawCircleLines(center, innerRadius, lineInfo, innerRotDeg, innerSideLength); - CircleDrawing.DrawCircleLines(center, outerRadius, lineInfo, outerRotDeg, outerSideLength); - } - - /// - /// Draws the outlines of a ring with separate rotation, side length, and line info for inner and outer circles. - /// - /// The center position of the ring. - /// The radius of the inner circle. - /// The radius of the outer circle. - /// Rotation of the inner circle in degrees. - /// Rotation of the outer circle in degrees. - /// Side length for the inner circle. - /// Side length for the outer circle. - /// Line drawing info for the inner circle. - /// Line drawing info for the outer circle. - public static void DrawRingLines(Vector2 center, float innerRadius, float outerRadius, float innerRotDeg, float outerRotDeg, float innerSideLength, float outerSideLength, LineDrawingInfo innerLineInfo, LineDrawingInfo outerLineInfo) - { - CircleDrawing.DrawCircleLines(center, innerRadius, innerLineInfo, innerRotDeg, innerSideLength); - CircleDrawing.DrawCircleLines(center, outerRadius, outerLineInfo, outerRotDeg, outerSideLength); - } - - /// - /// Draws a percentage of the outlines of a ring with separate rotation and side length for inner and outer circles. - /// - /// The center position of the ring. - /// The radius of the inner circle. - /// The radius of the outer circle. - /// The percentage of the ring to draw (0 to 1). - /// Rotation of the inner circle in degrees. - /// Rotation of the outer circle in degrees. - /// Side length for the inner circle. - /// Side length for the outer circle. - /// The line drawing style information. - public static void DrawRingLinesPercentage(Vector2 center, float innerRadius, float outerRadius, float f, float innerRotDeg, float outerRotDeg, float innerSideLength, float outerSideLength, LineDrawingInfo lineInfo) - { - CircleDrawing.DrawCircleLinesPercentage(center, innerRadius, f, lineInfo, innerRotDeg, innerSideLength); - CircleDrawing.DrawCircleLinesPercentage(center, outerRadius, f, lineInfo, outerRotDeg, outerSideLength); - } - - /// - /// Draws a percentage of the outlines of a ring with separate rotation, side length, and line info for inner and outer circles. - /// - /// The center position of the ring. - /// The radius of the inner circle. - /// The radius of the outer circle. - /// The percentage of the ring to draw (0 to 1). - /// Rotation of the inner circle in degrees. - /// Rotation of the outer circle in degrees. - /// Side length for the inner circle. - /// Side length for the outer circle. - /// Line drawing info for the inner circle. - /// Line drawing info for the outer circle. - public static void DrawRingLines(Vector2 center, float innerRadius, float outerRadius, float f, float innerRotDeg, float outerRotDeg, float innerSideLength, float outerSideLength, LineDrawingInfo innerLineInfo, LineDrawingInfo outerLineInfo) - { - CircleDrawing.DrawCircleLinesPercentage(center, outerRadius, f, outerLineInfo, outerRotDeg, outerSideLength); - CircleDrawing.DrawCircleLinesPercentage(center, innerRadius, f, innerLineInfo, innerRotDeg, innerSideLength); - } - - /// - /// Draws the outlines of a ring with a specified number of sides and rotation for inner and outer circles. - /// - /// The center position of the ring. - /// The radius of the inner circle. - /// The radius of the outer circle. - /// Rotation of the inner circle in degrees. - /// Rotation of the outer circle in degrees. - /// Number of sides for both circles. - /// The line drawing style information. - public static void DrawRingLines(Vector2 center, float innerRadius, float outerRadius, float innerRotDeg, float outerRotDeg, int sides, LineDrawingInfo lineInfo) - { - CircleDrawing.DrawCircleLines(center, innerRadius, lineInfo, innerRotDeg, sides); - CircleDrawing.DrawCircleLines(center, outerRadius, lineInfo, outerRotDeg, sides); - } - - /// - /// Draws the outlines of a ring with separate side counts and rotation for inner and outer circles. - /// - /// The center position of the ring. - /// The radius of the inner circle. - /// The radius of the outer circle. - /// Rotation of the inner circle in degrees. - /// Rotation of the outer circle in degrees. - /// Number of sides for the inner circle. - /// Number of sides for the outer circle. - /// The line drawing style information. - public static void DrawRingLines(Vector2 center, float innerRadius, float outerRadius, float innerRotDeg, float outerRotDeg, int innerSides, int outerSides, LineDrawingInfo lineInfo) - { - CircleDrawing.DrawCircleLines(center, innerRadius, lineInfo, innerRotDeg, innerSides); - CircleDrawing.DrawCircleLines(center, outerRadius, lineInfo, outerRotDeg, outerSides); - } - - /// - /// Draws the outlines of a ring with separate side counts, rotation, and line info for inner and outer circles. - /// - /// The center position of the ring. - /// The radius of the inner circle. - /// The radius of the outer circle. - /// Rotation of the inner circle in degrees. - /// Rotation of the outer circle in degrees. - /// Number of sides for the inner circle. - /// Number of sides for the outer circle. - /// Line drawing info for the inner circle. - /// Line drawing info for the outer circle. - public static void DrawRingLines(Vector2 center, float innerRadius, float outerRadius, float innerRotDeg, float outerRotDeg, int innerSides, int outerSides, LineDrawingInfo innerLineInfo, LineDrawingInfo outerLineInfo) - { - CircleDrawing.DrawCircleLines(center, innerRadius, innerLineInfo, innerRotDeg, innerSides); - CircleDrawing.DrawCircleLines(center, outerRadius, outerLineInfo, outerRotDeg, outerSides); - } - - /// - /// Draws a percentage of the outlines of a ring with separate side counts and rotation for inner and outer circles. - /// - /// The center position of the ring. - /// The radius of the inner circle. - /// The radius of the outer circle. - /// The percentage of the ring to draw (0 to 1). - /// Rotation of the inner circle in degrees. - /// Rotation of the outer circle in degrees. - /// Number of sides for the inner circle. - /// Number of sides for the outer circle. - /// The line drawing style information. - public static void DrawRingLinesPercentage(Vector2 center, float innerRadius, float outerRadius, float f, float innerRotDeg, float outerRotDeg, int innerSides, int outerSides, LineDrawingInfo lineInfo) - { - CircleDrawing.DrawCircleLinesPercentage(center, innerRadius, f, lineInfo, innerRotDeg, innerSides); - CircleDrawing.DrawCircleLinesPercentage(center, outerRadius, f, lineInfo, outerRotDeg, outerSides); - } - - /// - /// Draws a percentage of the outlines of a ring with separate side counts, rotation, and line info for inner and outer circles. - /// - /// The center position of the ring. - /// The radius of the inner circle. - /// The radius of the outer circle. - /// The percentage of the ring to draw (0 to 1). - /// Rotation of the inner circle in degrees. - /// Rotation of the outer circle in degrees. - /// Number of sides for the inner circle. - /// Number of sides for the outer circle. - /// Line drawing info for the inner circle. - /// Line drawing info for the outer circle. - public static void DrawRingLinesPercentage(Vector2 center, float innerRadius, float outerRadius, float f, float innerRotDeg, float outerRotDeg, int innerSides, int outerSides, LineDrawingInfo innerLineInfo, LineDrawingInfo outerLineInfo) - { - CircleDrawing.DrawCircleLinesPercentage(center, innerRadius, f, innerLineInfo, innerRotDeg, innerSides); - CircleDrawing.DrawCircleLinesPercentage(center, outerRadius, f, outerLineInfo, outerRotDeg, outerSides); - } - - /// - /// Draws the outlines of a ring with a specified number of sides for both circles. - /// - /// The center position of the ring. - /// The radius of the inner circle. - /// The radius of the outer circle. - /// Number of sides for both circles. - /// The line drawing style information. - public static void DrawRingLines(Vector2 center, float innerRadius, float outerRadius, int sides, LineDrawingInfo lineInfo) - { - CircleDrawing.DrawCircleLines(center, innerRadius, lineInfo, sides); - CircleDrawing.DrawCircleLines(center, outerRadius, lineInfo, sides); - } - - /// - /// Draws the outlines of a ring with separate side counts for inner and outer circles. - /// - /// The center position of the ring. - /// The radius of the inner circle. - /// The radius of the outer circle. - /// Number of sides for the inner circle. - /// Number of sides for the outer circle. - /// The line drawing style information. - public static void DrawRingLines(Vector2 center, float innerRadius, float outerRadius, int innerSides, int outerSides, LineDrawingInfo lineInfo) - { - CircleDrawing.DrawCircleLines(center, innerRadius, lineInfo, innerSides); - CircleDrawing.DrawCircleLines(center, outerRadius, lineInfo, outerSides); - } - - /// - /// Draws a percentage of the outlines of a ring with separate side counts for inner and outer circles. - /// - /// The center position of the ring. - /// The radius of the inner circle. - /// The radius of the outer circle. - /// The percentage of the ring to draw (0 to 1). - /// Number of sides for the inner circle. - /// Number of sides for the outer circle. - /// The line drawing style information. - public static void DrawRingLinesPercentage(Vector2 center, float innerRadius, float outerRadius, float f, int innerSides, int outerSides, LineDrawingInfo lineInfo) - { - CircleDrawing.DrawCircleLinesPercentage(center, innerRadius, f, lineInfo, innerSides); - CircleDrawing.DrawCircleLinesPercentage(center, outerRadius, f, lineInfo, outerSides); - } - - /// - /// Draws the outlines of a ring with separate side counts and line info for inner and outer circles. - /// - /// The center position of the ring. - /// The radius of the inner circle. - /// The radius of the outer circle. - /// Number of sides for the inner circle. - /// Number of sides for the outer circle. - /// Line drawing info for the inner circle. - /// Line drawing info for the outer circle. - public static void DrawRingLines(Vector2 center, float innerRadius, float outerRadius, int innerSides, int outerSides, LineDrawingInfo innerLineInfo, LineDrawingInfo outerLineInfo) - { - CircleDrawing.DrawCircleLines(center, innerRadius, innerLineInfo, innerSides); - CircleDrawing.DrawCircleLines(center, outerRadius, outerLineInfo, outerSides); - } - - /// - /// Draws a filled ring shape. - /// - /// The center position of the ring. - /// The radius of the inner circle. - /// The radius of the outer circle. - /// The color of the ring. - /// The length of each side segment. Default is 8. - /// - /// This draws a complete ring (360 degrees). - /// - public static void DrawRing(Vector2 center, float innerRadius, float outerRadius, ColorRgba color, float sideLength = 8f) - { - DrawSectorRing(center, innerRadius, outerRadius, 0, 360, color, sideLength); - } - - /// - /// Draws a filled ring shape with a specified number of sides. - /// - /// The center position of the ring. - /// The radius of the inner circle. - /// The radius of the outer circle. - /// Number of sides for the ring. - /// The color of the ring. - /// - /// This draws a complete ring (360 degrees). - /// - public static void DrawRing(Vector2 center, float innerRadius, float outerRadius, int sides, ColorRgba color) - { - DrawSectorRing(center, innerRadius, outerRadius, 0, 360, sides, color); - } - - /// - /// Draws a ring where each side can be scaled towards the origin of the side. - /// - /// The center position of the ring. - /// The radius of the inner circle. - /// The radius of the outer circle. - /// Rotation of the inner circle in degrees. - /// Rotation of the outer circle in degrees. - /// How to draw the lines. - /// Number of sides for the ring. - /// - /// The scale factor for each side. - /// - /// 0: No ring is drawn. - /// 1: The normal ring is drawn. - /// 0.5: Each side is half as long. - /// - /// - /// - /// The point along the line to scale from, in both directions (0 to 1). - /// - /// 0: Start of Segment - /// 0.5: Center of Segment - /// 1: End of Segment - /// - /// - /// - /// This method allows for creative effects such as dashed or rings. - /// - public static void DrawRingLinesScaled(Vector2 center, float innerRadius, float outerRadius, float innerRotDeg, float outerRotDeg, int sides, LineDrawingInfo lineInfo, float sideScaleFactor, float sideScaleOrigin = 0.5f) - { - if (sideScaleFactor <= 0f) return; - if (sideScaleFactor >= 1f) - { - CircleDrawing.DrawCircleLines(center, innerRadius, lineInfo, innerRotDeg, sides); - CircleDrawing.DrawCircleLines(center, outerRadius, lineInfo, outerRotDeg, sides); - return; - } - - var angleStep = (2f * ShapeMath.PI) / sides; - var rotRadInner = innerRotDeg * ShapeMath.DEGTORAD; - var rotRadOuter = outerRotDeg * ShapeMath.DEGTORAD; - - for (int i = 0; i < sides; i++) - { - var nextIndexInner = (i + 1) % sides; - var startInner = center + new Vector2(innerRadius, 0f).Rotate(rotRadInner + angleStep * i); - var endInner = center + new Vector2(innerRadius, 0f).Rotate(rotRadInner + angleStep * nextIndexInner); - - SegmentDrawing.DrawSegment(startInner, endInner, lineInfo, sideScaleFactor, sideScaleOrigin); - - var nextIndexOuter = (i + 1) % sides; - var startOuter = center + new Vector2(outerRadius, 0f).Rotate(rotRadOuter + angleStep * i); - var endOuter = center + new Vector2(outerRadius, 0f).Rotate(rotRadOuter + angleStep * nextIndexOuter); - - SegmentDrawing.DrawSegment(startOuter, endOuter, lineInfo, sideScaleFactor, sideScaleOrigin); - - } - } - - /// - /// Draws a ring where each side can be scaled towards the origin of the side, with separate side counts for inner and outer circles. - /// - /// The center position of the ring. - /// The radius of the inner circle. - /// The radius of the outer circle. - /// Rotation of the inner circle in degrees. - /// Rotation of the outer circle in degrees. - /// Number of sides for the inner circle. - /// Number of sides for the outer circle. - /// How to draw the lines. - /// - /// The scale factor for each side. - /// - /// 0: No ring is drawn. - /// 1: The normal ring is drawn. - /// 0.5: Each side is half as long. - /// - /// - /// - /// The point along the line to scale from, in both directions (0 to 1). - /// - /// 0: Start of Segment - /// 0.5: Center of Segment - /// 1: End of Segment - /// - /// - /// - /// This method allows for creative effects such as dashed rings. - /// - public static void DrawRingLinesScaled(Vector2 center, float innerRadius, float outerRadius, float innerRotDeg, float outerRotDeg, int innerSides, int outerSides, LineDrawingInfo lineInfo, float sideScaleFactor, float sideScaleOrigin = 0.5f) - { - if (sideScaleFactor <= 0f) return; - if (sideScaleFactor >= 1f) - { - CircleDrawing.DrawCircleLines(center, innerRadius, lineInfo, innerRotDeg, innerSides); - CircleDrawing.DrawCircleLines(center, outerRadius, lineInfo, outerRotDeg, outerSides); - return; - } - - var innerAngleStep = (2f * ShapeMath.PI) / innerSides; - var outerAngleStep = (2f * ShapeMath.PI) / outerSides; - var innerRotRad = innerRotDeg * ShapeMath.DEGTORAD; - var outerRotRad = outerRotDeg * ShapeMath.DEGTORAD; - - int maxSides = innerSides > outerSides ? innerSides : outerSides; - - for (int i = 0; i < maxSides; i++) - { - if (i < innerSides) - { - var nextIndexInner = (i + 1) % innerSides; - var startInner = center + new Vector2(innerRadius, 0f).Rotate(innerRotRad + innerAngleStep * i); - var endInner = center + new Vector2(innerRadius, 0f).Rotate(innerRotRad + innerAngleStep * nextIndexInner); - SegmentDrawing.DrawSegment(startInner, endInner, lineInfo, sideScaleFactor, sideScaleOrigin); - } - - if (i < outerSides) - { - var nextIndexOuter = (i + 1) % outerSides; - var startOuter = center + new Vector2(outerRadius, 0f).Rotate(outerRotRad + outerAngleStep * i); - var endOuter = center + new Vector2(outerRadius, 0f).Rotate(outerRotRad + outerAngleStep * nextIndexOuter); - SegmentDrawing.DrawSegment(startOuter, endOuter, lineInfo, sideScaleFactor, sideScaleOrigin); - } - - } - } - - /// - /// Draws a ring where each side can be scaled towards the origin of the side, with separate line info for inner and outer circles. - /// - /// The center position of the ring. - /// The radius of the inner circle. - /// The radius of the outer circle. - /// Rotation of the inner circle in degrees. - /// Rotation of the outer circle in degrees. - /// Number of sides for the inner circle. - /// Number of sides for the outer circle. - /// How to draw the inner lines. - /// How to draw the outer lines. - /// - /// The scale factor for each side. - /// - /// 0: No ring is drawn. - /// 1: The normal ring is drawn. - /// 0.5: Each side is half as long. - /// - /// - /// - /// The point along the line to scale from, in both directions (0 to 1). - /// - /// 0: Start of Segment - /// 0.5: Center of Segment - /// 1: End of Segment - /// - /// - /// - /// This method allows for creative effects such as dashed rings. - /// - public static void DrawRingLinesScaled(Vector2 center, float innerRadius, float outerRadius, float innerRotDeg, float outerRotDeg, int innerSides, int outerSides, LineDrawingInfo innerLineInfo, LineDrawingInfo outerLineInfo, float sideScaleFactor, float sideScaleOrigin = 0.5f) - { - if (sideScaleFactor <= 0f) return; - if (sideScaleFactor >= 1f) - { - CircleDrawing.DrawCircleLines(center, innerRadius, innerLineInfo, innerRotDeg, innerSides); - CircleDrawing.DrawCircleLines(center, outerRadius, outerLineInfo, outerRotDeg, outerSides); - return; - } - - var innerAngleStep = (2f * ShapeMath.PI) / innerSides; - var outerAngleStep = (2f * ShapeMath.PI) / outerSides; - var innerRotRad = innerRotDeg * ShapeMath.DEGTORAD; - var outerRotRad = outerRotDeg * ShapeMath.DEGTORAD; - - int maxSides = innerSides > outerSides ? innerSides : outerSides; - - for (int i = 0; i < maxSides; i++) - { - if (i < innerSides) - { - var nextIndexInner = (i + 1) % innerSides; - var startInner = center + new Vector2(innerRadius, 0f).Rotate(innerRotRad + innerAngleStep * i); - var endInner = center + new Vector2(innerRadius, 0f).Rotate(innerRotRad + innerAngleStep * nextIndexInner); - SegmentDrawing.DrawSegment(startInner, endInner, innerLineInfo, sideScaleFactor, sideScaleOrigin); - } - - if (i < outerSides) - { - var nextIndexOuter = (i + 1) % outerSides; - var startOuter = center + new Vector2(outerRadius, 0f).Rotate(outerRotRad + outerAngleStep * i); - var endOuter = center + new Vector2(outerRadius, 0f).Rotate(outerRotRad + outerAngleStep * nextIndexOuter); - SegmentDrawing.DrawSegment(startOuter, endOuter, outerLineInfo, sideScaleFactor, sideScaleOrigin); - } - - } - } - - /// - /// Draws a ring where each side can be scaled towards the origin of the side, with separate scale factors and origins for inner and outer circles. - /// - /// The center position of the ring. - /// The radius of the inner circle. - /// The radius of the outer circle. - /// Rotation of the inner circle in degrees. - /// Rotation of the outer circle in degrees. - /// Number of sides for the inner circle. - /// Number of sides for the outer circle. - /// How to draw the inner lines. - /// How to draw the outer lines. - /// - /// The scale factor for each side on the inner circle. - /// - /// 0: No inner circle is drawn. - /// 1: The normal inner circle is drawn. - /// 0.5: Each side on the inner ring is half as long. - /// - /// - /// - /// The point along the line to scale from, in both directions (0 to 1). - /// - /// 0: Start of Segment - /// 0.5: Center of Segment - /// 1: End of Segment - /// - /// - /// - /// The scale factor for each side on the outer circle. - /// - /// 0: No outer circle is drawn. - /// 1: The normal outer circle is drawn. - /// 0.5: Each side on the outer circle is half as long. - /// - /// - /// - /// The point along the line to scale from, in both directions (0 to 1). - /// - /// 0: Start of Segment - /// 0.5: Center of Segment - /// 1: End of Segment - /// - /// - /// - /// This method allows for creative effects such as dashed rings, - /// with independent control for inner and outer circles. - /// - public static void DrawRingLinesScaled(Vector2 center, float innerRadius, float outerRadius, float innerRotDeg, float outerRotDeg, int innerSides, int outerSides, LineDrawingInfo innerLineInfo, LineDrawingInfo outerLineInfo, float innerSideScaleFactor, float outerSideScaleFactor, float innerSideScaleOrigin, float outerSideScaleOrigin) - { - bool drawInner = true; - bool drawOuter = true; - if (innerSideScaleFactor >= 1f) - { - drawInner = false; - CircleDrawing.DrawCircleLines(center, innerRadius, innerLineInfo, innerRotDeg, innerSides); - } - if (outerSideScaleFactor >= 1f) - { - drawOuter = false; - CircleDrawing.DrawCircleLines(center, outerRadius, outerLineInfo, outerRotDeg, outerSides); - } - - if (!drawInner && !drawOuter) return; - - var innerAngleStep = (2f * ShapeMath.PI) / innerSides; - var outerAngleStep = (2f * ShapeMath.PI) / outerSides; - var innerRotRad = innerRotDeg * ShapeMath.DEGTORAD; - var outerRotRad = outerRotDeg * ShapeMath.DEGTORAD; - - int maxSides; - if (!drawInner) maxSides = outerSides; - else if (!drawOuter) maxSides = innerSides; - else maxSides = innerSides > outerSides ? innerSides : outerSides; - - for (int i = 0; i < maxSides; i++) - { - if (drawInner && i < innerSides) - { - var nextIndexInner = (i + 1) % innerSides; - var startInner = center + new Vector2(innerRadius, 0f).Rotate(innerRotRad + innerAngleStep * i); - var endInner = center + new Vector2(innerRadius, 0f).Rotate(innerRotRad + innerAngleStep * nextIndexInner); - SegmentDrawing.DrawSegment(startInner, endInner, innerLineInfo, innerSideScaleFactor, innerSideScaleOrigin); - } - - if (drawOuter && i < outerSides) - { - var nextIndexOuter = (i + 1) % outerSides; - var startOuter = center + new Vector2(outerRadius, 0f).Rotate(outerRotRad + outerAngleStep * i); - var endOuter = center + new Vector2(outerRadius, 0f).Rotate(outerRotRad + outerAngleStep * nextIndexOuter); - SegmentDrawing.DrawSegment(startOuter, endOuter, outerLineInfo, outerSideScaleFactor, outerSideScaleOrigin); - } - - } - } - - /// - /// Draws the outlines of a sector ring (arc-shaped ring) with specified line thickness and color. - /// - /// The center position of the sector ring. - /// The radius of the inner arc. - /// The radius of the outer arc. - /// The starting angle of the sector in degrees. - /// The ending angle of the sector in degrees. - /// The thickness of the ring lines. - /// The color of the ring lines. - /// The length of each side segment. Default is 8. - /// - /// This method also draws the connecting lines between the inner and outer arcs at the start and end angles. - /// - public static void DrawSectorRingLines(Vector2 center, float innerRadius, float outerRadius, float startAngleDeg, float endAngleDeg, float lineThickness, ColorRgba color, float sideLength = 8f) - { - CircleDrawing.DrawCircleSectorLines(center, innerRadius, startAngleDeg, endAngleDeg, lineThickness, color, false, sideLength); - CircleDrawing.DrawCircleSectorLines(center, outerRadius, startAngleDeg, endAngleDeg, lineThickness, color, false, sideLength); - - float startAngleRad = startAngleDeg * ShapeMath.DEGTORAD; - float endAngleRad = endAngleDeg * ShapeMath.DEGTORAD; - var innerStart = center + (ShapeVec.Right() * innerRadius).Rotate(startAngleRad); - var outerStart = center + (ShapeVec.Right() * outerRadius).Rotate(startAngleRad); - SegmentDrawing.DrawSegment(innerStart, outerStart, lineThickness, color, LineCapType.CappedExtended, 4); - - var innerEnd = center + (ShapeVec.Right() * innerRadius).Rotate(endAngleRad); - var outerEnd = center + (ShapeVec.Right() * outerRadius).Rotate(endAngleRad); - SegmentDrawing.DrawSegment(innerEnd, outerEnd, lineThickness, color, LineCapType.CappedExtended, 4); - } - - /// - /// Draws the outlines of a sector ring with an additional rotation offset. - /// - /// The center position of the sector ring. - /// The radius of the inner arc. - /// The radius of the outer arc. - /// The starting angle of the sector in degrees. - /// The ending angle of the sector in degrees. - /// The rotation offset in degrees. - /// The thickness of the ring lines. - /// The color of the ring lines. - /// The length of each side segment. Default is 8. - public static void DrawSectorRingLines(Vector2 center, float innerRadius, float outerRadius, float startAngleDeg, float endAngleDeg, float rotOffsetDeg, float lineThickness, ColorRgba color, float sideLength = 8f) - { - DrawSectorRingLines(center, innerRadius, outerRadius, startAngleDeg + rotOffsetDeg, endAngleDeg + rotOffsetDeg, lineThickness, color, sideLength); - } - - /// - /// Draws the outlines of a sector ring using line drawing information for style. - /// - /// The center position of the sector ring. - /// The radius of the inner arc. - /// The radius of the outer arc. - /// The starting angle of the sector in degrees. - /// The ending angle of the sector in degrees. - /// The line drawing style information. - /// The length of each side segment. Default is 8. - public static void DrawSectorRingLines(Vector2 center, float innerRadius, float outerRadius, float startAngleDeg, float endAngleDeg, LineDrawingInfo lineInfo, float sideLength = 8f) - { - CircleDrawing.DrawCircleSectorLines(center, innerRadius, startAngleDeg, endAngleDeg, lineInfo, false, sideLength); - CircleDrawing.DrawCircleSectorLines(center, outerRadius, startAngleDeg, endAngleDeg, lineInfo, false, sideLength); - - float startAngleRad = startAngleDeg * ShapeMath.DEGTORAD; - float endAngleRad = endAngleDeg * ShapeMath.DEGTORAD; - var innerStart = center + (ShapeVec.Right() * innerRadius - new Vector2(lineInfo.Thickness / 2, 0)).Rotate(startAngleRad); - var outerStart = center + (ShapeVec.Right() * outerRadius + new Vector2(lineInfo.Thickness / 2, 0)).Rotate(startAngleRad); - SegmentDrawing.DrawSegment(innerStart, outerStart, lineInfo); - - var innerEnd = center + (ShapeVec.Right() * innerRadius - new Vector2(lineInfo.Thickness / 2, 0)).Rotate(endAngleRad); - var outerEnd = center + (ShapeVec.Right() * outerRadius + new Vector2(lineInfo.Thickness / 2, 0)).Rotate(endAngleRad); - SegmentDrawing.DrawSegment(innerEnd, outerEnd, lineInfo); - } - - /// - /// Draws the outlines of a sector ring using line drawing information and an additional rotation offset. - /// - /// The center position of the sector ring. - /// The radius of the inner arc. - /// The radius of the outer arc. - /// The starting angle of the sector in degrees. - /// The ending angle of the sector in degrees. - /// The rotation offset in degrees. - /// The line drawing style information. - /// The length of each side segment. Default is 8. - public static void DrawSectorRingLines(Vector2 center, float innerRadius, float outerRadius, float startAngleDeg, float endAngleDeg, float rotOffsetDeg, LineDrawingInfo lineInfo, float sideLength = 8f) - { - DrawSectorRingLines(center, innerRadius, outerRadius, startAngleDeg + rotOffsetDeg, endAngleDeg + rotOffsetDeg, lineInfo, sideLength); - } - - /// - /// Draws a filled sector ring (arc-shaped ring) with a specified color. - /// - /// The center position of the sector ring. - /// The radius of the inner arc. - /// The radius of the outer arc. - /// The starting angle of the sector in degrees. - /// The ending angle of the sector in degrees. - /// The color of the sector ring. - /// The length of each side segment. Default is 10. - public static void DrawSectorRing(Vector2 center, float innerRadius, float outerRadius, float startAngleDeg, float endAngleDeg, ColorRgba color, float sideLength = 10f) - { - float anglePiece = endAngleDeg - startAngleDeg; - int sides = CircleDrawing.GetCircleArcSideCount(outerRadius, MathF.Abs(anglePiece), sideLength); - Raylib.DrawRing(center, innerRadius, outerRadius, startAngleDeg, endAngleDeg, sides, color.ToRayColor()); - } - - /// - /// Draws a filled sector ring (arc-shaped ring) with a specified number of sides and color. - /// - /// The center position of the sector ring. - /// The radius of the inner arc. - /// The radius of the outer arc. - /// The starting angle of the sector in degrees. - /// The ending angle of the sector in degrees. - /// Number of sides for the sector ring. - /// The color of the sector ring. - public static void DrawSectorRing(Vector2 center, float innerRadius, float outerRadius, float startAngleDeg, float endAngleDeg, int sides, ColorRgba color) - { - Raylib.DrawRing(center, innerRadius, outerRadius, startAngleDeg, endAngleDeg, sides, color.ToRayColor()); - } - - /// - /// Draws a filled sector ring (arc-shaped ring) with a specified color and an additional rotation offset. - /// - /// The center position of the sector ring. - /// The radius of the inner arc. - /// The radius of the outer arc. - /// The starting angle of the sector in degrees. - /// The ending angle of the sector in degrees. - /// The rotation offset in degrees. - /// The color of the sector ring. - /// The length of each side segment. Default is 10. - public static void DrawSectorRing(Vector2 center, float innerRadius, float outerRadius, float startAngleDeg, float endAngleDeg, float rotOffsetDeg, ColorRgba color, float sideLength = 10f) - { - DrawSectorRing(center, innerRadius, outerRadius, startAngleDeg + rotOffsetDeg, endAngleDeg + rotOffsetDeg, color, sideLength); - } - - /// - /// Draws a filled sector ring (arc-shaped ring) with a specified number of sides, color, and an additional rotation offset. - /// - /// The center position of the sector ring. - /// The radius of the inner arc. - /// The radius of the outer arc. - /// The starting angle of the sector in degrees. - /// The ending angle of the sector in degrees. - /// The rotation offset in degrees. - /// Number of sides for the sector ring. - /// The color of the sector ring. - public static void DrawSectorRing(Vector2 center, float innerRadius, float outerRadius, float startAngleDeg, float endAngleDeg, float rotOffsetDeg, int sides, ColorRgba color) - { - DrawSectorRing(center, innerRadius, outerRadius, startAngleDeg + rotOffsetDeg, endAngleDeg + rotOffsetDeg, sides, color); - } - -} \ No newline at end of file diff --git a/ShapeEngine/Geometry/CollisionSystem/Collider.cs b/ShapeEngine/Geometry/CollisionSystem/Collider.cs index b86e0a62..8e399ee0 100644 --- a/ShapeEngine/Geometry/CollisionSystem/Collider.cs +++ b/ShapeEngine/Geometry/CollisionSystem/Collider.cs @@ -296,6 +296,7 @@ protected virtual void OnRemovedFromCollisionBody(CollisionObject formerParent) /// The bounding . public abstract Rect GetBoundingBox(); + //TODO: All ProjectShape functions below need to thread safe (no points buffer)! /// /// Projects this collider's shape along a given vector. /// diff --git a/ShapeEngine/Geometry/CollisionSystem/CollisionHandlerDef/CollisionHandler.cs b/ShapeEngine/Geometry/CollisionSystem/CollisionHandlerDef/CollisionHandler.cs index d3373255..cb2c726d 100644 --- a/ShapeEngine/Geometry/CollisionSystem/CollisionHandlerDef/CollisionHandler.cs +++ b/ShapeEngine/Geometry/CollisionSystem/CollisionHandlerDef/CollisionHandler.cs @@ -5,7 +5,6 @@ namespace ShapeEngine.Geometry.CollisionSystem.CollisionHandlerDef; - /// /// Handles collision detection, resolution, and spatial queries for registered instances. /// diff --git a/ShapeEngine/Geometry/CustomDrawing.cs b/ShapeEngine/Geometry/CustomDrawing.cs index a71e1cc8..504fddbb 100644 --- a/ShapeEngine/Geometry/CustomDrawing.cs +++ b/ShapeEngine/Geometry/CustomDrawing.cs @@ -47,13 +47,14 @@ public static class CustomDrawing /// /// Each intersection point is visualized as a small circle, and its normal is drawn as a line. /// - public static void Draw(this IntersectionPoints colPoints, float lineThickness, ColorRgba intersectColorRgba, ColorRgba normalColorRgba) + public static void Draw(this IntersectionPoints colPoints, float lineThickness, ColorRgba intersectColorRgba, ColorRgba normalColorRgba, float pointSmoothness = 0.5f) { if ( colPoints.Count <= 0) return; foreach (var i in colPoints) { - CircleDrawing.DrawCircle(i.Point, lineThickness * 2f, intersectColorRgba, 12); + var circle = new Circle(i.Point, lineThickness * 2f); + circle.Draw(intersectColorRgba, pointSmoothness); SegmentDrawing.DrawSegment(i.Point, i.Point + i.Normal * lineThickness * 10f, lineThickness, normalColorRgba); } } @@ -213,7 +214,8 @@ public static void DrawArrow(Vector2 tailPoint, Vector2 headPoint, float headWid var b = tailEnd + pl * headWidth * 0.5f; var c = tailEnd + pr * headWidth * 0.5f; if(headFillColor.A > 0) TriangleDrawing.DrawTriangle(headPoint, b, c, headFillColor); - TriangleDrawing.DrawTriangleLines(headPoint, b, c, info); + var triangle = new Triangle(headPoint, b, c); + triangle.DrawLines(info); } /// @@ -247,7 +249,8 @@ public static void DrawArrow2(Vector2 tailPoint, Vector2 headPoint, float headWi var b = tailEnd + pl * headWidth * 0.5f; var c = tailEnd + pr * headWidth * 0.5f; if(headFillColor.A > 0) TriangleDrawing.DrawTriangle(headPoint, b, c, headFillColor); - TriangleDrawing.DrawTriangleLines(headPoint, b, c, info); + var triangle = new Triangle(headPoint, b, c); + triangle.DrawLines(info); } /// @@ -283,7 +286,8 @@ public static void DrawArrow3(Vector2 tailPoint, Vector2 headPoint, float headWi var b = tailEnd + pl * headWidth * 0.5f; var c = tailEnd + pr * headWidth * 0.5f; if(headFillColor.A > 0) TriangleDrawing.DrawTriangle(headPoint, b, c, headFillColor); - TriangleDrawing.DrawTriangleLines(headPoint, b, c, info); + var triangle = new Triangle(headPoint, b, c); + triangle.DrawLines(info); } #endregion } \ No newline at end of file diff --git a/ShapeEngine/Geometry/FractureHelper.cs b/ShapeEngine/Geometry/FractureHelper.cs index 2bd60184..01acb3dc 100644 --- a/ShapeEngine/Geometry/FractureHelper.cs +++ b/ShapeEngine/Geometry/FractureHelper.cs @@ -1,6 +1,6 @@ using ShapeEngine.Geometry.PolygonDef; using ShapeEngine.Geometry.TriangulationDef; -using ShapeEngine.StaticLib; +using ShapeEngine.ShapeClipper; namespace ShapeEngine.Geometry; @@ -12,6 +12,9 @@ namespace ShapeEngine.Geometry; /// public class FractureHelper { + private static Triangulation buffer = new(); + private static Triangulation buffer2 = new(); + /// /// The minimum area for a fracture piece to be kept. /// @@ -52,13 +55,13 @@ public FractureHelper(float minArea, float maxArea, float keepChance = 0.5f, flo this.KeepChance = keepChance; this.NarrowValue = narrowValue; } - + /// /// Fractures a polygon by cutting it with another polygon and subdividing the resulting pieces. /// /// The original polygon to be fractured. /// The polygon used to cut the original shape. - /// A object containing the new shapes, cutouts, and fracture pieces. + /// A object containing the new shapes, cutouts, and fracture pieces. /// /// /// @@ -69,17 +72,22 @@ public FractureHelper(float minArea, float maxArea, float keepChance = 0.5f, flo /// /// /// - public FractureInfo Fracture(Polygon shape, Polygon cutShape) + public void Fracture(Polygon shape, Polygon cutShape, FractureInfo result) { - var cutOuts = ShapeClipper.Intersect(shape, cutShape).ToPolygons(true); - var newShapes = ShapeClipper.Difference(shape, cutShape).ToPolygons(true); - Triangulation pieces = new(); - foreach (var cutOut in cutOuts) + ClipperImmediate2D.ClipEngine.Execute(shape, cutShape, ShapeClipperClipType.Intersection, result.Cutouts); + ClipperImmediate2D.ClipEngine.Execute(shape, cutShape, ShapeClipperClipType.Difference, result.NewShapes); + + result.Cutouts.RemoveAllHoles(); + result.NewShapes.RemoveAllHoles(); + + result.Pieces.Clear(); + foreach (var cutOut in result.Cutouts) { - var fracturePieces = cutOut.Triangulate().Subdivide(MinArea, MaxArea, KeepChance, NarrowValue); - pieces.AddRange(fracturePieces); + buffer.Clear(); + buffer2.Clear(); + cutOut.Triangulate(buffer); + buffer.Subdivide(buffer2, MinArea, MaxArea, KeepChance, NarrowValue); + result.Pieces.AddRange(buffer2); } - - return new(newShapes, cutOuts, pieces); } } \ No newline at end of file diff --git a/ShapeEngine/Geometry/FractureInfo.cs b/ShapeEngine/Geometry/FractureInfo.cs index ff82812c..5fc89bc4 100644 --- a/ShapeEngine/Geometry/FractureInfo.cs +++ b/ShapeEngine/Geometry/FractureInfo.cs @@ -1,5 +1,5 @@ +using ShapeEngine.Geometry.PolygonDef; using ShapeEngine.Geometry.TriangulationDef; -using ShapeEngine.StaticLib; namespace ShapeEngine.Geometry; @@ -38,4 +38,10 @@ public FractureInfo(Polygons newShapes, Polygons cutouts, Triangulation pieces) this.Cutouts = cutouts; this.Pieces = pieces; } + public FractureInfo() + { + this.NewShapes = new(); + this.Cutouts = new(); + this.Pieces = new(); + } } \ No newline at end of file diff --git a/ShapeEngine/Geometry/GappedDrawing.cs b/ShapeEngine/Geometry/GappedDrawing.cs deleted file mode 100644 index c4e65b97..00000000 --- a/ShapeEngine/Geometry/GappedDrawing.cs +++ /dev/null @@ -1,1137 +0,0 @@ -using System.Numerics; -using ShapeEngine.Core.Structs; -using ShapeEngine.Geometry.CircleDef; -using ShapeEngine.Geometry.PointsDef; -using ShapeEngine.Geometry.PolygonDef; -using ShapeEngine.Geometry.PolylineDef; -using ShapeEngine.Geometry.QuadDef; -using ShapeEngine.Geometry.RectDef; -using ShapeEngine.Geometry.SegmentDef; -using ShapeEngine.Geometry.TriangleDef; -using ShapeEngine.StaticLib; - -namespace ShapeEngine.Geometry; - -/// -/// Provides static methods for drawing shapes and outlines with configurable gaps (dashed or segmented effects). -/// These methods allow for drawing lines, outlines, and various shapes with gaps, based on the specified parameters. -/// -/// -/// All methods in this class are static and intended for rendering shapes with customizable gaps. -/// Useful for visual effects such as dashed lines, segmented outlines, or highlighting. -/// -public static class GappedDrawing -{ - /// - /// Draws a line segment with gaps, creating a dashed or segmented effect. - /// - /// The starting point of the line segment. - /// The ending point of the line segment. - /// - /// The length of the line. - /// If zero or negative, the method calculates it automatically. - /// Providing a known length avoids redundant calculations and improves performance, especially for static segments. - /// Parameters describing how to draw the line (e.g., color, thickness). - /// Parameters describing the gap configuration (number of gaps, gap percentage, etc.). - /// - /// The length of the line if positive; otherwise, -1. - /// If the shape does not change, the valid length can be reused in subsequent frames to avoid recalculating. - /// - /// - /// - If is 0 or is 0, the line is drawn solid. - /// - If is 1 or greater, no line is drawn. - /// - public static float DrawGappedSegment(Vector2 start, Vector2 end, float length, LineDrawingInfo lineInfo, GappedOutlineDrawingInfo gapDrawingInfo) - { - if (gapDrawingInfo.Gaps <= 0 || gapDrawingInfo.GapPerimeterPercentage <= 0f) - { - SegmentDrawing.DrawSegment(start, end, lineInfo); - return length > 0f ? length : -1f; - } - - if (gapDrawingInfo.GapPerimeterPercentage >= 1f) return length > 0f ? length : -1f; - - var linePercentage = 1f - gapDrawingInfo.GapPerimeterPercentage; - var lines = gapDrawingInfo.Gaps; - var gapPercentageRange = gapDrawingInfo.GapPerimeterPercentage / gapDrawingInfo.Gaps; - var linePercentageRange = linePercentage / lines; - - var w = end - start; - if (length <= 0) length = w.Length(); - if (length <= 0) return -1f; - var dir = w / length; - - var lineLength = length * linePercentageRange; - var gapLength = length * gapPercentageRange; - - var curDistance = gapDrawingInfo.StartOffset < 1f ? gapDrawingInfo.StartOffset * length : 0f; - var curStart = start + dir * curDistance; - Vector2 curEnd; - var remainingLineLength = 0f; - if (curDistance + lineLength >= length) - { - curEnd = end; - var tempLength = (curEnd - curStart).Length(); - curDistance = 0; - remainingLineLength = lineLength - tempLength; - } - else - { - curEnd = curStart + dir * lineLength; - curDistance += lineLength; - } - - int drawnLines = 0; - while (drawnLines <= lines) - { - SegmentDrawing.DrawSegment(curStart, curEnd, lineInfo); - - if (remainingLineLength > 0f) - { - curStart = start; - curEnd = curStart + dir * remainingLineLength; - curDistance = remainingLineLength; - remainingLineLength = 0f; - drawnLines++; - } - else - { - if (curDistance + gapLength >= length) //gap overshoots end - { - var tempLength = (end - curEnd).Length(); - var remaining = gapLength - tempLength; - curDistance = remaining; - - curStart = start + dir * curDistance; - } - else //advance gap length to find new start - { - curStart = curEnd + dir * gapLength; - curDistance += gapLength; - } - - if (curDistance + lineLength >= length) //line overshoots end - { - curEnd = end; - var tempLength = (curEnd - curStart).Length(); - curDistance = 0; - remainingLineLength = lineLength - tempLength; - } - else //advance line length to find new end - { - curEnd = curStart + dir * lineLength; - curDistance += lineLength; - drawnLines++; - } - } - - } - - return length; - } - - /// - /// Draws an outline (closed polyline) with gaps, creating a dashed or segmented effect. - /// - /// The list of points defining the outline. - /// - /// The total length of the outline. - /// If zero or negative, the method calculates it automatically. - /// Providing a known length avoids redundant calculations and improves performance, especially for static segments. - /// - /// Parameters describing how to draw the outline. - /// Parameters describing the gap configuration. - /// - /// The perimeter of the outline if positive; otherwise, -1. - /// If the shape does not change, the valid length can be reused in subsequent frames to avoid recalculating. - /// - /// - /// - If is 0 or is 0, the outline is drawn solid. - /// - If is 1 or greater, no outline is drawn. - /// - public static float DrawGappedOutline(this List shapePoints, float perimeter, LineDrawingInfo lineInfo, GappedOutlineDrawingInfo gapDrawingInfo) - { - if (gapDrawingInfo.Gaps <= 0 || gapDrawingInfo.GapPerimeterPercentage <= 0f) - { - shapePoints.DrawOutline(lineInfo); - return perimeter > 0f ? perimeter : -1f; - } - - if (gapDrawingInfo.GapPerimeterPercentage >= 1f) return perimeter > 0f ? perimeter : -1f; - - var nonGapPercentage = 1f - gapDrawingInfo.GapPerimeterPercentage; - - var gapPercentageRange = gapDrawingInfo.GapPerimeterPercentage / gapDrawingInfo.Gaps; - var nonGapPercentageRange = nonGapPercentage / gapDrawingInfo.Gaps; - - if (perimeter <= 0f) - { - for (int i = 0; i < shapePoints.Count; i++) - { - var curP = shapePoints[i]; - var nextP = shapePoints[(i + 1) % shapePoints.Count]; - - perimeter += (nextP - curP).Length(); - } - } - - var startDistance = perimeter * gapDrawingInfo.StartOffset; - var curDistance = 0f; - var nextDistance = startDistance; - - - var curIndex = 0; - var curPoint = shapePoints[0]; - var nextPoint= shapePoints[1 % shapePoints.Count]; - var curW = nextPoint - curPoint; - var curDis = curW.Length(); - - var points = new List(3); - - int whileCounter = gapDrawingInfo.Gaps; - - while (whileCounter > 0) - { - if (curDistance + curDis >= nextDistance) - { - var p = curPoint + (curW / curDis) * (nextDistance - curDistance); - - - if (points.Count == 0) - { - nextDistance += nonGapPercentageRange * perimeter; - points.Add(p); - - } - else - { - nextDistance += gapPercentageRange * perimeter; - points.Add(p); - if (points.Count == 2) - { - SegmentDrawing.DrawSegment(points[0], points[1], lineInfo); - } - else - { - for (var i = 0; i < points.Count - 1; i++) - { - var p1 = points[i]; - var p2 = points[(i + 1) % points.Count]; - SegmentDrawing.DrawSegment(p1, p2, lineInfo); - } - } - - points.Clear(); - whileCounter--; - } - - } - else - { - - if(points.Count > 0) points.Add(nextPoint); - - curDistance += curDis; - curIndex = (curIndex + 1) % shapePoints.Count; - curPoint = shapePoints[curIndex]; - nextPoint = shapePoints[(curIndex + 1) % shapePoints.Count]; - curW = nextPoint - curPoint; - curDis = curW.Length(); - } - - } - - return perimeter; - } - - /// - /// Draws an outline (closed polyline) with gaps for a collection. - /// - /// The points defining the outline. - /// - /// The total length of the outline. - /// If zero or negative, the method calculates it automatically. - /// Providing a known length avoids redundant calculations and improves performance, especially for static segments. - /// - /// Parameters describing how to draw the outline. - /// Parameters describing the gap configuration. - /// - /// The perimeter of the outline if positive; otherwise, -1. - /// If the shape does not change, the valid length can be reused in subsequent frames to avoid recalculating. - /// - /// - /// - If is 0 or is 0, the outline is drawn solid. - /// - If is 1 or greater, no outline is drawn. - /// - public static float DrawGappedOutline(this Points shapePoints, float perimeter, LineDrawingInfo lineInfo, GappedOutlineDrawingInfo gapDrawingInfo) - { - if (gapDrawingInfo.Gaps <= 0 || gapDrawingInfo.GapPerimeterPercentage <= 0f) - { - shapePoints.DrawOutline(lineInfo); - return perimeter > 0f ? perimeter : -1f; - } - - if (gapDrawingInfo.GapPerimeterPercentage >= 1f) return perimeter > 0f ? perimeter : -1f; - - var nonGapPercentage = 1f - gapDrawingInfo.GapPerimeterPercentage; - - var gapPercentageRange = gapDrawingInfo.GapPerimeterPercentage / gapDrawingInfo.Gaps; - var nonGapPercentageRange = nonGapPercentage / gapDrawingInfo.Gaps; - - if (perimeter <= 0f) - { - for (int i = 0; i < shapePoints.Count; i++) - { - var curP = shapePoints[i]; - var nextP = shapePoints[(i + 1) % shapePoints.Count]; - - perimeter += (nextP - curP).Length(); - } - } - - var startDistance = perimeter * gapDrawingInfo.StartOffset; - var curDistance = 0f; - var nextDistance = startDistance; - - - var curIndex = 0; - var curPoint = shapePoints[0]; - var nextPoint= shapePoints[1 % shapePoints.Count]; - var curW = nextPoint - curPoint; - var curDis = curW.Length(); - - var points = new List(3); - - int whileCounter = gapDrawingInfo.Gaps; - - while (whileCounter > 0) - { - if (curDistance + curDis >= nextDistance) - { - var p = curPoint + (curW / curDis) * (nextDistance - curDistance); - - - if (points.Count == 0) - { - nextDistance += nonGapPercentageRange * perimeter; - points.Add(p); - - } - else - { - nextDistance += gapPercentageRange * perimeter; - points.Add(p); - if (points.Count == 2) - { - SegmentDrawing.DrawSegment(points[0], points[1], lineInfo); - } - else - { - for (var i = 0; i < points.Count - 1; i++) - { - var p1 = points[i]; - var p2 = points[(i + 1) % points.Count]; - SegmentDrawing.DrawSegment(p1, p2, lineInfo); - } - } - - points.Clear(); - whileCounter--; - } - - } - else - { - - if(points.Count > 0) points.Add(nextPoint); - - curDistance += curDis; - curIndex = (curIndex + 1) % shapePoints.Count; - curPoint = shapePoints[curIndex]; - nextPoint = shapePoints[(curIndex + 1) % shapePoints.Count]; - curW = nextPoint - curPoint; - curDis = curW.Length(); - } - - } - - return perimeter; - } - - - /// - /// Draws a segment with gaps, creating a dashed or segmented effect. - /// - /// The segment to draw. - /// - /// The length of the segment. - /// If zero or negative, the method calculates it automatically. - /// Providing a known length avoids redundant calculations and improves performance, especially for static segments. - /// - /// Parameters describing how to draw the segment. - /// Parameters describing the gap configuration. - /// - /// Returns the segment length if positive; otherwise, returns -1. - /// If the shape does not change, the valid length can be reused in subsequent frames to avoid recalculating. - /// - /// - /// This is a convenience wrapper for . - /// - public static float DrawGapped(this Segment s, float length, LineDrawingInfo lineInfo, - GappedOutlineDrawingInfo gapDrawingInfo) => DrawGappedSegment(s.Start, s.End, length, lineInfo, gapDrawingInfo); - - /// - /// Draws a gapped outline for a circle, creating a dashed or segmented circular outline. - /// - /// The circle to draw. - /// Parameters describing how to draw the outline. - /// Parameters describing the gap configuration. - /// The rotation of the circle in degrees. - /// The number of sides to approximate the circle (minimum 3). - /// - /// - If is 0 or is 0, the outline is drawn solid. - /// - If is 1 or greater, no outline is drawn. - /// - The parameter controls the smoothness of the circle. - /// - public static void DrawGappedOutline(this Circle circle, LineDrawingInfo lineInfo, GappedOutlineDrawingInfo gapDrawingInfo, float rotDeg, int sides = 18) - { - if (sides < 3) return; - if (gapDrawingInfo.Gaps <= 0 || gapDrawingInfo.GapPerimeterPercentage <= 0f) - { - circle.DrawLines(lineInfo, rotDeg, sides); - return; - } - - if (gapDrawingInfo.GapPerimeterPercentage >= 1f) return; - - var nonGapPercentage = 1f - gapDrawingInfo.GapPerimeterPercentage; - - var gapPercentageRange = gapDrawingInfo.GapPerimeterPercentage / gapDrawingInfo.Gaps; - var nonGapPercentageRange = nonGapPercentage / gapDrawingInfo.Gaps; - - float angleStep = (MathF.PI * 2) / sides; - float angleRad = rotDeg * ShapeMath.DEGTORAD; - Vector2[] circlePoints = new Vector2[sides]; - - float circumference = 0f; - for (int i = 0; i < sides; i++) - { - var curP = circle.GetVertex(angleRad, angleStep, i); - circlePoints[i] = curP; - var nextP = circle.GetVertex(angleRad, angleStep, (i + 1) % sides); - circumference += (nextP - curP).Length(); - } - - var startDistance = circumference * gapDrawingInfo.StartOffset; - var curDistance = 0f; - var nextDistance = startDistance; - - var curIndex = 0; - var curPoint = circlePoints[0]; - var nextPoint= circlePoints[1]; - var curW = nextPoint - curPoint; - var curDis = curW.Length(); - - var points = new List(3); - - int whileCounter = gapDrawingInfo.Gaps; - - while (whileCounter > 0) - { - if (curDistance + curDis >= nextDistance) - { - var p = curPoint + (curW / curDis) * (nextDistance - curDistance); - - - if (points.Count == 0) - { - nextDistance += nonGapPercentageRange * circumference; - points.Add(p); - - } - else - { - nextDistance += gapPercentageRange * circumference; - points.Add(p); - if (points.Count == 2) - { - SegmentDrawing.DrawSegment(points[0], points[1], lineInfo); - } - else - { - for (var i = 0; i < points.Count - 1; i++) - { - var p1 = points[i]; - var p2 = points[(i + 1) % points.Count]; - SegmentDrawing.DrawSegment(p1, p2, lineInfo); - } - } - - points.Clear(); - whileCounter--; - } - - } - else - { - - if(points.Count > 0) points.Add(nextPoint); - - curDistance += curDis; - curIndex = (curIndex + 1) % sides; - curPoint = circlePoints[curIndex]; - nextPoint = circlePoints[(curIndex + 1) % sides]; - curW = nextPoint - curPoint; - curDis = curW.Length(); - } - - } - } - - /// - /// Draws a gapped outline for a ring (annulus), creating dashed or segmented outlines for both inner and outer circles. - /// - /// The center of the ring. - /// The radius of the inner circle. If zero or negative, only the outer circle is drawn. - /// The radius of the outer circle. If zero or negative, only the inner circle is drawn. - /// Parameters describing how to draw the outlines. - /// Parameters describing the gap configuration. - /// The rotation of the ring in degrees. - /// The approximate length of each side used to approximate the circles. - /// - /// - If both radii are zero or negative, nothing is drawn. - /// - The number of sides for each circle is determined by the radius and . - /// - public static void DrawGappedRing(Vector2 center, float innerRadius, float outerRadius, LineDrawingInfo lineInfo, GappedOutlineDrawingInfo gapDrawingInfo, float rotDeg, float sideLength = 8f) - { - if (innerRadius <= 0 && outerRadius <= 0) return; - - - int outerSides = CircleDrawing.GetCircleSideCount(outerRadius, sideLength); - if (innerRadius <= 0) - { - DrawGappedOutline(new Circle(center, outerRadius), lineInfo, gapDrawingInfo, rotDeg, outerSides); - return; - } - - int innerSides = CircleDrawing.GetCircleSideCount(innerRadius, sideLength); - if (outerRadius <= 0) - { - DrawGappedOutline(new Circle(center, innerRadius), lineInfo, gapDrawingInfo, rotDeg, innerSides); - return; - } - - DrawGappedOutline(new Circle(center, innerRadius), lineInfo, gapDrawingInfo, rotDeg, innerSides); - DrawGappedOutline(new Circle(center, outerRadius), lineInfo, gapDrawingInfo, rotDeg, outerSides); - } - - - /// - /// Draws a gapped outline for a triangle, creating a dashed or segmented effect along the triangle's perimeter. - /// - /// The triangle to draw. - /// - /// The total length of the triangle's perimeter. - /// If zero or negative, the method calculates it automatically. - /// Providing a known length avoids redundant calculations and improves performance, especially for static segments. - /// - /// Parameters describing how to draw the outline. - /// Parameters describing the gap configuration. - /// - /// The perimeter of the triangle if positive; otherwise, -1. - /// If the shape does not change, the valid length can be reused in subsequent frames to avoid recalculating. - /// - /// - /// - If is 0 or is 0, the outline is drawn solid. - /// - If is 1 or greater, no outline is drawn. - /// - public static float DrawGappedOutline(this Triangle triangle, float perimeter, LineDrawingInfo lineInfo, GappedOutlineDrawingInfo gapDrawingInfo) - { - if (gapDrawingInfo.Gaps <= 0 || gapDrawingInfo.GapPerimeterPercentage <= 0f) - { - triangle.DrawLines(lineInfo); - return perimeter > 0f ? perimeter : -1f; - } - - if (gapDrawingInfo.GapPerimeterPercentage >= 1f) return perimeter > 0f ? perimeter : -1f; - - var nonGapPercentage = 1f - gapDrawingInfo.GapPerimeterPercentage; - - var gapPercentageRange = gapDrawingInfo.GapPerimeterPercentage / gapDrawingInfo.Gaps; - var nonGapPercentageRange = nonGapPercentage / gapDrawingInfo.Gaps; - - - var shapePoints = new[] {triangle.A, triangle.B, triangle.C}; - int sides = shapePoints.Length; - - if (perimeter <= 0f) - { - perimeter = 0f; - for (int i = 0; i < sides; i++) - { - var curP = shapePoints[i]; - var nextP = shapePoints[(i + 1) % sides]; - perimeter += (nextP - curP).Length(); - } - } - - - var startDistance = perimeter * gapDrawingInfo.StartOffset; - var curDistance = 0f; - var nextDistance = startDistance; - - var curIndex = 0; - var curPoint = shapePoints[0]; - var nextPoint= shapePoints[1]; - var curW = nextPoint - curPoint; - var curDis = curW.Length(); - - var points = new List(3); - - int whileCounter = gapDrawingInfo.Gaps; - - while (whileCounter > 0) - { - if (curDistance + curDis >= nextDistance) - { - var p = curPoint + (curW / curDis) * (nextDistance - curDistance); - - - if (points.Count == 0) - { - nextDistance += nonGapPercentageRange * perimeter; - points.Add(p); - - } - else - { - nextDistance += gapPercentageRange * perimeter; - points.Add(p); - if (points.Count == 2) - { - SegmentDrawing.DrawSegment(points[0], points[1], lineInfo); - } - else - { - for (var i = 0; i < points.Count - 1; i++) - { - var p1 = points[i]; - var p2 = points[(i + 1) % points.Count]; - SegmentDrawing.DrawSegment(p1, p2, lineInfo); - } - } - - points.Clear(); - whileCounter--; - } - - } - else - { - - if(points.Count > 0) points.Add(nextPoint); - - curDistance += curDis; - curIndex = (curIndex + 1) % sides; - curPoint = shapePoints[curIndex]; - nextPoint = shapePoints[(curIndex + 1) % sides]; - curW = nextPoint - curPoint; - curDis = curW.Length(); - } - - } - - return perimeter; - } - - /// - /// Draws a gapped outline for a rectangle, creating a dashed or segmented effect along the rectangle's perimeter. - /// - /// The rectangle to draw. - /// - /// The total length of the rectangle's perimeter. - /// If zero or negative, the method calculates it automatically. - /// Providing a known length avoids redundant calculations and improves performance, especially for static segments. - /// - /// Parameters describing how to draw the outline. - /// Parameters describing the gap configuration. - /// - /// The perimeter of the rectangle if positive; otherwise, -1. - /// If the shape does not change, the valid length can be reused in subsequent frames to avoid recalculating. - /// - /// - /// - If is 0 or is 0, the outline is drawn solid. - /// - If is 1 or greater, no outline is drawn. - /// - public static float DrawGappedOutline(this Rect rect, float perimeter, LineDrawingInfo lineInfo, GappedOutlineDrawingInfo gapDrawingInfo) - { - if (gapDrawingInfo.Gaps <= 0 || gapDrawingInfo.GapPerimeterPercentage <= 0f) - { - rect.DrawLines(lineInfo); - return perimeter > 0f ? perimeter : -1f; - } - - if (gapDrawingInfo.GapPerimeterPercentage >= 1f) return perimeter > 0f ? perimeter : -1f; - - var nonGapPercentage = 1f - gapDrawingInfo.GapPerimeterPercentage; - - var gapPercentageRange = gapDrawingInfo.GapPerimeterPercentage / gapDrawingInfo.Gaps; - var nonGapPercentageRange = nonGapPercentage / gapDrawingInfo.Gaps; - - - var shapePoints = new[] {rect.A, rect.B, rect.C, rect.D}; - int sides = shapePoints.Length; - - if (perimeter <= 0f) - { - perimeter = 0f; - for (int i = 0; i < sides; i++) - { - var curP = shapePoints[i]; - var nextP = shapePoints[(i + 1) % sides]; - perimeter += (nextP - curP).Length(); - } - } - - var startDistance = perimeter * gapDrawingInfo.StartOffset; - var curDistance = 0f; - var nextDistance = startDistance; - - var curIndex = 0; - var curPoint = shapePoints[0]; - var nextPoint= shapePoints[1]; - var curW = nextPoint - curPoint; - var curDis = curW.Length(); - - var points = new List(3); - - int whileCounter = gapDrawingInfo.Gaps; - - while (whileCounter > 0) - { - if (curDistance + curDis >= nextDistance) - { - var p = curPoint + (curW / curDis) * (nextDistance - curDistance); - - - if (points.Count == 0) - { - nextDistance += nonGapPercentageRange * perimeter; - points.Add(p); - - } - else - { - nextDistance += gapPercentageRange * perimeter; - points.Add(p); - if (points.Count == 2) - { - SegmentDrawing.DrawSegment(points[0], points[1], lineInfo); - } - else - { - for (var i = 0; i < points.Count - 1; i++) - { - var p1 = points[i]; - var p2 = points[(i + 1) % points.Count]; - SegmentDrawing.DrawSegment(p1, p2, lineInfo); - } - } - - points.Clear(); - whileCounter--; - } - - } - else - { - - if(points.Count > 0) points.Add(nextPoint); - - curDistance += curDis; - curIndex = (curIndex + 1) % sides; - curPoint = shapePoints[curIndex]; - nextPoint = shapePoints[(curIndex + 1) % sides]; - curW = nextPoint - curPoint; - curDis = curW.Length(); - } - - } - - return perimeter; - } - - /// - /// Draws a gapped outline for a quadrilateral, creating a dashed or segmented effect along the quad's perimeter. - /// - /// The quadrilateral to draw. - /// - /// The total length of the quad's perimeter. - /// If zero or negative, the method calculates it automatically. - /// Providing a known length avoids redundant calculations and improves performance, especially for static segments. - /// - /// Parameters describing how to draw the outline. - /// Parameters describing the gap configuration. - /// - /// The perimeter of the quad if positive; otherwise, -1. - /// If the shape does not change, the valid length can be reused in subsequent frames to avoid recalculating. - /// - /// - /// - If is 0 or is 0, the outline is drawn solid. - /// - If is 1 or greater, no outline is drawn. - /// - public static float DrawGappedOutline(this Quad quad, float perimeter, LineDrawingInfo lineInfo, GappedOutlineDrawingInfo gapDrawingInfo) - { - if (gapDrawingInfo.Gaps <= 0 || gapDrawingInfo.GapPerimeterPercentage <= 0f) - { - quad.DrawLines(lineInfo); - return perimeter > 0f ? perimeter : -1f; - } - - if (gapDrawingInfo.GapPerimeterPercentage >= 1f) return perimeter > 0f ? perimeter : -1f; - - var nonGapPercentage = 1f - gapDrawingInfo.GapPerimeterPercentage; - - var gapPercentageRange = gapDrawingInfo.GapPerimeterPercentage / gapDrawingInfo.Gaps; - var nonGapPercentageRange = nonGapPercentage / gapDrawingInfo.Gaps; - - - var shapePoints = new[] {quad.A, quad.B, quad.C, quad.D}; - int sides = shapePoints.Length; - - if (perimeter <= 0f) - { - perimeter = 0f; - for (int i = 0; i < sides; i++) - { - var curP = shapePoints[i]; - var nextP = shapePoints[(i + 1) % sides]; - perimeter += (nextP - curP).Length(); - } - } - - - var startDistance = perimeter * gapDrawingInfo.StartOffset; - var curDistance = 0f; - var nextDistance = startDistance; - - var curIndex = 0; - var curPoint = shapePoints[0]; - var nextPoint= shapePoints[1]; - var curW = nextPoint - curPoint; - var curDis = curW.Length(); - - var points = new List(3); - - int whileCounter = gapDrawingInfo.Gaps; - - while (whileCounter > 0) - { - if (curDistance + curDis >= nextDistance) - { - var p = curPoint + (curW / curDis) * (nextDistance - curDistance); - - - if (points.Count == 0) - { - nextDistance += nonGapPercentageRange * perimeter; - points.Add(p); - - } - else - { - nextDistance += gapPercentageRange * perimeter; - points.Add(p); - if (points.Count == 2) - { - SegmentDrawing.DrawSegment(points[0], points[1], lineInfo); - } - else - { - for (var i = 0; i < points.Count - 1; i++) - { - var p1 = points[i]; - var p2 = points[(i + 1) % points.Count]; - SegmentDrawing.DrawSegment(p1, p2, lineInfo); - } - } - - points.Clear(); - whileCounter--; - } - - } - else - { - - if(points.Count > 0) points.Add(nextPoint); - - curDistance += curDis; - curIndex = (curIndex + 1) % sides; - curPoint = shapePoints[curIndex]; - nextPoint = shapePoints[(curIndex + 1) % sides]; - curW = nextPoint - curPoint; - curDis = curW.Length(); - } - - } - - return perimeter; - } - - /// - /// Draws a gapped outline for a polygon, creating a dashed or segmented effect along the polygon's perimeter. - /// - /// The polygon to draw. - /// - /// The total length of the polygon's perimeter. - /// If zero or negative, the method calculates it automatically. - /// Providing a known length avoids redundant calculations and improves performance, especially for static segments. - /// - /// Parameters describing how to draw the outline. - /// Parameters describing the gap configuration. - /// - /// The perimeter of the polygon if positive; otherwise, -1. - /// If the shape does not change, the valid length can be reused in subsequent frames to avoid recalculating. - /// - /// - /// - If is 0 or is 0, the outline is drawn solid. - /// - If is 1 or greater, no outline is drawn. - /// - public static float DrawGappedOutline(this Polygon poly, float perimeter, LineDrawingInfo lineInfo, GappedOutlineDrawingInfo gapDrawingInfo) - { - if (gapDrawingInfo.Gaps <= 0 || gapDrawingInfo.GapPerimeterPercentage <= 0f) - { - poly.DrawLines(lineInfo); - return perimeter > 0f ? perimeter : -1f; - } - - if (gapDrawingInfo.GapPerimeterPercentage >= 1f) return perimeter > 0f ? perimeter : -1f; - - var nonGapPercentage = 1f - gapDrawingInfo.GapPerimeterPercentage; - - var gapPercentageRange = gapDrawingInfo.GapPerimeterPercentage / gapDrawingInfo.Gaps; - var nonGapPercentageRange = nonGapPercentage / gapDrawingInfo.Gaps; - - if (perimeter <= 0f) - { - for (int i = 0; i < poly.Count; i++) - { - var curP = poly[i]; - var nextP = poly[(i + 1) % poly.Count]; - - perimeter += (nextP - curP).Length(); - } - } - - var startDistance = perimeter * gapDrawingInfo.StartOffset; - var curDistance = 0f; - var nextDistance = startDistance; - - - var curIndex = 0; - var curPoint = poly[0]; - var nextPoint= poly[1 % poly.Count]; - var curW = nextPoint - curPoint; - var curDis = curW.Length(); - - var points = new List(3); - - int whileCounter = gapDrawingInfo.Gaps; - - while (whileCounter > 0) - { - if (curDistance + curDis >= nextDistance) - { - var p = curPoint + (curW / curDis) * (nextDistance - curDistance); - - - if (points.Count == 0) - { - nextDistance += nonGapPercentageRange * perimeter; - points.Add(p); - - } - else - { - nextDistance += gapPercentageRange * perimeter; - points.Add(p); - if (points.Count == 2) - { - SegmentDrawing.DrawSegment(points[0], points[1], lineInfo); - } - else - { - for (var i = 0; i < points.Count - 1; i++) - { - var p1 = points[i]; - var p2 = points[(i + 1) % points.Count]; - SegmentDrawing.DrawSegment(p1, p2, lineInfo); - } - } - - points.Clear(); - whileCounter--; - } - - } - else - { - - if(points.Count > 0) points.Add(nextPoint); - - curDistance += curDis; - curIndex = (curIndex + 1) % poly.Count; - curPoint = poly[curIndex]; - nextPoint = poly[(curIndex + 1) % poly.Count]; - curW = nextPoint - curPoint; - curDis = curW.Length(); - } - - } - - return perimeter; - } - - /// - /// Draws a gapped outline for a polyline (open or closed), creating a dashed or segmented effect along the polyline's length. - /// - /// The polyline to draw. - /// - /// The total length of the polyline. - /// If zero or negative, the method calculates it automatically. - /// Providing a known length avoids redundant calculations and improves performance, especially for static segments. - /// - /// Parameters describing how to draw the polyline. - /// Parameters describing the gap configuration. - /// - /// The perimeter of the polyline if positive; otherwise, -1. - /// If the shape does not change, the valid length can be reused in subsequent frames to avoid recalculating. - /// - /// - /// - If is 0 or is 0, the polyline is drawn solid. - /// - If is 1 or greater, no polyline is drawn. - /// - public static float DrawGappedOutline(this Polyline polyline, float perimeter, LineDrawingInfo lineInfo, GappedOutlineDrawingInfo gapDrawingInfo) - { - if (gapDrawingInfo.Gaps <= 0 || gapDrawingInfo.GapPerimeterPercentage <= 0f) - { - polyline.Draw(lineInfo); - return perimeter > 0f ? perimeter : -1f; - } - - if (gapDrawingInfo.GapPerimeterPercentage >= 1f) return perimeter > 0f ? perimeter : -1f; - - var nonGapPercentage = 1f - gapDrawingInfo.GapPerimeterPercentage; - - var gapPercentageRange = gapDrawingInfo.GapPerimeterPercentage / gapDrawingInfo.Gaps; - var nonGapPercentageRange = nonGapPercentage / gapDrawingInfo.Gaps; - - if (perimeter <= 0f) - { - perimeter = polyline.GetLength(); - } - - var startDistance = perimeter * gapDrawingInfo.StartOffset; - var curDistance = 0f; - var nextDistance = startDistance; - - - var curIndex = 0; - var curPoint = polyline[0]; - var nextPoint= polyline[1]; - var curW = nextPoint - curPoint; - var curDis = curW.Length(); - - var points = new List(3); - - int whileCounter = gapDrawingInfo.Gaps; - - while (whileCounter > 0) - { - if (curDistance + curDis >= nextDistance) //as long as next distance in smaller than the distance to the next polyline point - { - var p = curPoint + (curW / curDis) * (nextDistance - curDistance); - - - if (points.Count == 0) - { - // var prevDistance = nextDistance; - nextDistance += nonGapPercentageRange * perimeter; - points.Add(p); - - } - else - { - // var prevDistance = nextDistance; - nextDistance += gapPercentageRange * perimeter; - points.Add(p); - - if (points.Count == 2) - { - SegmentDrawing.DrawSegment(points[0], points[1], lineInfo); - } - else - { - for (var i = 0; i < points.Count - 1; i++) - { - var p1 = points[i]; - var p2 = points[(i + 1) % points.Count]; - SegmentDrawing.DrawSegment(p1, p2, lineInfo); - } - } - points.Clear(); - whileCounter--; - } - - } - else - { - if (curIndex >= polyline.Count - 2) //last point - { - if (points.Count > 0) - { - points.Add(nextPoint); - if (points.Count == 2) - { - SegmentDrawing.DrawSegment(points[0], points[1], lineInfo); - } - else - { - for (var i = 0; i < points.Count - 1; i++) - { - var p1 = points[i]; - var p2 = points[(i + 1) % points.Count]; - SegmentDrawing.DrawSegment(p1, p2, lineInfo); - } - } - points.Clear(); - points.Add(polyline[0]); - } - - curDistance += curDis; - curIndex = 0; - curPoint = polyline[curIndex]; - nextPoint = polyline[(curIndex + 1) % polyline.Count]; - curW = nextPoint - curPoint; - curDis = curW.Length(); - } - else - { - if(points.Count > 0) points.Add(nextPoint); - - curDistance += curDis; - curIndex += 1;// (curIndex + 1) % polyline.Count; - curPoint = polyline[curIndex]; - nextPoint = polyline[(curIndex + 1) % polyline.Count]; - curW = nextPoint - curPoint; - curDis = curW.Length(); - } - } - - } - - return perimeter; - } - -} \ No newline at end of file diff --git a/ShapeEngine/Geometry/LineDrawingInfo.cs b/ShapeEngine/Geometry/LineDrawingInfo.cs index 50fc4b7f..c7361537 100644 --- a/ShapeEngine/Geometry/LineDrawingInfo.cs +++ b/ShapeEngine/Geometry/LineDrawingInfo.cs @@ -103,6 +103,19 @@ public LineDrawingInfo(float thickness, ColorRgba color) /// /// The thickness of the line. /// The color of the line. + /// The number of cap points to use. + public LineDrawingInfo(float thickness, ColorRgba color, int capPoints) + { + Thickness = MathF.Max(thickness, LineMinThickness); + Color = color; + CapType = LineCapType.None; + CapPoints = capPoints; + } + /// + /// Creates a new line drawing info. + /// + /// The thickness of the line. + /// The color of the line. /// The line cap type. /// The amount of points for the cap. public LineDrawingInfo(float thickness, ColorRgba color, LineCapType capType, int capPoints) @@ -113,30 +126,51 @@ public LineDrawingInfo(float thickness, ColorRgba color, LineCapType capType, in CapPoints = capPoints; } + /// + /// Returns a new with the thickness changed by the specified amount. + /// + /// The amount to change the thickness by. + /// A new with the updated thickness. + public LineDrawingInfo ChangeThickness(float amount) => new(Thickness + amount, Color, CapType, CapPoints); + + /// + /// Returns a new with the color alpha set to the specified value. + /// + /// The new alpha value for the color. + /// A new with the updated color alpha. + public LineDrawingInfo SetColorAlpha(byte newAlpha) => new(Thickness, Color.SetAlpha(newAlpha), CapType, CapPoints); + + /// + /// Returns a new with the color alpha changed by the specified amount. + /// + /// The amount to change the alpha by. + /// A new with the updated color alpha. + public LineDrawingInfo ChangeColorAlpha(byte amount) => new(Thickness, Color.ChangeAlpha(amount), CapType, CapPoints); + /// /// Creates a new line drawing info with a new thickness. /// /// The new thickness. /// A new line drawing info with the new thickness. - public LineDrawingInfo ChangeThickness(float newThickness) => new(newThickness, Color, CapType, CapPoints); + public LineDrawingInfo SetThickness(float newThickness) => new(newThickness, Color, CapType, CapPoints); /// /// Creates a new line drawing info with a new color. /// /// The new color. /// A new line drawing info with the new color. - public LineDrawingInfo ChangeColor(ColorRgba newColor) => new(Thickness, newColor, CapType, CapPoints); + public LineDrawingInfo SetColor(ColorRgba newColor) => new(Thickness, newColor, CapType, CapPoints); /// /// Creates a new line drawing info with a new line cap type. /// /// The new line cap type. /// A new line drawing info with the new line cap type. - public LineDrawingInfo ChangeCapType(LineCapType newCapType) => new(Thickness, Color, newCapType, CapPoints); + public LineDrawingInfo SetCapType(LineCapType newCapType) => new(Thickness, Color, newCapType, CapPoints); /// /// Creates a new line drawing info with a new amount of cap points. /// /// The new amount of cap points. /// A new line drawing info with the new amount of cap points. - public LineDrawingInfo ChangeCapPoints(int newCapPoints) => new(Thickness, Color, CapType, newCapPoints); + public LineDrawingInfo SetCapPoints(int newCapPoints) => new(Thickness, Color, CapType, newCapPoints); /// /// Linearly interpolates between two line drawing infos. diff --git a/ShapeEngine/Geometry/PointDrawing.cs b/ShapeEngine/Geometry/PointDrawing.cs index 313e6385..64cd8b42 100644 --- a/ShapeEngine/Geometry/PointDrawing.cs +++ b/ShapeEngine/Geometry/PointDrawing.cs @@ -16,8 +16,16 @@ public static class PointDrawing /// The position of the point. /// The radius of the circle to draw. /// The color of the circle. - /// The number of segments to use for the circle. Default is 16. - public static void Draw(this Vector2 p, float radius, ColorRgba color, int segments = 16) => CircleDrawing.DrawCircle(p, radius, color, segments); + /// + /// The smoothness value (0-1). This controls the visual quality of the circle by inversely interpolating the current . + /// A value of 0 uses the maximum side length (fewer sides, less smooth), while 1 uses the minimum side length (more sides, smoother). + /// The resulting side length determines the number of polygon sides used to approximate the circle. + /// + public static void Draw(this Vector2 p, float radius, ColorRgba color, float smoothness = 0.25f) + { + var circle = new Circle(p, radius); + circle.Draw(color, smoothness); + } /// /// Draws each point in a collection as a circle. @@ -25,12 +33,16 @@ public static class PointDrawing /// The collection of points to draw. /// The radius of each circle. /// The color of the circles. - /// The number of segments to use for each circle. Default is 16. - public static void Draw(this Points points, float r, ColorRgba color, int segments = 16) + /// + /// The smoothness value (0-1). This controls the visual quality of the circle by inversely interpolating the current . + /// A value of 0 uses the maximum side length (fewer sides, less smooth), while 1 uses the minimum side length (more sides, smoother). + /// The resulting side length determines the number of polygon sides used to approximate the circle. + /// + public static void Draw(this Points points, float r, ColorRgba color, float smoothness = 0.25f) { foreach (var p in points) { - p.Draw(r, color, segments); + p.Draw(r, color, smoothness); } } diff --git a/ShapeEngine/Geometry/PointsDef/Points.cs b/ShapeEngine/Geometry/PointsDef/Points.cs index daf13b02..b4f9174b 100644 --- a/ShapeEngine/Geometry/PointsDef/Points.cs +++ b/ShapeEngine/Geometry/PointsDef/Points.cs @@ -1,8 +1,14 @@ using System.Numerics; -using ShapeEngine.Core; +using ShapeEngine.Color; using ShapeEngine.Core.Structs; +using ShapeEngine.Geometry.CircleDef; using ShapeEngine.Geometry.PolygonDef; using ShapeEngine.Geometry.PolylineDef; +using ShapeEngine.Geometry.RectDef; +using ShapeEngine.Geometry.SegmentDef; +using ShapeEngine.Geometry.SegmentsDef; +using ShapeEngine.Geometry.TriangleDef; +using ShapeEngine.Geometry.TriangulationDef; using ShapeEngine.StaticLib; using Game = ShapeEngine.Core.GameDef.Game; @@ -16,6 +22,15 @@ namespace ShapeEngine.Geometry.PointsDef; /// public partial class Points : ShapeList, IEquatable { + #region Helper + + protected static Points pointsBuffer = new(); + private static Segments segmentsBuffer1 = new(); + private static Segments segmentsBuffer2 = new(); + private static HashSet uniquePointsBuffer = new(); + + #endregion + #region Constructors /// /// Initializes a new instance of the class. @@ -71,6 +86,7 @@ public Vector2 GetPoint(int index) return GetItem(index); //return Count <= 0 ? new() : this[index % Count]; } + /// /// Finds the index of the point in the collection that is closest to the specified position. /// @@ -95,6 +111,7 @@ public int GetClosestIndex(Vector2 p) } return closestIndex; } + /// /// Finds the point in the collection that is closest to the specified position. /// @@ -119,6 +136,7 @@ public Vector2 GetClosestVertex(Vector2 p) } return closestPoint; } + /// /// Returns a new instance containing only unique points from the collection. /// @@ -128,25 +146,65 @@ public Vector2 GetClosestVertex(Vector2 p) /// public Points GetUniquePoints() { - var uniqueVertices = new HashSet(); + uniquePointsBuffer.Clear(); + uniquePointsBuffer.EnsureCapacity(Count); for (var i = 0; i < Count; i++) { - uniqueVertices.Add(this[i]); + uniquePointsBuffer.Add(this[i]); } - return new(uniqueVertices); + + var result = new Points(uniquePointsBuffer.Count); + result.AddRange(uniquePointsBuffer); + return result; } + + /// + /// Collects all unique points from this collection and writes them into . + /// + /// The destination collection that will be cleared and populated with the unique points. + /// + /// This method does not modify the current collection. Point uniqueness is determined by the equality comparer used by the internal . + /// + public void GetUniquePoints(Points result) + { + uniquePointsBuffer.Clear(); + uniquePointsBuffer.EnsureCapacity(Count); + for (var i = 0; i < Count; i++) + { + uniquePointsBuffer.Add(this[i]); + } + + result.Clear(); + result.EnsureCapacity(uniquePointsBuffer.Count); + result.AddRange(uniquePointsBuffer); + } + /// /// Returns a random point from the collection. /// /// A randomly selected from the collection. public Vector2 GetRandomPoint() => GetRandomItem(); + /// /// Returns a list of random points from the collection. /// /// The number of random points to retrieve. /// A list of randomly selected points. - public List GetRandomPoints(int amount) => GetRandomItems(amount); + public List GetRandomPoints(int amount) + { + return GetRandomItems(amount); + } + /// + /// Selects random points from this collection and writes them into . + /// + /// The destination list that will receive the selected points. + /// The number of random points to select. + /// The number of points written to . + public int GetRandomPoints(List result, int amount) + { + return GetRandomItems(result, amount); + } #endregion #region Shapes @@ -160,7 +218,15 @@ public Points GetUniquePoints() /// A new instance containing the same points. public Polygon ToPolygon() => new(this); - public bool ToPolygon(ref Polygon result) + /// + /// Copies this point collection into the specified instance. + /// + /// The destination polygon that will receive the points from this collection. + /// true if this collection contains at least three points and was populated; otherwise, false. + /// + /// This method does not modify the current instance. If the conversion succeeds, any existing points in are cleared before the new points are added. + /// + public bool ToPolygon(Polygon result) { if (Count < 3) return false; @@ -182,20 +248,38 @@ public bool ToPolygon(ref Polygon result) public Polyline ToPolyline() => new(this); /// - /// Returns a tuple containing a relative transform and a normalized polygon shape based on the specified center. + /// Copies this point collection into the specified instance. + /// + /// The destination polyline that will receive the points from this collection. + /// true if this collection contains at least three points and was populated; otherwise, false. + /// + /// This method does not modify the current instance. If the conversion succeeds, any existing points in are cleared before the new points are added. + /// + public bool ToPolyline(Polyline result) + { + if (Count < 3) return false; + + if(result.Count > 0) result.Clear(); + + for (var i = 0; i < Count; i++) + { + var point = this[i]; + result.Add(point); + } + + return true; + } + + /// + /// Normalizes this point collection relative to the specified and writes the normalized points into . /// - /// The center point to use as the origin for normalization. - /// - /// A tuple where: - /// - /// : The transform representing the original center and scale. - /// : The normalized polygon shape with points in the range 0-1 relative to the center. - /// - /// + /// The center point used as the origin for normalization. + /// The destination polygon that will be cleared and populated with the normalized points. + /// A whose position is and whose size stores the maximum point distance from that center. /// - /// Useful for storing shapes in a normalized form and reconstructing them with a transform. + /// This method does not modify the current instance. Each output point is computed by subtracting from the source point and dividing by the maximum distance from to any point in the collection. /// - public (Transform2D transform, Polygon shape) ToRelative(Vector2 center) + public Transform2D ToRelative(Vector2 center, Polygon result) { var maxLengthSq = 0f; for (int i = 0; i < this.Count; i++) @@ -205,120 +289,121 @@ public bool ToPolygon(ref Polygon result) } var size = MathF.Sqrt(maxLengthSq); - - var relativeShape = new Polygon(); + + result.Clear(); + result.EnsureCapacity(this.Count); for (int i = 0; i < this.Count; i++) { var w = this[i] - center; - relativeShape.Add(w / size); //transforms it to range 0 - 1 + result.Add(w / size); //transforms it to range 0 - 1 } - return (new Transform2D(center, 0f, new Size(size, 0f), 1f), relativeShape); + return (new Transform2D(center, 0f, new Size(size, 0f), 1f)); } /// - /// Returns a list of points relative to the specified origin. + /// Writes all points offset relative to the specified into . /// /// The origin to subtract from each point. - /// A list of points relative to the origin. - public List GetRelativeVector2List(Vector2 origin) + /// The destination list that will be cleared and populated with the relative points. + public void GetRelativeVector2List(Vector2 origin, List result) { - var relative = new List(Count); - foreach (var p in this) relative.Add(p - origin); - return relative; + result.Clear(); + result.EnsureCapacity(Count); + foreach (var p in this) result.Add(p - origin); } + /// - /// Returns a list of points relative to the specified transform. + /// Writes all points transformed into the local space of the specified into . /// /// The transform to revert each point by. - /// A list of points relative to the transform. - public List GetRelativeVector2List(Transform2D transform) + /// The destination list that will be cleared and populated with the transformed points. + public void GetRelativeVector2List(Transform2D transform, List result) { - var relative = new List(Count); - foreach (var p in this) relative.Add(transform.RevertPosition(p)); - return relative; + result.Clear(); + result.EnsureCapacity(Count); + foreach (var p in this) result.Add(transform.RevertPosition(p)); } + /// - /// Returns a new collection with all points relative to the specified origin. + /// Writes all points offset relative to the specified into . /// /// The origin to subtract from each point. - /// A new instance with points relative to the origin. - public Points GetRelativePoints(Vector2 origin) + /// The destination collection that will be cleared and populated with the relative points. + public void GetRelativePoints(Vector2 origin, Points result) { - var relative = new Points(Count); - foreach (var p in this) relative.Add(p - origin); - return relative; + result.Clear(); + result.EnsureCapacity(Count); + foreach (var p in this) result.Add(p - origin); } + /// - /// Returns a new collection with all points relative to the specified transform. + /// Writes all points transformed into the local space of the specified into . /// /// The transform to revert each point by. - /// A new instance with points relative to the transform. - public Points GetRelativePoints(Transform2D transform) + /// The destination collection that will be cleared and populated with the transformed points. + public void GetRelativePoints(Transform2D transform, Points result) { - var relative = new Points(Count); - foreach (var p in this) relative.Add(transform.RevertPosition(p)); - return relative; + result.Clear(); + result.EnsureCapacity(Count); + foreach (var p in this) result.Add(transform.RevertPosition(p)); } #endregion #region Interpolated Edge Points - /// - /// Interpolate the edge(segment) between each pair of points using t and return the new interpolated points. - /// Interplates between last and first point as well (closed shape) + /// Computes one interpolated point along each edge of the closed point loop and writes the results into . /// - /// The value t for interpolation. Should be between 0 - 1. - /// - public Points? GetInterpolatedEdgePoints(float t) + /// The interpolation factor used for each edge. Values between 0 and 1 produce points between each vertex and the next vertex. + /// The destination collection that will be cleared and populated with the interpolated points. + /// + /// The last point is interpolated toward the first point, so the input is treated as a closed shape. If the collection contains fewer than two points, the method returns without modifying . + /// + public void GetInterpolatedEdgePoints(float t, Points result) { - if (Count < 2) return null; + if (Count < 2) return; - var result = new Points(); + result.Clear(); + result.EnsureCapacity(Count); for (int i = 0; i < Count; i++) { var cur = this[i]; var next = this[(i + 1) % Count]; - var interpolated = cur.Lerp(next, t);// Vector2.Lerp(cur, next, t); + var interpolated = cur.Lerp(next, t); result.Add(interpolated); } - - return result; } + /// - /// Interpolate the edge(segment) between each pair of points using t and return the new interpolated points. - /// Interplates between last and first point as well (closed shape) + /// Repeatedly computes interpolated edge points for the closed point loop and writes the final result into . /// - /// The value t for interpolation. Should be between 0 - 1. - /// Recursive steps. The amount of times the result of InterpolatedEdgesPoints will be run through InterpolateEdgePoints. - /// - public Points? GetInterpolatedEdgePoints(float t, int steps) + /// The interpolation factor used for each edge on every pass. Values between 0 and 1 produce points between each vertex and the next vertex. + /// The number of interpolation passes to perform. Values less than or equal to 1 perform a single pass. + /// The destination collection that will receive the interpolated points. + /// + /// The last point is interpolated toward the first point on each pass, so the input is treated as a closed shape. If the collection contains fewer than two points, the method returns without modifying . + /// + public void GetInterpolatedEdgePoints(float t, int steps, Points result) { - if (Count < 2) return null; - if (steps <= 1) return GetInterpolatedEdgePoints(t); + if (Count < 2) return; + if (steps <= 1) + { + GetInterpolatedEdgePoints(t, result); + return; + } int remainingSteps = steps; - var result = new Points(); - var buffer = new Points(); + result.Clear(); while (remainingSteps > 0) { var target = result.Count <= 0 ? this : result; - for (int i = 0; i < target.Count; i++) - { - var cur = target[i]; - var next = target[(i + 1) % target.Count]; - var interpolated = cur.Lerp(next, t); - buffer.Add(interpolated); - } - - (result, buffer) = (buffer, result);//switch buffer and result - buffer.Clear(); + target.GetInterpolatedEdgePoints(t, pointsBuffer); + result.Clear(); + result.AddRange(pointsBuffer); + pointsBuffer.Clear(); remainingSteps--; } - - - return result; } #endregion @@ -346,6 +431,7 @@ public bool SortClosestFirst(Vector2 referencePoint) ); return true; } + /// /// Sorts the points in the collection so that the furthest points from the specified come first. /// @@ -370,5 +456,291 @@ public bool SortFurthestFirst(Vector2 referencePoint) return true; } #endregion + + #region Triangulate Point Cloud + + /// + /// Computes an axis-aligned bounding rectangle that encloses the points in this collection. + /// + /// The bounding for the point cloud, or the default rectangle if the collection contains fewer than two points. + public Rect GetPointCloudBoundingBox() + { + if (Count < 2) return new(); + var start = this[0]; + Rect r = new(start.X, start.Y, 0, 0); + + foreach (var p in this) + { + r = r.Enlarge(p); + } + + return r; + } + + /// + /// Computes a triangle that encloses the current point cloud, optionally expanded by a margin factor. + /// + /// A multiplier applied to the bounding box extent when constructing the enclosing triangle. + /// A that surrounds the current point cloud. + /// + /// This is typically used as a supra-triangle when triangulating the point cloud. + /// + public Triangle GetPointCloudBoundingTriangle(float marginFactor = 1f) + { + var bounds = GetPointCloudBoundingBox(); + float dMax = bounds.Size.Max() * marginFactor; + var center = bounds.Center; + var a = new Vector2(center.X, bounds.BottomLeft.Y + dMax); + var b = new Vector2(center.X - dMax * 1.25f, bounds.TopLeft.Y - dMax / 4); + var c = new Vector2(center.X + dMax * 1.25f, bounds.TopLeft.Y - dMax / 4); + + return new Triangle(a, b, c); + } + + /// + /// Triangulates this point cloud and writes the resulting triangles into using an automatically generated supra-triangle. + /// + /// The destination triangulation that will be cleared and populated with the generated triangles. + /// A multiplier applied when generating the enclosing supra-triangle. + public void TriangulatePointCloud(Triangulation result, float marginFactor = 2f) + { + var supraTriangle = GetPointCloudBoundingTriangle(marginFactor); + TriangulatePointCloud(supraTriangle, result); + } + + /// + /// Triangulates this point cloud and writes the resulting triangles into using the specified supra-triangle. + /// + /// An enclosing triangle large enough to contain all points in the cloud. + /// The destination triangulation that will be cleared and populated with the generated triangles. + /// + /// This method removes any triangles that still share a vertex with before returning. + /// + public void TriangulatePointCloud(Triangle supraTriangle, Triangulation result) + { + result.Clear(); + result.Add(supraTriangle); + + foreach (var p in this) + { + Triangulation badTriangles = new(); + + //Identify 'bad triangles' + for (int triIndex = result.Count - 1; triIndex >= 0; triIndex--) + { + Triangle triangle = result[triIndex]; + + //A 'bad triangle' is defined as a triangle who's CircumCentre contains the current point + var circumCircle = triangle.GetCircumCircle(); + float distSq = Vector2.DistanceSquared(p, circumCircle.Center); + if (distSq < circumCircle.Radius * circumCircle.Radius) + { + badTriangles.Add(triangle); + result.RemoveAt(triIndex); + } + } + + segmentsBuffer1.Clear(); + segmentsBuffer1.EnsureCapacity(badTriangles.Count * 3); + foreach (var badTriangle in badTriangles) + { + segmentsBuffer1.AddRange(badTriangle.GetEdges()); + } + + segmentsBuffer1.GetUniqueSegments(segmentsBuffer2); + //Create new triangles + for (int i = 0; i < segmentsBuffer2.Count; i++) + { + var edge = segmentsBuffer2[i]; + result.Add(new(p, edge)); + } + } + + //Remove all triangles that share a vertex with the supra triangle to recieve the final triangulation + for (int i = result.Count - 1; i >= 0; i--) + { + var t = result[i]; + if (t.SharesVertex(supraTriangle)) result.RemoveAt(i); + } + } + + #endregion + + #region Convex Hull + + // Convex Hull Algorithms + // This class implements the Jarvis March (Gift Wrapping) algorithm to find the convex hull of a set of points. + // Reference: https://github.com/allfii/ConvexHull/tree/master + + // Alternative algorithms for convex hull computation: + // - Graham scan: https://en.wikipedia.org/wiki/Graham_scan + // - Chan's algorithm: https://en.wikipedia.org/wiki/Chan%27s_algorithm + + // Gift Wrapping algorithm resources: + // - Coding Train video: https://www.youtube.com/watch?v=YNyULRrydVI + // - Wikipedia: https://en.wikipedia.org/wiki/Gift_wrapping_algorithm + + private static int Turn_JarvisMarch(Vector2 p, Vector2 q, Vector2 r) + { + return ((q.X - p.X) * (r.Y - p.Y) - (r.X - p.X) * (q.Y - p.Y)).CompareTo(0); + // return ((q.getX() - p.getX()) * (r.getY() - p.getY()) - (r.getX() - p.getX()) * (q.getY() - p.getY())).CompareTo(0); + } + private static Vector2 NextHullPoint_JarvisMarch(IReadOnlyList points, Vector2 p) + { + // const int TurnLeft = 1; + const int turnRight = -1; + const int turnNone = 0; + var q = p; + int t; + foreach (var r in points) + { + t = Turn_JarvisMarch(p, q, r); + if (t == turnRight || t == turnNone && p.DistanceSquared(r) > p.DistanceSquared(q)) // dist(p, r) > dist(p, q)) + q = r; + } + + return q; + } + + /// + /// Finds the convex hull of a set of points using the Jarvis March algorithm. + /// + /// The list of points to compute the convex hull for. + /// A representing the convex hull. + public static Points ConvexHull_JarvisMarch(IReadOnlyList points) + { + var hull = new Points(); + if (points.Count <= 3) + { + foreach (var p in points) + { + hull.Add(p); + } + return hull; + } + + foreach (var p in points) + { + if (hull.Count == 0) + hull.Add(p); + else + { + if (hull[0].X > p.X) + hull[0] = p; + else if (ShapeMath.EqualsF(hull[0].X, p.X)) + if (hull[0].Y > p.Y) + hull[0] = p; + } + } + + var counter = 0; + while (counter < hull.Count) + { + var q = NextHullPoint_JarvisMarch(points, hull[counter]); + if (q != hull[0]) + { + hull.Add(q); + } + + counter++; + } + + return hull; + } + + /// + /// Computes the convex hull of the specified points using the Jarvis March algorithm and writes the hull vertices into . + /// + /// The points for which to compute the convex hull. + /// The destination list that will be cleared and populated with the hull vertices in traversal order. + /// + /// If contains three or fewer points, they are copied directly into . + /// + public static void ConvexHull_JarvisMarch(IReadOnlyList points, List result) + { + result.Clear(); + result.EnsureCapacity(points.Count); + if (points.Count <= 3) + { + foreach (var p in points) + { + result.Add(p); + } + return; + } + + foreach (var p in points) + { + if (result.Count == 0) + result.Add(p); + else + { + if (result[0].X > p.X) + result[0] = p; + else if (ShapeMath.EqualsF(result[0].X, p.X)) + if (result[0].Y > p.Y) + result[0] = p; + } + } + + var counter = 0; + while (counter < result.Count) + { + var q = NextHullPoint_JarvisMarch(points, result[counter]); + if (q != result[0]) + { + result.Add(q); + } + + counter++; + } + } + + /// + /// Computes the convex hull of this point collection. + /// + /// A new instance containing the convex hull vertices in traversal order. + /// + /// This method uses the Jarvis March algorithm and does not modify the current collection. + /// + public Points FindConvexHull() + { + var poly = new Polyline(); + poly.FindConvexHull(poly); + + return ConvexHull_JarvisMarch(this); + } + + /// + /// Computes the convex hull of this point collection and writes the hull vertices into . + /// + /// The destination list that will be cleared and populated with the convex hull vertices in traversal order. + /// + /// This method uses the Jarvis March algorithm and does not modify the current collection. + /// + public void FindConvexHull(List result) + { + ConvexHull_JarvisMarch(this, result); + } + #endregion + + /// + /// Draws a circle at each vertex in this collection using the provided radius, color and smoothness. + /// + /// Radius of the drawn circle for each vertex (in world units). + /// Color of the vertex circles. + /// + /// The smoothness value (0-1). This controls the visual quality of the circle by inversely interpolating the current . + /// A value of 0 uses the maximum side length (fewer sides, less smooth), while 1 uses the minimum side length (more sides, smoother). + /// The resulting side length determines the number of polygon sides used to approximate the circle. + /// + public void DrawVertices(float vertexRadius, ColorRgba color, float smoothness) + { + foreach (var p in this) + { + var circle = new Circle(p, vertexRadius); + circle.Draw(color, smoothness); + } + } } diff --git a/ShapeEngine/Geometry/PointsDef/PointsMath.cs b/ShapeEngine/Geometry/PointsDef/PointsMath.cs index a92869e8..37ce65b1 100644 --- a/ShapeEngine/Geometry/PointsDef/PointsMath.cs +++ b/ShapeEngine/Geometry/PointsDef/PointsMath.cs @@ -7,7 +7,7 @@ namespace ShapeEngine.Geometry.PointsDef; public partial class Points { - #region Transform + #region Transform Self /// /// Sets the position of all points in the collection by translating them so that the specified moves to . @@ -20,6 +20,7 @@ public partial class Points /// public void SetPosition(Vector2 newPosition, Vector2 origin) { + if (Count <= 0) return; var delta = newPosition - origin; ChangePosition(delta); } @@ -33,6 +34,7 @@ public void SetPosition(Vector2 newPosition, Vector2 origin) /// public void ChangePosition(Vector2 offset) { + if (Count <= 0) return; for (int i = 0; i < Count; i++) { this[i] += offset; @@ -49,7 +51,7 @@ public void ChangePosition(Vector2 offset) /// public void ChangeRotation(float rotRad, Vector2 origin) { - if (Count < 2) return; + if (Count <= 0) return; for (int i = 0; i < Count; i++) { var w = this[i] - origin; @@ -67,7 +69,7 @@ public void ChangeRotation(float rotRad, Vector2 origin) /// public void SetRotation(float angleRad, Vector2 origin) { - if (Count < 2) return; + if (Count <= 0) return; var curAngle = (this[0] - origin).AngleRad(); var rotRad = ShapeMath.GetShortestAngleRad(curAngle, angleRad); @@ -84,7 +86,7 @@ public void SetRotation(float angleRad, Vector2 origin) /// public void ScaleSize(float scale, Vector2 origin) { - if (Count < 2) return; + if (Count <= 0) return; for (int i = 0; i < Count; i++) { var w = this[i] - origin; @@ -102,13 +104,12 @@ public void ScaleSize(float scale, Vector2 origin) /// public void ScaleSize(Vector2 scale, Vector2 origin) { - if (Count < 3) return; // new(); + if (Count <= 0) return; for (int i = 0; i < Count; i++) { var w = this[i] - origin; this[i] = origin + w * scale; } - //return path; } /// @@ -121,7 +122,7 @@ public void ScaleSize(Vector2 scale, Vector2 origin) /// public void ChangeSize(float amount, Vector2 origin) { - if (Count < 2) return; + if (Count <= 0) return; for (var i = 0; i < Count; i++) { var w = this[i] - origin; @@ -139,7 +140,7 @@ public void ChangeSize(float amount, Vector2 origin) /// public void SetSize(float size, Vector2 origin) { - if (Count < 2) return; + if (Count <= 0) return; for (var i = 0; i < Count; i++) { var w = this[i] - origin; @@ -157,9 +158,16 @@ public void SetSize(float size, Vector2 origin) /// public void SetTransform(Transform2D transform, Vector2 origin) { - SetPosition(transform.Position, origin); - SetRotation(transform.RotationRad, origin); - SetSize(transform.ScaledSize.Length, origin); + if (Count <= 0) return; + + var offset = transform.Position - origin; + var curAngle = (this[0] - origin).AngleRad(); + var rotRad = ShapeMath.GetShortestAngleRad(curAngle, transform.RotationRad); + for (var i = 0; i < Count; i++) + { + var w = this[i] - origin; + this[i] = (origin + w.SetLength(transform.ScaledSize.Length).Rotate(rotRad)) + offset; + } } /// @@ -172,232 +180,489 @@ public void SetTransform(Transform2D transform, Vector2 origin) /// public void ApplyOffset(Transform2D offset, Vector2 origin) { - ChangePosition(offset.Position); - ChangeRotation(offset.RotationRad, origin); - ChangeSize(offset.ScaledSize.Length, origin); + if (Count <= 0) return; + + for (var i = 0; i < Count; i++) + { + var w = this[i] - origin; + this[i] = (origin + w.ChangeLength(offset.ScaledSize.Length).Rotate(offset.RotationRad)) + offset.Position; + } } + #endregion + + #region Transform Copy + /// - /// Returns a new instance with all points translated so that the specified origin moves to a new position. + /// Copies all points into , translated so that the specified moves to . /// /// The new position to which the origin point will be moved. /// The reference origin point in the current collection to be moved to . - /// A new instance with translated points, or null if the collection has fewer than 2 points. + /// The destination collection that will be cleared and populated with the translated points. + /// true if the current collection contains at least one point and was populated; otherwise, false. /// - /// This method does not modify the current instance. It returns a new collection with all points translated by the vector difference between and . + /// This method does not modify the current instance. It writes the translated points into by applying the vector difference between and . /// - public Points? SetPositionCopy(Vector2 newPosition, Vector2 origin) + public bool SetPositionCopy(Points result, Vector2 newPosition, Vector2 origin) { - if (Count < 2) return null; + if (Count <= 0) return false; var delta = newPosition - origin; - return ChangePositionCopy(delta); + return ChangePositionCopy(result, delta); } /// - /// Returns a new instance with all points translated by the specified offset vector. + /// Copies all points into , translated by the specified offset vector. /// /// The vector by which to move every point in the collection. - /// A new instance with translated points, or null if the collection has fewer than 3 points. + /// The destination collection that will be cleared and populated with the translated points. + /// true if the current collection contains at least one point and was populated; otherwise, false. /// - /// This method does not modify the current instance. It returns a new collection with all points shifted by the given offset. + /// This method does not modify the current instance. It writes the shifted points into . /// - public Points? ChangePositionCopy(Vector2 offset) + public bool ChangePositionCopy(Points result, Vector2 offset) { - if (Count < 3) return null; - var newPolygon = new Polygon(this.Count); + if (Count <= 0) return false; + result.Clear(); + result.EnsureCapacity(Count); for (int i = 0; i < Count; i++) { - newPolygon.Add(this[i] + offset); + result.Add(this[i] + offset); } - return newPolygon; + return true; } /// - /// Returns a new instance with all points rotated around a specified origin by a given angle in radians. + /// Copies all points into , rotated around the specified by the given angle in radians. /// /// The rotation angle in radians. Positive values rotate counterclockwise. /// The point around which all points will be rotated. - /// A new instance with rotated points, or null if the collection has fewer than 3 points. + /// The destination collection that will be cleared and populated with the rotated points. + /// true if the current collection contains at least one point and was populated; otherwise, false. /// - /// This method does not modify the current instance. It returns a new collection with all points rotated by the specified angle around the given origin. + /// This method does not modify the current instance. It writes the rotated points into . /// - public Points? ChangeRotationCopy(float rotRad, Vector2 origin) + public bool ChangeRotationCopy(Points result, float rotRad, Vector2 origin) { - if (Count < 3) return null; - var newPolygon = new Polygon(this.Count); + if (Count <= 0) return false; + result.Clear(); + result.EnsureCapacity(Count); for (var i = 0; i < Count; i++) { var w = this[i] - origin; - newPolygon.Add(origin + w.Rotate(rotRad)); + result.Add(origin + w.Rotate(rotRad)); } - return newPolygon; + return true; } /// - /// Returns a new instance with all points rotated around a specified origin to a given angle in radians. + /// Copies all points into , rotating them around the specified so the first point aligns with the target angle. /// /// The target rotation angle in radians. Positive values rotate counterclockwise. /// The point around which all points will be rotated. - /// A new instance with rotated points, or null if the collection has fewer than 3 points. + /// The destination collection that will be cleared and populated with the rotated points. + /// true if the current collection contains at least one point and was populated; otherwise, false. /// - /// This method does not modify the current instance. It returns a new collection with all points rotated to the specified angle around the given origin. + /// This method does not modify the current instance. It computes the shortest rotation from the current angle of the first point relative to to , then writes the rotated points into . /// - public Points? SetRotationCopy(float angleRad, Vector2 origin) + public bool SetRotationCopy(Points result, float angleRad, Vector2 origin) { - if (Count < 3) return null; + if (Count <= 0) return false; var curAngle = (this[0] - origin).AngleRad(); var rotRad = ShapeMath.GetShortestAngleRad(curAngle, angleRad); - return ChangeRotationCopy(rotRad, origin); + return ChangeRotationCopy(result, rotRad, origin); } /// - /// Returns a new instance with all points scaled uniformly from a specified origin. + /// Copies all points into , scaled uniformly relative to the specified . /// /// The uniform scale factor to apply to all points relative to the origin. Values greater than 1 enlarge, less than 1 shrink. /// The point from which scaling is performed. - /// A new instance with scaled points, or null if the collection has fewer than 3 points. + /// The destination collection that will be cleared and populated with the scaled points. + /// true if the current collection contains at least one point and was populated; otherwise, false. /// - /// This method does not modify the current instance. It returns a new collection with all points scaled by the given factor from the specified origin. + /// This method does not modify the current instance. It writes the uniformly scaled points into . /// - public Points? ScaleSizeCopy(float scale, Vector2 origin) + public bool ScaleSizeCopy(Points result, float scale, Vector2 origin) { - if (Count < 3) return null; - var newPolygon = new Polygon(this.Count); + if (Count <= 0) return false; + + result.Clear(); + result.EnsureCapacity(Count); for (var i = 0; i < Count; i++) { var w = this[i] - origin; - newPolygon.Add(origin + w * scale); + result.Add(origin + w * scale); } - return newPolygon; + return true; } /// - /// Returns a new instance with all points scaled from the specified origin by a non-uniform (per-axis) scale factor. + /// Copies all points into , scaled relative to the specified by a non-uniform per-axis factor. /// /// The scale factor to apply to all points relative to the origin, per axis. For example, (2, 1) doubles the width but keeps the height unchanged. /// The point from which scaling is performed. - /// A new instance with scaled points, or null if the collection has fewer than 3 points. + /// The destination collection that will be cleared and populated with the scaled points. + /// true if the current collection contains at least one point and was populated; otherwise, false. /// - /// This method does not modify the current instance. It returns a new collection with all points scaled by the given factor from the specified origin. + /// This method does not modify the current instance. It writes the per-axis scaled points into . /// - public Points? ScaleSizeCopy(Vector2 scale, Vector2 origin) + public bool ScaleSizeCopy(Points result, Vector2 scale, Vector2 origin) { - if (Count < 3) return null; - var newPolygon = new Polygon(this.Count); + if (Count <= 0) return false; + + result.Clear(); + result.EnsureCapacity(Count); for (var i = 0; i < Count; i++) { var w = this[i] - origin; - newPolygon.Add(origin + w * scale); + result.Add(origin + w * scale); } - return newPolygon; + return true; } /// - /// Returns a new instance with all points' distances from the specified origin changed by the given amount. + /// Copies all points into , changing each point's distance from by the specified amount. /// /// The amount by which to change the length of each point's distance from the origin. Positive values increase size, negative values decrease size. /// The point from which size is adjusted. - /// A new instance with modified point distances, or null if the collection has fewer than 3 points. + /// The destination collection that will be cleared and populated with the resized points. + /// true if the current collection contains at least one point and was populated; otherwise, false. /// - /// This method does not modify the current instance. It returns a new collection with all points' distances from the origin changed by the specified amount. + /// This method does not modify the current instance. It writes the adjusted points into . /// - public Points? ChangeSizeCopy(float amount, Vector2 origin) + public bool ChangeSizeCopy(Points result, float amount, Vector2 origin) { - if (Count < 3) return null; - var newPolygon = new Polygon(this.Count); + if (Count <= 0) return false; + result.Clear(); + result.EnsureCapacity(Count); for (var i = 0; i < Count; i++) { var w = this[i] - origin; - newPolygon.Add(origin + w.ChangeLength(amount)); + result.Add(origin + w.ChangeLength(amount)); } - return newPolygon; + return true; } /// - /// Returns a new instance with all points set to a specified distance from the origin. + /// Copies all points into , setting each point to the specified distance from . /// /// The target distance from the origin for each point. /// The reference point from which distances are measured and set. - /// - /// A new instance with all points set to the specified distance from the origin, - /// or null if the collection has fewer than 3 points. - /// + /// The destination collection that will be cleared and populated with the resized points. + /// true if the current collection contains at least one point and was populated; otherwise, false. /// - /// This method does not modify the current instance. It returns a new collection with all points equidistant from the origin. + /// This method does not modify the current instance. It writes the points with their distances set to into . /// - public Points? SetSizeCopy(float size, Vector2 origin) + public bool SetSizeCopy(Points result, float size, Vector2 origin) { - if (Count < 3) return null; - var newPolygon = new Polygon(this.Count); + if (Count <= 0) return false; + result.Clear(); + result.EnsureCapacity(Count); for (var i = 0; i < Count; i++) { var w = this[i] - origin; - newPolygon.Add(origin + w.SetLength(size)); + result.Add(origin + w.SetLength(size)); } - return newPolygon; + return true; } /// - /// Returns a new instance with all points transformed by the specified and origin. + /// Copies all points into , transformed by the specified relative to . /// /// The containing the target position, rotation (in radians), and scaled size. /// The reference point from which transformations are applied. - /// - /// A new instance with all points transformed by the given transform and origin, - /// or null if the collection has fewer than 3 points. - /// + /// The destination collection that will be cleared and populated with the transformed points. + /// true if the current collection contains at least one point and was populated; otherwise, false. /// - /// This method does not modify the current instance. It applies translation, rotation, and scaling in sequence to all points, - /// aligning the shape with the given transform relative to the specified origin. + /// This method does not modify the current instance. It applies translation, rotation, and size adjustment in sequence, then writes the transformed points into . /// - public Points? SetTransformCopy(Transform2D transform, Vector2 origin) + public bool SetTransformCopy(Points result, Transform2D transform, Vector2 origin) { - if (Count < 3) return null; - var newPoints = SetPositionCopy(transform.Position, origin); - if (newPoints == null) return null; - newPoints.SetRotation(transform.RotationRad, origin); - newPoints.SetSize(transform.ScaledSize.Length, origin); - return newPoints; + if (Count <= 0) return false; + + result.Clear(); + result.EnsureCapacity(Count); + + var offset = transform.Position - origin; + var curAngle = (this[0] - origin).AngleRad(); + var rotRad = ShapeMath.GetShortestAngleRad(curAngle, transform.RotationRad); + for (var i = 0; i < Count; i++) + { + var w = this[i] - origin; + var p = (origin + w.SetLength(transform.ScaledSize.Length).Rotate(rotRad)) + offset; + result.Add(p); + } + + return true; } /// - /// Returns a new instance with all points transformed by the specified offset and origin. + /// Copies all points into , applying the specified offset relative to . /// /// The containing the position, rotation (in radians), and scaled size offsets to apply. /// The reference point from which transformations are applied. - /// - /// A new instance with all points transformed by the given offset and origin, - /// or null if the collection has fewer than 3 points. - /// + /// The destination collection that will be cleared and populated with the transformed points. + /// true if the current collection contains at least one point and was populated; otherwise, false. + /// + /// This method does not modify the current instance. It applies translation, rotation, and size offsets in sequence, then writes the transformed points into . + /// + public bool ApplyOffsetCopy(Points result, Transform2D offset, Vector2 origin) + { + if (Count <= 0) return false; + + result.Clear(); + result.EnsureCapacity(Count); + + for (var i = 0; i < Count; i++) + { + var w = this[i] - origin; + var p = (origin + w.ChangeLength(offset.ScaledSize.Length).Rotate(offset.RotationRad)) + offset.Position; + result.Add(p); + } + + return true; + } + + + /// + /// Copies all points into , translated so the mean centroid moves to . + /// + /// The destination collection that will be cleared and populated with the translated points. + /// The new position to which the mean centroid will be moved. + /// true if the current collection contains at least one point and was populated; otherwise, false. /// - /// This method does not modify the current instance. - /// It applies translation, rotation, and scaling offsets in sequence to all points, - /// modifying the shape relative to the given origin. + /// This convenience overload uses as the translation origin. /// - public Points? ApplyOffsetCopy(Transform2D offset, Vector2 origin) + public bool SetPositionCopy(Points result, Vector2 newPosition) { - if (Count < 3) return null; + return SetPositionCopy(result, newPosition, GetCentroidMean()); + } - var newPoints = ChangePositionCopy(offset.Position); - if (newPoints == null) return null; - newPoints.ChangeRotation(offset.RotationRad, origin); - newPoints.ChangeSize(offset.ScaledSize.Length, origin); - return newPoints; + /// + /// Copies all points into , rotated around the mean centroid by the given angle in radians. + /// + /// The destination collection that will be cleared and populated with the rotated points. + /// The rotation angle in radians. Positive values rotate counterclockwise. + /// true if the current collection contains at least one point and was populated; otherwise, false. + /// + /// This convenience overload uses as the rotation origin. + /// + public bool ChangeRotationCopy(Points result, float rotRad) + { + return ChangeRotationCopy(result, rotRad, GetCentroidMean()); } + /// + /// Copies all points into , rotating them around the mean centroid so the first point aligns with the target angle. + /// + /// The destination collection that will be cleared and populated with the rotated points. + /// The target rotation angle in radians. Positive values rotate counterclockwise. + /// true if the current collection contains at least one point and was populated; otherwise, false. + /// + /// This convenience overload uses as the rotation origin. + /// + public bool SetRotationCopy(Points result, float angleRad) + { + return SetRotationCopy(result, angleRad, GetCentroidMean()); + } + + /// + /// Copies all points into , scaled uniformly relative to the mean centroid. + /// + /// The destination collection that will be cleared and populated with the scaled points. + /// The uniform scale factor to apply relative to the mean centroid. + /// true if the current collection contains at least one point and was populated; otherwise, false. + /// + /// This convenience overload uses as the scaling origin. + /// + public bool ScaleSizeCopy(Points result, float scale) + { + return ScaleSizeCopy(result, scale, GetCentroidMean()); + } + + /// + /// Copies all points into , changing each point's distance from the mean centroid by the specified amount. + /// + /// The destination collection that will be cleared and populated with the resized points. + /// The amount by which to change the length of each point's distance from the mean centroid. + /// true if the current collection contains at least one point and was populated; otherwise, false. + /// + /// This convenience overload uses as the size-adjustment origin. + /// + public bool ChangeSizeCopy(Points result, float amount) + { + return ChangeSizeCopy(result, amount, GetCentroidMean()); + } + + /// + /// Copies all points into , setting each point to the specified distance from the mean centroid. + /// + /// The destination collection that will be cleared and populated with the resized points. + /// The target distance from the mean centroid for each point. + /// true if the current collection contains at least one point and was populated; otherwise, false. + /// + /// This convenience overload uses as the reference origin. + /// + public bool SetSizeCopy(Points result, float size) + { + return SetSizeCopy(result, size, GetCentroidMean()); + } + + /// + /// Copies all points into , transformed by the specified relative to the mean centroid. + /// + /// The destination collection that will be cleared and populated with the transformed points. + /// The transform containing the target position, rotation, and scaled size. + /// true if the current collection contains at least one point and was populated; otherwise, false. + /// + /// This convenience overload uses as the transformation origin. + /// + public bool SetTransformCopy(Points result, Transform2D transform) + { + return SetTransformCopy(result, transform, GetCentroidMean()); + } + + /// + /// Copies all points into , applying the specified offset transform relative to the mean centroid. + /// + /// The destination collection that will be cleared and populated with the transformed points. + /// The offset transform containing the position, rotation, and scaled size offsets to apply. + /// true if the current collection contains at least one point and was populated; otherwise, false. + /// + /// This convenience overload uses as the transformation origin. + /// + public bool ApplyOffsetCopy(Points result, Transform2D offset) + { + return ApplyOffsetCopy(result, offset, GetCentroidMean()); + } #endregion #region Math + /// + /// Calculates the mean centroid (arithmetic average) of all points in the polyline. + /// + /// + /// The mean centroid as a . Returns (0,0) if the polyline is empty, or the single point if only one exists. + /// + public Vector2 GetCentroidMean() + { + if (Count <= 0) return new(0f); + else if (Count == 1) return this[0]; + Vector2 total = new(0f); + foreach (Vector2 p in this) + { + total += p; + } + + return total / Count; + } + + /// + /// Returns a set of points representing the projection of the polyline along a given vector. + /// + /// The vector along which to project each point of the polyline. + /// + /// A collection containing the original and projected points, or null if the vector is zero. + /// + /// + /// Each point in the polyline is duplicated and offset by the vector . + /// + public Points? GetProjectedShapePoints(Vector2 v) + { + if (v.LengthSquared() <= 0f) return null; + var points = new Points(Count); + for (var i = 0; i < Count; i++) + { + points.Add(this[i]); + points.Add(this[i] + v); + } + + return points; + } + + /// + /// Writes the original points and their projected counterparts into . + /// + /// The destination collection that will be cleared and populated with the original and projected points. + /// The vector used to offset each projected point. + /// true if is non-zero and was populated; otherwise, false. + /// + /// For each point in the current collection, this method appends the original point followed by that point translated by . + /// + public bool GetProjectedShapePoints(Points result, Vector2 v) + { + if (v.LengthSquared() <= 0f) return false; + result.Clear(); + result.EnsureCapacity(Count * 2); + for (var i = 0; i < Count; i++) + { + result.Add(this[i]); + result.Add(this[i] + v); + } + + return true; + } + + + /// + /// Projects the polyline along a given vector and returns the convex hull of the resulting points as a polygon. + /// + /// The vector along which to project each point of the polyline. + /// + /// A representing the convex hull of the projected points, + /// or null if the vector is zero. + /// + public Polygon? ProjectShape(Vector2 v) + { + if (v.LengthSquared() <= 0f || Count < 2) return null; + + var points = new Points(Count * 2); + for (var i = 0; i < Count; i++) + { + points.Add(this[i]); + points.Add(this[i] + v); + } + + Polygon result = new(points.Count); + points.FindConvexHull(result); + return result; + } + + /// + /// Projects the points along the given vector and writes the convex hull of the combined point set into . + /// + /// The destination polygon that receives the convex hull of the original and projected points. + /// The vector used to offset each projected point. + /// true if is non-zero, the collection contains at least two points, and was populated; otherwise, false. + /// + /// This method creates a temporary doubled point set containing each original point and its translated counterpart, then computes the convex hull into . + /// + public bool ProjectShape(Polygon result, Vector2 v) + { + if (v.LengthSquared() <= 0f || Count < 2) return false; + + var points = new Points(Count * 2); + for (var i = 0; i < Count; i++) + { + points.Add(this[i]); + points.Add(this[i] + v); + } + + points.FindConvexHull(result); + return true; + } + + /// /// Applies the floor operation to each in the provided list, /// modifying each coordinate to the largest integer less than or equal to it. @@ -410,6 +675,7 @@ public static void Floor(List points) points[i] = points[i].Floor(); } } + /// /// Applies the ceiling operation to each in the provided list, /// modifying each coordinate to the smallest integer greater than or equal to it. @@ -422,6 +688,7 @@ public static void Ceiling(List points) points[i] = points[i].Ceiling(); } } + /// /// Rounds each in the provided list to the nearest integer values for both coordinates. /// @@ -433,6 +700,7 @@ public static void Round(List points) points[i] = points[i].Round(); } } + /// /// Truncates each in the provided list, removing the fractional part of each coordinate. /// @@ -444,6 +712,7 @@ public static void Truncate(List points) points[i] = points[i].Truncate(); } } + /// /// Applies the floor operation to all points in this collection, modifying each coordinate to the largest integer less than or equal to it. /// @@ -489,4 +758,54 @@ public void Round() } #endregion + + #region Index Helpers + /// + /// Returns the vertex immediately after the vertex at the specified index, wrapping around the points list. + /// + /// Zero-based vertex index. Values outside the valid range are wrapped using ShapeMath.WrapIndex. + /// + /// The next vertex as a . If the points list contains no vertices, returns the default (zero vector). + /// + public Vector2 GetNextVertex(int index) + { + return Count <= 0 ? new Vector2() : this[ShapeMath.WrapIndex(Count, index + 1)]; + } + + /// + /// Returns the vertex immediately before the vertex at the specified index, wrapping around the points list. + /// + /// Zero-based vertex index. Values outside the valid range are wrapped using ShapeMath.WrapIndex. + /// + /// The previous vertex as a . If the points list contains no vertices, returns the default (zero vector). + /// + public Vector2 GetPreviousVertex(int index) + { + return Count <= 0 ? new Vector2() : this[ShapeMath.WrapIndex(Count, index - 1)]; + } + + /// + /// Returns the next vertex index after , wrapped into the valid range. + /// + /// Zero-based index. Values outside the valid range are wrapped using ShapeMath.WrapIndex. + /// + /// The next index wrapped into the range [0, Count). If the points list has no vertices, the behavior is determined by ShapeMath.WrapIndex. + /// + public int GetNextIndex(int index) + { + return ShapeMath.WrapIndex(Count, index + 1); + } + + /// + /// Returns the previous vertex index before , wrapped into the valid range. + /// + /// Zero-based index. Values outside the valid range are wrapped using ShapeMath.WrapIndex. + /// + /// The previous index wrapped into the range [0, Count). If the points list has no vertices, the behavior is determined by ShapeMath.WrapIndex. + /// + public int GetPreviousIndex(int index) + { + return ShapeMath.WrapIndex(Count, index - 1); + } + #endregion } \ No newline at end of file diff --git a/ShapeEngine/Geometry/PolygonDef/Polygon.cs b/ShapeEngine/Geometry/PolygonDef/Polygon.cs index 21936d48..1d2b9e72 100644 --- a/ShapeEngine/Geometry/PolygonDef/Polygon.cs +++ b/ShapeEngine/Geometry/PolygonDef/Polygon.cs @@ -1,17 +1,17 @@ using System.Numerics; -using ShapeEngine.Core; +using Clipper2Lib; using ShapeEngine.Core.Structs; using ShapeEngine.Geometry.CircleDef; using ShapeEngine.Geometry.CollisionSystem; using ShapeEngine.Geometry.PointsDef; using ShapeEngine.Geometry.PolylineDef; -using ShapeEngine.Geometry.QuadDef; using ShapeEngine.Geometry.RectDef; using ShapeEngine.Geometry.SegmentDef; using ShapeEngine.Geometry.SegmentsDef; using ShapeEngine.Geometry.TriangleDef; using ShapeEngine.Geometry.TriangulationDef; using ShapeEngine.Random; +using ShapeEngine.ShapeClipper; using ShapeEngine.StaticLib; using Game = ShapeEngine.Core.GameDef.Game; using Size = ShapeEngine.Core.Structs.Size; @@ -28,7 +28,7 @@ namespace ShapeEngine.Geometry.PolygonDef; /// Use or if your points are in clockwise (CW) order. /// /// -///A convex polygon is a polygon where all interior angles are less than 180°, +/// A convex polygon is a polygon where all interior angles are less than 180°, /// and every line segment between any two points inside the polygon lies entirely within the polygon. /// In other words, no vertices "point inward." /// @@ -41,7 +41,21 @@ namespace ShapeEngine.Geometry.PolygonDef; /// public partial class Polygon : Points, IEquatable, IShapeTypeProvider, IClosedShapeTypeProvider { + #region Helper Members + private static IntersectionPoints intersectionPointsReference = new(4); + private static Triangulation triangulationBuffer = new(); + private static Polyline polylinePerimeterBuffer = new(); + private static Paths64 clipResultBuffer = new(); + private static Polygon clipPolygonBuffer = new(); + private static Paths64PooledBuffer clipPooledBuffer = new(); + private static List> weightedTrianglesBuffer = new(); + private static List pickedTrianglesBuffer = new(); + private static Segments segmentsBuffer = new(); + + #endregion + + #region Getters /// /// Creates a deep copy of the current polygon. /// @@ -76,14 +90,14 @@ public partial class Polygon : Points, IEquatable, IShapeTypeProvider, /// Gets the diameter (maximum distance between any two vertices) of the polygon. /// public float Diameter => GetDiameter(); - - private Polygon? compoundHelperPolygon = null; + #endregion #region Constructors /// /// Initializes an empty polygon. /// public Polygon() { } + /// /// Initializes a polygon with a specified capacity. /// @@ -92,22 +106,26 @@ public Polygon(int capacity) : base(capacity) { } + /// /// Initializes a polygon from a collection of points. /// /// The points that define the polygon. Should be in CCW order. /// Use or if in CW order. public Polygon(IEnumerable points) { AddRange(points); } + /// /// Initializes a polygon from another instance. /// /// The points to copy. Should be in CCW order. public Polygon(Points points) : base(points.Count) { AddRange(points); } + /// /// Initializes a polygon by copying another polygon. /// /// The polygon to copy. public Polygon(Polygon poly) : base(poly.Count) { AddRange(poly); } + /// /// Initializes a polygon from a polyline. /// @@ -136,14 +154,23 @@ public bool Equals(Polygon? other) } return true; } + /// /// Returns a hash code for the polygon. /// /// A hash code for the current polygon. public override int GetHashCode() => Game.GetHashCode(this); + /// + /// Gets the closed shape type represented by this polygon. + /// + /// . public ClosedShapeType GetClosedShapeType() => ClosedShapeType.Poly; + /// + /// Gets the general shape type represented by this polygon. + /// + /// . public ShapeType GetShapeType() => ShapeType.Poly; /// @@ -188,6 +215,7 @@ public void FixWindingOrder() Reverse(); } } + /// /// Converts the polygon's winding order to clockwise (CW). /// @@ -199,6 +227,7 @@ public void MakeClockwise() if (IsClockwise()) return; Reverse(); } + /// /// Converts the polygon's winding order to counter-clockwise (CCW). /// @@ -210,6 +239,7 @@ public void MakeCounterClockwise() if (!IsClockwise()) return; Reverse(); } + /// /// Reduces the number of vertices in the polygon to the specified count. /// @@ -235,6 +265,7 @@ public void ReduceVertexCount(int newCount) } } + /// /// Reduces the number of vertices by a factor. /// @@ -243,6 +274,7 @@ public void ReduceVertexCount(float factor) { ReduceVertexCount(Count - (int)(Count * factor)); } + /// /// Increases the number of vertices in the polygon to the specified count by subdividing the longest edges. /// @@ -268,40 +300,52 @@ public void IncreaseVertexCount(int newCount) this.Insert(longestID + 1, m); } } + /// /// Gets the vertex at the specified index, wrapping around if necessary. /// /// The index of the vertex. /// The vertex at the specified index. public Vector2 GetVertex(int index) => this[ShapeMath.WrapIndex(Count, index)]; + /// - /// Removes collinear vertices from the polygon. + /// Removes vertices that are approximately collinear with their neighboring vertices. /// + /// + /// The threshold angle in degrees used to determine collinearity. If the angle between the + /// vectors (prev -> cur) and (cur -> next) is smaller than this threshold the current vertex + /// is considered collinear and removed. + /// /// - /// Collinear vertices are those that lie on a straight line with their neighbors. + /// - No action is taken when the polygon has fewer than three vertices. + /// - Uses to test collinearity. + /// - Builds a temporary result and replaces the polygon's vertices + /// after filtering to preserve iteration stability. /// - public void RemoveColinearVertices() + public void RemoveCollinearVertices(float angleThresholdDegrees = 5f) { if (Count < 3) return; Points result = []; for (var i = 0; i < Count; i++) { - var cur = this[i]; var prev = Game.GetItem(this, i - 1); + var cur = this[i]; var next = Game.GetItem(this, i + 1); - var prevCur = prev - cur; - var nextCur = next - cur; - if (prevCur.Cross(nextCur) != 0f) result.Add(cur); + if(ShapeVec.IsColinearAngle(prev, cur, next, angleThresholdDegrees)) continue; + result.Add(cur); } Clear(); AddRange(result); } + /// - /// Removes duplicate vertices from the polygon. + /// Removes vertices that are effectively duplicates by comparing each vertex to its next neighbor. + /// If the squared distance between two consecutive vertices is less than or equal to + /// , the current vertex is omitted. + /// No action is performed when the polygon has fewer than three vertices. /// - /// The squared distance below which two vertices are considered duplicates. - /// Default is 0.001. + /// Squared distance threshold used to detect duplicate vertices (default: 0.001f). public void RemoveDuplicates(float toleranceSquared = 0.001f) { if (Count < 3) return; @@ -316,6 +360,7 @@ public void RemoveDuplicates(float toleranceSquared = 0.001f) Clear(); AddRange(result); } + /// /// Smooths the polygon by moving each vertex towards the average of its neighbors and the centroid. /// @@ -338,6 +383,174 @@ public void Smooth(float amount, float baseWeight) Clear(); AddRange(result); } + + /// + /// Creates and returns a new polygon that is a copy of this polygon with vertices + /// that are approximately collinear removed. + /// + /// + /// The threshold angle in degrees used to determine collinearity. If the angle between + /// the vectors (prev -> cur) and (cur -> next) is smaller than this threshold the current + /// vertex is considered collinear and omitted from the returned polygon. Default is 5 degrees. + /// + /// + /// A new containing the filtered vertices, or null if this + /// polygon has fewer than three vertices (no meaningful polygon can be produced). + /// + public Polygon? RemoveCollinearVerticesCopy(float angleThresholdDegrees = 5f) + { + if (Count < 3) return null; + Polygon result = []; + for (var i = 0; i < Count; i++) + { + var prev = Game.GetItem(this, i - 1); + var cur = this[i]; + var next = Game.GetItem(this, i + 1); + + if(ShapeVec.IsColinearAngle(prev, cur, next, angleThresholdDegrees)) continue; + result.Add(cur); + } + + return result; + } + + /// + /// Creates and returns a new with consecutive duplicate vertices removed. + /// Each vertex is compared to its next neighbor; if the squared distance between them is less than + /// or equal to , the current vertex is omitted from the result. + /// + /// + /// Squared distance threshold used to detect duplicate vertices. Vertices closer than or equal to + /// this threshold are treated as duplicates. Default is 0.001f. + /// + /// + /// A new containing the filtered vertices, or null if this polygon + /// has fewer than three vertices (no meaningful polygon can be produced). + /// + public Polygon? RemoveDuplicatesCopy(float toleranceSquared = 0.001f) + { + if (Count < 3) return null; + Polygon result = []; + + for (var i = 0; i < Count; i++) + { + var cur = this[i]; + var next = Game.GetItem(this, i + 1); + if ((cur - next).LengthSquared() > toleranceSquared) result.Add(cur); + } + + return result; + } + + /// + /// Creates and returns a smoothed copy of this polygon. + /// Each vertex is moved towards the average of its neighboring vertices and towards the polygon centroid + /// according to the provided smoothing parameters. + /// + /// Smoothing factor in the range [0,1]. 0 means no change, 1 applies the full computed displacement. + /// Weight applied to the centroid contribution when computing the smoothing direction. + /// + /// A new containing the smoothed vertices, or null if this polygon has fewer than three vertices. + /// + public Polygon? SmoothCopy(float amount, float baseWeight) + { + if (Count < 3) return null; + Polygon result = []; + var centroid = GetCentroid(); + for (var i = 0; i < Count; i++) + { + var cur = this[i]; + var prev = this[ShapeMath.WrapIndex(Count, i - 1)]; + var next = this[ShapeMath.WrapIndex(Count, i + 1)]; + var dir = (prev - cur) + (next - cur) + ((cur - centroid) * baseWeight); + result.Add(cur + dir * amount); + } + + return result; + } + + /// + /// Writes a copy of this polygon into with approximately collinear vertices removed. + /// + /// The destination polygon that will be cleared and populated with the filtered vertices. + /// + /// The threshold angle in degrees used to determine collinearity. If the angle between + /// the vectors (prev -> cur) and (cur -> next) is smaller than this threshold the current + /// vertex is considered collinear and omitted from the copied polygon. Default is 5 degrees. + /// + /// true if this polygon has at least three vertices and was populated; otherwise, false. + public bool RemoveCollinearVerticesCopy(Polygon result, float angleThresholdDegrees = 5f) + { + if (Count < 3) return false; + result.Clear(); + result.EnsureCapacity(Count); + for (var i = 0; i < Count; i++) + { + var prev = Game.GetItem(this, i - 1); + var cur = this[i]; + var next = Game.GetItem(this, i + 1); + + if(ShapeVec.IsColinearAngle(prev, cur, next, angleThresholdDegrees)) continue; + result.Add(cur); + } + + return true; + } + + /// + /// Writes a copy of this polygon into with consecutive duplicate vertices removed. + /// Each vertex is compared to its next neighbor; if the squared distance between them is less than + /// or equal to , the current vertex is omitted from the result. + /// + /// The destination polygon that will be cleared and populated with the filtered vertices. + /// + /// Squared distance threshold used to detect duplicate vertices. Vertices closer than or equal to + /// this threshold are treated as duplicates. Default is 0.001f. + /// + /// true if this polygon has at least three vertices and was populated; otherwise, false. + public bool RemoveDuplicatesCopy(Polygon result, float toleranceSquared = 0.001f) + { + if (Count < 3) return false; + + result.Clear(); + result.EnsureCapacity(Count); + + for (var i = 0; i < Count; i++) + { + var cur = this[i]; + var next = Game.GetItem(this, i + 1); + if ((cur - next).LengthSquared() > toleranceSquared) result.Add(cur); + } + + return true; + } + + /// + /// Writes a smoothed copy of this polygon into . + /// Each vertex is moved towards the average of its neighboring vertices and towards the polygon centroid + /// according to the provided smoothing parameters. + /// + /// The destination polygon that will be cleared and populated with the smoothed vertices. + /// Smoothing factor in the range [0,1]. 0 means no change, 1 applies the full computed displacement. + /// Weight applied to the centroid contribution when computing the smoothing direction. + /// true if this polygon has at least three vertices and was populated; otherwise, false. + public bool SmoothCopy(Polygon result, float amount, float baseWeight) + { + if (Count < 3) return false; + result.Clear(); + result.EnsureCapacity(Count); + var centroid = GetCentroid(); + for (var i = 0; i < Count; i++) + { + var cur = this[i]; + var prev = this[ShapeMath.WrapIndex(Count, i - 1)]; + var next = this[ShapeMath.WrapIndex(Count, i + 1)]; + var dir = (prev - cur) + (next - cur) + ((cur - centroid) * baseWeight); + result.Add(cur + dir * amount); + } + + return true; + } #endregion #region Shape @@ -366,6 +579,7 @@ public void Smooth(float amount, float baseWeight) return (new Transform2D(pos, 0f, new Size(size, 0f), 1f), relativeShape); } + /// /// Converts the polygon's points to relative coordinates using a given transform. /// @@ -382,6 +596,7 @@ public Points ToRelativePoints(Transform2D transform) return points; } + /// /// Converts the polygon to a new polygon in relative coordinates using a given transform. /// @@ -398,6 +613,7 @@ public Polygon ToRelativePolygon(Transform2D transform) return points; } + /// /// Converts the polygon's points to a list of relative coordinates using a given transform. /// @@ -414,84 +630,14 @@ public List ToRelative(Transform2D transform) return points; } + /// /// Gets the minimal bounding triangle of the polygon. /// /// Optional margin to expand the bounding triangle. Default is 3. /// The bounding triangle. - public Triangle GetBoundingTriangle(float margin = 3f) => Polygon.GetBoundingTriangle(this, margin); - /// - /// Triangulates the polygon into a set of triangles. - /// - /// A containing the triangles. - /// - /// Uses an ear-clipping algorithm. The polygon must be simple (non-self-intersecting). - /// - public Triangulation Triangulate() - { - if (Count < 3) return new(); - if (Count == 3) return new() { new(this[0], this[1], this[2]) }; - - Triangulation triangles = new(); - List vertices = new(); - vertices.AddRange(this); - List validIndices = new(); - for (int i = 0; i < vertices.Count; i++) - { - validIndices.Add(i); - } - while (vertices.Count > 3) - { - if (validIndices.Count <= 0) - break; - - int i = validIndices[Rng.Instance.RandI(0, validIndices.Count)]; - var a = vertices[i]; - var b = Game.GetItem(vertices, i + 1); - var c = Game.GetItem(vertices, i - 1); - - var ba = b - a; - var ca = c - a; - float cross = ba.Cross(ca); - if (cross >= 0f)//makes sure that ear is not self intersecting - { - validIndices.Remove(i); - continue; - } - - Triangle t = new(a, b, c); - - bool isValid = true; - foreach (var p in this) - { - if (p == a || p == b || p == c) continue; - if (t.ContainsPoint(p)) - { - isValid = false; - break; - } - } - - if (isValid) - { - triangles.Add(t); - vertices.RemoveAt(i); - - validIndices.Clear(); - for (int j = 0; j < vertices.Count; j++) - { - validIndices.Add(j); - } - //break; - } - } - - - triangles.Add(new(vertices[0], vertices[1], vertices[2])); - - - return triangles; - } + public Triangle GetBoundingTriangle(float margin = 3f) => GetPointCloudBoundingTriangle(margin); + /// /// Returns the edges (segments) of the polygon. /// @@ -511,6 +657,37 @@ public Segments GetEdges() return segments; } + /// + /// Computes and returns the normal vectors for the polygon edges. + /// + /// + /// A containing one normal per edge: + /// - If the polygon has 0 or 1 vertex an empty list is returned. + /// - If the polygon has 2 vertices a single normal for the segment is returned. + /// - For 3+ vertices returns normals for every edge (edge from vertex i to i+1). + /// + /// + /// Normals are computed using with the third parameter set to false. + /// If the polygon points are in counter-clockwise (CCW) order the normals will face outward. + /// + public List GetEdgeNormals() + { + switch (Count) + { + case <= 1: + return []; + case 2: + return [Segment.GetNormal(this[0], this[1], false)]; + } + + var normals = new List(Count); + for (var i = 0; i < Count; i++) + { + normals.Add(Segment.GetNormal(this[i], this[(i + 1) % Count], false)); + } + return normals; + } + /// /// Returns a simple (non-minimal) enclosing circle for the polygon. /// You should always use unless performance is critical. @@ -543,6 +720,7 @@ public Circle GetBoundingCircleSimple() return new Circle(origin, MathF.Sqrt(maxD)); } + /// /// Gets the minimal bounding circle of the polygon using Welzl's algorithm. /// @@ -562,127 +740,161 @@ public Circle GetBoundingCircle() var boundary = new List(); return WelzlHelper(points, boundary, points.Count); } + /// - /// Recursively computes the minimal enclosing circle for a set of points using Welzl's algorithm. + /// Gets the axis-aligned bounding box of the polygon. /// - /// List of input points. Only the first elements are considered in this recursion. - /// Auxiliary list of boundary (support) points that must lie on the circle. Its size will be 0..3. - /// Number of points from to consider in this recursion. - /// The minimal enclosing for the considered points given the current boundary. - /// - /// The algorithm randomly selects a point from the first points, computes the minimal circle - /// for the remaining points, and if the selected point lies outside that circle, it is added to the boundary - /// and the algorithm recurses. Termination occurs when is 0 or the boundary contains 3 points. - /// - private Circle WelzlHelper(List points, List boundary, int n) + /// The bounding . + public Rect GetBoundingBox() { - if (n == 0 || boundary.Count == 3) + if (Count == 0) return new Rect(); + if (Count == 1) return new Rect(this[0].X, this[0].Y, 0, 0); + + float minX = this[0].X; + float maxX = this[0].X; + float minY = this[0].Y; + float maxY = this[0].Y; + + for (int i = 1; i < Count; i++) { - return GetCircleFromBoundary(boundary); + var p = this[i]; + if (p.X < minX) minX = p.X; + else if (p.X > maxX) maxX = p.X; + + if (p.Y < minY) minY = p.Y; + else if (p.Y > maxY) maxY = p.Y; } - int idx = Rng.Instance.RandI(0, n); - var p = points[idx]; - - (points[idx], points[n - 1]) = (points[n - 1], points[idx]); - - var circle = WelzlHelper(points, boundary, n - 1); - - if (circle.ContainsPoint(p)) + return new Rect(minX, minY, maxX - minX, maxY - minY); + } + + /// + /// Returns the convex hull of the polygon as a new polygon. + /// + /// A convex polygon containing all the original points. + public Polygon? ToConvex() + { + var result = new Polygon(); + FindConvexHull(result); + return result.Count >= 3 ? result : null; + } + + /// + /// Converts this polygon's vertices into relative coordinates using the supplied transform and writes them into . + /// + /// The destination list that will be cleared and populated with the relative coordinates. + /// The transform whose inverse position mapping is applied to each polygon vertex. + public void ToRelative(List result, Transform2D transform) + { + result.Clear(); + result.EnsureCapacity(Count); + for (int i = 0; i < Count; i++) { - return circle; + var p = transform.RevertPosition(this[i]); + result.Add(p); } - - boundary.Add(p); - circle = WelzlHelper(points, boundary, n - 1); - boundary.RemoveAt(boundary.Count - 1); - - return circle; } + /// - /// Constructs the minimal circle determined by the given boundary (support) points. + /// Appends the edge normal vectors of this polygon to the provided list. /// - /// A list of support points (0..3) which must lie on the resulting circle. + /// A that will receive the computed normals. Existing contents are preserved and the normals are appended. /// - /// A that is the minimal circle passing through the provided boundary points: - /// - If boundary.Count == 0 returns an empty/default circle. - /// - If boundary.Count == 1 returns a circle centered at the single point with radius 0. - /// - If boundary.Count == 2 returns the circle with the two points as endpoints of a diameter. - /// - If boundary.Count == 3 returns the circumcircle; if the three points are (nearly) collinear, - /// the function falls back to a circle using the largest pair as diameter. + /// The number of normals added: + /// - 0 when the polygon has 0 or 1 vertex (nothing is added). + /// - 1 when the polygon has exactly 2 vertices (a single segment normal is added). + /// - Count when the polygon has 3 or more vertices (one normal per edge). /// /// - /// This helper is used by Welzl's algorithm to compute the minimal enclosing circle for a set of points. - /// Numerical stability is considered: when the determinant is very small the points are treated as collinear. + /// Normals are computed using with the third parameter set to false. + /// When the polygon vertices are in counter-clockwise (CCW) order the normals will face outward. /// - private Circle GetCircleFromBoundary(List boundary) + public int GetEdgeNormals(ref List result) { - if (boundary.Count == 0) return new Circle(); - if (boundary.Count == 1) return new Circle(boundary[0], 0f); - if (boundary.Count == 2) + switch (Count) { - var center1 = (boundary[0] + boundary[1]) * 0.5f; - var radius1 = (boundary[1] - boundary[0]).Length() * 0.5f; - return new Circle(center1, radius1); + case <= 1: + return 0; + case 2: + result.Add(Segment.GetNormal(this[0], this[1], false)); + return 1; } - var a = boundary[0]; - var b = boundary[1]; - var c = boundary[2]; - - var d = 2f * (a.X * (b.Y - c.Y) + b.X * (c.Y - a.Y) + c.X * (a.Y - b.Y)); - if (MathF.Abs(d) < 0.0001f)//check for collinearity + for (var i = 0; i < Count; i++) { - var center2 = (a + b) * 0.5f; - // var radius2 = MathF.Max((b - a).Length(), (c - a).Length()) * 0.5f; - var radius2 = MathF.Max(MathF.Max((b - a).Length(), (c - a).Length()), (c - b).Length()) * 0.5f; - return new Circle(center2, radius2); + result.Add(Segment.GetNormal(this[i], this[(i + 1) % Count], false)); } - - var aSq = a.LengthSquared(); - var bSq = b.LengthSquared(); - var cSq = c.LengthSquared(); - - var ux = (aSq * (b.Y - c.Y) + bSq * (c.Y - a.Y) + cSq * (a.Y - b.Y)) / d; - var uy = (aSq * (c.X - b.X) + bSq * (a.X - c.X) + cSq * (b.X - a.X)) / d; - - var center3 = new Vector2(ux, uy); - var radius3 = (center3 - a).Length(); - - return new Circle(center3, radius3); + return Count; } + /// - /// Gets the axis-aligned bounding box of the polygon. + /// Computes the convex hull of this polygon and writes it into . /// - /// The bounding . - public Rect GetBoundingBox() + /// The destination polygon that will receive the convex hull vertices. + /// + /// This method delegates to using the polygon instance as the destination collection. + /// + public void ToConvex(Polygon result) { - if (Count == 0) return new Rect(); - if (Count == 1) return new Rect(this[0].X, this[0].Y, 0, 0); - - float minX = this[0].X; - float maxX = this[0].X; - float minY = this[0].Y; - float maxY = this[0].Y; - - for (int i = 1; i < Count; i++) + FindConvexHull(result); + } + + /// + /// Computes the direction vector of each polygon edge and writes them into . + /// + /// The destination list that will be cleared and populated with one direction vector per edge. + /// true to normalize each direction vector; otherwise, full edge displacement vectors are written. + /// true if the polygon has at least two vertices and was populated; otherwise, false. + public bool GetEdgeDirections(List result, bool normalized = false) + { + if (Count <= 1) return false; + result.Clear(); + if (Count == 2) { - var p = this[i]; - if (p.X < minX) minX = p.X; - else if (p.X > maxX) maxX = p.X; - - if (p.Y < minY) minY = p.Y; - else if (p.Y > maxY) maxY = p.Y; + result.Add(this[1] - this[0]); + return true; } - - return new Rect(minX, minY, maxX - minX, maxY - minY); + for (var i = 0; i < Count; i++) + { + var start = this[i]; + var end = this[(i + 1) % Count]; + var a = end - start; + result.Add(normalized ? a.Normalize() : a); + } + + return true; } /// - /// Returns the convex hull of the polygon as a new polygon. + /// Writes the polygon edges into as segments. /// - /// A convex polygon containing all the original points. - public Polygon? ToConvex() => Polygon.FindConvexHull(this); + /// The destination segment collection that will be cleared and populated with the polygon edges. + /// true if the polygon has at least two vertices and was populated; otherwise, false. + /// + /// When the polygon has exactly two vertices, a single segment is produced. For three or more vertices, the closing segment from the last vertex back to the first is included. + /// + public bool GetEdges(Segments result) + { + if (Count <= 1) return false; + + result.Clear(); + result.EnsureCapacity(Count); + + if (Count == 2) + { + var segment = new Segment(this[0], this[1]); + result.Add(segment); + return true; + } + + for (int i = 0; i < Count; i++) + { + result.Add(new(this[i], this[(i + 1) % Count])); + } + + return true; + } + #endregion #region Random @@ -692,15 +904,20 @@ public Rect GetBoundingBox() /// A random point inside the polygon. public Vector2 GetRandomPointInside() { - var triangles = Triangulate(); - List> items = new(); - foreach (var t in triangles) + Triangulate(triangulationBuffer); + + weightedTrianglesBuffer.Clear(); + weightedTrianglesBuffer.EnsureCapacity(triangulationBuffer.Count); + + foreach (var t in triangulationBuffer) { - items.Add(new(t, (int)t.GetArea())); + weightedTrianglesBuffer.Add(new(t, (int)t.GetArea())); } - var item = Rng.Instance.PickRandomItem(items.ToArray()); + + var item = Rng.Instance.PickRandomItem(weightedTrianglesBuffer); return item.GetRandomPointInside(); } + /// /// Gets a set of random points inside the polygon. /// @@ -708,42 +925,64 @@ public Vector2 GetRandomPointInside() /// A set of random points inside the polygon. public Points GetRandomPointsInside(int amount) { - var triangles = Triangulate(); - var items = new WeightedItem[triangles.Count]; - for (var i = 0; i < items.Length; i++) + Triangulate(triangulationBuffer); + + weightedTrianglesBuffer.Clear(); + weightedTrianglesBuffer.EnsureCapacity(triangulationBuffer.Count); + + foreach (var t in triangulationBuffer) { - var t = triangles[i]; - items[i] = new(t, (int)t.GetArea()); + weightedTrianglesBuffer.Add(new(t, (int)t.GetArea())); + } + + Rng.Instance.PickRandomItems(pickedTrianglesBuffer, amount, weightedTrianglesBuffer); + Points randomPoints = new(amount); + foreach (var tri in pickedTrianglesBuffer) + { + randomPoints.Add(tri.GetRandomPointInside()); } - - - var pickedTriangles = Rng.Instance.PickRandomItems(amount, items); - Points randomPoints = []; - foreach (var tri in pickedTriangles) randomPoints.Add(tri.GetRandomPointInside()); return randomPoints; } + /// /// Gets a random vertex from the polygon. /// /// A random vertex. public Vector2 GetRandomVertex() { return Rng.Instance.RandCollection(this); } + /// /// Gets a random edge (segment) from the polygon. /// /// A random edge. - public Segment GetRandomEdge() => GetEdges().GetRandomSegment(); + public Segment GetRandomEdge() + { + GetEdges(segmentsBuffer); + return segmentsBuffer.GetRandomSegment(); + } + /// /// Gets a random point on the polygon's edge. /// /// A random point on an edge. - public Vector2 GetRandomPointOnEdge() => GetRandomEdge().GetRandomPoint(); + public Vector2 GetRandomPointOnEdge() + { + return GetRandomEdge().GetRandomPoint(); + } + /// /// Gets a set of random points on the polygon's edges. /// /// The number of random points to generate. /// A set of random points on the edges. - public Points GetRandomPointsOnEdge(int amount) => GetEdges().GetRandomPoints(amount); + public Points GetRandomPointsOnEdge(int amount) + { + GetEdges(segmentsBuffer); + var points = new Points(); + segmentsBuffer.GetRandomPoints(amount, points); + return points; + } + /// /// Gets a random point inside the convex hull of the polygon by interpolating between random edges. /// @@ -758,263 +997,302 @@ public Vector2 GetRandomPointConvex() var pb = eb.Start.Lerp(eb.End, Rng.Instance.RandF()); return pa.Lerp(pb, Rng.Instance.RandF()); } - - - #endregion - - #region Cutout & Compound - private bool GetPolygonShape(IShape shape, ref Polygon result) - { - switch (shape.GetShapeType()) - { - default: - case ShapeType.None: - case ShapeType.Ray: - case ShapeType.Line: - case ShapeType.Segment: - case ShapeType.PolyLine: - return false; - case ShapeType.Circle: - return shape.GetCircleShape().ToPolygon(ref result); - case ShapeType.Triangle: - shape.GetTriangleShape().ToPolygon(ref result); - return true; - case ShapeType.Quad: - shape.GetQuadShape().ToPolygon(ref result); - return true; - case ShapeType.Rect: - shape.GetRectShape().ToPolygon(ref result); - return true; - case ShapeType.Poly: - return shape.GetPolygonShape().ToPolygon(ref result); - } - } /// - /// Adds a compound shape to the polygon by converting the given to a polygon and merging it. + /// Generates random points inside the polygon and writes them into . /// - /// The shape to add. - /// True if the shape was successfully merged; otherwise, false. - public bool AddCompoundShape(IShape shape) - { - compoundHelperPolygon ??= []; - var valid = GetPolygonShape(shape, ref compoundHelperPolygon); - if (!valid || compoundHelperPolygon.Count <= 2) return false; - return MergeShapeSelf(compoundHelperPolygon); - - } - - /// - /// Adds a compound shape and its children from a to the polygon. - /// - /// The shape container to add. - /// The number of shapes successfully merged. (Parent Shape + Children Shapes) - public int AddCompoundShape(ShapeContainer shape) + /// The destination collection that will be cleared and populated with the generated points. + /// The number of random points to generate. + /// + /// This method triangulates the polygon, randomly selects triangles weighted by their area, and samples one random interior point from each selected triangle. + /// + public void GetRandomPointsInside(Points result, int amount) { - int count = 0; - if (AddCompoundShape((IShape)shape)) count++; + Triangulate(triangulationBuffer); - foreach (var child in shape.GetChildrenCopy()) + weightedTrianglesBuffer.Clear(); + weightedTrianglesBuffer.EnsureCapacity(triangulationBuffer.Count); + + foreach (var t in triangulationBuffer) { - if(AddCompoundShape((IShape)child)) - { - count++; - } + weightedTrianglesBuffer.Add(new(t, (int)t.GetArea())); + } + + Rng.Instance.PickRandomItems(pickedTrianglesBuffer, amount, weightedTrianglesBuffer); + + result.Clear(); + result.EnsureCapacity(pickedTrianglesBuffer.Count); + + foreach (var tri in pickedTrianglesBuffer) + { + result.Add(tri.GetRandomPointInside()); } - - return count; - } - - /// - /// Adds a shape to the polygon by converting it to a polygon with the specified number of points. - /// - /// The circle to add. - /// The number of points to use for the polygon approximation. Default is 16. - /// True if the shape was successfully merged; otherwise, false. - public bool AddCompoundShape(Circle shape, int pointCount = 16) - { - compoundHelperPolygon ??= []; - shape.ToPolygon(ref compoundHelperPolygon, pointCount); - if(compoundHelperPolygon.Count <= 2) return false; - return MergeShapeSelf(compoundHelperPolygon); - } - - /// - /// Adds a shape to the polygon by converting it to a polygon. - /// - /// The triangle to add. - /// True if the shape was successfully merged; otherwise, false. - public bool AddCompoundShape(Triangle shape) - { - compoundHelperPolygon ??= []; - shape.ToPolygon(ref compoundHelperPolygon); - if(compoundHelperPolygon.Count <= 2) return false; - return MergeShapeSelf(compoundHelperPolygon); - } - - /// - /// Adds a shape to the polygon by converting it to a polygon. - /// - /// The quad to add. - /// True if the shape was successfully merged; otherwise, false. - public bool AddCompoundShape(Quad shape) - { - compoundHelperPolygon ??= []; - shape.ToPolygon(ref compoundHelperPolygon); - if(compoundHelperPolygon.Count <= 2) return false; - return MergeShapeSelf(compoundHelperPolygon); } /// - /// Adds a shape to the polygon by converting it to a polygon. + /// Generates random points on the polygon's edges and writes them into . /// - /// The rectangle to add. - /// True if the shape was successfully merged; otherwise, false. - public bool AddCompoundShape(Rect shape) + /// The destination collection that will be cleared and populated with the generated edge points. + /// The number of random points to generate. + /// + /// Edge selection is delegated to after building the polygon edge list. + /// + public void GetRandomPointsOnEdge(Points result, int amount) { - compoundHelperPolygon ??= []; - shape.ToPolygon(ref compoundHelperPolygon); - if(compoundHelperPolygon.Count <= 2) return false; - return MergeShapeSelf(compoundHelperPolygon); + GetEdges(segmentsBuffer); + segmentsBuffer.GetRandomPoints(amount, result); } + #endregion - /// - /// Adds another shape to the polygon by merging it. - /// - /// The polygon to add. - /// True if the shape was successfully merged; otherwise, false. - public bool AddCompoundShape(Polygon shape) - { - if (shape.Count <= 2) return false; - return MergeShapeSelf(shape); - } - + #region Static Methods /// - /// Cuts out the given from the polygon by converting it to a polygon and performing a cut operation. + /// Transforms the supplied relative points and appends them to as polygon vertices. /// - /// The shape to cut out. - /// True if the cutout was successful; otherwise, false. + /// The relative points defining the shape. + /// The transform applied to each point before it is added to . + /// The destination polygon that receives the transformed vertices. + /// true if contains at least three points and the transformed vertices were added; otherwise, false. /// - /// Does not support cutting out holes. - /// If the shape for cutting is completely contained within the polygon, it will not be cut out! + /// This method does not clear before adding vertices. /// - public bool AddCutoutShape(IShape shape) + public static bool GetShape(Points relative, Transform2D transform, Polygon result) { - compoundHelperPolygon ??= []; - bool valid = GetPolygonShape(shape, ref compoundHelperPolygon); - if (!valid || compoundHelperPolygon.Count <= 2) return false; - return CutShapeSelf(compoundHelperPolygon); + if (relative.Count < 3) return false; + for (var i = 0; i < relative.Count; i++) + { + result.Add(transform.ApplyTransformTo(relative[i])); + } + + return true; } - + /// - /// Cuts out the parent and child shapes from a from the polygon. + /// Generates vertices for a random polygon around the origin and appends them to . /// - /// The shape container whose shapes will be cut out. - /// The number of shapes successfully cut out. + /// Number of points (vertices) in the polygon. Must be at least 3. + /// Minimum distance from the origin for each point. + /// Maximum distance from the origin for each point. + /// The destination polygon that receives the generated vertices. + /// true if the polygon parameters are valid and vertices were generated; otherwise, false. /// - /// Does not support cutting out holes. - /// If the shape for cutting is completely contained within the polygon, it will not be cut out! + /// The generated points are evenly distributed by angle and randomized by radial distance. If is greater than , the values are swapped. This method does not clear before adding vertices. /// - public int AddCutoutShape(ShapeContainer shape) + public static bool GenerateRelative(int pointCount, float minLength, float maxLength, Polygon result) { - var count = 0; - if (AddCutoutShape((IShape)shape)) count++; + if (pointCount < 3) return false; + if (Math.Abs(minLength - maxLength) < ShapeMath.Epsilon) return false; + if (minLength > maxLength) + { + //swap + (minLength, maxLength) = (maxLength, minLength); + } - foreach (var child in shape.GetChildrenCopy()) + float angleStep = ShapeMath.PI * 2.0f / pointCount; + + for (var i = 0; i < pointCount; i++) { - if(AddCutoutShape((IShape)child)) - { - count++; - } + float randLength = Rng.Instance.RandF(minLength, maxLength); + var p = ShapeVec.Right().Rotate(-angleStep * i) * randLength; + result.Add(p); } - - return count; + + return true; } - + /// - /// Cuts out a shape from the polygon by converting it to a polygon with the specified number of points. + /// Generates vertices for a random polygon centered at and writes them into . /// - /// The circle to cut out. - /// The number of points to use for the polygon approximation. Default is 16. - /// True if the cutout was successful; otherwise, false. + /// The center position of the polygon. + /// Number of points (vertices) in the polygon. Must be at least 3. + /// Minimum distance from the center for each point. + /// Maximum distance from the center for each point. + /// The destination polygon that will be cleared and populated with the generated vertices. + /// true if the polygon parameters are valid and vertices were generated; otherwise, false. /// - /// Does not support cutting out holes. - /// If the shape for cutting is completely contained within the polygon, it will not be cut out! + /// The generated points are evenly distributed by angle and randomized by radial distance. If is greater than , the values are swapped. /// - public bool AddCutoutShape(Circle shape, int pointCount = 16) + public static bool Generate(Vector2 center, int pointCount, float minLength, float maxLength, Polygon result) { - compoundHelperPolygon ??= []; - shape.ToPolygon(ref compoundHelperPolygon, pointCount); - if(compoundHelperPolygon.Count <= 2) return false; - return CutShapeSelf(compoundHelperPolygon); + if (pointCount < 3) return false; + if (Math.Abs(minLength - maxLength) < ShapeMath.Epsilon) return false; + if (minLength > maxLength) + { + //swap + (minLength, maxLength) = (maxLength, minLength); + } + float angleStep = ShapeMath.PI * 2.0f / pointCount; + result.Clear(); + for (int i = 0; i < pointCount; i++) + { + float randLength = Rng.Instance.RandF(minLength, maxLength); + var p = ShapeVec.Right().Rotate(-angleStep * i) * randLength; + p += center; + result.Add(p); + } + + return true; } - + /// - /// Cuts out a shape from the polygon by converting it to a polygon. + /// Generates a polygonal shape around the specified and appends its vertices to . /// - /// The triangle to cut out. - /// True if the cutout was successful; otherwise, false. + /// The segment to build a polygon around. + /// The destination polygon that receives the generated vertices. + /// The minimum perpendicular offset magnitude used for intermediate points on either side of the segment. + /// The maximum perpendicular offset magnitude used for intermediate points on either side of the segment. + /// The minimum step size along the segment, expressed as a fraction of the segment length. + /// The maximum step size along the segment, expressed as a fraction of the segment length. + /// true if the segment and generation parameters are valid and vertices were added; otherwise, false. /// - /// Does not support cutting out holes. - /// If the shape for cutting is completely contained within the polygon, it will not be cut out! + /// The generated vertex order starts at , progresses toward along one side of the segment, then returns along the opposite side. If the minimum values are greater than the corresponding maximum values, the pairs are swapped. This method does not clear before adding vertices. /// - public bool AddCutoutShape(Triangle shape) + public static bool Generate(Segment segment, Polygon result, float magMin = 0.1f, float magMax = 0.25f, float minSectionLength = 0.025f, float maxSectionLength = 0.1f) { - compoundHelperPolygon ??= []; - shape.ToPolygon(ref compoundHelperPolygon); - if(compoundHelperPolygon.Count <= 2) return false; - return CutShapeSelf(compoundHelperPolygon); + if (segment.LengthSquared <= 0) return false; + if (magMin <= 0 || magMax <= 0) return false; + if (minSectionLength <= 0 || maxSectionLength <= 0) return false; + if (magMin > magMax) + { + (magMin, magMax) = (magMax, magMin); + } + + if (minSectionLength > maxSectionLength) + { + (minSectionLength, maxSectionLength) = (maxSectionLength, minSectionLength); + } + result.Add(segment.Start); + var dir = segment.Dir; + var dirRight = dir.GetPerpendicularRight(); + var dirLeft = dir.GetPerpendicularLeft(); + float len = segment.Length; + float minSectionLengthSq = (minSectionLength * len) * (minSectionLength * len); + var cur = segment.Start; + while (true) + { + cur += dir * Rng.Instance.RandF(minSectionLength, maxSectionLength) * len; + if ((cur - segment.End).LengthSquared() < minSectionLengthSq) break; + result.Add(cur + dirRight * Rng.Instance.RandF(magMin, magMax)); + } + + cur = segment.End; + result.Add(cur); + while (true) + { + cur -= dir * Rng.Instance.RandF(minSectionLength, maxSectionLength) * len; + if ((cur - segment.Start).LengthSquared() < minSectionLengthSq) break; + result.Add(cur + dirLeft * Rng.Instance.RandF(magMin, magMax)); + } + + return true; } - /// - /// Cuts out a shape from the polygon by converting it to a polygon. - /// - /// The quad to cut out. - /// True if the cutout was successful; otherwise, false. - /// - /// Does not support cutting out holes. - /// If the shape for cutting is completely contained within the polygon, it will not be cut out! - /// - public bool AddCutoutShape(Quad shape) + + #endregion + + #region Private + internal static bool ContainsPointCheck(Vector2 a, Vector2 b, Vector2 pointToCheck) { - compoundHelperPolygon ??= []; - shape.ToPolygon(ref compoundHelperPolygon); - if(compoundHelperPolygon.Count <= 2) return false; - return CutShapeSelf(compoundHelperPolygon); + if (a.Y < pointToCheck.Y && b.Y >= pointToCheck.Y || b.Y < pointToCheck.Y && a.Y >= pointToCheck.Y) + { + if (a.X + (pointToCheck.Y - a.Y) / (b.Y - a.Y) * (b.X - a.X) < pointToCheck.X) + { + return true; + } + } + + return false; } /// - /// Cuts out a shape from the polygon by converting it to a polygon. + /// Recursively computes the minimal enclosing circle for a set of points using Welzl's algorithm. /// - /// The rectangle to cut out. - /// True if the cutout was successful; otherwise, false. + /// List of input points. Only the first elements are considered in this recursion. + /// Auxiliary list of boundary (support) points that must lie on the circle. Its size will be 0..3. + /// Number of points from to consider in this recursion. + /// The minimal enclosing for the considered points given the current boundary. /// - /// Does not support cutting out holes. - /// If the shape for cutting is completely contained within the polygon, it will not be cut out! + /// The algorithm randomly selects a point from the first points, computes the minimal circle + /// for the remaining points, and if the selected point lies outside that circle, it is added to the boundary + /// and the algorithm recurses. Termination occurs when is 0 or the boundary contains 3 points. /// - public bool AddCutoutShape(Rect shape) + private Circle WelzlHelper(List points, List boundary, int n) { - compoundHelperPolygon ??= []; - shape.ToPolygon(ref compoundHelperPolygon); - if(compoundHelperPolygon.Count <= 2) return false; - return CutShapeSelf(compoundHelperPolygon); + if (n == 0 || boundary.Count == 3) + { + return GetCircleFromBoundary(boundary); + } + + int idx = Rng.Instance.RandI(0, n); + var p = points[idx]; + + (points[idx], points[n - 1]) = (points[n - 1], points[idx]); + + var circle = WelzlHelper(points, boundary, n - 1); + + if (circle.ContainsPoint(p)) + { + return circle; + } + + boundary.Add(p); + circle = WelzlHelper(points, boundary, n - 1); + boundary.RemoveAt(boundary.Count - 1); + + return circle; } /// - /// Cuts out another shape from the polygon. + /// Constructs the minimal circle determined by the given boundary (support) points. /// - /// The polygon to cut out. - /// True if the cutout was successful; otherwise, false. + /// A list of support points (0..3) which must lie on the resulting circle. + /// + /// A that is the minimal circle passing through the provided boundary points: + /// - If boundary.Count == 0 returns an empty/default circle. + /// - If boundary.Count == 1 returns a circle centered at the single point with radius 0. + /// - If boundary.Count == 2 returns the circle with the two points as endpoints of a diameter. + /// - If boundary.Count == 3 returns the circumcircle; if the three points are (nearly) collinear, + /// the function falls back to a circle using the largest pair as diameter. + /// /// - /// Does not support cutting out holes. - /// If the shape for cutting is completely contained within the polygon, it will not be cut out! + /// This helper is used by Welzl's algorithm to compute the minimal enclosing circle for a set of points. + /// Numerical stability is considered: when the determinant is very small the points are treated as collinear. /// - public bool AddCutoutShape(Polygon shape) + private Circle GetCircleFromBoundary(List boundary) { - if (shape.Count <= 2) return false; - return CutShapeSelf(shape); + if (boundary.Count == 0) return new Circle(); + if (boundary.Count == 1) return new Circle(boundary[0], 0f); + if (boundary.Count == 2) + { + var center1 = (boundary[0] + boundary[1]) * 0.5f; + var radius1 = (boundary[1] - boundary[0]).Length() * 0.5f; + return new Circle(center1, radius1); + } + + var a = boundary[0]; + var b = boundary[1]; + var c = boundary[2]; + + var d = 2f * (a.X * (b.Y - c.Y) + b.X * (c.Y - a.Y) + c.X * (a.Y - b.Y)); + if (MathF.Abs(d) < 0.0001f)//check for collinearity + { + var center2 = (a + b) * 0.5f; + // var radius2 = MathF.Max((b - a).Length(), (c - a).Length()) * 0.5f; + var radius2 = MathF.Max(MathF.Max((b - a).Length(), (c - a).Length()), (c - b).Length()) * 0.5f; + return new Circle(center2, radius2); + } + + var aSq = a.LengthSquared(); + var bSq = b.LengthSquared(); + var cSq = c.LengthSquared(); + + var ux = (aSq * (b.Y - c.Y) + bSq * (c.Y - a.Y) + cSq * (a.Y - b.Y)) / d; + var uy = (aSq * (c.X - b.X) + bSq * (a.X - c.X) + cSq * (b.X - a.X)) / d; + + var center3 = new Vector2(ux, uy); + var radius3 = (center3 - a).Length(); + + return new Circle(center3, radius3); } - #endregion } diff --git a/ShapeEngine/Geometry/PolygonDef/PolygonClipping.cs b/ShapeEngine/Geometry/PolygonDef/PolygonClipping.cs index 85220ba7..54073a42 100644 --- a/ShapeEngine/Geometry/PolygonDef/PolygonClipping.cs +++ b/ShapeEngine/Geometry/PolygonDef/PolygonClipping.cs @@ -1,58 +1,154 @@ using System.Numerics; -using Clipper2Lib; using ShapeEngine.Geometry.LineDef; using ShapeEngine.Geometry.SegmentDef; using ShapeEngine.Geometry.SegmentsDef; using ShapeEngine.Random; +using ShapeEngine.ShapeClipper; using ShapeEngine.StaticLib; namespace ShapeEngine.Geometry.PolygonDef; - public partial class Polygon { + #region Intersection + /// + /// Computes the geometric intersection between this polygon and and writes the first resulting polygon into . + /// + /// The polygon to intersect with this polygon. + /// The destination polygon that receives the first intersection result. + /// true if at least one intersection polygon was produced; otherwise, false. + /// + /// If multiple intersection polygons are produced, only the first one is copied into . + /// + public bool ClipIntersection(Polygon other, Polygon result) + { + clipResultBuffer.Clear(); + ClipperImmediate2D.ClipEngine.Execute(this, other, ShapeClipperClipType.Intersection, clipResultBuffer); + if (clipResultBuffer.Count <= 0) return false; + clipResultBuffer[0].ToVector2List(result); + return true; + } + + /// + /// Computes the geometric intersection between this polygon and and writes all resulting polygons into . + /// + /// The polygon to intersect with this polygon. + /// The destination collection that receives all intersection polygons. + /// true if at least one intersection polygon was produced; otherwise, false. + public bool ClipIntersection(Polygon other, Polygons result) + { + clipResultBuffer.Clear(); + ClipperImmediate2D.ClipEngine.Execute(this, other, ShapeClipperClipType.Intersection, clipResultBuffer); + if (clipResultBuffer.Count <= 0) return false; + clipResultBuffer.ToPolygons(result, true); + return true; + } + + /// + /// Computes the geometric intersection between this polygon and all polygons in , writing the resulting polygons into . + /// + /// The polygons to intersect with this polygon. + /// The destination collection that receives all resulting intersection polygons. + /// true if at least one intersection polygon was produced; otherwise, false. + public bool ClipIntersectionMany(Polygons others, Polygons result) + { + clipResultBuffer.Clear(); + ClipperImmediate2D.ClipEngine.ExecuteMany(this, others, ShapeClipperClipType.Intersection, clipResultBuffer); + if (clipResultBuffer.Count <= 0) return false; + clipResultBuffer.ToPolygons(result, true); + return true; + } + #endregion + #region Difference /// - /// Unions this polygon with another polygon and replaces the current shape with the result. + /// Computes the geometric difference of this polygon minus and writes the first resulting polygon into . /// - /// The polygon to union with. - /// The fill rule to use for the union operation. + /// The polygon to subtract from this polygon. + /// The destination polygon that receives the first resulting difference polygon. + /// true if at least one difference polygon was produced; otherwise, false. /// - /// Uses the Clipper library for polygon union. The result replaces the current polygon if successful. + /// If multiple difference polygons are produced, only the first one is copied into . /// - public void UnionShapeSelf(Polygon b, FillRule fillRule = FillRule.NonZero) + public bool ClipDifference(Polygon other, Polygon result) { - var result = Clipper.Union(this.ToClipperPaths(), b.ToClipperPaths(), fillRule); - if (result.Count > 0) - { - this.Clear(); - foreach (var p in result[0]) - { - this.Add(p.ToVec2()); - } - } + clipResultBuffer.Clear(); + ClipperImmediate2D.ClipEngine.Execute(this, other, ShapeClipperClipType.Difference, clipResultBuffer); + if (clipResultBuffer.Count <= 0) return false; + clipResultBuffer[0].ToVector2List(result); + return true; } + + /// + /// Computes the geometric difference of this polygon minus and writes all resulting polygons into . + /// + /// The polygon to subtract from this polygon. + /// The destination collection that receives the resulting difference polygons. + /// true if at least one difference polygon was produced; otherwise, false. + public bool ClipDifference(Polygon other, Polygons result) + { + clipResultBuffer.Clear(); + ClipperImmediate2D.ClipEngine.Execute(this, other, ShapeClipperClipType.Difference, clipResultBuffer); + if (clipResultBuffer.Count <= 0) return false; + clipResultBuffer.ToPolygons(result, true); + return true; + } + + /// + /// Computes the geometric difference of this polygon minus all polygons in and writes the resulting polygons into . + /// + /// The polygons to subtract from this polygon. + /// The destination collection that receives the resulting difference polygons. + /// true if at least one difference polygon was produced; otherwise, false. + public bool ClipDifferenceMany(Polygons others, Polygons result) + { + clipResultBuffer.Clear(); + ClipperImmediate2D.ClipEngine.ExecuteMany(this, others, ShapeClipperClipType.Difference, clipResultBuffer); + if (clipResultBuffer.Count <= 0) return false; + clipResultBuffer.ToPolygons(result, true); + return true; + } + #endregion + #region Union /// - /// Attempts to merge this polygon with another if their closest points are within a given distance threshold. + /// Computes the geometric union of this polygon and and writes the first resulting polygon into . /// - /// The polygon to merge with. - /// The maximum allowed distance between closest points to perform a merge. - /// True if a merge was performed; otherwise, false. + /// The polygon to union with this polygon. + /// The destination polygon that receives the first union result. + /// true if at least one union polygon was produced; otherwise, false. /// - /// Merges by generating a fill shape between the closest points and performing a union. + /// If multiple union polygons are produced, only the first one is copied into . /// - public bool MergeShapeSelf(Polygon other, float distanceThreshold) + public bool ClipUnion(Polygon other, Polygon result) + { + clipResultBuffer.Clear(); + ClipperImmediate2D.ClipEngine.Execute(this, other, ShapeClipperClipType.Union, clipResultBuffer); + if (clipResultBuffer.Count <= 0) return false; + clipResultBuffer[0].ToVector2List(result); + return true; + } + + /// + /// Attempts to union this polygon with by first bridging nearby shapes when they are within . + /// + /// The polygon to union with this polygon. + /// The maximum allowed distance between the polygons before a temporary bridge polygon is generated. + /// The destination polygon that receives the union result. + /// true if a bridge polygon was generated and the union succeeded; otherwise, false. + /// + /// This overload is intended for shapes that are close but not yet touching. It generates a temporary polygon at the closest point before performing the union. + /// + public bool ClipUnion(Polygon other, float distanceThreshold, Polygon result) { if (distanceThreshold <= 0f) return false; - var result = GetClosestPoint(other); - if (result.Valid && result.DistanceSquared < distanceThreshold * distanceThreshold) + var cp = GetClosestPoint(other); + if (cp.Valid && cp.DistanceSquared < distanceThreshold * distanceThreshold) { - var fillShape = Generate(result.Self.Point, 7, distanceThreshold, distanceThreshold * 2); - if (fillShape == null) return false; - UnionShapeSelf(fillShape); - UnionShapeSelf(other); + if (!Generate(cp.Self.Point, 7, distanceThreshold, distanceThreshold * 2, clipPolygonBuffer)) return false; + this.ClipUnion(clipPolygonBuffer, result); + result.ClipUnion(other, result); return true; } @@ -60,191 +156,247 @@ public bool MergeShapeSelf(Polygon other, float distanceThreshold) } /// - /// Attempts to merge this polygon with another if they overlap. + /// Computes the geometric union of this polygon and and writes all resulting polygons into . + /// + /// The polygon to union with this polygon. + /// The destination collection that receives the union result polygons. + /// true if at least one union polygon was produced; otherwise, false. + public bool ClipUnion(Polygon other, Polygons result) + { + clipResultBuffer.Clear(); + ClipperImmediate2D.ClipEngine.Execute(this, other, ShapeClipperClipType.Union, clipResultBuffer); + if (clipResultBuffer.Count <= 0) return false; + clipResultBuffer.ToPolygons(result, true); + return true; + } + + /// + /// Attempts to union this polygon with by first bridging nearby shapes when they are within , writing all resulting polygons into . /// - /// The polygon to merge with. - /// True if a merge was performed; otherwise, false. - public bool MergeShapeSelf(Polygon other) + /// The polygon to union with this polygon. + /// The maximum allowed distance between the polygons before a temporary bridge polygon is generated. + /// The destination collection that receives the union result polygons. + /// true if the bridge polygon was generated and the union produced a single intermediate shape that could be unioned with ; otherwise, false. + /// + /// This overload requires the first intermediate union to collapse to exactly one polygon before continuing. + /// + public bool ClipUnion(Polygon other, float distanceThreshold, Polygons result) { - var overlap = OverlapShape(other); - if (overlap) + if (distanceThreshold <= 0f) return false; + + var cp = GetClosestPoint(other); + if (cp.Valid && cp.DistanceSquared < distanceThreshold * distanceThreshold) { - UnionShapeSelf(other); + if (!Generate(cp.Self.Point, 7, distanceThreshold, distanceThreshold * 2, clipPolygonBuffer)) return false; + this.ClipUnion(clipPolygonBuffer, result); + if (result.Count > 1) return false; + + result[0].ClipUnion(other, result); return true; } return false; } - + /// - /// Subtracts the specified polygon (`cutShape`) from this polygon. - /// Optionally keeps only the intersected (cut-out) region if `keepCutout` is true. - /// Returns true if the operation resulted in a valid polygon; otherwise, false. + /// Computes both the overlap and the union between this polygon and . /// - /// The polygon to subtract from this polygon. - /// If true, keeps only the intersected region; otherwise, subtracts the cutShape. - /// True if the difference operation produced a valid polygon; otherwise, false. - public bool CutShapeSelf(Polygon cutShape, bool keepCutout = false) + /// The polygon to union with this polygon. + /// The destination collection that receives the union result polygons. + /// The destination collection that receives the intersection polygons. + /// true. + /// + /// This method always returns true after updating both output collections, even if either collection ends up empty. + /// + public bool ClipUnion(Polygon other, Polygons newShapesResult, Polygons overlapsResult) { - var newShapes = keepCutout ? this.Intersect(cutShape) : this.Difference(cutShape); + ClipperImmediate2D.ClipEngine.Execute(this, other, ShapeClipperClipType.Intersection, clipResultBuffer); + clipResultBuffer.ToPolygons(overlapsResult, true); - if (newShapes.Count > 0) - { - var polygons = newShapes.ToPolygons(true); - if (polygons.Count > 0) - { - foreach (var polygon in polygons) - { - if(polygon.Count < 3) continue; // Skip invalid polygons - this.Clear(); - - if (polygon.IsClockwise()) - { - for (int i = polygon.Count - 1; i >= 0; i--)//reverse order clockwise to counter-clockwise - { - Add(polygon[i]); - } - } - else - { - for (int i = 0; i < polygon.Count; i++) - { - Add(polygon[i]); - } - } - - return true; - } - } - } - - return false; + ClipperImmediate2D.ClipEngine.Execute(this, other, ShapeClipperClipType.Union, clipResultBuffer); + clipResultBuffer.ToPolygons(newShapesResult, true); + + return true; } - + /// - /// Attempts to merge this polygon with another and returns the merged result as a new polygon. + /// Computes the geometric union of this polygon with all polygons in and writes the resulting polygons into . /// - /// The polygon to merge with. - /// The maximum allowed distance between closest points to perform a merge. - /// The merged polygon if successful; otherwise, null. - public Polygon? MergeShape(Polygon other, float distanceThreshold) + /// The polygons to union with this polygon. + /// The destination collection that receives the union result polygons. + /// true if at least one union polygon was produced; otherwise, false. + public bool ClipUnionMany(Polygons others, Polygons result) { - if(distanceThreshold <= 0f) return null; - var result = GetClosestPoint(other); - if (result.Valid && result.DistanceSquared < distanceThreshold * distanceThreshold) - { - var fillShape = Generate(result.Self.Point, 7, distanceThreshold, distanceThreshold * 2); - if(fillShape == null) return null; - var clip = this.Union(fillShape); - if (clip.Count > 0) - { - clip = clip[0].ToPolygon().Union(other); - if (clip.Count > 0) return clip[0].ToPolygon(); - } - } - - return null; + clipResultBuffer.Clear(); + ClipperImmediate2D.ClipEngine.ExecuteMany(this, others, ShapeClipperClipType.Union, clipResultBuffer); + if (clipResultBuffer.Count <= 0) return false; + clipResultBuffer.ToPolygons(result, true); + return true; } - + /// - /// Cuts the current polygon with another polygon, returning the resulting shapes and the cut-out regions. + /// Computes both the overlap and the union between this polygon and all polygons in . /// - /// The polygon to cut with. - /// A tuple containing the new shapes and the cut-out regions. - public (Polygons newShapes, Polygons cutOuts) CutShape(Polygon cutShape) + /// The polygons to union with this polygon. + /// The destination collection that receives the union result polygons. + /// The destination collection that receives the intersection polygons. + /// true. + /// + /// This method always returns true after updating both output collections, even if either collection ends up empty. + /// + public bool ClipUnionMany(Polygons others, Polygons newShapesResult, Polygons overlapsResult) { - var cutOuts = this.Intersect(cutShape).ToPolygons(true); - var newShapes = this.Difference(cutShape).ToPolygons(true); - - return (newShapes, cutOuts); + ClipperImmediate2D.ClipEngine.ExecuteMany(this, others, ShapeClipperClipType.Intersection, clipResultBuffer); + clipResultBuffer.ToPolygons(overlapsResult, true); + + ClipperImmediate2D.ClipEngine.ExecuteMany(this, others, ShapeClipperClipType.Union, clipResultBuffer); + clipResultBuffer.ToPolygons(newShapesResult, true); + + return true; } + #endregion + #region Cut + /// - /// Cuts the current polygon with multiple polygons, returning the resulting shapes and the cut-out regions. + /// Cuts this polygon in place using , optionally collecting the removed intersections and any additional remaining pieces. /// - /// The polygons to cut with. - /// A tuple containing the new shapes and the cut-out regions. - public (Polygons newShapes, Polygons cutOuts) CutShapeMany(Polygons cutShapes) + /// The polygon used to cut this polygon. + /// An optional destination collection that receives the intersecting cut-out polygons. + /// An optional destination collection that receives any remaining polygons beyond the first retained shape. + /// true if at least one remaining difference polygon was produced and this polygon was updated; otherwise, false. + /// + /// The first remaining polygon replaces the contents of this polygon. Any additional remaining polygons are written to when provided. + /// + public bool CutSelf(Polygon cutShape, Polygons? cutOutsResult, Polygons? extraShapesResult) { - var cutOuts = this.IntersectMany(cutShapes).ToPolygons(true); - var newShapes = this.DifferenceMany(cutShapes).ToPolygons(true); - return (newShapes, cutOuts); + if (cutOutsResult != null) + { + ClipperImmediate2D.ClipEngine.Execute(this, cutShape, ShapeClipperClipType.Intersection, clipResultBuffer); + clipResultBuffer.ToPolygons(cutOutsResult, true); + } + clipResultBuffer.Clear(); + ClipperImmediate2D.ClipEngine.Execute(this, cutShape, ShapeClipperClipType.Difference, clipResultBuffer); + clipResultBuffer.RemoveAllHoles(); + if(clipResultBuffer.Count <= 0) return false; + clipResultBuffer[0].ToVector2List(this); + if (clipResultBuffer.Count > 1 && extraShapesResult != null) + { + clipResultBuffer.RemoveAt(0); + clipResultBuffer.ToPolygons(extraShapesResult, true); + } + return true; } - + /// - /// Combines this polygon with another, returning the union and the overlapping regions. + /// Cuts this polygon with and writes both the removed overlap polygons and the remaining polygons into the provided result collections. /// - /// The polygon to combine with. - /// A tuple containing the new shapes and the overlapping regions. - public (Polygons newShapes, Polygons overlaps) CombineShape(Polygon other) + /// The polygon used to cut this polygon. + /// The destination collection that receives the intersecting cut-out polygons. + /// The destination collection that receives the remaining polygons after the cut. + /// true. + /// + /// This method always returns true after updating both output collections. The current polygon is not modified. + /// + public bool Cut(Polygon cutShape, Polygons cutOutsResult, Polygons newShapesResult) { - var overlaps = this.Intersect(other).ToPolygons(true); - var newShapes = this.Union(other).ToPolygons(true); - return (newShapes, overlaps); + clipResultBuffer.Clear(); + ClipperImmediate2D.ClipEngine.Execute(this, cutShape, ShapeClipperClipType.Intersection, clipResultBuffer); + clipResultBuffer.ToPolygons(cutOutsResult, true); + + clipResultBuffer.Clear(); + ClipperImmediate2D.ClipEngine.Execute(this, cutShape, ShapeClipperClipType.Difference, clipResultBuffer); + clipResultBuffer.ToPolygons(newShapesResult, true); + Console.WriteLine($"Shape Clockwise: {this.IsClockwise()}, Cut Shape Clockwise: {cutShape.IsClockwise()}"); + foreach (Polygon polygon in newShapesResult) + { + Console.WriteLine($"New Shape Clockwise: {polygon.IsClockwise()}"); + } + return true; } - + /// - /// Combines this polygon with multiple polygons, returning the union and the overlapping regions. + /// Cuts this polygon with all polygons in and writes both the removed overlap polygons and the remaining polygons into the provided result collections. /// - /// The polygons to combine with. - /// A tuple containing the new shapes and the overlapping regions. - public (Polygons newShapes, Polygons overlaps) CombineShape(Polygons others) + /// The polygons used to cut this polygon. + /// The destination collection that receives the intersecting cut-out polygons. + /// The destination collection that receives the remaining polygons after the cut. + /// true. + /// + /// This method always returns true after updating both output collections. The current polygon is not modified. + /// + public bool CutMany(Polygons cutShapes, Polygons cutOutsResult, Polygons newShapesResult) { - var overlaps = this.IntersectMany(others).ToPolygons(true); - var newShapes = this.UnionMany(others).ToPolygons(true); - return (newShapes, overlaps); + clipResultBuffer.Clear(); + ClipperImmediate2D.ClipEngine.ExecuteMany(this, cutShapes, ShapeClipperClipType.Intersection, clipResultBuffer); + clipResultBuffer.ToPolygons(cutOutsResult, true); + + clipResultBuffer.Clear(); + ClipperImmediate2D.ClipEngine.ExecuteMany(this, cutShapes, ShapeClipperClipType.Difference, clipResultBuffer); + clipResultBuffer.ToPolygons(newShapesResult, true); + return true; } - + /// - /// Cuts the current polygon with a simple generated shape at a position, returning the resulting shapes and the cut-out regions. + /// Generates a random polygonal cut shape around and uses it to cut this polygon. /// - /// The center position for the cut shape. - /// The minimum radius for the cut shape. - /// The maximum radius for the cut shape. - /// The number of points for the generated cut shape. Default is 16. - /// A tuple containing the new shapes and the cut-out regions. - public (Polygons newShapes, Polygons cutOuts)? CutShapeSimple(Vector2 cutPos, float minCutRadius, float maxCutRadius, int pointCount = 16) + /// The center position used to generate the temporary cut polygon. + /// The destination collection that receives the remaining polygons after the cut. + /// The destination collection that receives the intersecting cut-out polygons. + /// The minimum radius of the generated cut polygon. + /// The maximum radius of the generated cut polygon. + /// The number of vertices used to generate the temporary cut polygon. + /// true if the temporary cut polygon was generated and the cut operation completed; otherwise, false. + public bool CutSimple(Vector2 cutPos, Polygons newShapesResult, Polygons cutOutsResult, float minCutRadius, float maxCutRadius, int pointCount = 16) { - var cut = Generate(cutPos, pointCount, minCutRadius, maxCutRadius); - return cut == null ? null : CutShape(cut); + if (!Generate(cutPos, pointCount, minCutRadius, maxCutRadius, clipPolygonBuffer)) return false; + return Cut(clipPolygonBuffer, cutOutsResult, newShapesResult); } - + /// - /// Cuts the current polygon with a simple generated shape along a segment, returning the resulting shapes and the cut-out regions. + /// Generates a random polygonal cut shape along and uses it to cut this polygon. /// - /// The segment along which to generate the cut shape. - /// Minimum section length for the generated cut. Default is 0.025. - /// Maximum section length for the generated cut. Default is 0.1. - /// Minimum magnitude for the cut. Default is 0.05. - /// Maximum magnitude for the cut. Default is 0.25. - /// A tuple containing the new shapes and the cut-out regions. - public (Polygons newShapes, Polygons cutOuts)? CutShapeSimple(Segment cutLine, float minSectionLength = 0.025f, float maxSectionLength = 0.1f, - float minMagnitude = 0.05f, float maxMagnitude = 0.25f) + /// The guiding segment used to generate the temporary cut polygon. + /// The destination collection that receives the remaining polygons after the cut. + /// The destination collection that receives the intersecting cut-out polygons. + /// The minimum section length used when generating the cut polygon. + /// The maximum section length used when generating the cut polygon. + /// The minimum magnitude used for the cut polygon offsets. + /// The maximum magnitude used for the cut polygon offsets. + /// true if the temporary cut polygon was generated and the cut operation completed; otherwise, false. + public bool CutSimple(Segment cutLine, Polygons newShapesResult, Polygons cutOutsResult, float minSectionLength = 0.025f, float maxSectionLength = 0.1f, float minMagnitude = 0.05f, float maxMagnitude = 0.25f) { - var cut = Generate(cutLine, minMagnitude, maxMagnitude, minSectionLength, maxSectionLength); - if (cut == null) return null; - return CutShape(cut); + if(!Generate(cutLine, clipPolygonBuffer, minMagnitude, maxMagnitude, minSectionLength, maxSectionLength)) return false; + return Cut(clipPolygonBuffer, cutOutsResult, newShapesResult); } - + + #endregion + + #region Split /// - /// Splits the polygon using a line defined by a point and direction. + /// Splits this polygon along the infinite line defined by and , writing the resulting polygons into . /// /// A point on the splitting line. /// The direction of the splitting line. - /// The resulting polygons after the split, or null if no split occurred. - public Polygons? Split(Vector2 point, Vector2 direction) + /// The destination collection that receives the split polygons. + /// true if the split produced at least one resulting polygon; otherwise, false. + public bool Split(Vector2 point, Vector2 direction, Polygons result) { var line = new Line(point, direction); - return Split(line); + return Split(line, result); } - + /// - /// Splits the polygon using a line. + /// Splits this polygon along the specified line, writing the resulting polygons into . /// - /// The line to split with. - /// The resulting polygons after the split, or null if no split occurred. - public Polygons? Split(Line line) + /// The line used to split the polygon. + /// The destination collection that receives the split polygons. + /// true if the split produced at least one resulting polygon; otherwise, false. + /// + /// The line is converted to a sufficiently long segment based on the polygon center and diameter before clipping. + /// + public bool Split(Line line, Polygons result) { var w = Center - line.Point; var l = w.Length(); @@ -252,74 +404,96 @@ public bool CutShapeSelf(Polygon cutShape, bool keepCutout = false) else l *= 2f; var segment = line.ToSegment(l); - return Split(segment); + return Split(segment, result); } - + /// - /// Splits the polygon using a segment. + /// Splits this polygon using the specified segment as a cutting path, writing the resulting polygons into . /// - /// The segment to split with. - /// The resulting polygons after the split, or null if no split occurred. - public Polygons? Split(Segment segment) + /// The segment used to split the polygon. + /// The destination collection that receives the split polygons. + /// true if the split produced at least one resulting polygon; otherwise, false. + public bool Split(Segment segment, Polygons result) { - var result = this.Difference(segment); - if (result.Count <= 0) return null; - return result.ToPolygons(); + clipPolygonBuffer.Clear(); + clipPolygonBuffer.Add(segment.Start); + clipPolygonBuffer.Add(segment.End); + + ClipperImmediate2D.ClipEngine.Execute(this, clipPolygonBuffer, ShapeClipperClipType.Difference, clipResultBuffer); + if(clipResultBuffer.Count <= 0) return false; + + clipResultBuffer.ToPolygons(result, true); + return true; } - + /// - /// Splits the polygon using multiple segments. + /// Splits this polygon using multiple segments as cutting paths, writing the resulting polygons into . /// - /// The segments to split with. - /// The resulting polygons after the split, or null if no split occurred. - public Polygons? Split(Segments segments) + /// The segments used to split the polygon. + /// The destination collection that receives the split polygons. + /// true if at least one segment was provided and the split produced at least one resulting polygon; otherwise, false. + public bool Split(Segments segments, Polygons result) { - var result = this.DifferenceMany(segments); - if (result.Count <= 0) return null; - return result.ToPolygons(); + if(segments.Count <= 0) return false; + clipPooledBuffer.PrepareBuffer(segments.Count); + for (int i = 0; i < segments.Count; i++) + { + var segment = segments[i]; + var buffer = clipPooledBuffer.Buffer[i]; + buffer.Clear(); + buffer.Add(segment.Start.ToPoint64()); + buffer.Add(segment.End.ToPoint64()); + } + + ClipperImmediate2D.ClipEngine.ExecuteMany(this, clipPooledBuffer.Buffer, ShapeClipperClipType.Difference, clipResultBuffer); + if(clipResultBuffer.Count <= 0) return false; + + clipResultBuffer.ToPolygons(result, true); + return true; } - + /// - /// Generates a fracture line in the specified direction and splits the polygon with it. + /// Splits this polygon using a randomly perturbed fracture line derived from the specified direction. /// - /// The direction for the fracture. - /// Maximum offset for each point along the fracture line, - /// as a percentage of the segment length. Value Range 0-1. - /// Number of points to generate for the fracture line. - /// The resulting polygons after the split, or null if no split occurred. + /// The main fracture direction. + /// The maximum lateral offset of fracture points, expressed as a fraction of each fracture segment length. + /// The number of intermediate fracture points used to build the fracture line. + /// The destination collection that receives the split polygons. + /// true if a valid fracture line was generated and the split succeeded; otherwise, false. /// - /// The fracture line is generated with random offsets to simulate a jagged break. + /// The fracture line is generated from the intersection of this polygon with a line through the polygon center in the given direction. /// - public Polygons? FractureSplit(Vector2 dir, float maxOffsetPercentage, int fractureLineComplexity) + public bool FractureSplit(Vector2 dir, float maxOffsetPercentage, int fractureLineComplexity, Polygons result) { - if (fractureLineComplexity < 1 || dir.LengthSquared() <= 0f) return null; + if (fractureLineComplexity < 1 || dir.LengthSquared() <= 0f) return false; var center = Center; - var result = IntersectShape(new Line(center, dir)); - if (result == null || result.Count < 2) return null; + var intersectionPoints = IntersectShape(new Line(center, dir)); + if (intersectionPoints == null || intersectionPoints.Count < 2) return false; - var start = result[0].Point; - var end = result[1].Point; - if (result.Count > 2) + var start = intersectionPoints[0].Point; + var end = intersectionPoints[1].Point; + if (intersectionPoints.Count > 2) { - end = result.GetFurthestCollisionPoint(start).Point; + end = intersectionPoints.GetFurthestCollisionPoint(start).Point; } var fractureLine = GenerateFractureLine(start, end, maxOffsetPercentage, fractureLineComplexity); - if (fractureLine == null) return null; - return Split(fractureLine); + if (fractureLine == null) return false; + Split(fractureLine, result); + return true; } - + /// - /// Generates a fracture line from start to end with random offsets. + /// Generates a fractured polyline between and . /// - /// Start of the fracture line. - /// End of the fracture line. - /// Maximum offset for each point, as a percentage of the segment length. Value Range 0-1. - /// Number of points to generate along the line. - /// The generated fracture line as a set of segments, or null if invalid parameters. + /// The starting point of the fracture line. + /// The ending point of the fracture line. + /// The maximum lateral offset of each intermediate fracture point, expressed as a fraction of the base segment length. + /// The number of intermediate fracture points to generate. + /// A collection representing the generated fracture line, or null if the inputs are invalid. /// - /// The resulting fracture line can be used to split the polygon with a jagged effect. + /// When is negative, a single straight segment from to is returned. /// public Segments? GenerateFractureLine(Vector2 start, Vector2 end, float maxOffsetPercentage, int linePoints) { @@ -362,5 +536,65 @@ public bool CutShapeSelf(Polygon cutShape, bool keepCutout = false) return result; } + + /// + /// Generates a fractured polyline between and and writes the resulting segments into . + /// + /// The destination collection that receives the generated fracture line segments. + /// The starting point of the fracture line. + /// The ending point of the fracture line. + /// The maximum lateral offset of each intermediate fracture point, expressed as a fraction of the base segment length. + /// The number of intermediate fracture points to generate. + /// true if the fracture line was generated successfully; otherwise, false. + /// + /// When is negative, a single straight segment from to is returned. + /// + public bool GenerateFractureLine(Segments result, Vector2 start, Vector2 end, float maxOffsetPercentage, int linePoints) + { + if (linePoints < 1) return false; + + result.Clear(); + result.EnsureCapacity(linePoints + 1); + if (maxOffsetPercentage < 0f) + { + result.Add(new Segment(start, end)); + return true; + } + + var w = end - start; + var disSquared = w.LengthSquared(); + if (disSquared <= 0f) return false; + + var l = MathF.Sqrt(disSquared); + var segmentLength = l / (linePoints + 1); + var dir = w / l; + var p = dir.GetPerpendicularLeft(); + + var curStart = start; + var curLinePoint = start; + for (int i = 0; i < linePoints; i++) + { + var point = curStart + dir * segmentLength; + curStart = point; + + var offsetLength = Rng.Instance.RandF(0f, segmentLength * maxOffsetPercentage); + var offset = p * offsetLength; + if (Rng.Instance.Chance(0.5f)) + { + offset = -p * offsetLength; + } + + var nextLinePoint = point + offset; + + var segment = new Segment(curLinePoint, nextLinePoint); + curLinePoint = nextLinePoint; + result.Add(segment); + } + + result.Add(new Segment(curLinePoint, end)); + + return true; + } + #endregion } \ No newline at end of file diff --git a/ShapeEngine/Geometry/PolygonDef/PolygonConvexHull.cs b/ShapeEngine/Geometry/PolygonDef/PolygonConvexHull.cs deleted file mode 100644 index 4e7b6fd9..00000000 --- a/ShapeEngine/Geometry/PolygonDef/PolygonConvexHull.cs +++ /dev/null @@ -1,141 +0,0 @@ -using System.Numerics; -using ShapeEngine.Geometry.PointsDef; -using ShapeEngine.StaticLib; - -namespace ShapeEngine.Geometry.PolygonDef; - - -public partial class Polygon -{ - // Convex Hull Algorithms - // This class implements the Jarvis March (Gift Wrapping) algorithm to find the convex hull of a set of points. - // Reference: https://github.com/allfii/ConvexHull/tree/master - - // Alternative algorithms for convex hull computation: - // - Graham scan: https://en.wikipedia.org/wiki/Graham_scan - // - Chan's algorithm: https://en.wikipedia.org/wiki/Chan%27s_algorithm - - // Gift Wrapping algorithm resources: - // - Coding Train video: https://www.youtube.com/watch?v=YNyULRrydVI - // - Wikipedia: https://en.wikipedia.org/wiki/Gift_wrapping_algorithm - - private static int Turn_JarvisMarch(Vector2 p, Vector2 q, Vector2 r) - { - return ((q.X - p.X) * (r.Y - p.Y) - (r.X - p.X) * (q.Y - p.Y)).CompareTo(0); - // return ((q.getX() - p.getX()) * (r.getY() - p.getY()) - (r.getX() - p.getX()) * (q.getY() - p.getY())).CompareTo(0); - } - private static Vector2 NextHullPoint_JarvisMarch(List points, Vector2 p) - { - // const int TurnLeft = 1; - const int turnRight = -1; - const int turnNone = 0; - var q = p; - int t; - foreach (var r in points) - { - t = Turn_JarvisMarch(p, q, r); - if (t == turnRight || t == turnNone && p.DistanceSquared(r) > p.DistanceSquared(q)) // dist(p, r) > dist(p, q)) - q = r; - } - - return q; - } - - /// - /// Finds the convex hull of a set of points using the Jarvis March algorithm. - /// - /// The list of points to compute the convex hull for. - /// A representing the convex hull. - private static Polygon? ConvexHull_JarvisMarch(List points) - { - if (points.Count < 3) return null; // Polygon must have at least 3 points - var hull = new Polygon(); - foreach (var p in points) - { - if (hull.Count == 0) - hull.Add(p); - else - { - if (hull[0].X > p.X) - hull[0] = p; - else if (ShapeMath.EqualsF(hull[0].X, p.X)) - if (hull[0].Y > p.Y) - hull[0] = p; - } - } - - var counter = 0; - while (counter < hull.Count) - { - var q = NextHullPoint_JarvisMarch(points, hull[counter]); - if (q != hull[0]) - { - hull.Add(q); - } - - counter++; - } - - // return new Polygon(hull); - return hull; - } - /// - /// Finds the convex hull of a list of points. - /// - /// The list of points. - /// A representing the convex hull. - public static Polygon? FindConvexHull(List points) - { - if (points.Count < 3) return null; // Polygon must have at least 3 points - return ConvexHull_JarvisMarch(points); - } - /// - /// Finds the convex hull of a set of points. - /// - /// The points as a collection. - /// A representing the convex hull. - public static Polygon? FindConvexHull(Points points) - { - if (points.Count < 3) return null; // Polygon must have at least 3 points - return ConvexHull_JarvisMarch(points); - } - /// - /// Finds the convex hull of a set of points. - /// - /// The points as an array of . - /// A representing the convex hull. - public static Polygon? FindConvexHull(params Vector2[] points) - { - if (points.Length < 3) return null; // Polygon must have at least 3 points - return ConvexHull_JarvisMarch(points.ToList()); - } - /// - /// Finds the convex hull of a polygon's points. - /// - /// The polygon whose points are used. - /// A representing the convex hull. - public static Polygon? FindConvexHull(Polygon points) - { - if (points.Count < 3) return null; // Polygon must have at least 3 points - return ConvexHull_JarvisMarch(points); - } - - /// - /// Finds the convex hull of multiple polygons by combining all their points. - /// - /// The polygons to combine. - /// A representing the convex hull of all points. - public static Polygon? FindConvexHull(params Polygon[] shapes) - { - List? allPoints = null; - foreach (var shape in shapes) - { - if(shape.Count < 3) continue; // Skip polygons with less than 3 points - - allPoints ??= []; - allPoints.AddRange(shape); - } - - return allPoints == null ? null : ConvexHull_JarvisMarch(allPoints); - } -} \ No newline at end of file diff --git a/ShapeEngine/Geometry/PolygonDef/PolygonDrawing.cs b/ShapeEngine/Geometry/PolygonDef/PolygonDrawing.cs index eb580063..2d76445e 100644 --- a/ShapeEngine/Geometry/PolygonDef/PolygonDrawing.cs +++ b/ShapeEngine/Geometry/PolygonDef/PolygonDrawing.cs @@ -1,4 +1,5 @@ using System.Numerics; +using System.Runtime.Intrinsics.X86; using Raylib_cs; using ShapeEngine.Color; using ShapeEngine.Core.Structs; @@ -7,7 +8,9 @@ using ShapeEngine.Geometry.RectDef; using ShapeEngine.Geometry.SegmentDef; using ShapeEngine.Geometry.TriangleDef; +using ShapeEngine.Geometry.TriangulationDef; using ShapeEngine.StaticLib; +using Ray = ShapeEngine.Geometry.RayDef.Ray; namespace ShapeEngine.Geometry.PolygonDef; @@ -21,7 +24,14 @@ namespace ShapeEngine.Geometry.PolygonDef; /// public static class PolygonDrawing { + #region Helper Members + + private static Triangulation drawHelperTriangulation = []; + + #endregion + #region Draw Masked + /// /// Draws the polygon's edges while applying a triangular mask to each segment. /// @@ -32,7 +42,7 @@ public static class PolygonDrawing public static void DrawLinesMasked(this Polygon poly, Triangle mask, LineDrawingInfo lineInfo, bool reversedMask = false) { if (poly.Count < 3) return; - + for (var i = 0; i < poly.Count; i++) { var start = poly[i]; @@ -41,6 +51,7 @@ public static void DrawLinesMasked(this Polygon poly, Triangle mask, LineDrawing segment.DrawMasked(mask, lineInfo, reversedMask); } } + /// /// Draws the polygon's edges while applying a circular mask to each segment. /// @@ -51,7 +62,7 @@ public static void DrawLinesMasked(this Polygon poly, Triangle mask, LineDrawing public static void DrawLinesMasked(this Polygon poly, Circle mask, LineDrawingInfo lineInfo, bool reversedMask = false) { if (poly.Count < 3) return; - + for (var i = 0; i < poly.Count; i++) { var start = poly[i]; @@ -60,6 +71,7 @@ public static void DrawLinesMasked(this Polygon poly, Circle mask, LineDrawingIn segment.DrawMasked(mask, lineInfo, reversedMask); } } + /// /// Draws the polygon's edges while applying a rectangular mask to each segment. /// @@ -70,7 +82,7 @@ public static void DrawLinesMasked(this Polygon poly, Circle mask, LineDrawingIn public static void DrawLinesMasked(this Polygon poly, Rect mask, LineDrawingInfo lineInfo, bool reversedMask = false) { if (poly.Count < 3) return; - + for (var i = 0; i < poly.Count; i++) { var start = poly[i]; @@ -79,6 +91,7 @@ public static void DrawLinesMasked(this Polygon poly, Rect mask, LineDrawingInfo segment.DrawMasked(mask, lineInfo, reversedMask); } } + /// /// Draws the polygon's edges while applying a quadrilateral mask to each segment. /// @@ -89,7 +102,7 @@ public static void DrawLinesMasked(this Polygon poly, Rect mask, LineDrawingInfo public static void DrawLinesMasked(this Polygon poly, Quad mask, LineDrawingInfo lineInfo, bool reversedMask = false) { if (poly.Count < 3) return; - + for (var i = 0; i < poly.Count; i++) { var start = poly[i]; @@ -98,6 +111,7 @@ public static void DrawLinesMasked(this Polygon poly, Quad mask, LineDrawingInfo segment.DrawMasked(mask, lineInfo, reversedMask); } } + /// /// Draws the polygon's edges while applying a polygonal mask to each segment. /// @@ -108,7 +122,7 @@ public static void DrawLinesMasked(this Polygon poly, Quad mask, LineDrawingInfo public static void DrawLinesMasked(this Polygon poly, Polygon mask, LineDrawingInfo lineInfo, bool reversedMask = false) { if (poly.Count < 3) return; - + for (var i = 0; i < poly.Count; i++) { var start = poly[i]; @@ -117,6 +131,7 @@ public static void DrawLinesMasked(this Polygon poly, Polygon mask, LineDrawingI segment.DrawMasked(mask, lineInfo, reversedMask); } } + /// /// Draws the polygon's edges while applying a generic closed-shape mask to each segment. /// @@ -128,7 +143,7 @@ public static void DrawLinesMasked(this Polygon poly, Polygon mask, LineDrawingI public static void DrawLinesMasked(this Polygon poly, T mask, LineDrawingInfo lineInfo, bool reversedMask = false) where T : IClosedShapeTypeProvider { if (poly.Count < 3) return; - + for (var i = 0; i < poly.Count; i++) { var start = poly[i]; @@ -137,11 +152,14 @@ public static void DrawLinesMasked(this Polygon poly, T mask, LineDrawingInfo segment.DrawMasked(mask, lineInfo, reversedMask); } } + #endregion - - + + #region Draw Convex + /// - /// Draws a convex polygon filled with the specified color, using the polygon's centroid as the center. + /// Draws a convex non-intersecting polygon (pentagon, hexagon, etc.) filled with the specified color, using the polygon's centroid as the center. + /// This function should be used when Polygon is known to be convex. /// /// The polygon to draw. /// The fill color. @@ -153,7 +171,8 @@ public static void DrawPolygonConvex(this Polygon poly, ColorRgba color, bool cl } /// - /// Draws a convex polygon filled with the specified color, using a custom center point. + /// Draws a convex non-intersecting polygon (pentagon, hexagon, etc.) filled with the specified color, using the polygon's centroid as the center. + /// This function should be used when Polygon is known to be convex. /// /// The polygon to draw. /// The center point for triangulation. @@ -164,87 +183,29 @@ public static void DrawPolygonConvex(this Polygon poly, Vector2 center, ColorRgb if (poly.Count < 3) return; // Polygon must have at least 3 points if (clockwise) { - for (var i = 0; i < poly.Count - 1; i++) - { - Raylib.DrawTriangle(poly[i], center, poly[i + 1], color.ToRayColor()); - } + for (var i = 0; i < poly.Count - 1; i++) Raylib.DrawTriangle(poly[i], center, poly[i + 1], color.ToRayColor()); Raylib.DrawTriangle(poly[^1], center, poly[0], color.ToRayColor()); } else { - for (var i = 0; i < poly.Count - 1; i++) - { - Raylib.DrawTriangle(poly[i], poly[i + 1], center, color.ToRayColor()); - } + for (var i = 0; i < poly.Count - 1; i++) Raylib.DrawTriangle(poly[i], poly[i + 1], center, color.ToRayColor()); Raylib.DrawTriangle(poly[^1], poly[0], center, color.ToRayColor()); } } - /// - /// Draws a convex polygon filled with the specified color, applying position, size, and rotation. - /// - /// The polygon to draw, with points relative to the origin. - /// The position of the polygon's center. - /// The scale factor for the polygon. - /// The rotation in degrees. - /// The fill color. - /// If true, draws triangles in clockwise order; otherwise, counter-clockwise. - public static void DrawPolygonConvex(this Polygon relativePoly, Vector2 pos, float size, float rotDeg, ColorRgba color, bool clockwise = false) - { - if (relativePoly.Count < 3) return; // Polygon must have at least 3 points - if (clockwise) - { - for (int i = 0; i < relativePoly.Count - 1; i++) - { - var a = pos + (relativePoly[i] * size).Rotate(rotDeg * ShapeMath.DEGTORAD); - var b = pos; - var c = pos + (relativePoly[i + 1] * size).Rotate(rotDeg * ShapeMath.DEGTORAD); - Raylib.DrawTriangle(a, b, c, color.ToRayColor()); - } - - var aFinal = pos + (relativePoly[^1] * size).Rotate(rotDeg * ShapeMath.DEGTORAD); - var bFinal = pos; - var cFinal = pos + (relativePoly[0] * size).Rotate(rotDeg * ShapeMath.DEGTORAD); - Raylib.DrawTriangle(aFinal, bFinal, cFinal, color.ToRayColor()); - } - else - { - for (int i = 0; i < relativePoly.Count - 1; i++) - { - var a = pos + (relativePoly[i] * size).Rotate(rotDeg * ShapeMath.DEGTORAD); - var b = pos + (relativePoly[i + 1] * size).Rotate(rotDeg * ShapeMath.DEGTORAD); - var c = pos; - Raylib.DrawTriangle(a, b, c, color.ToRayColor()); - } - - var aFinal = pos + (relativePoly[^1] * size).Rotate(rotDeg * ShapeMath.DEGTORAD); - var bFinal = pos + (relativePoly[0] * size).Rotate(rotDeg * ShapeMath.DEGTORAD); - var cFinal = pos; - Raylib.DrawTriangle(aFinal, bFinal, cFinal, color.ToRayColor()); - } - } + #endregion - /// - /// Draws a convex polygon filled with the specified color, using a . - /// - /// The polygon to draw, with points relative to the origin. - /// The transform to apply (position, scale, rotation). - /// The fill color. - /// If true, draws triangles in clockwise order; otherwise, counter-clockwise. - public static void DrawPolygonConvex(this Polygon relativePoly, Transform2D transform, ColorRgba color, bool clockwise = false) - { - DrawPolygonConvex(relativePoly, transform.Position, transform.ScaledSize.Length, transform.RotationDeg, color, clockwise); - } + #region Draw /// - /// Draws the polygon filled with the specified color. Uses triangulation for polygons with more than 3 vertices. + /// Draws the polygon filled with the provided color. /// - /// The polygon to draw. - /// The fill color. + /// The polygon to draw. Polygons with fewer than 3 points are ignored; triangles are drawn directly. + /// Fill color used when rendering the polygon. /// - /// For polygons with exactly 3 vertices, an optimized triangle drawing method is used. - /// For polygons with more than 3 vertices, the polygon is triangulated and each triangle is drawn. - /// For best performance with static polygons, precompute the triangulation and draw the result directly. + /// Caution:This method will triangulate the polygon each call when the polygon contains more than 3 points, + /// which can be performance-intensive for complex polygons. + /// Precompute triangulation for best performance and then transform/draw the triangulation as needed. /// public static void Draw(this Polygon poly, ColorRgba color) { @@ -254,391 +215,1518 @@ public static void Draw(this Polygon poly, ColorRgba color) TriangleDrawing.DrawTriangle(poly[0], poly[1], poly[2], color); return; } - poly.Triangulate().Draw(color); + + drawHelperTriangulation.Clear(); + poly.Triangulate(drawHelperTriangulation); + drawHelperTriangulation.Draw(color); } + #endregion + + //TODO: Rework all below with new ClipperImmediate2d system! + + #region Draw Lines Perimeter & Percentage + /// - /// Debug method: draws the polygon's outline with a color gradient from start to end, and highlights the first and last vertices. + /// Draws a portion of the polygon's perimeter as a thick line, starting at a given index. + /// Handles miter and beveled joins, and allows for custom miter limits. /// - /// The polygon to draw. + /// The polygon whose perimeter will be drawn. + /// + /// The length of the perimeter to draw. If negative, draws in reverse order. + /// + /// + /// The index of the starting vertex. Drawing proceeds forward or backward depending on the sign of . + /// /// The thickness of the outline. - /// The color at the start vertex. - /// The color at the end vertex. - /// - /// Useful for debugging polygon winding and vertex order. - /// - public static void DEBUG_DrawLinesCCW(this Polygon poly, float lineThickness, ColorRgba startColorRgba, ColorRgba endColorRgba) + /// The color of the outline, including alpha for transparency. + /// + /// The miter limit for joins. If the miter length exceeds this value times the line thickness, a bevel or squared join is used instead. Default is 2.0f. + /// + /// + /// If true, forces beveled joins instead of squared joins when the miter limit is exceeded. + /// + public static void DrawLinesPerimeter(this Polygon poly, float perimeterToDraw, int startIndex, float lineThickness, ColorRgba color, float miterLimit = 2f, bool beveled = false) { - if (poly.Count < 3) return; + if (poly.Count < 3 || perimeterToDraw == 0) return; + + var rayColor = color.ToRayColor(); + float totalMiterLengthLimit = lineThickness * 0.5f * MathF.Max(2f, miterLimit); + float f = -1f; + + bool reverse = perimeterToDraw < 0; + if (reverse) perimeterToDraw *= -1; + + int i = ShapeMath.Clamp(startIndex, 0, poly.Count - 1); + int steps = poly.Count; + + var lastCornerType = 1; + var prev = poly[reverse ? ShapeMath.WrapIndex(poly.Count, i + 1) : ShapeMath.WrapIndex(poly.Count, i - 1)]; + var cur = poly[i]; + var next = poly[reverse ? ShapeMath.WrapIndex(poly.Count, i - 1) : ShapeMath.WrapIndex(poly.Count, i + 1)]; + var wPrev = cur - prev; + var wNext = next - cur; + float lsPrev = wPrev.LengthSquared(); + float lsNext = wNext.LengthSquared(); + if (lsPrev <= 0 || lsNext <= 0) return; - DrawLines(poly, lineThickness, startColorRgba, endColorRgba); - CircleDrawing.DrawCircle(poly[0], lineThickness * 2f, startColorRgba); - CircleDrawing.DrawCircle(poly[^1], lineThickness * 2f, endColorRgba); - } + var dirPrev = wPrev.Normalize(); + var dirNext = wNext.Normalize(); + + //flip based on corner type + var cornerType = dirPrev.ClassifyCorner(dirNext); + if (cornerType.type == 0)//collinear + { + //we dont treat collinear differently so we set it to 1 (ccw outwards corner) + cornerType = (1, cornerType.angle); + } + Vector2 normalPrev, normalNext; + if (cornerType.type >= 0) + { + normalPrev = dirPrev.GetPerpendicularRight(); + normalNext = dirNext.GetPerpendicularRight(); + } + else + { + normalPrev = dirPrev.GetPerpendicularLeft(); + normalNext = dirNext.GetPerpendicularLeft(); + } + + var miterDir = (normalPrev + normalNext).Normalize(); + float miterAngleRad = MathF.Abs(miterDir.AngleRad(normalNext)); + float miterLength = lineThickness / MathF.Cos(miterAngleRad); - /// - /// Draws the polygon's outline with a color gradient from start to end. - /// - /// The polygon to draw. - /// The thickness of the outline. - /// The color at the start vertex. - /// The color at the end vertex. - /// The type of line cap to use. - /// The number of points for the cap. - public static void DrawLines(this Polygon poly, float lineThickness, ColorRgba startColorRgba, ColorRgba endColorRgba, LineCapType capType = LineCapType.CappedExtended, int capPoints = 2) - { - if (poly.Count < 3) return; + Vector2 lastInner, lastOuter; + if (miterLimit < 2f || miterLength < totalMiterLengthLimit) + { + lastOuter = cur + miterDir * miterLength; + lastInner = cur - miterDir * miterLength; + } + else + { + miterLength = totalMiterLengthLimit; - int redStep = (endColorRgba.R - startColorRgba.R) / poly.Count; - int greenStep = (endColorRgba.G - startColorRgba.G) / poly.Count; - int blueStep = (endColorRgba.B - startColorRgba.B) / poly.Count; - int alphaStep = (endColorRgba.A - startColorRgba.A) / poly.Count; - for (var i = 0; i < poly.Count; i++) + var prevOuter = prev + normalPrev * lineThickness; + var prevInner = prev - normalPrev * lineThickness; + var nextInner = next - normalNext * lineThickness; + var intersection = Ray.IntersectRayRay(prevInner, dirPrev, nextInner, -dirNext); + lastInner = intersection.Valid ? intersection.Point : cur - miterDir * miterLength; + + if (beveled) + { + // lastOuter = cur + normalPrev * lineThickness; + lastOuter = cur + normalNext * lineThickness; + } + else + { + var cornerOuter = cur + miterDir * miterLength; + var miterPerpRight = cornerType.type >= 0 ? miterDir.GetPerpendicularRight() : miterDir.GetPerpendicularLeft(); + intersection = Ray.IntersectRayRay(prevOuter, dirPrev, cornerOuter, miterPerpRight); + if (intersection.Valid) + { + // lastOuter = intersection.Point; + float l = (cornerOuter - intersection.Point).Length(); + lastOuter = cornerOuter - miterPerpRight * l; + } + else //bevel fallback + { + // lastOuter = cur + normalPrev * lineThickness; + lastOuter = cur + normalNext * lineThickness; + } + } + } + + if (cornerType.type < 0) { - var start = poly[i]; - var end = poly[(i + 1) % poly.Count]; - ColorRgba finalColorRgba = new - ( - startColorRgba.R + redStep * i, - startColorRgba.G + greenStep * i, - startColorRgba.B + blueStep * i, - startColorRgba.A + alphaStep * i - ); - SegmentDrawing.DrawSegment(start, end, lineThickness, finalColorRgba, capType, capPoints); + (lastInner, lastOuter) = (lastOuter, lastInner); } - } - /// - /// Draws the polygon's outline with a uniform color. - /// - /// The polygon to draw. - /// The thickness of the outline. - /// The color of the outline. - /// The type of line cap to use. - /// The number of points for the cap. - public static void DrawLines(this Polygon poly, float lineThickness, ColorRgba color, LineCapType capType = LineCapType.CappedExtended, int capPoints = 2) - { - if (poly.Count < 3) return; + // var dir = (nextPoint - curPoint).Normalize(); + // + // var perp = dir.GetPerpendicularRight(); + // var lastOuter = curPoint + perp * lineThickness; + // var lastInner = curPoint - perp * lineThickness; - for (var i = 0; i < poly.Count; i++) + i = reverse ? i - 1 : i + 1; + + while(steps > 0 && f < 0f) { - var start = poly[i]; - var end = poly[(i + 1) % poly.Count]; - SegmentDrawing.DrawSegment(start, end, lineThickness, color, capType, capPoints); + prev = poly[reverse ? ShapeMath.WrapIndex(poly.Count, i + 1) : ShapeMath.WrapIndex(poly.Count, i - 1)]; + cur = poly[ShapeMath.WrapIndex(poly.Count, i)]; + next = poly[reverse ? ShapeMath.WrapIndex(poly.Count, i - 1) : ShapeMath.WrapIndex(poly.Count, i + 1)]; + + i = reverse ? i - 1 : i + 1; + steps--; + + float length = (next - cur).Length(); + if (length <= perimeterToDraw) + { + perimeterToDraw -= length; + } + else + { + f = ShapeMath.Clamp(perimeterToDraw / length, 0f, 1f); + } + + wPrev = cur - prev; + wNext = next - cur; + lsPrev = wPrev.LengthSquared(); + lsNext = wNext.LengthSquared(); + if (lsPrev <= 0 || lsNext <= 0) return; + + dirPrev = wPrev.Normalize(); + dirNext = wNext.Normalize(); + + //flip based on corner type + cornerType = dirPrev.ClassifyCorner(dirNext); + if (cornerType.type == 0)//collinear + { + //we dont treat collinear differently so we set it to 1 (ccw outwards corner) + cornerType = (1, cornerType.angle); + } + + if (cornerType.type >= 0) + { + normalPrev = dirPrev.GetPerpendicularRight(); + normalNext = dirNext.GetPerpendicularRight(); + } + else + { + normalPrev = dirPrev.GetPerpendicularLeft(); + normalNext = dirNext.GetPerpendicularLeft(); + } + + miterDir = (normalPrev + normalNext).Normalize(); + miterAngleRad = MathF.Abs(miterDir.AngleRad(normalNext)); + miterLength = lineThickness / MathF.Cos(miterAngleRad); + + if (miterLimit < 2f || miterLength < totalMiterLengthLimit) + { + var cornerOuter = cur + miterDir * miterLength; + var cornerInner = cur - miterDir * miterLength; + + if (f >= 0f) + { + if (cornerType.type == lastCornerType) + { + cornerOuter = lastOuter.Lerp(cornerOuter, f); + cornerInner = lastInner.Lerp(cornerInner, f); + } + else + { + cornerOuter = lastInner.Lerp(cornerOuter, f); + cornerInner = lastOuter.Lerp(cornerInner, f); + } + } + + if (cornerType.type >= 0) + { + if (lastCornerType >= 0) + { + Raylib.DrawTriangle(cornerInner, lastInner, lastOuter, rayColor); + } + else + { + Raylib.DrawTriangle(cornerOuter, lastOuter, lastInner, rayColor); + } + + Raylib.DrawTriangle(cornerInner, lastOuter, cornerOuter, rayColor); + } + else + { + if (lastCornerType >= 0) + { + Raylib.DrawTriangle(cornerOuter, lastInner, lastOuter, rayColor); + } + else + { + Raylib.DrawTriangle(lastInner, cornerInner, lastOuter, rayColor); + } + + Raylib.DrawTriangle(lastOuter, cornerInner, cornerOuter, rayColor); + } + + lastInner = cornerInner; + lastOuter = cornerOuter; + lastCornerType = cornerType.type; + } + else + { + miterLength = totalMiterLengthLimit; + Vector2 cornerOuterPrev, cornerOuterNext; + + var prevOuter = prev + normalPrev * lineThickness; + var prevInner = prev - normalPrev * lineThickness; + var nextInner = next - normalNext * lineThickness; + var intersection = Ray.IntersectRayRay(prevInner, dirPrev, nextInner, -dirNext); + var cornerInner = intersection.Valid ? intersection.Point : cur - miterDir * miterLength; + + if (beveled) + { + cornerOuterPrev = cur + normalPrev * lineThickness; + cornerOuterNext = cur + normalNext * lineThickness; + } + else + { + var cornerOuter = cur + miterDir * miterLength; + var miterPerpRight = cornerType.type >= 0 ? miterDir.GetPerpendicularRight() : miterDir.GetPerpendicularLeft(); + intersection = Ray.IntersectRayRay(prevOuter, dirPrev, cornerOuter, miterPerpRight); + if (intersection.Valid) + { + cornerOuterPrev = intersection.Point; + float l = (cornerOuter - intersection.Point).Length(); + cornerOuterNext = cornerOuter - miterPerpRight * l; + } + else //bevel fallback + { + cornerOuterPrev = cur + normalPrev * lineThickness; + cornerOuterNext = cur + normalNext * lineThickness; + } + } + + bool drawCorner = true; + if (f >= 0f) + { + // Calculate segment lengths + float lenA = (cornerOuterPrev - lastOuter).Length(); + float lenB = (cornerOuterNext - cornerOuterPrev).Length(); + float totalLen = lenA + lenB; + + // Calculate normalized thresholds + float tA = lenA / totalLen; // The fraction of the stroke spent on the first segment + + if (cornerType.type == lastCornerType) + { + if (f <= tA) + { + // Lerp from lastOuter to cornerOuterPrev + float localF = f / tA; + cornerOuterPrev = Vector2.Lerp(lastOuter, cornerOuterPrev, localF); + cornerInner = lastInner.Lerp(cornerInner, localF); + drawCorner = false; + } + else + { + // Lerp from cornerOuterPrev to cornerOuterNext + float localF = (f - tA) / (1f - tA); + cornerOuterNext = Vector2.Lerp(cornerOuterPrev, cornerOuterNext, localF); + } + } + else + { + if (f <= tA) + { + // Lerp from lastOuter to cornerOuterPrev + float localF = f / tA; + cornerOuterPrev = Vector2.Lerp(lastInner, cornerOuterPrev, localF); + cornerInner = lastOuter.Lerp(cornerInner, localF); + drawCorner = false; + } + else + { + // Lerp from cornerOuterPrev to cornerOuterNext + float localF = (f - tA) / (1f - tA); + cornerOuterNext = Vector2.Lerp(cornerOuterPrev, cornerOuterNext, localF); + } + } + } + + if (cornerType.type >= 0) + { + if (lastCornerType >= 0) + { + Raylib.DrawTriangle(cornerInner, lastInner, lastOuter, rayColor); + Raylib.DrawTriangle(cornerInner, lastOuter, cornerOuterPrev, rayColor); + } + else + { + Raylib.DrawTriangle(cornerInner, lastOuter, lastInner, rayColor); + Raylib.DrawTriangle(cornerInner, lastInner, cornerOuterPrev, rayColor); + } + + if(drawCorner) Raylib.DrawTriangle(cornerOuterPrev, cornerOuterNext, cornerInner, rayColor); + } + else + { + if (lastCornerType >= 0) + { + Raylib.DrawTriangle(cornerInner, lastInner, lastOuter, rayColor); + Raylib.DrawTriangle(cornerInner, cornerOuterPrev, lastInner, rayColor); + } + else + { + Raylib.DrawTriangle(cornerInner, lastOuter, lastInner, rayColor); + Raylib.DrawTriangle(cornerInner, cornerOuterPrev, lastOuter, rayColor); + } + + if(drawCorner) Raylib.DrawTriangle(cornerOuterNext, cornerOuterPrev, cornerInner, rayColor); + } + + lastInner = cornerInner; + lastOuter = cornerOuterNext; + lastCornerType = cornerType.type; + } } } - + /// - /// Draws the polygon's outline with a uniform color, scaling each side by a factor. + /// Draws a portion of the polygon's perimeter as a thick line, starting at a given index, using for line options. + /// Handles miter and beveled joins, and allows for custom miter limits. /// - /// The polygon to draw. + /// The polygon whose perimeter will be drawn. + /// + /// The length of the perimeter to draw. If negative, draws in reverse order. + /// + /// + /// The index of the starting vertex. Drawing proceeds forward or backward depending on the sign of . + /// + /// The line drawing information (thickness & color only). + /// + /// The miter limit for joins. If the miter length exceeds this value times the line thickness, a bevel or squared join is used instead. Default is 2.0f. + /// + /// + /// If true, forces beveled joins instead of squared joins when the miter limit is exceeded. + /// + public static void DrawLinesPerimeter(this Polygon poly, float perimeterToDraw, int startIndex, LineDrawingInfo lineInfo, float miterLimit = 2f, bool beveled = false) + { + poly.DrawLinesPerimeter(perimeterToDraw, startIndex, lineInfo.Thickness, lineInfo.Color, miterLimit, beveled); + } + + /// + /// Draws a portion of the polygon's perimeter as a thick line, starting at a given index, based on a percentage of the total perimeter. + /// + /// The polygon whose perimeter will be drawn. + /// + /// The percentage (0-1) of the perimeter to draw. If negative, draws in reverse order. + /// + /// + /// The index of the starting vertex. Drawing proceeds forward or backward depending on the sign of . + /// /// The thickness of the outline. - /// The color of the outline. - /// The factor by which to scale each side's length. - /// The type of line cap to use. - /// The number of points for the cap. - public static void DrawLines(this Polygon poly, float lineThickness, ColorRgba color, float sideLengthFactor, LineCapType capType = LineCapType.CappedExtended, int capPoints = 2) + /// The color of the outline, including alpha for transparency. + /// + /// The miter limit for joins. If the miter length exceeds this value times the line thickness, a bevel or squared join is used instead. Default is 2.0f. + /// + /// + /// If true, forces beveled joins instead of squared joins when the miter limit is exceeded. + /// + public static void DrawLinesPercentage(this Polygon poly, float f, int startIndex, float lineThickness, ColorRgba color, float miterLimit = 2f, bool beveled = false) { - if (poly.Count < 3) return; + if (poly.Count < 3 || f == 0f) return; + + var negative = false; + if (f < 0) + { + negative = true; + f *= -1; + } + + f = ShapeMath.WrapF(f, 0f, 1f); + startIndex = ShapeMath.WrapIndex(startIndex, poly.Count); + if (f <= 0f) return; + if (f >= 1f) + { + poly.DrawLines(lineThickness, color, miterLimit, beveled); + return; + } + var perimeter = 0f; for (var i = 0; i < poly.Count; i++) { var start = poly[i]; var end = poly[(i + 1) % poly.Count]; - SegmentDrawing.DrawSegment(start, end, lineThickness, color, sideLengthFactor, capType, capPoints); + float l = (end - start).Length(); + perimeter += l; } - } + poly.DrawLinesPerimeter(perimeter * f * (negative ? -1 : 1), startIndex, lineThickness, color, miterLimit, beveled); + } + /// - /// Draws the polygon's outline with a uniform color, applying position, size, and rotation. + /// Draws a portion of the polygon's perimeter as a thick line, starting at a given index, based on a percentage of the total perimeter, + /// using for line options. /// - /// The polygon to draw, with points relative to the origin. - /// The position of the polygon's center. - /// The scale factor for the polygon. - /// The rotation in degrees. - /// The thickness of the outline. - /// The color of the outline. - /// The type of line cap to use. - /// The number of points for the cap. - public static void DrawLines(this Polygon relative, Vector2 pos, float size, float rotDeg, float lineThickness, ColorRgba color, LineCapType capType = LineCapType.CappedExtended, int capPoints = 2) + /// The polygon whose perimeter will be drawn. + /// + /// The percentage (0-1) of the perimeter to draw. If negative, draws in reverse order. + /// + /// + /// The index of the starting vertex. Drawing proceeds forward or backward depending on the sign of . + /// + /// The line drawing information (thickness & color only). + /// + /// The miter limit for joins. If the miter length exceeds this value times the line thickness, a bevel or squared join is used instead. Default is 2.0f. + /// + /// + /// If true, forces beveled joins instead of squared joins when the miter limit is exceeded. + /// + public static void DrawLinesPercentage(this Polygon poly, float f, int startIndex, LineDrawingInfo lineInfo, float miterLimit = 2f, bool beveled = false) { - if (relative.Count < 3) return; - - for (var i = 0; i < relative.Count; i++) - { - var start = pos + (relative[i] * size).Rotate(rotDeg * ShapeMath.DEGTORAD); - var end = pos + (relative[(i + 1) % relative.Count] * size).Rotate(rotDeg * ShapeMath.DEGTORAD); - SegmentDrawing.DrawSegment(start, end, lineThickness, color, capType, capPoints); - } + poly.DrawLinesPercentage(f, startIndex, lineInfo.Thickness, lineInfo.Color, miterLimit, beveled); } + #endregion + + #region Draw Lines Perimeter & Percentage Capped /// - /// Draws the polygon's outline with a uniform color, using a . + /// Draws a portion of the polygon's perimeter as a thick line with capped ends, starting at a given index. + /// Supports custom line thickness, color, cap type, and number of cap points. + /// This function is slower than + /// due to drawing more triangles for the caps. /// - /// The polygon to draw, with points relative to the origin. - /// The transform to apply (position, scale, rotation). + /// The polygon whose perimeter will be drawn. + /// + /// The length of the perimeter to draw. If negative, draws in reverse order. + /// + /// + /// The index of the starting vertex. Drawing proceeds forward or backward depending on the sign of . + /// /// The thickness of the outline. - /// The color of the outline. - /// The type of line cap to use. - /// The number of points for the cap. - public static void DrawLines(this Polygon relative, Transform2D transform, float lineThickness, ColorRgba color, LineCapType capType = LineCapType.CappedExtended, int capPoints = 2) + /// The color of the outline. Does not support transparency, alpha will be set to 255 internally. + /// The type of line cap to use at the ends of the drawn segment. + /// The number of points for the cap (used for round caps). + public static void DrawLinesPerimeterCapped(this Polygon poly, float perimeterToDraw, int startIndex, float lineThickness, ColorRgba color, LineCapType capType = LineCapType.CappedExtended, int capPoints = 2) { - DrawLines(relative, transform.Position, transform.ScaledSize.Length, transform.RotationDeg, lineThickness, color, capType, capPoints); - } + if (poly.Count < 3 || perimeterToDraw == 0) return; - /// - /// Draws the polygon's outline using the provided . - /// - /// The polygon to draw. - /// The line drawing information (thickness, color, cap type, etc.). - public static void DrawLines(this Polygon poly, LineDrawingInfo lineInfo) => DrawLines(poly, lineInfo.Thickness, lineInfo.Color, lineInfo.CapType, lineInfo.CapPoints); + color = color.SetAlpha(255); - /// - /// Draws the polygon's outline using the provided , applying position, size, and rotation. - /// - /// The polygon to draw, with points relative to the origin. - /// The position of the polygon's center. - /// The scale factor for the polygon. - /// The rotation in degrees. - /// The line drawing information (thickness, color, cap type, etc.). - public static void DrawLines(this Polygon relative, Vector2 pos, float size, float rotDeg, LineDrawingInfo lineInfo) => DrawLines(relative, pos, size, rotDeg, lineInfo.Thickness, lineInfo.Color, lineInfo.CapType, lineInfo.CapPoints); + int currentIndex = ShapeMath.Clamp(startIndex, 0, poly.Count - 1); - /// - /// Draws the polygon's outline using the provided and a . - /// - /// The polygon to draw, with points relative to the origin. - /// The transform to apply (position, scale, rotation). - /// The line drawing information (thickness, color, cap type, etc.). - public static void DrawLines(this Polygon relative, Transform2D transform, LineDrawingInfo lineInfo) => DrawLines(relative, transform.Position, transform.ScaledSize.Length, transform.RotationDeg, lineInfo.Thickness, lineInfo.Color, lineInfo.CapType, lineInfo.CapPoints); + bool reverse = perimeterToDraw < 0; + if (reverse) perimeterToDraw *= -1; + for (var i = 0; i < poly.Count; i++) + { + var start = poly[currentIndex]; + if (reverse) currentIndex = ShapeMath.WrapIndex(poly.Count, currentIndex - 1); + else currentIndex = (currentIndex + 1) % poly.Count; + var end = poly[currentIndex]; + float l = (end - start).Length(); + if (l <= perimeterToDraw) + { + perimeterToDraw -= l; + SegmentDrawing.DrawSegment(start, end, lineThickness, color, capType, capPoints); + } + else + { + float f = perimeterToDraw / l; + end = start.Lerp(end, f); + SegmentDrawing.DrawSegment(start, end, lineThickness, color, capType, capPoints); + return; + } + } + } + /// - /// Draws a certain amount of the polygon's perimeter as an outline. + /// Draws a portion of the polygon's perimeter as a thick line with capped ends, starting at a given index, + /// based on a percentage of the total perimeter. + /// This function is slower than + /// due to drawing more triangles for the caps. /// - /// The polygon to draw. - /// - /// The length of the perimeter to draw. If negative, draws in clockwise direction. + /// The polygon whose perimeter will be drawn. + /// + /// The percentage (0-1) of the perimeter to draw. If negative, draws in reverse order. + /// + /// + /// The index of the starting vertex. Drawing proceeds forward or backward depending on the sign of . /// - /// The index of the vertex at which to start drawing. /// The thickness of the outline. - /// The color of the outline. - /// The type of line cap to use. - /// The number of points for the cap. - /// - /// Useful for animating outlines or drawing partial polygons. - /// - public static void DrawLinesPerimeter(this Polygon poly, float perimeterToDraw, int startIndex, float lineThickness, ColorRgba color, LineCapType capType = LineCapType.CappedExtended, int capPoints = 2) + /// + /// The color of the outline. Does not support transparency, alpha will be set to 255 internally. + /// + /// The type of line cap to use at the ends of the drawn segment. + /// The number of points for the cap (used for round caps). + public static void DrawLinesPercentageCapped(this Polygon poly, float f, int startIndex, float lineThickness, ColorRgba color, LineCapType capType = LineCapType.CappedExtended, int capPoints = 2) { - ShapeDrawing.DrawOutlinePerimeter(poly, perimeterToDraw, startIndex, lineThickness, color, capType, capPoints); + if (poly.Count < 3 || f == 0f) return; + + var negative = false; + if (f < 0) + { + negative = true; + f *= -1; + } + + f = ShapeMath.WrapF(f, 0f, 1f); + startIndex = ShapeMath.WrapIndex(startIndex, poly.Count); + if (f <= 0f) return; + if (f >= 1f) + { + poly.DrawLines(lineThickness, color); + return; + } + + var perimeter = 0f; + for (var i = 0; i < poly.Count; i++) + { + var start = poly[i]; + var end = poly[(i + 1) % poly.Count]; + float l = (end - start).Length(); + perimeter += l; + } + + poly.DrawLinesPerimeterCapped(perimeter * f * (negative ? -1 : 1), startIndex, lineThickness, color, capType, capPoints); } /// - /// Draws a certain percentage of the polygon's outline. + /// Draws a portion of the polygon's perimeter as a thick line with capped ends, starting at a given index, + /// using for line options. Supports custom cap types and cap points. + /// This function is slower than due to drawing more triangles for the caps. /// - /// The polygon to draw. + /// The polygon whose perimeter will be drawn. /// - /// Specifies the starting corner and the percentage of the outline to draw. - /// - /// The integer part selects the starting corner (0 = first corner, 1 = second, etc.). - /// The decimal part specifies the percentage of the outline to draw, as a fraction (0.0 to 1.0). - /// Negative values draw in the clockwise direction; positive values draw counter-clockwise. - /// Example: 0.35 starts at corner 0, draws 35% of the outline counter-clockwise. - /// Example: -2.7 starts at corner 2, draws 70% of the outline clockwise. - /// + /// The percentage (0-1) of the perimeter to draw. If negative, draws in reverse order. /// - /// The thickness of the outline. - /// The color of the outline. - /// The type of line cap to use. - /// The number of points for the cap. - /// - /// Useful for progress indicators or animated outlines. - /// - public static void DrawLinesPercentage(this Polygon poly, float f, float lineThickness, ColorRgba color, LineCapType capType = LineCapType.CappedExtended, int capPoints = 2) + /// + /// The index of the starting vertex. Drawing proceeds forward or backward depending on the sign of . + /// + /// + /// The line drawing information (thickness, color, cap type, cap points). + /// Color is expected to be opaque (alpha will be set to 255 internally). + /// + public static void DrawLinesPercentageCapped(this Polygon poly, float f, int startIndex, LineDrawingInfo lineInfo) { - ShapeDrawing.DrawOutlinePercentage(poly, f, lineThickness, color, capType, capPoints); + poly.DrawLinesPercentageCapped(f, startIndex, lineInfo.Thickness, lineInfo.Color, lineInfo.CapType, lineInfo.CapPoints); } + #endregion + + //TODO: still creates artifacts in corners (ignore?) + #region Draw Lines + /// - /// Draws a certain percentage of the polygon's outline using . + /// Draws the outline of a polygon with transparent color support. + /// This method handles miter and beveled joins and allows for custom miter limits. /// - /// The polygon to draw. - /// - /// Specifies the starting corner and the percentage of the outline to draw. - /// - /// The integer part selects the starting corner (0 = first corner, 1 = second, etc.). - /// The decimal part specifies the percentage of the outline to draw, as a fraction (0.0 to 1.0). - /// Negative values draw in the clockwise direction; positive values draw counter-clockwise. - /// Example: 0.35 starts at corner 0, draws 35% of the outline counter-clockwise. - /// Example: -2.7 starts at corner 2, draws 70% of the outline clockwise. - /// + /// The polygon whose outline will be drawn. + /// The thickness of the outline in world units. + /// The color of the outline, including alpha for transparency. + /// + /// The miter limit for joins. If the miter length exceeds this value times the line thickness, a bevel is used instead. + /// Default is 2.0f. /// - /// The line drawing information (thickness, color, cap type, etc.). - public static void DrawLinesPercentage(this Polygon poly, float f, LineDrawingInfo lineInfo) + /// + /// If true, forces beveled joins instead of miters when the miter limit is exceeded. + /// + public static void DrawLines(this Polygon poly, float lineThickness, ColorRgba color, float miterLimit = 2f, bool beveled = false) { - ShapeDrawing.DrawOutlinePercentage(poly, f, lineInfo.Thickness, lineInfo.Color, lineInfo.CapType, lineInfo.CapPoints); + Vector2 lastOuter = Vector2.Zero, lastInner = Vector2.Zero; + var lastCornerType = 0; + var initialized = false; + + var rayColor = color.ToRayColor(); + float totalMiterLengthLimit = lineThickness * 0.5f * MathF.Max(2f, miterLimit); + + for (var i = 0; i <= poly.Count; i++) + { + var prev = poly[ShapeMath.WrapIndex(poly.Count, i - 1)]; + var cur = poly[ShapeMath.WrapIndex(poly.Count, i)]; + var next = poly[ShapeMath.WrapIndex(poly.Count, i + 1)]; + + var wPrev = cur - prev; + var wNext = next - cur; + float lsPrev = wPrev.LengthSquared(); + float lsNext = wNext.LengthSquared(); + if (lsPrev <= 0 || lsNext <= 0) return; + + var dirPrev = wPrev.Normalize(); + var dirNext = wNext.Normalize(); + + //flip based on corner type + var cornerType = dirPrev.ClassifyCorner(dirNext); + Vector2 normalPrev, normalNext; + if (cornerType.type >= 0) + { + normalPrev = dirPrev.GetPerpendicularRight(); + normalNext = dirNext.GetPerpendicularRight(); + } + else + { + normalPrev = dirPrev.GetPerpendicularLeft(); + normalNext = dirNext.GetPerpendicularLeft(); + } + + if (cornerType.type == 0) //collinear + { + var cornerOuter = cur + normalNext * lineThickness; + var cornerInner = cur - normalNext * lineThickness; + + if (!initialized) + { + lastInner = cornerInner; + lastOuter = cornerOuter; + lastCornerType = cornerType.type; + initialized = true; + continue; + } + + Raylib.DrawTriangle(cornerInner, lastInner, lastOuter, rayColor); + Raylib.DrawTriangle(cornerInner, lastOuter, cornerOuter, rayColor); + + lastInner = cornerInner; + lastOuter = cornerOuter; + lastCornerType = cornerType.type; + continue; + } + + var miterDir = (normalPrev + normalNext).Normalize(); + float miterAngleRad = MathF.Abs(miterDir.AngleRad(normalNext)); + float miterLength = lineThickness / MathF.Cos(miterAngleRad); + + if (miterLimit < 2f || miterLength < totalMiterLengthLimit) + { + var cornerOuter = cur + miterDir * miterLength; + var cornerInner = cur - miterDir * miterLength; + + if (!initialized) + { + lastInner = cornerInner; + lastOuter = cornerOuter; + lastCornerType = cornerType.type; + initialized = true; + continue; + } + + if (cornerType.type >= 0) + { + if (lastCornerType >= 0) + { + Raylib.DrawTriangle(cornerInner, lastInner, lastOuter, rayColor); + } + else + { + Raylib.DrawTriangle(cornerOuter, lastOuter, lastInner, rayColor); + } + + Raylib.DrawTriangle(cornerInner, lastOuter, cornerOuter, rayColor); + } + else + { + if (lastCornerType >= 0) + { + Raylib.DrawTriangle(cornerOuter, lastInner, lastOuter, rayColor); + } + else + { + Raylib.DrawTriangle(lastInner, cornerInner, lastOuter, rayColor); + } + + Raylib.DrawTriangle(lastOuter, cornerInner, cornerOuter, rayColor); + } + + lastInner = cornerInner; + lastOuter = cornerOuter; + lastCornerType = cornerType.type; + } + else + { + miterLength = totalMiterLengthLimit; + Vector2 cornerOuterPrev, cornerOuterNext; + + var prevOuter = prev + normalPrev * lineThickness; + var prevInner = prev - normalPrev * lineThickness; + var nextInner = next - normalNext * lineThickness; + var intersection = Ray.IntersectRayRay(prevInner, dirPrev, nextInner, -dirNext); + var cornerInner = intersection.Valid ? intersection.Point : cur - miterDir * miterLength; + + + if (beveled) + { + cornerOuterPrev = cur + normalPrev * lineThickness; + cornerOuterNext = cur + normalNext * lineThickness; + + // cornerOuterPrev.Draw(2, new ColorRgba(System.Drawing.Color.Orange)); + // cornerOuterNext.Draw(2, new ColorRgba(System.Drawing.Color.Orange)); + } + else + { + var cornerOuter = cur + miterDir * miterLength; + var miterPerpRight = cornerType.type >= 0 ? miterDir.GetPerpendicularRight() : miterDir.GetPerpendicularLeft(); + intersection = Ray.IntersectRayRay(prevOuter, dirPrev, cornerOuter, miterPerpRight); + if (intersection.Valid) + { + cornerOuterPrev = intersection.Point; + float l = (cornerOuter - intersection.Point).Length(); + cornerOuterNext = cornerOuter - miterPerpRight * l; + } + else //bevel fallback + { + cornerOuterPrev = cur + normalPrev * lineThickness; + cornerOuterNext = cur + normalNext * lineThickness; + } + } + + if (!initialized) + { + lastInner = cornerInner; + lastOuter = cornerOuterNext; + lastCornerType = cornerType.type; + initialized = true; + continue; + } + + + if (cornerType.type >= 0) + { + if (lastCornerType >= 0) + { + Raylib.DrawTriangle(cornerInner, lastInner, lastOuter, rayColor); + Raylib.DrawTriangle(cornerInner, lastOuter, cornerOuterPrev, rayColor); + } + else + { + Raylib.DrawTriangle(cornerInner, lastOuter, lastInner, rayColor); + Raylib.DrawTriangle(cornerInner, lastInner, cornerOuterPrev, rayColor); + } + + Raylib.DrawTriangle(cornerOuterPrev, cornerOuterNext, cornerInner, rayColor); + } + else + { + if (lastCornerType >= 0) + { + Raylib.DrawTriangle(cornerInner, lastInner, lastOuter, rayColor); + Raylib.DrawTriangle(cornerInner, cornerOuterPrev, lastInner, rayColor); + } + else + { + Raylib.DrawTriangle(cornerInner, lastOuter, lastInner, rayColor); + Raylib.DrawTriangle(cornerInner, cornerOuterPrev, lastOuter, rayColor); + } + + Raylib.DrawTriangle(cornerOuterNext, cornerOuterPrev, cornerInner, rayColor); + } + + lastInner = cornerInner; + lastOuter = cornerOuterNext; + lastCornerType = cornerType.type; + } + } + } + + /// + /// Draws the outline of a polygon with transparent color support using the specified . + /// Handles miter and beveled joins, and allows for custom miter limits. + /// + /// The polygon whose outline will be drawn. + /// The line drawing information (thickness, color, cap type, etc.). + /// + /// The miter limit for joins. If the miter length exceeds this value times the line thickness, a bevel is used instead. + /// Default is 2.0f. + /// + /// + /// If true, forces beveled joins instead of miters when the miter limit is exceeded. + /// + public static void DrawLines(this Polygon poly, LineDrawingInfo lineInfo, float miterLimit = 2f, bool beveled = false) + { + poly.DrawLines(lineInfo.Thickness, lineInfo.Color, miterLimit, beveled); + } + + #endregion + + //TODO: still creates artifacts in corners (ignore?) + #region Draw Lines Convex + + /// + /// Draws the outline of a convex polygon with the specified line thickness and color. + /// This method is optimized for convex polygons and supports miter and beveled joins. + /// + /// The convex polygon to draw. Must have at least 3 points. + /// The thickness of the outline in world units. + /// The color of the outline.. + /// + /// The miter limit for joins. If the miter length exceeds this value times the line thickness, a bevel is used instead. + /// Default is 2.0f. + /// + /// + /// If true, forces beveled joins instead of miters when the miter limit is exceeded. + /// + public static void DrawLinesConvex(this Polygon poly, float lineThickness, ColorRgba color, float miterLimit = 2f, bool beveled = false) + { + if (poly.Count < 3) return; + + Vector2 outsidePrev = Vector2.Zero, insidePrev = Vector2.Zero; + var initialized = false; + + var rayColor = color.ToRayColor(); + float totalMiterLengthLimit = lineThickness * 0.5f * MathF.Max(2f, miterLimit); + + for (var i = 0; i <= poly.Count; i++) + { + var prev = poly[ShapeMath.WrapIndex(poly.Count, i - 1)]; + var cur = poly[ShapeMath.WrapIndex(poly.Count, i)]; + var next = poly[ShapeMath.WrapIndex(poly.Count, i + 1)]; + + var wPrev = cur - prev; + var wNext = next - cur; + float lsPrev = wPrev.LengthSquared(); + float lsNext = wNext.LengthSquared(); + if (lsPrev <= 0 || lsNext <= 0) continue; + + var dirPrev = wPrev.Normalize(); + var dirNext = wNext.Normalize(); + + var normalPrev = dirPrev.GetPerpendicularRight(); + var normalNext = dirNext.GetPerpendicularRight(); + + var miterDir = (normalPrev + normalNext).Normalize(); + float miterAngleRad = MathF.Abs(miterDir.AngleRad(normalNext)); + float miterLength = lineThickness / MathF.Cos(miterAngleRad); + + if (miterLimit < 2f || miterLength < totalMiterLengthLimit) + { + if (!initialized) + { + insidePrev = cur - miterDir * miterLength; + outsidePrev = cur + miterDir * miterLength; + initialized = true; + continue; + } + + var outsideCur = cur + miterDir * miterLength; + var insideCur = cur - miterDir * miterLength; + + Raylib.DrawTriangle(outsidePrev, outsideCur, insideCur, rayColor); + Raylib.DrawTriangle(outsidePrev, insideCur, insidePrev, rayColor); + + outsidePrev = outsideCur; + insidePrev = insideCur; + } + else + { + miterLength = totalMiterLengthLimit; + + var insideCur = cur - miterDir * miterLength; + + Vector2 outsideLeftCur, outsideRightCur; + + if (beveled) + { + outsideLeftCur = cur + normalNext * lineThickness; + + if (!initialized) + { + insidePrev = insideCur; + outsidePrev = outsideLeftCur; + initialized = true; + continue; + } + + outsideRightCur = cur + normalPrev * lineThickness; + } + else + { + var p = cur + miterDir * miterLength; + var dir = (p - cur).Normalize(); + var perp = dir.GetPerpendicularRight(); + + var start = next + normalNext * lineThickness; + var intersection = Ray.IntersectRayRay(start, -dirNext, p, -perp); + if (intersection.Valid) + { + outsideLeftCur = intersection.Point; + } + else //fallback bevel + { + outsideLeftCur = cur + normalNext * lineThickness; + } + + if (!initialized) + { + insidePrev = insideCur; + outsidePrev = outsideLeftCur; + initialized = true; + continue; + } + + start = prev + normalPrev * lineThickness; + intersection = Ray.IntersectRayRay(start, dirPrev, p, perp); + if (intersection.Valid) + { + outsideRightCur = intersection.Point; + } + else //fallback bevel + { + outsideRightCur = cur + normalPrev * lineThickness; + } + } + + Raylib.DrawTriangle(outsidePrev, outsideRightCur, insideCur, rayColor); + Raylib.DrawTriangle(outsidePrev, insideCur, insidePrev, rayColor); + Raylib.DrawTriangle(outsideRightCur, outsideLeftCur, insideCur, rayColor); + + insidePrev = insideCur; + outsidePrev = outsideLeftCur; + } + } + } + + /// + /// Draws the outline of a convex polygon using the specified . + /// This method is optimized for convex polygons and supports miter and beveled joins. + /// + /// The convex polygon to draw. Must have at least 3 points. + /// The line drawing information (thickness, color, cap type, etc.). + /// + /// The miter limit for joins. If the miter length exceeds this value times the line thickness, a bevel is used instead. + /// Default is 2.0f. + /// + /// + /// If true, forces beveled joins instead of miters when the miter limit is exceeded. + /// + public static void DrawLinesConvex(this Polygon poly, LineDrawingInfo lineInfo, float miterLimit = 2f, bool beveled = false) + { + if (poly.Count < 3) return; + poly.DrawLinesConvex(lineInfo.Thickness, lineInfo.Color, miterLimit, beveled); } + #endregion + + #region Draw Lines Scaled + /// - /// Draws a circle at each vertex of the polygon. + /// Draws a polygon where each side can be scaled towards the origin of the side. /// - /// The polygon whose vertices to draw. - /// The radius of each vertex circle. - /// The color of the vertex circles. - /// The number of segments for each circle. + /// The polygon to draw. + /// The line drawing information (thickness, color, cap type, etc.). + /// + /// The scale factor for each side. + /// + /// 0: No polyline is drawn. + /// 1: The normal polyline is drawn. + /// 0.5: Each side is half as long. + /// + /// + /// + /// The point along the line to scale from, in both directions (0 to 1). + /// + /// 0: Start of Segment + /// 0.5: Center of Segment + /// 1: End of Segment + /// + /// /// - /// Useful for debugging or highlighting polygon vertices. + /// Useful for creating stylized or animated polygon outlines. /// - public static void DrawVertices(this Polygon poly, float vertexRadius, ColorRgba color, int circleSegments) + public static void DrawLinesScaled(this Polygon poly, LineDrawingInfo lineInfo, float sideScaleFactor, float sideScaleOrigin = 0.5f) { - foreach (var p in poly) + if (poly.Count < 3) return; + if (sideScaleFactor <= 0) return; + + if (sideScaleFactor >= 1) + { + poly.DrawLines(lineInfo); + return; + } + + for (var i = 0; i < poly.Count; i++) { - CircleDrawing.DrawCircle(p, vertexRadius, color, circleSegments); + var start = poly[i]; + var end = poly[(i + 1) % poly.Count]; + SegmentDrawing.DrawSegment(start, end, lineInfo, sideScaleFactor, sideScaleOrigin); } } + #endregion + + #region Draw Cornered Absolute Transparent + /// - /// Draws lines from each corner of the polygon outward, with custom lengths for each corner. + /// Draws a polygon corner based on an absolute , handling miter/bevel joins, custom cap types and transparent colors are supported. /// - /// The polygon to draw. - /// The thickness of the lines. - /// The color of the lines. - /// A list of lengths for each corner. Cycles if fewer than corners. + /// The previous vertex of the polygon. + /// The current corner vertex. + /// The next vertex of the polygon. + /// The length of the corner segment to render. + /// The thickness of the outline. + /// The color used for rendering, including alpha for transparency. /// The type of line cap to use. /// The number of points for the cap. - /// - /// Useful for stylized or decorative polygon outlines. - /// A corner is defined by three points: previous [i-1], current [i], and next [i+1]. - /// - public static void DrawCornered(this Polygon poly, float lineThickness, ColorRgba color, List cornerLengths, LineCapType capType = LineCapType.CappedExtended, int capPoints = 2) + /// The miter limit for joins. If exceeded, the corner is either squared or beveled depeding on . + /// If true, forces beveled joins instead of squared joins when the miter limit is exceeded. + /// Beveling is faster and simpler but does not look as good. + private static void DrawCornerAbsoluteTransparent(Vector2 prev, Vector2 corner, Vector2 next, float cornerLength, float lineThickness, ColorRgba color, + LineCapType capType, int capPoints, float miterLimit = 2f, bool beveled = false) + { + var wPrev = corner - prev; + var wNext = next - corner; + + var dirPrev = wPrev.Normalize(); + var dirNext = wNext.Normalize(); + + //flip based on corner type + var cornerType = dirPrev.ClassifyCorner(dirNext); + Vector2 normalPrev, normalNext; + if (cornerType.type >= 0) + { + normalPrev = dirPrev.GetPerpendicularRight(); + normalNext = dirNext.GetPerpendicularRight(); + } + else + { + normalPrev = dirPrev.GetPerpendicularLeft(); + normalNext = dirNext.GetPerpendicularLeft(); + } + + prev = corner - dirPrev * cornerLength; + next = corner + dirNext * cornerLength; + + if (cornerType.type == 0) //collinear + { + SegmentDrawing.DrawSegment(prev, next, lineThickness, color, capType, capPoints); + return; + } + + var rayColor = color.ToRayColor(); + float totalMiterLengthLimit = lineThickness * 0.5f * MathF.Max(2f, miterLimit); + var miterDir = (normalPrev + normalNext).Normalize(); + float miterAngleRad = MathF.Abs(miterDir.AngleRad(normalNext)); + float miterLength = lineThickness / MathF.Cos(miterAngleRad); + + if (miterLimit < 2f || miterLength < totalMiterLengthLimit) + { + var cornerOuter = corner + miterDir * miterLength; + var prevOuter = prev + normalPrev * lineThickness; + var prevInner = prev - normalPrev * lineThickness; + var nextOuter = next + normalNext * lineThickness; + var nextInner = next - normalNext * lineThickness; + var intersection = Ray.IntersectRayRay(prevInner, dirPrev, nextInner, -dirNext); + var cornerInner = intersection.Valid ? intersection.Point : corner - miterDir * miterLength; + + if (cornerType.type >= 0) + { + Raylib.DrawTriangle(cornerInner, prevInner, prevOuter, rayColor); + Raylib.DrawTriangle(cornerInner, prevOuter, cornerOuter, rayColor); + Raylib.DrawTriangle(cornerInner, cornerOuter, nextOuter, rayColor); + Raylib.DrawTriangle(cornerInner, nextOuter, nextInner, rayColor); + } + else + { + Raylib.DrawTriangle(prevInner, cornerInner, prevOuter, rayColor); + Raylib.DrawTriangle(prevOuter, cornerInner, cornerOuter, rayColor); + Raylib.DrawTriangle(cornerOuter, cornerInner, nextOuter, rayColor); + Raylib.DrawTriangle(nextOuter, cornerInner, nextInner, rayColor); + } + } + else + { + miterLength = totalMiterLengthLimit; + Vector2 cornerOuterPrev, cornerOuterNext; + + var prevOuter = prev + normalPrev * lineThickness; + var prevInner = prev - normalPrev * lineThickness; + var nextOuter = next + normalNext * lineThickness; + var nextInner = next - normalNext * lineThickness; + var intersection = Ray.IntersectRayRay(prevInner, dirPrev, nextInner, -dirNext); + var cornerInner = intersection.Valid ? intersection.Point : corner - miterDir * miterLength; + + if (beveled) + { + cornerOuterPrev = corner + normalPrev * lineThickness; + cornerOuterNext = corner + normalNext * lineThickness; + } + else + { + var cornerOuter = corner + miterDir * miterLength; + var miterPerpRight = cornerType.type >= 0 ? miterDir.GetPerpendicularRight() : miterDir.GetPerpendicularLeft(); + intersection = Ray.IntersectRayRay(prevOuter, dirPrev, cornerOuter, miterPerpRight); + if (intersection.Valid) + { + cornerOuterPrev = intersection.Point; + float l = (cornerOuter - intersection.Point).Length(); + cornerOuterNext = cornerOuter - miterPerpRight * l; + } + else //bevel fallback + { + cornerOuterPrev = corner + normalPrev * lineThickness; + cornerOuterNext = corner + normalNext * lineThickness; + } + } + + if (cornerType.type >= 0) + { + Raylib.DrawTriangle(cornerInner, prevInner, prevOuter, rayColor); + Raylib.DrawTriangle(cornerInner, prevOuter, cornerOuterPrev, rayColor); + Raylib.DrawTriangle(cornerInner, cornerOuterNext, nextOuter, rayColor); + Raylib.DrawTriangle(cornerInner, nextOuter, nextInner, rayColor); + Raylib.DrawTriangle(cornerInner, cornerOuterPrev, cornerOuterNext, rayColor); + } + else + { + Raylib.DrawTriangle(prevInner, cornerInner, prevOuter, rayColor); + Raylib.DrawTriangle(prevOuter, cornerInner, cornerOuterPrev, rayColor); + Raylib.DrawTriangle(cornerOuterNext, cornerInner, nextOuter, rayColor); + Raylib.DrawTriangle(nextOuter, cornerInner, nextInner, rayColor); + Raylib.DrawTriangle(cornerOuterPrev, cornerInner, cornerOuterNext, rayColor); + } + } + + if (capType is LineCapType.Capped or LineCapType.CappedExtended && capPoints > 0) + { + SegmentDrawing.DrawRoundCap(prev, -dirPrev, lineThickness, capPoints, color); + SegmentDrawing.DrawRoundCap(next, dirNext, lineThickness, capPoints, color); + } + } + + /// + /// Draws each corner of the polygon with an absolute , handling miter/bevel joins, custom cap types and transparent colors are supported. + /// + /// The polygon whose corners will be drawn. + /// The thickness of the outline. + /// The color used for rendering, including alpha for transparency. + /// The length of the corner segment to render. + /// The type of line cap to use. + /// The number of points for the cap. + /// The miter limit for joins. If exceeded, the corner is either squared or beveled depeding on . + /// If true, forces beveled joins instead of squared joins when the miter limit is exceeded. + /// Beveling is faster and simpler but does not look as good. + public static void DrawCorneredAbsoluteTransparent(this Polygon poly, float lineThickness, ColorRgba color, float cornerLength, + LineCapType capType = LineCapType.CappedExtended, int capPoints = 2, + float miterLimit = 2f, bool beveled = false) { for (var i = 0; i < poly.Count; i++) { - float cornerLength = cornerLengths[i%cornerLengths.Count]; - var prev = poly[(i - 1) % poly.Count]; + var prev = poly[ShapeMath.WrapIndex(poly.Count, i - 1)]; var cur = poly[i]; - var next = poly[(i + 1) % poly.Count]; - SegmentDrawing.DrawSegment(cur, cur + next.Normalize() * cornerLength, lineThickness, color, capType, capPoints); - SegmentDrawing.DrawSegment(cur, cur + prev.Normalize() * cornerLength, lineThickness, color, capType, capPoints); + var next = poly[ShapeMath.WrapIndex(poly.Count, i + 1)]; + + var wPrev = cur - prev; + var wNext = next - cur; + float lsPrev = wPrev.LengthSquared(); + float lsNext = wNext.LengthSquared(); + if (lsPrev <= 0 || lsNext <= 0) return; + + float minLength = MathF.Sqrt(MathF.Min(lsPrev, lsNext)); + float newLineThickness = MathF.Min(lineThickness, MathF.Min(cornerLength * 0.5f, minLength * 0.25f)); + float newCornerLength; + + if (capType is LineCapType.None) + newCornerLength = MathF.Min(cornerLength, minLength * 0.5f); + else if (capType is LineCapType.Extended) + newCornerLength = MathF.Min(cornerLength + newLineThickness, minLength * 0.5f); + else if (capType is LineCapType.Capped) + newCornerLength = MathF.Min(cornerLength - newLineThickness, minLength * 0.5f - newLineThickness); + else //Capped Extended + newCornerLength = MathF.Min(cornerLength, minLength * 0.5f - newLineThickness); + + if (newCornerLength <= 0) continue; + + DrawCornerAbsoluteTransparent(prev, cur, next, newCornerLength, newLineThickness, color, capType, capPoints, miterLimit, beveled); } } /// - /// Draws lines from each corner of the polygon outward, using relative factors for each corner. + /// Draws each corner of the polygon with an absolute , handling miter/bevel joins, custom cap types, and transparent colors. + /// Uses for line thickness, color, cap type, and cap points. /// - /// The polygon to draw. - /// The thickness of the lines. - /// The color of the lines. - /// A list of factors (0-1) for each corner. Cycles if fewer than corners. + /// The polygon whose corners will be drawn. + /// The length of the corner segment to render. + /// The line drawing information (thickness, color, cap type, etc.). + /// The miter limit for joins. If exceeded, the corner is either squared or beveled depeding on . + /// If true, forces beveled joins instead of squared joins when the miter limit is exceeded. + /// Beveling is faster and simpler but does not look as good. + public static void DrawCorneredAbsoluteTransparent(this Polygon poly, float cornerLength, LineDrawingInfo lineInfo, float miterLimit = 2f, + bool beveled = false) + { + poly.DrawCorneredAbsoluteTransparent(lineInfo.Thickness, lineInfo.Color, cornerLength, lineInfo.CapType, lineInfo.CapPoints, miterLimit, beveled); + } + + /// + /// Draws each corner of the polygon with an absolute length specified per corner, handling miter/bevel joins, custom cap types, and transparent colors. + /// + /// The polygon whose corners will be drawn. + /// The thickness of the outline. + /// The color used for rendering, including alpha for transparency. + /// A list of corner segment lengths to render for each corner. /// The type of line cap to use. /// The number of points for the cap. - /// - /// Each factor determines how far along the edge the corner line extends. - /// A corner is defined by three points: previous [i-1], current [i], and next [i+1]. - /// CornerFactors are used to interpolate from previous to current and from current to next point to form a corner. - /// - public static void DrawCorneredRelative(this Polygon poly, float lineThickness, ColorRgba color, List cornerFactors, LineCapType capType = LineCapType.CappedExtended, int capPoints = 2) + /// The miter limit for joins. If exceeded, the corner is either squared or beveled depeding on . + /// If true, forces beveled joins instead of squared joins when the miter limit is exceeded. + /// Beveling is faster and simpler but does not look as good. + public static void DrawCorneredAbsoluteTransparent(this Polygon poly, float lineThickness, ColorRgba color, List cornerLengths, + LineCapType capType = LineCapType.CappedExtended, int capPoints = 2, + float miterLimit = 2f, bool beveled = false) { + if (cornerLengths.Count <= 0) return; for (var i = 0; i < poly.Count; i++) { - float cornerF = cornerFactors[i%cornerFactors.Count]; - var prev = poly[(i - 1) % poly.Count]; + float cornerLength = cornerLengths[i % cornerLengths.Count]; + var prev = poly[ShapeMath.WrapIndex(poly.Count, i - 1)]; var cur = poly[i]; - var next = poly[(i + 1) % poly.Count]; - SegmentDrawing.DrawSegment(cur, cur.Lerp(next, cornerF), lineThickness, color, capType, capPoints); - SegmentDrawing.DrawSegment(cur, cur.Lerp(prev, cornerF), lineThickness, color, capType, capPoints); + var next = poly[ShapeMath.WrapIndex(poly.Count, i + 1)]; + + var wPrev = cur - prev; + var wNext = next - cur; + float lsPrev = wPrev.LengthSquared(); + float lsNext = wNext.LengthSquared(); + if (lsPrev <= 0 || lsNext <= 0) return; + + float minLength = MathF.Sqrt(MathF.Min(lsPrev, lsNext)); + float newLineThickness = MathF.Min(lineThickness, MathF.Min(cornerLength * 0.5f, minLength * 0.25f)); + float newCornerLength; + + if (capType is LineCapType.None) + newCornerLength = MathF.Min(cornerLength, minLength * 0.5f); + else if (capType is LineCapType.Extended) + newCornerLength = MathF.Min(cornerLength + newLineThickness, minLength * 0.5f); + else if (capType is LineCapType.Capped) + newCornerLength = MathF.Min(cornerLength - newLineThickness, minLength * 0.5f - newLineThickness); + else //Capped Extended + newCornerLength = MathF.Min(cornerLength, minLength * 0.5f - newLineThickness); + + if (newCornerLength <= 0) continue; + + DrawCornerAbsoluteTransparent(prev, cur, next, newCornerLength, newLineThickness, color, capType, capPoints, miterLimit, beveled); } } /// - /// Draws lines from each corner of the polygon outward, with custom lengths for each corner, using . + /// Draws each corner of the polygon with an absolute length specified per corner, handling miter/bevel joins, custom cap types, and transparent colors. + /// Uses for line thickness, color, cap type, and cap points. /// - /// The polygon to draw. - /// A list of lengths for each corner. Cycles if fewer than corners. + /// The polygon whose corners will be drawn. + /// A list of corner segment lengths to render for each corner. /// The line drawing information (thickness, color, cap type, etc.). - /// - /// A corner is defined by three points: previous [i-1], current [i], and next [i+1]. - /// - public static void DrawCornered(this Polygon poly, List cornerLengths, LineDrawingInfo lineInfo) + /// The miter limit for joins. If exceeded, the corner is either squared or beveled depeding on . + /// If true, forces beveled joins instead of squared joins when the miter limit is exceeded. + /// Beveling is faster and simpler but does not look as good. + public static void DrawCorneredAbsoluteTransparent(this Polygon poly, List cornerLength, LineDrawingInfo lineInfo, float miterLimit = 2f, + bool beveled = false) + { + poly.DrawCorneredAbsoluteTransparent(lineInfo.Thickness, lineInfo.Color, cornerLength, lineInfo.CapType, lineInfo.CapPoints, miterLimit, beveled); + } + + /// + /// Calculates parameters for drawing a polygon corner with an absolute and . + /// Returns true if parameters are valid for drawing, otherwise false. + /// + /// The polygon whose corner parameters will be calculated. + /// The length of the corner segment to render. + /// The thickness of the outline. + /// The type of line cap to use. + /// The calculated corner length to use for drawing. + /// The calculated line thickness to use for drawing. + public static bool CaluclateDrawCornerAbsoluteParameters(this Polygon poly, float cornerLength, float lineThickness, LineCapType capType, + out float newCornerLength, out float newLineThickness) { + newCornerLength = -1f; + newLineThickness = -1f; + for (var i = 0; i < poly.Count; i++) { - float cornerLength = cornerLengths[i%cornerLengths.Count]; - var prev = poly[(i - 1) % poly.Count]; var cur = poly[i]; - var next = poly[(i + 1) % poly.Count]; - SegmentDrawing.DrawSegment(cur, cur + next.Normalize() * cornerLength, lineInfo); - SegmentDrawing.DrawSegment(cur, cur + prev.Normalize() * cornerLength, lineInfo); + var next = poly[ShapeMath.WrapIndex(poly.Count, i + 1)]; + + var wNext = next - cur; + + float lsNext = wNext.LengthSquared(); + if (lsNext <= 0) + { + newCornerLength = 0f; + newLineThickness = 0f; + return false; + } + + float minLength = MathF.Sqrt(lsNext); + float curLineThickness = MathF.Min(lineThickness, MathF.Min(cornerLength * 0.5f, minLength * 0.25f)); + float curCornerLength; + if (capType is LineCapType.None) + curCornerLength = MathF.Min(cornerLength, minLength * 0.5f); + else if (capType is LineCapType.Extended) + curCornerLength = MathF.Min(cornerLength + curLineThickness, minLength * 0.5f); + else if (capType is LineCapType.Capped) + curCornerLength = MathF.Min(cornerLength - curLineThickness, minLength * 0.5f - curLineThickness); + else //Capped Extended + curCornerLength = MathF.Min(cornerLength, minLength * 0.5f - curLineThickness); + + if (curCornerLength <= 0 || curLineThickness <= 0) + { + newCornerLength = 0f; + newLineThickness = 0f; + return false; + } + + if (curCornerLength < newCornerLength || newCornerLength < 0f) newCornerLength = curCornerLength; + if (curLineThickness < newLineThickness || newLineThickness < 0f) newLineThickness = curLineThickness; } + + if (newCornerLength > 0 && newLineThickness > 0) return true; + + newCornerLength = 0; + newLineThickness = 0; + return false; } + #endregion + + #region Draw Cornered Relative Transparent + /// - /// Draws lines from each corner of the polygon outward, using relative factors for each corner, with . + /// Draws each corner of the polygon with a relative length based on , handling miter/bevel joins, custom cap types, and transparent colors. /// - /// The polygon to draw. - /// A list of factors (0-1) for each corner. Cycles if fewer than corners. - /// The line drawing information (thickness, color, cap type, etc.). - /// - /// A corner is defined by three points: previous [i-1], current [i], and next [i+1]. - /// CornerFactors are used to interpolate from previous to current and from current to next point to form a corner. - /// - public static void DrawCorneredRelative(this Polygon poly, List cornerFactors, LineDrawingInfo lineInfo) + /// The polygon whose corners will be drawn. + /// The thickness of the outline. + /// The color used for rendering, including alpha for transparency. + /// + /// The factor (0-1) that determines the relative length of each corner segment to render. + /// 0: No corner is drawn. + /// 1: Maximum relative length. + /// + /// The type of line cap to use. + /// The number of points for the cap. + /// The miter limit for joins. If exceeded, the corner is either squared or beveled depeding on . + /// If true, forces beveled joins instead of squared joins when the miter limit is exceeded. + /// Beveling is faster and simpler but does not look as good. + public static void DrawCorneredRelativeTransparent(this Polygon poly, float lineThickness, ColorRgba color, float cornerLengthFactor, + LineCapType capType = LineCapType.CappedExtended, int capPoints = 2, + float miterLimit = 2f, bool beveled = false) { + cornerLengthFactor = ShapeMath.Clamp(cornerLengthFactor, 0f, 1f); + for (var i = 0; i < poly.Count; i++) { - float cornerF = cornerFactors[i%cornerFactors.Count]; - var prev = poly[(i - 1) % poly.Count]; + var prev = poly[ShapeMath.WrapIndex(poly.Count, i - 1)]; var cur = poly[i]; - var next = poly[(i + 1) % poly.Count]; - SegmentDrawing.DrawSegment(cur, cur.Lerp(next, cornerF), lineInfo); - SegmentDrawing.DrawSegment(cur, cur.Lerp(prev, cornerF), lineInfo); + var next = poly[ShapeMath.WrapIndex(poly.Count, i + 1)]; + + var wPrev = cur - prev; + var wNext = next - cur; + float lsPrev = wPrev.LengthSquared(); + float lsNext = wNext.LengthSquared(); + if (lsPrev <= 0 || lsNext <= 0) return; + + float minLength = MathF.Sqrt(MathF.Min(lsPrev, lsNext)); + float cornerLength = minLength * cornerLengthFactor; + float newLineThickness = MathF.Min(lineThickness, MathF.Min(cornerLength * 0.5f, minLength * 0.25f)); + float newCornerLength; + + if (capType is LineCapType.None) + newCornerLength = MathF.Min(cornerLength, minLength * 0.5f); + else if (capType is LineCapType.Extended) + newCornerLength = MathF.Min(cornerLength + newLineThickness, minLength * 0.5f); + else if (capType is LineCapType.Capped) + newCornerLength = MathF.Min(cornerLength - newLineThickness, minLength * 0.5f - newLineThickness); + else //Capped Extended + newCornerLength = MathF.Min(cornerLength, minLength * 0.5f - newLineThickness); + + if (newCornerLength <= 0) continue; + + DrawCornerAbsoluteTransparent(prev, cur, next, newCornerLength, newLineThickness, color, capType, capPoints, miterLimit, beveled); } } /// - /// Draws lines from each corner of the polygon outward, with a uniform length for all corners. + /// Draws each corner of the polygon with a relative length based on , handling miter/bevel joins, custom cap types, and transparent colors. + /// Uses for line thickness, color, cap type, and cap points. /// - /// The polygon to draw. - /// The thickness of the lines. - /// The color of the lines. - /// The length of each corner line. + /// The polygon whose corners will be drawn. + /// + /// The factor (0-1) that determines the relative length of each corner segment to render. + /// 0: No corner is drawn. + /// 1: Maximum relative length. + /// + /// The line drawing information (thickness, color, cap type, etc.). + /// The miter limit for joins. If exceeded, the corner is either squared or beveled depending on . + /// If true, forces beveled joins instead of squared joins when the miter limit is exceeded. Beveling is faster and simpler but does not look as good. + public static void DrawCorneredRelativeTransparent(this Polygon poly, float cornerLengthFactor, LineDrawingInfo lineInfo, float miterLimit = 2f, + bool beveled = false) + { + poly.DrawCorneredRelativeTransparent(lineInfo.Thickness, lineInfo.Color, cornerLengthFactor, lineInfo.CapType, lineInfo.CapPoints, miterLimit, beveled); + } + + /// + /// Draws each corner of the polygon with a relative length specified per corner, handling miter/bevel joins, custom cap types, and transparent colors. + /// + /// The polygon whose corners will be drawn. + /// The thickness of the outline. + /// The color used for rendering, including alpha for transparency. + /// A list of relative corner length factors (0-1) for each corner. /// The type of line cap to use. /// The number of points for the cap. - /// - /// A corner is defined by three points: previous [i-1], current [i], and next [i+1]. - /// - public static void DrawCornered(this Polygon poly, float lineThickness, ColorRgba color, float cornerLength, LineCapType capType = LineCapType.CappedExtended, int capPoints = 2) + /// The miter limit for joins. If exceeded, the corner is either squared or beveled depending on . + /// If true, forces beveled joins instead of squared joins when the miter limit is exceeded. Beveling is faster and simpler but does not look as good. + public static void DrawCorneredRelativeTransparent(this Polygon poly, float lineThickness, ColorRgba color, List cornerLengthFactors, + LineCapType capType = LineCapType.CappedExtended, int capPoints = 2, + float miterLimit = 2f, bool beveled = false) { for (var i = 0; i < poly.Count; i++) { - var prev = poly[(i-1)%poly.Count]; + float cornerLengthFactor = cornerLengthFactors[i % cornerLengthFactors.Count]; + cornerLengthFactor = ShapeMath.Clamp(cornerLengthFactor, 0f, 1f); + var prev = poly[ShapeMath.WrapIndex(poly.Count, i - 1)]; var cur = poly[i]; - var next = poly[(i+1)%poly.Count]; - SegmentDrawing.DrawSegment(cur, cur + next.Normalize() * cornerLength, lineThickness, color, capType, capPoints); - SegmentDrawing.DrawSegment(cur, cur + prev.Normalize() * cornerLength, lineThickness, color, capType, capPoints); + var next = poly[ShapeMath.WrapIndex(poly.Count, i + 1)]; + + var wPrev = cur - prev; + var wNext = next - cur; + float lsPrev = wPrev.LengthSquared(); + float lsNext = wNext.LengthSquared(); + if (lsPrev <= 0 || lsNext <= 0) return; + + float minLength = MathF.Sqrt(MathF.Min(lsPrev, lsNext)); + float cornerLength = minLength * cornerLengthFactor; + float newLineThickness = MathF.Min(lineThickness, MathF.Min(cornerLength * 0.5f, minLength * 0.25f)); + float newCornerLength; + + if (capType is LineCapType.None) + newCornerLength = MathF.Min(cornerLength, minLength * 0.5f); + else if (capType is LineCapType.Extended) + newCornerLength = MathF.Min(cornerLength + newLineThickness, minLength * 0.5f); + else if (capType is LineCapType.Capped) + newCornerLength = MathF.Min(cornerLength - newLineThickness, minLength * 0.5f - newLineThickness); + else //Capped Extended + newCornerLength = MathF.Min(cornerLength, minLength * 0.5f - newLineThickness); + + if (newCornerLength <= 0) continue; + + DrawCornerAbsoluteTransparent(prev, cur, next, newCornerLength, newLineThickness, color, capType, capPoints, miterLimit, beveled); } } /// - /// Draws lines from each corner of the polygon outward, using a uniform relative factor for all corners. + /// Draws each corner of the polygon with a relative length specified per corner, handling miter/bevel joins, custom cap types, and transparent colors. + /// Uses for line thickness, color, cap type, and cap points. + /// + /// The polygon whose corners will be drawn. + /// A list of relative corner length factors (0-1) for each corner. + /// The line drawing information (thickness, color, cap type, etc.). + /// The miter limit for joins. If exceeded, the corner is either squared or beveled depending on . + /// If true, forces beveled joins instead of squared joins when the miter limit is exceeded. Beveling is faster and simpler but does not look as good. + public static void DrawCorneredRelativeTransparent(this Polygon poly, List cornerLengthFactors, LineDrawingInfo lineInfo, float miterLimit = 2f, + bool beveled = false) + { + poly.DrawCorneredRelativeTransparent(lineInfo.Thickness, lineInfo.Color, cornerLengthFactors, lineInfo.CapType, lineInfo.CapPoints, miterLimit, + beveled); + } + + #endregion + + #region Draw Cornered + + /// + /// Draws lines from each corner of the polygon outward, with a uniform length for all corners. /// /// The polygon to draw. /// The thickness of the lines. /// The color of the lines. - /// The factor (0-1) for all corners. + /// The length of each corner line. /// The type of line cap to use. /// The number of points for the cap. /// /// A corner is defined by three points: previous [i-1], current [i], and next [i+1]. - /// CornerF is used to interpolate from previous to current and from current to next point to form a corner. /// - public static void DrawCorneredRelative(this Polygon poly, float lineThickness, ColorRgba color, float cornerF, LineCapType capType = LineCapType.CappedExtended, int capPoints = 2) + public static void DrawCornered(this Polygon poly, float lineThickness, ColorRgba color, float cornerLength, + LineCapType capType = LineCapType.CappedExtended, int capPoints = 2) { + color = color.SetAlpha(255); for (var i = 0; i < poly.Count; i++) { - var prev = poly[(i - 1) % poly.Count]; + var prev = poly[ShapeMath.WrapIndex(poly.Count, i - 1)]; var cur = poly[i]; - var next = poly[(i + 1) % poly.Count]; - SegmentDrawing.DrawSegment(cur, cur.Lerp(next, cornerF), lineThickness, color, capType, capPoints); - SegmentDrawing.DrawSegment(cur, cur.Lerp(prev, cornerF), lineThickness, color, capType, capPoints); + var next = poly[ShapeMath.WrapIndex(poly.Count, i + 1)]; + SegmentDrawing.DrawSegment(cur, cur + (next - cur).Normalize() * cornerLength, lineThickness, color, capType, capPoints); + SegmentDrawing.DrawSegment(cur, cur + (prev - cur).Normalize() * cornerLength, lineThickness, color, capType, capPoints); } } @@ -653,236 +1741,185 @@ public static void DrawCorneredRelative(this Polygon poly, float lineThickness, /// public static void DrawCornered(this Polygon poly, float cornerLength, LineDrawingInfo lineInfo) { + lineInfo = lineInfo.SetColor(lineInfo.Color.SetAlpha(255)); for (var i = 0; i < poly.Count; i++) { - var prev = poly[(i-1)%poly.Count]; + var prev = poly[ShapeMath.WrapIndex(poly.Count, i - 1)]; var cur = poly[i]; - var next = poly[(i+1)%poly.Count]; - SegmentDrawing.DrawSegment(cur, cur + next.Normalize() * cornerLength, lineInfo); - SegmentDrawing.DrawSegment(cur, cur + prev.Normalize() * cornerLength, lineInfo); + var next = poly[ShapeMath.WrapIndex(poly.Count, i + 1)]; + SegmentDrawing.DrawSegment(cur, cur + (next - cur).Normalize() * cornerLength, lineInfo); + SegmentDrawing.DrawSegment(cur, cur + (prev - cur).Normalize() * cornerLength, lineInfo); } } + /// - /// Draws lines from each corner of the polygon outward, using a uniform relative factor for all corners, with . + /// Draws lines from each corner of the polygon outward, with custom lengths for each corner. /// /// The polygon to draw. - /// The factor (0-1) for all corners. - /// The line drawing information (thickness, color, cap type, etc.). + /// The thickness of the lines. + /// The color of the lines. + /// A list of lengths for each corner. Cycles if fewer than corners. + /// The type of line cap to use. + /// The number of points for the cap. /// + /// Useful for stylized or decorative polygon outlines. /// A corner is defined by three points: previous [i-1], current [i], and next [i+1]. - /// CornerF is used to interpolate from previous to current and from current to next point to form a corner. /// - public static void DrawCorneredRelative(this Polygon poly, float cornerF, LineDrawingInfo lineInfo) + public static void DrawCornered(this Polygon poly, float lineThickness, ColorRgba color, List cornerLengths, + LineCapType capType = LineCapType.CappedExtended, int capPoints = 2) { + color = color.SetAlpha(255); for (var i = 0; i < poly.Count; i++) { - var prev = poly[(i - 1) % poly.Count]; + float cornerLength = cornerLengths[i % cornerLengths.Count]; + var prev = poly[ShapeMath.WrapIndex(poly.Count, i - 1)]; var cur = poly[i]; - var next = poly[(i + 1) % poly.Count]; - SegmentDrawing.DrawSegment(cur, cur.Lerp(next, cornerF), lineInfo); - SegmentDrawing.DrawSegment(cur, cur.Lerp(prev, cornerF), lineInfo); + var next = poly[ShapeMath.WrapIndex(poly.Count, i + 1)]; + SegmentDrawing.DrawSegment(cur, cur + (next - cur).Normalize() * cornerLength, lineThickness, color, capType, capPoints); + SegmentDrawing.DrawSegment(cur, cur + (prev - cur).Normalize() * cornerLength, lineThickness, color, capType, capPoints); } } - /// - /// Draws lines from each corner of the polygon outward, with a uniform length for all corners, using . - /// - /// The polygon to draw. - /// The line drawing information (thickness, color, cap type, etc.). - /// The length of each corner line. - /// - /// A corner is defined by three points: previous [i-1], current [i], and next [i+1]. - /// - public static void DrawCornered(this Polygon poly, LineDrawingInfo lineInfo, float cornerLength) => DrawCornered(poly, lineInfo.Thickness,lineInfo.Color, cornerLength, lineInfo.CapType, lineInfo.CapPoints); - /// /// Draws lines from each corner of the polygon outward, with custom lengths for each corner, using . /// /// The polygon to draw. - /// The line drawing information (thickness, color, cap type, etc.). /// A list of lengths for each corner. Cycles if fewer than corners. - /// - /// A corner is defined by three points: previous [i-1], current [i], and next [i+1]. - /// - public static void DrawCornered(this Polygon poly, LineDrawingInfo lineInfo, List cornerLengths) => DrawCornered(poly, lineInfo.Thickness,lineInfo.Color, cornerLengths, lineInfo.CapType, lineInfo.CapPoints); - - /// - /// Draws lines from each corner of the polygon outward, using a uniform relative factor for all corners, with . - /// - /// The polygon to draw. /// The line drawing information (thickness, color, cap type, etc.). - /// The factor (0-1) for all corners. /// /// A corner is defined by three points: previous [i-1], current [i], and next [i+1]. - /// CornerF is used to interpolate from previous to current and from current to next point to form a corner. /// - public static void DrawCorneredRelative(this Polygon poly, LineDrawingInfo lineInfo, float cornerF) => DrawCornered(poly, lineInfo.Thickness,lineInfo.Color, cornerF, lineInfo.CapType, lineInfo.CapPoints); - - /// - /// Draws lines from each corner of the polygon outward, using relative factors for each corner, with . - /// - /// The polygon to draw. - /// The line drawing information (thickness, color, cap type, etc.). - /// A list of factors (0-1) for each corner. Cycles if fewer than corners. - /// - /// A corner is defined by three points: previous [i-1], current [i], and next [i+1]. - /// CornerFactors are used to interpolate from previous to current and from current to next point to form a corner. - /// - public static void DrawCorneredRelative(this Polygon poly, LineDrawingInfo lineInfo, List cornerFactors) => DrawCornered(poly, lineInfo.Thickness,lineInfo.Color, cornerFactors, lineInfo.CapType, lineInfo.CapPoints); + public static void DrawCornered(this Polygon poly, List cornerLengths, LineDrawingInfo lineInfo) + { + lineInfo = lineInfo.SetColor(lineInfo.Color.SetAlpha(255)); + for (var i = 0; i < poly.Count; i++) + { + float cornerLength = cornerLengths[i % cornerLengths.Count]; + var prev = poly[ShapeMath.WrapIndex(poly.Count, i - 1)]; + var cur = poly[i]; + var next = poly[ShapeMath.WrapIndex(poly.Count, i + 1)]; + SegmentDrawing.DrawSegment(cur, cur + (next - cur).Normalize() * cornerLength, lineInfo); + SegmentDrawing.DrawSegment(cur, cur + (prev - cur).Normalize() * cornerLength, lineInfo); + } + } + #endregion + + #region Gapped /// - /// Draws a polygon where each side can be scaled towards the origin of the side. + /// Draws a gapped outline for a polygon, creating a dashed or segmented effect along the polygon's perimeter. /// /// The polygon to draw. - /// The line drawing information (thickness, color, cap type, etc.). - /// - /// The scale factor for each side. - /// - /// 0: No polyline is drawn. - /// 1: The normal polyline is drawn. - /// 0.5: Each side is half as long. - /// - /// - /// - /// The point along the line to scale from, in both directions (0 to 1). - /// - /// 0: Start of Segment - /// 0.5: Center of Segment - /// 1: End of Segment - /// + /// + /// The total length of the polygon's perimeter. + /// If zero or negative, the method calculates it automatically. + /// Providing a known length avoids redundant calculations and improves performance, especially for static segments. /// + /// Parameters describing how to draw the outline. + /// Parameters describing the gap configuration. + /// + /// The perimeter of the polygon if positive; otherwise, -1. + /// If the shape does not change, the valid length can be reused in subsequent frames to avoid recalculating. + /// /// - /// Useful for creating stylized or animated polygon outlines. + /// - If is 0 or is 0, the outline is drawn solid. + /// - If is 1 or greater, no outline is drawn. /// - public static void DrawLinesScaled(this Polygon poly, LineDrawingInfo lineInfo, float sideScaleFactor, float sideScaleOrigin = 0.5f) + public static float DrawGappedOutline(this Polygon poly, float perimeter, LineDrawingInfo lineInfo, GappedOutlineDrawingInfo gapDrawingInfo) { - if (poly.Count < 3) return; - if (sideScaleFactor <= 0) return; - - if (sideScaleFactor >= 1) + if (gapDrawingInfo.Gaps <= 0 || gapDrawingInfo.GapPerimeterPercentage <= 0f) { poly.DrawLines(lineInfo); - return; + return perimeter > 0f ? perimeter : -1f; } - for (var i = 0; i < poly.Count; i++) + + if (gapDrawingInfo.GapPerimeterPercentage >= 1f) return perimeter > 0f ? perimeter : -1f; + + var nonGapPercentage = 1f - gapDrawingInfo.GapPerimeterPercentage; + + var gapPercentageRange = gapDrawingInfo.GapPerimeterPercentage / gapDrawingInfo.Gaps; + var nonGapPercentageRange = nonGapPercentage / gapDrawingInfo.Gaps; + + if (perimeter <= 0f) { - var start = poly[i]; - var end = poly[(i + 1) % poly.Count]; - SegmentDrawing.DrawSegment(start, end, lineInfo, sideScaleFactor, sideScaleOrigin); + for (int i = 0; i < poly.Count; i++) + { + var curP = poly[i]; + var nextP = poly[(i + 1) % poly.Count]; + + perimeter += (nextP - curP).Length(); + } } - - } - /// - /// Draws a polygon where each side can be scaled towards the origin of the side, applying position, size, and rotation. - /// - /// The polygon to draw, with points relative to the origin. - /// The position of the polygon's center. - /// The scale factor for the polygon. - /// The rotation in degrees. - /// The line drawing information (thickness, color, cap type, etc.). - /// - /// The scale factor for each side. - /// - /// 0: No polyline is drawn. - /// 1: The normal polyline is drawn. - /// 0.5: Each side is half as long. - /// - /// - /// - /// The point along the line to scale from, in both directions (0 to 1). - /// - /// 0: Start of Segment - /// 0.5: Center of Segment - /// 1: End of Segment - /// - /// - public static void DrawLinesScaled(this Polygon relative, Vector2 pos, float size, float rotDeg, LineDrawingInfo lineInfo, float sideScaleFactor, float sideScaleOrigin = 0.5f) - { - if (relative.Count < 3) return; - if (sideScaleFactor <= 0) return; + var startDistance = perimeter * gapDrawingInfo.StartOffset; + var curDistance = 0f; + var nextDistance = startDistance; - if (sideScaleFactor >= 1) - { - relative.DrawLines(pos, size, rotDeg, lineInfo); - return; - } - for (var i = 0; i < relative.Count; i++) - { - var start = pos + (relative[i] * size).Rotate(rotDeg * ShapeMath.DEGTORAD); - var end = pos + (relative[(i + 1) % relative.Count] * size).Rotate(rotDeg * ShapeMath.DEGTORAD); - SegmentDrawing.DrawSegment(start, end, lineInfo, sideScaleFactor, sideScaleOrigin); - } + var curIndex = 0; + var curPoint = poly[0]; + var nextPoint= poly[1 % poly.Count]; + var curW = nextPoint - curPoint; + var curDis = curW.Length(); - } + var points = new List(3); - /// - /// Draws a polygon where each side can be scaled towards the origin of the side, using a . - /// - /// The polygon to draw, with points relative to the origin. - /// The transform to apply (position, scale, rotation). - /// The line drawing information (thickness, color, cap type, etc.). - /// - /// The scale factor for each side. - /// - /// 0: No polyline is drawn. - /// 1: The normal polyline is drawn. - /// 0.5: Each side is half as long. - /// - /// - /// - /// The point along the line to scale from, in both directions (0 to 1). - /// - /// 0: Start of Segment - /// 0.5: Center of Segment - /// 1: End of Segment - /// - /// - public static void DrawLinesScaled(this Polygon relative, Transform2D transform, LineDrawingInfo lineInfo, float sideScaleFactor, float sideScaleOrigin = 0.5f) - { - DrawLinesScaled(relative, transform.Position, transform.ScaledSize.Length, transform.RotationDeg, lineInfo, sideScaleFactor, sideScaleOrigin); + int whileCounter = gapDrawingInfo.Gaps; + + while (whileCounter > 0) + { + if (curDistance + curDis >= nextDistance) + { + var p = curPoint + (curW / curDis) * (nextDistance - curDistance); + + + if (points.Count == 0) + { + nextDistance += nonGapPercentageRange * perimeter; + points.Add(p); - } - - /// - /// Draws the polygon with a glow effect, interpolating width and color along each segment. - /// - /// The polygon to draw. - /// The starting width of the glow. Should be bigger than endWidth - /// The ending width of the glow. Should be smaller than width - /// The starting color of the glow. - /// The ending color of the glow. - /// The number of interpolation steps for the glow effect. - /// The type of line cap to use at segment ends. - /// The number of points used for the cap rendering. - /// - /// This method creates a glowing outline effect by drawing multiple segments on top of each other, interpolating width and color across all steps. - /// - /// The first step uses and . - /// The last step uses and . - /// Intermediate steps interpolate between / and / . - /// Because steps are drawn on top of each other should be bigger than . - /// - /// - public static void DrawGlow(this Polygon polygon, float width, float endWidth, ColorRgba color, - ColorRgba endColorRgba, int steps, LineCapType capType = LineCapType.CappedExtended, int capPoints = 2) - { - if (polygon.Count < 2 || steps <= 0) return; + } + else + { + nextDistance += gapPercentageRange * perimeter; + points.Add(p); + if (points.Count == 2) + { + SegmentDrawing.DrawSegment(points[0], points[1], lineInfo); + } + else + { + for (var i = 0; i < points.Count - 1; i++) + { + var p1 = points[i]; + var p2 = points[(i + 1) % points.Count]; + SegmentDrawing.DrawSegment(p1, p2, lineInfo); + } + } + + points.Clear(); + whileCounter--; + } - if (steps == 1) - { - DrawLines(polygon, width, color, capType, capPoints); - return; - } - - for (var s = 0; s < steps; s++) - { - var f = s / (float)(steps - 1); - var currentWidth = ShapeMath.LerpFloat(width, endWidth, f); - var currentColor = color.Lerp(endColorRgba, f); - DrawLines(polygon, currentWidth, currentColor, capType, capPoints); + } + else + { + + if(points.Count > 0) points.Add(nextPoint); + + curDistance += curDis; + curIndex = (curIndex + 1) % poly.Count; + curPoint = poly[curIndex]; + nextPoint = poly[(curIndex + 1) % poly.Count]; + curW = nextPoint - curPoint; + curDis = curW.Length(); + } + } + + return perimeter; } + + #endregion } \ No newline at end of file diff --git a/ShapeEngine/Geometry/PolygonDef/PolygonMath.cs b/ShapeEngine/Geometry/PolygonDef/PolygonMath.cs index c706ff34..23d268ee 100644 --- a/ShapeEngine/Geometry/PolygonDef/PolygonMath.cs +++ b/ShapeEngine/Geometry/PolygonDef/PolygonMath.cs @@ -1,51 +1,101 @@ using System.Numerics; +using ShapeEngine.ShapeClipper; using ShapeEngine.Core.Structs; +using ShapeEngine.Geometry.CircleDef; using ShapeEngine.Geometry.PointsDef; +using ShapeEngine.Geometry.PolylineDef; +using ShapeEngine.Geometry.RayDef; +using ShapeEngine.Geometry.SegmentDef; +using ShapeEngine.Geometry.TriangulationDef; using ShapeEngine.StaticLib; namespace ShapeEngine.Geometry.PolygonDef; - public partial class Polygon { #region Math + /// - /// Gets the projected shape points by translating each vertex by a vector. + /// Computes an approximate incircle (largest inscribed circle) for the polygon by searching + /// for a point inside the polygon that maximizes the distance to the nearest edge. + /// The method uses iterative radial sampling (hill-climb) from an initial seed to refine + /// the circle center. /// - /// The vector to project along. - /// A collection of projected points, or null if the vector is zero. - public Points? GetProjectedShapePoints(Vector2 v) + /// Number of outer iterations to perform. Higher values increase refinement. + /// Number of radial samples per iteration. Higher values increase angular resolution. + /// + /// A whose center is the found interior point and whose radius is the distance + /// from that point to the closest polygon edge. For polygons with fewer than three vertices a + /// degenerate circle is returned (zero radius or default center). + /// + /// + /// The algorithm is heuristic and may return a suboptimal incircle for complex or highly concave polygons. + /// Default parameters balance performance and accuracy. + /// + public Circle GetIncircle(int iterations = 100, int samples = 100) { - if (v.LengthSquared() <= 0f) return null; + if (Count < 3) return new(new Vector2(), 0f); - var points = new Points(Count); - for (var i = 0; i < Count; i++) + var bounds = GetBoundingBox(); + if (bounds.Size.Width <= 0f || bounds.Size.Height <= 0f) { - points.Add(this[i]); - points.Add(this[i] + v); + return new(bounds.Center, 0f); } - return points; - } - /// - /// Projects the polygon along a vector and returns the convex hull of the result. - /// - /// The vector to project along. - /// A new representing the projected convex hull, - /// or null if the vector is zero. - public Polygon? ProjectShape(Vector2 v) - { - if (v.LengthSquared() <= 0f || Count < 3) return null; + var bestCenter = GetCentroid(); + if (!ContainsPoint(bestCenter)) + { + // Fallback if centroid is outside (can happen with concave polygons) + // Find a point inside the polygon to start. + // A simple way is to average the first three vertices. + if (Count >= 3) + { + bestCenter = (this[0] + this[1] + this[2]) / 3.0f; + if (!ContainsPoint(bestCenter)) // if that is also outside, fallback to bounds center + { + bestCenter = bounds.Center; + } + } + else + { + bestCenter = bounds.Center; + } + } - var points = new Points(Count * 2); - for (var i = 0; i < Count; i++) + GetClosestSegment(bestCenter, out float disSquared); + + // Iteratively search for a better center point + for (int i = 0; i < iterations; i++) { - points.Add(this[i]); - points.Add(this[i] + v); + bool foundBetter = false; + float searchRadius = bounds.Size.Length * (1.0f / (i + 1)); // Decrease search radius over time + + for (int j = 0; j < samples; j++) + { + float angle = (float)j / samples * 2.0f * MathF.PI; + Vector2 testPoint = bestCenter + new Vector2(MathF.Cos(angle), MathF.Sin(angle)) * searchRadius; + + if (ContainsPoint(testPoint)) + { + GetClosestSegment(testPoint, out float newDisSquared); + if (newDisSquared > disSquared) + { + disSquared = newDisSquared; + bestCenter = testPoint; + foundBetter = true; + } + } + } + + if (!foundBetter && i > 10) // Early exit if no improvement is found + { + break; + } } - return FindConvexHull(points); + return new Circle(bestCenter, MathF.Sqrt(disSquared)); } + /// /// Calculates the centroid (center of mass) of the polygon. /// @@ -71,22 +121,8 @@ public Vector2 GetCentroid() area *= 0.5f; return centroid / (area * 6); - - //return GetCentroidMean(); - // Vector2 result = new(); - - // for (int i = 0; i < Count; i++) - // { - // var a = this[i]; - // var b = this[(i + 1) % Count]; - //// float factor = a.X * b.Y - b.X * a.Y; //clockwise - // float factor = a.Y * b.X - a.X * b.Y; //counter clockwise - // result.X += (a.X + b.X) * factor; - // result.Y += (a.Y + b.Y) * factor; - // } - - // return result * (1f / (GetArea() * 6f)); } + /// /// Calculates the perimeter (total edge length) of the polygon. /// @@ -103,6 +139,7 @@ public float GetPerimeter() return length; } + /// /// Calculates the squared perimeter of the polygon. /// @@ -119,6 +156,7 @@ public float GetPerimeterSquared() return lengthSq; } + /// /// Calculates the squared diameter (maximum distance from centroid to a vertex). /// @@ -140,6 +178,7 @@ private float GetDiameterSquared() return maxDisSquared; } + /// /// Calculates the diameter (maximum distance from centroid to a vertex). /// @@ -149,6 +188,7 @@ private float GetDiameter() if (Count <= 2) return 0; return MathF.Sqrt(GetDiameterSquared()); } + /// /// Calculates the signed area of the polygon. /// @@ -168,6 +208,19 @@ public float GetArea() return area / 2f; } + + /// + /// Calculates the absolute area of the polygon, independent of winding order. + /// + /// The non-negative area of the polygon. + /// + /// This is equivalent to MathF.Abs(GetArea()) and is useful when only the area magnitude matters. + /// + public float GetAreaAbs() + { + return MathF.Abs(GetArea()); + } + /// /// Determines if the polygon's winding order is clockwise. /// @@ -196,19 +249,19 @@ public bool IsConvex() { int num = Count; if (num < 3) return false; - + bool? sign = null; for (var i = 0; i < num; i++) { - int prev = (i + num - 1) % num;//wraps around to last index if i is 0 + int prev = (i + num - 1) % num; //wraps around to last index if i is 0 int next = (i + 1) % num; //wraps around to 0 if i is last index var d0 = this[i] - this[prev]; var d1 = this[next] - this[i]; float cross = d0.Cross(d1); - + //Ignores collinear points and only sets the reference sign on the first valid cross product. if (cross == 0f) continue; - + //Checks if the current sign of the cross-product is the same as the last //If it is not, the polygon is self-intersecting bool currentSign = cross > 0f; @@ -217,11 +270,11 @@ public bool IsConvex() else if (sign != currentSign) return false; } - + //all cross-product signs were either collinear or the same. return true; } - + /// /// Determines if a polygon defined by a list of vertices is convex. /// @@ -235,26 +288,26 @@ public bool IsConvex() /// This means none of its vertices "point inward," and the polygon does not have any indentations. /// Concave: /// A concave polygon has at least one interior angle greater than 180° - /// and at least one vertex that points inward (self-intersecting). + /// and at least one inward-pointing indentation. A concave polygon is not necessarily self-intersecting. /// /// public static bool IsConvex(List vertices) { int num = vertices.Count; if (num < 3) return false; - + bool? sign = null; for (var i = 0; i < num; i++) { - int prev = (i + num - 1) % num;//wraps around to last index if i is 0 + int prev = (i + num - 1) % num; //wraps around to last index if i is 0 int next = (i + 1) % num; //wraps around to 0 if i is last index var d0 = vertices[i] - vertices[prev]; var d1 = vertices[next] - vertices[i]; float cross = d0.Cross(d1); - + //Ignores collinear points and only sets the reference sign on the first valid cross-product. if (cross == 0f) continue; - + //Checks if the current sign of the cross-product is the same as the last //If it is not, the polygon is self-intersecting (concave) bool currentSign = cross > 0f; @@ -263,12 +316,11 @@ public static bool IsConvex(List vertices) else if (sign != currentSign) return false; } - + //all cross-product signs were either collinear or the same. return true; } - - + /// /// Converts the polygon to a collection. /// @@ -277,33 +329,134 @@ public Points ToPoints() { return new(this); } + /// - /// Calculates the mean centroid (average of all vertices). + /// Copies this polygon's vertices into the provided collection. /// - /// The mean centroid as a . - public Vector2 GetCentroidMean() + /// The destination collection that will be cleared and populated with the polygon's vertices. + /// + /// This method does not modify the polygon itself. It reuses by clearing it before adding all vertices. + /// + public void ToPoints(Points result) { - if (Count <= 0) return new(0f); - if (Count == 1) return this[0]; - if (Count == 2) return (this[0] + this[1]) / 2; - if (Count == 3) return (this[0] + this[1] + this[2]) / 3; - var total = new Vector2(0f); - foreach (var p in this) - { - total += p; - } - - return total / Count; + result.Clear(); + result.EnsureCapacity(Count); + result.AddRange(this); } + /// /// Computes the length of this polygon's apothem. Only valid for regular polygons. /// /// The length of the apothem. /// More info: http://en.wikipedia.org/wiki/Apothem public float GetApothem() => (this.GetCentroid() - (this[0].Lerp(this[1], 0.5f))).Length(); + + /// + /// Calculates the maximum line thickness that can be safely used to draw the outline of a polygon + /// without causing self-intersections or rendering artifacts. The result is scaled by the given safety margin factor. + /// + /// + /// A factor (0-1) to reduce the maximum thickness for safety. Default is 0.95 (5% margin). + /// + /// The maximum safe line thickness for the polygon, or 0 if the polygon is invalid. + public float CalculatePolygonMaxLineThickness(float safetyMarginFactor = 0.95f) + { + if (Count < 3) return 0f; + + var minDisSquared = float.MaxValue; + + Vector2 lastPoint = Vector2.Zero, lastDir = Vector2.Zero; + for (var i = 0; i <= Count; i++) + { + var prev = this[ShapeMath.WrapIndex(Count, i - 1)]; + var cur = this[ShapeMath.WrapIndex(Count, i)]; + var next = this[ShapeMath.WrapIndex(Count, i + 1)]; + + var wPrev = cur - prev; + var wNext = next - cur; + float lsPrev = wPrev.LengthSquared(); + float lsNext = wNext.LengthSquared(); + if (lsPrev <= 0 || lsNext <= 0) continue; + + var dirPrev = wPrev.Normalize(); + var dirNext = wNext.Normalize(); + + var normalPrev = dirPrev.GetPerpendicularRight(); + var normalNext = dirNext.GetPerpendicularRight(); + + var miterDir = (normalPrev + normalNext).Normalize(); + if (lastDir == Vector2.Zero) + { + lastPoint = cur; + lastDir = miterDir; + continue; + } + + var intersection = Ray.IntersectRayRay(lastPoint, -lastDir, cur, -miterDir); + if (intersection.Valid) + { + float curLsSquared = (intersection.Point - cur).LengthSquared(); + float prevLsSquared = (intersection.Point - lastPoint).LengthSquared(); + if (curLsSquared > 0 && prevLsSquared > 0) + { + float min = MathF.Min(curLsSquared, prevLsSquared); + if (min < minDisSquared) minDisSquared = min; + } + } + + lastPoint = cur; + lastDir = miterDir; + } + + return MathF.Sqrt(minDisSquared) * safetyMarginFactor; + } + #endregion - + #region Transform + + /// + /// Converts this polygon from local coordinates to absolute world coordinates by applying + /// the provided . Vertices are scaled, rotated and translated + /// using the transform's scaled size, rotation (in radians) and position. + /// + /// Transform containing Position, RotationRad and ScaledSize to apply. + /// + /// A new with transformed (absolute) vertices, or null if this polygon has fewer than three vertices. + /// + public Polygon? ToAbsolute(Transform2D transform) + { + if (Count < 3) return null; + var newPolygon = new Polygon(Count); + for (var i = 0; i < Count; i++) + { + var p = transform.Position + (this[i] * transform.ScaledSize.Radius).Rotate(transform.RotationRad); + newPolygon.Add(p); + } + + return newPolygon; + } + + /// + /// Transforms this polygon's vertices into absolute coordinates and writes them into the provided list. + /// + /// The destination list that will be cleared and populated with transformed vertices. + /// The transform containing the position, rotation, and scaled size to apply. + /// + /// This method does not modify the polygon itself. If the polygon contains no vertices, the method returns without changing . + /// + public void ToAbsolute(List result, Transform2D transform) + { + if(Count <= 0) return; + result.Clear(); + result.EnsureCapacity(Count); + for (var i = 0; i < Count; i++) + { + var p = transform.Position + (this[i] * transform.ScaledSize.Radius).Rotate(transform.RotationRad); + result.Add(p); + } + } + /// /// Sets the position of the polygon's centroid. /// @@ -314,6 +467,7 @@ public void SetPosition(Vector2 newPosition) var delta = newPosition - centroid; ChangePosition(delta); } + /// /// Rotates the polygon by a given angle in radians. /// @@ -328,6 +482,7 @@ public void ChangeRotation(float rotRad) this[i] = origin + w.Rotate(rotRad); } } + /// /// Sets the absolute rotation of the polygon. /// @@ -341,6 +496,7 @@ public void SetRotation(float angleRad) var rotRad = ShapeMath.GetShortestAngleRad(curAngle, angleRad); ChangeRotation(rotRad, origin); } + /// /// Scales the polygon uniformly about its centroid. /// @@ -355,6 +511,7 @@ public void ScaleSize(float scale) this[i] = origin + w * scale; } } + /// /// Changes the size of the polygon by a given amount (relative to each vertex). /// @@ -369,6 +526,7 @@ public void ChangeSize(float amount) this[i] = origin + w.ChangeLength(amount); } } + /// /// Sets the size (distance from centroid) of the polygon. /// @@ -383,225 +541,641 @@ public void SetSize(float size) this[i] = origin + w.SetLength(size); } } + + /// - /// Returns a copy of the polygon with its centroid set to a new position. + /// Copies this polygon's vertices into , translated so the polygon centroid moves to . /// + /// The destination collection that will be cleared and populated with the translated vertices. /// The new centroid position. - /// A new with the updated position, or null if invalid. - public Polygon? SetPositionCopy(Vector2 newPosition) + /// true if the polygon contains at least one vertex and was populated; otherwise, false. + /// + /// This method does not modify the current polygon. It uses as the origin for the translation. + /// + public new bool SetPositionCopy(Points result, Vector2 newPosition) { - if (Count < 3) return null; - var centroid = GetCentroid(); - var delta = newPosition - centroid; - return ChangePositionCopy(delta); + return SetPositionCopy(result, newPosition, GetCentroid()); } + /// - /// Returns a copy of the polygon translated by an offset. + /// Copies this polygon's vertices into , rotated around the polygon centroid by the specified angle. /// - /// The translation offset. - /// A new with the updated position, or null if invalid. - public new Polygon? ChangePositionCopy(Vector2 offset) + /// The destination collection that will be cleared and populated with the rotated vertices. + /// The rotation angle in radians. + /// true if the polygon contains at least one vertex and was populated; otherwise, false. + /// + /// This method does not modify the current polygon. It uses as the rotation origin. + /// + public new bool ChangeRotationCopy(Points result, float rotRad) { - if (Count < 3) return null; - var newPolygon = new Polygon(this.Count); - for (int i = 0; i < Count; i++) - { - newPolygon.Add(this[i] + offset); - } - - return newPolygon; + return ChangeRotationCopy(result, rotRad, GetCentroid()); } + /// - /// Returns a copy of the polygon rotated by a given angle around an origin. + /// Copies this polygon's vertices into , rotating them around the polygon centroid so the first vertex aligns with the target angle. /// - /// The rotation angle in radians. - /// The origin to rotate around. - /// A new with the updated rotation, or null if invalid. - public new Polygon? ChangeRotationCopy(float rotRad, Vector2 origin) + /// The destination collection that will be cleared and populated with the rotated vertices. + /// The target rotation angle in radians. + /// true if the polygon contains at least one vertex and was populated; otherwise, false. + /// + /// This method does not modify the current polygon. It uses as the rotation origin. + /// + public new bool SetRotationCopy(Points result, float angleRad) { - if (Count < 3) return null; - var newPolygon = new Polygon(this.Count); - for (var i = 0; i < Count; i++) - { - var w = this[i] - origin; - newPolygon.Add(origin + w.Rotate(rotRad)); - } - - return newPolygon; + return SetRotationCopy(result, angleRad, GetCentroid()); } + /// - /// Returns a copy of the polygon rotated by a given angle around its centroid. + /// Copies this polygon's vertices into , scaled uniformly relative to the polygon centroid. /// - /// The rotation angle in radians. - /// A new with the updated rotation, or null if invalid. - public Polygon? ChangeRotationCopy(float rotRad) + /// The destination collection that will be cleared and populated with the scaled vertices. + /// The scale factor. + /// true if the polygon contains at least one vertex and was populated; otherwise, false. + /// + /// This method does not modify the current polygon. It uses as the scaling origin. + /// + public new bool ScaleSizeCopy(Points result, float scale) { - if (Count < 3) return null; - return ChangeRotationCopy(rotRad, GetCentroid()); + return ScaleSizeCopy(result, scale, GetCentroid()); } + /// - /// Returns a copy of the polygon with its absolute rotation set around an origin. + /// Copies this polygon's vertices into , changing each vertex distance from the polygon centroid by the specified amount. /// - /// The target rotation angle in radians. - /// The origin to rotate around. - /// A new with the updated rotation, or null if invalid. - public new Polygon? SetRotationCopy(float angleRad, Vector2 origin) + /// The destination collection that will be cleared and populated with the resized vertices. + /// The amount to change the size. + /// true if the polygon contains at least one vertex and was populated; otherwise, false. + /// + /// This method does not modify the current polygon. It uses as the size-adjustment origin. + /// + public new bool ChangeSizeCopy(Points result,float amount) { - if (Count < 3) return null; - var curAngle = (this[0] - origin).AngleRad(); - var rotRad = ShapeMath.GetShortestAngleRad(curAngle, angleRad); - return ChangeRotationCopy(rotRad, origin); + return ChangeSizeCopy(result, amount, GetCentroid()); } + /// - /// Returns a copy of the polygon with its absolute rotation set around its centroid. + /// Copies this polygon's vertices into , setting each vertex to the specified distance from the polygon centroid. /// - /// The target rotation angle in radians. - /// A new with the updated rotation, or null if invalid. - public Polygon? SetRotationCopy(float angleRad) + /// The destination collection that will be cleared and populated with the resized vertices. + /// The new size (distance from centroid to each vertex). + /// true if the polygon contains at least one vertex and was populated; otherwise, false. + /// + /// This method does not modify the current polygon. It uses as the reference origin. + /// + public new bool SetSizeCopy(Points result, float size) { - if (Count < 3) return null; + return SetSizeCopy(result, size, GetCentroid()); + } - var origin = GetCentroid(); - var curAngle = (this[0] - origin).AngleRad(); - var rotRad = ShapeMath.GetShortestAngleRad(curAngle, angleRad); - return ChangeRotationCopy(rotRad, origin); + /// + /// Copies this polygon's vertices into , applying the specified transform relative to the polygon centroid. + /// + /// The destination collection that will be cleared and populated with the transformed vertices. + /// The transform to apply. + /// true if the polygon contains at least one vertex and was populated; otherwise, false. + /// + /// This method does not modify the current polygon. It uses as the transformation origin. + /// + public new bool SetTransformCopy(Points result, Transform2D transform) + { + return SetTransformCopy(result, transform, GetCentroid()); } + /// - /// Returns a copy of the polygon scaled uniformly about its centroid. + /// Copies this polygon's vertices into , applying the specified offset transform relative to the polygon centroid. /// - /// The scale factor. - /// A new with the updated scale, or null if invalid. - public Polygon? ScaleSizeCopy(float scale) + /// The destination collection that will be cleared and populated with the transformed vertices. + /// The offset transform to apply. + /// true if the polygon contains at least one vertex and was populated; otherwise, false. + /// + /// This method does not modify the current polygon. It uses as the transformation origin. + /// + public new bool ApplyOffsetCopy(Points result, Transform2D offset) { - if (Count < 3) return null; - return ScaleSizeCopy(scale, GetCentroid()); + return ApplyOffsetCopy(result, offset, GetCentroid()); } + + #endregion + + #region Generate Rounded Corners + /// - /// Returns a copy of the polygon scaled uniformly about a given origin. + /// Creates a copy of this polygon with rounded corners. /// - /// The scale factor. - /// The origin to scale about. - /// A new with the updated scale, or null if invalid. - public new Polygon? ScaleSizeCopy(float scale, Vector2 origin) + /// Number of points to use to approximate each rounded corner. Must be > 0. + /// + /// Relative strength/size of the rounded corner. Expected range [0, 1]. Defaults to 0.5f. + /// Larger values produce a larger rounded arc limited by adjacent edge lengths. + /// + /// + /// Angle threshold in degrees below which three consecutive vertices are considered collinear + /// and the corner is left unchanged. Defaults to 5 degrees. + /// + /// + /// Minimum adjacent edge length required to attempt rounding. If either adjacent edge is shorter + /// than this threshold the original vertex is preserved. Defaults to 1.0f. + /// + /// + /// A new containing the rounded-corner approximation, or null if the + /// input parameters are invalid (for example: non-positive cornerPoints or cornerStrength out of range). + /// + /// + /// The implementation clamps corner geometry so rounded arcs do not extend beyond portions of adjacent edges. + /// If is 1 a single replacement point is inserted for the rounded corner. + /// Complex or highly concave polygons may yield unexpected results since this is a geometric approximation. + /// + public Polygon? RoundCopy(int cornerPoints, float cornerStrength = 0.5f, float collinearAngleThresholdDeg = 5f, float distanceThreshold = 1f) { - if (Count < 3) return null; - var newPolygon = new Polygon(this.Count); + if (cornerPoints <= 0 || Count < 3 || cornerStrength <= 0 || cornerStrength > 1) return null; - for (var i = 0; i < Count; i++) + var roundedPolygon = new Polygon(); + + if (RoundCopy(ref roundedPolygon, cornerPoints, cornerStrength, collinearAngleThresholdDeg, distanceThreshold)) { - var w = this[i] - origin; - newPolygon.Add(origin + w * scale); + return roundedPolygon; } - return newPolygon; + return null; } + /// - /// Returns a copy of the polygon scaled non-uniformly about a given origin. + /// Attempts to produce a rounded-corner copy of this polygon and write it into . /// - /// The scale vector. - /// The origin to scale about. - /// A new with the updated scale, or null if invalid. - public new Polygon? ScaleSizeCopy(Vector2 scale, Vector2 origin) + /// + /// Reference to a that will be cleared and populated with the rounded polygon + /// when the operation succeeds. + /// + /// Number of points to use to approximate each rounded corner. Must be > 0. + /// + /// Relative strength/size of the rounded corner. Expected range (0,1]. Larger values produce larger arcs. + /// Defaults to 0.5f. + /// + /// + /// Angle threshold in degrees under which three consecutive vertices are considered collinear and left unchanged. + /// Defaults to 5f. + /// + /// + /// Minimum adjacent edge length required to attempt rounding. If either adjacent edge is shorter than this value + /// the original vertex is preserved. Defaults to 1f. + /// + /// + /// True if rounding was applied successfully and contains the rounded polygon; + /// otherwise false for invalid parameters or when no modification was performed. + /// + /// + /// This method is a non-mutating operation with respect to the source polygon (it writes results into + /// ). The method performs parameter validation and will clear + /// before populating it when processing occurs. + /// + public bool RoundCopy(ref Polygon copy, int cornerPoints, float cornerStrength = 0.5f, float collinearAngleThresholdDeg = 5f, float distanceThreshold = 1f) { - if (Count < 3) return null; - var newPolygon = new Polygon(this.Count); + if (cornerPoints <= 0 || Count < 3 || cornerStrength <= 0 || cornerStrength > 1) return false; + + copy.Clear(); for (var i = 0; i < Count; i++) { - var w = this[i] - origin; - newPolygon.Add(origin + w * scale); + var prevP = this[ShapeMath.WrapIndex(Count, i - 1)]; + var p = this[i]; + var nextP = this[ShapeMath.WrapIndex(Count, i + 1)]; + + var prevEdge = p - prevP; + var curEdge = nextP - p; + + float prevEdgeLength = prevEdge.Length(); + float curEdgeLength = curEdge.Length(); + + if (prevEdgeLength < distanceThreshold || curEdgeLength < distanceThreshold) + { + copy.Add(p); + continue; + } + + if (ShapeVec.IsColinearAngle(prevP, p, nextP, collinearAngleThresholdDeg)) + { + copy.Add(p); + continue; + } + + var v1 = (prevP - p).Normalize(); + var v2 = (nextP - p).Normalize(); + + float angle = MathF.Acos(Vector2.Dot(v1, v2)); + float cornerRadius = MathF.Min(prevEdgeLength, curEdgeLength) * 0.5f * cornerStrength; + + float t = cornerRadius / MathF.Tan(angle / 2); + + // Prevent the rounded corner from extending beyond the midpoint of the adjacent edges + t = MathF.Min(t, prevEdgeLength * 0.45f); //45 used as a safety margin (from 50) + t = MathF.Min(t, curEdgeLength * 0.45f); //45 used as a safety margin (from 50) + + // Recalculate cornerRadius based on the clamped t + cornerRadius = t * MathF.Tan(angle / 2); + + var startPoint = p + v1 * t; + var endPoint = p + v2 * t; + + var center = p + (v1 + v2).Normalize() * (cornerRadius / MathF.Sin(angle / 2)); + + float startAngle = (startPoint - center).AngleRad(); + float endAngle = (endPoint - center).AngleRad(); + + float angleDiff = ShapeMath.GetShortestAngleRad(startAngle, endAngle); + + if (cornerPoints == 1) + { + copy.Add(p + (v1 + v2) * t); + } + else + { + for (var j = 0; j < cornerPoints; j++) + { + float frac = j / (float)(cornerPoints - 1); + float currentAngle = startAngle + angleDiff * frac; + copy.Add(center + new Vector2(MathF.Cos(currentAngle), MathF.Sin(currentAngle)) * cornerRadius); + } + } } - return newPolygon; + return true; } + /// - /// Returns a copy of the polygon with its size changed by a given amount about a given origin. + /// Creates rounded corners for this polygon in-place by replacing each sharp vertex with + /// an approximated arc of points. The method mutates the polygon's vertex list. /// - /// The amount to change the size. - /// The origin to scale about. - /// A new with the updated size, or null if invalid. - public new Polygon? ChangeSizeCopy(float amount, Vector2 origin) + /// + /// Number of points used to approximate each rounded corner. Must be greater than 0. + /// If set to 1 a single replacement point will be used per corner. + /// + /// + /// Relative strength/size of the rounded corner in the range [0, 1]. Larger values produce + /// a larger arc limited by adjacent edge lengths. Defaults to 0.5f. + /// + /// + /// Angle threshold in degrees below which three consecutive vertices are considered + /// collinear and the corner is left unchanged. Defaults to 5 degrees. + /// + /// + /// Minimum adjacent edge length required to attempt rounding. If either adjacent edge is + /// shorter than this threshold the original vertex is preserved. Defaults to 1.0f. + /// + /// + /// True if rounding was applied successfully; otherwise false for invalid parameters or when + /// no modification was performed. + /// + /// + /// This is a heuristic geometric approximation and may produce unexpected results on highly + /// concave or self-intersecting polygons. Use for a + /// non-mutating alternative. + /// + public bool Round(int cornerPoints, float cornerStrength = 0.5f, float collinearAngleThresholdDeg = 5f, float distanceThreshold = 1f) { - if (Count < 3) return null; - var newPolygon = new Polygon(this.Count); + if (cornerPoints <= 0 || Count < 3 || cornerStrength <= 0 || cornerStrength > 1) return false; + + var vertices = new List(); for (var i = 0; i < Count; i++) { - var w = this[i] - origin; - newPolygon.Add(origin + w.ChangeLength(amount)); + var prevP = this[ShapeMath.WrapIndex(Count, i - 1)]; + var p = this[i]; + var nextP = this[ShapeMath.WrapIndex(Count, i + 1)]; + + var prevEdge = p - prevP; + var curEdge = nextP - p; + + float prevEdgeLength = prevEdge.Length(); + float curEdgeLength = curEdge.Length(); + + if (prevEdgeLength < distanceThreshold || curEdgeLength < distanceThreshold) + { + vertices.Add(p); + continue; + } + + if (ShapeVec.IsColinearAngle(prevP, p, nextP, collinearAngleThresholdDeg)) + { + vertices.Add(p); + continue; + } + + var v1 = (prevP - p).Normalize(); + var v2 = (nextP - p).Normalize(); + + float angle = MathF.Acos(Vector2.Dot(v1, v2)); + float cornerRadius = MathF.Min(prevEdgeLength, curEdgeLength) * 0.5f * cornerStrength; + + float t = cornerRadius / MathF.Tan(angle / 2); + + // Prevent the rounded corner from extending beyond the midpoint of the adjacent edges + t = MathF.Min(t, prevEdgeLength * 0.45f); //45 used as a safety margin (from 50) + t = MathF.Min(t, curEdgeLength * 0.45f); //45 used as a safety margin (from 50) + + // Recalculate cornerRadius based on the clamped t + cornerRadius = t * MathF.Tan(angle / 2); + + var startPoint = p + v1 * t; + var endPoint = p + v2 * t; + + var center = p + (v1 + v2).Normalize() * (cornerRadius / MathF.Sin(angle / 2)); + + float startAngle = (startPoint - center).AngleRad(); + float endAngle = (endPoint - center).AngleRad(); + + float angleDiff = ShapeMath.GetShortestAngleRad(startAngle, endAngle); + + if (cornerPoints == 1) + { + vertices.Add(p + (v1 + v2) * t); + } + else + { + for (var j = 0; j < cornerPoints; j++) + { + float frac = j / (float)(cornerPoints - 1); + float currentAngle = startAngle + angleDiff * frac; + vertices.Add(center + new Vector2(MathF.Cos(currentAngle), MathF.Sin(currentAngle)) * cornerRadius); + } + } } - return newPolygon; + if (vertices.Count > 3) + { + Clear(); + AddRange(vertices); + } + + return true; } + + #endregion + + #region Triangulation /// - /// Returns a copy of the polygon with its size changed by a given amount about its centroid. + /// Triangulates the filled polygon and writes the generated triangles into the provided . /// - /// The amount to change the size. - /// A new with the updated size, or null if invalid. - public Polygon? ChangeSizeCopy(float amount) + /// The destination triangulation that receives the generated triangles. + /// Whether to apply Delaunay refinement when creating the triangulation. + /// + /// This method delegates to the clipping/triangulation backend and does not modify the polygon itself. + /// + public void Triangulate(Triangulation result, bool useDelaunay = false) { - if (Count < 3) return null; - return ChangeSizeCopy(amount, GetCentroid()); + ClipperImmediate2D.CreatePolygonTriangulation(this, useDelaunay, result); } + /// - /// Returns a copy of the polygon with its size set to a given value about a given origin. + /// Triangulates the filled polygon and writes the generated triangles into the provided . /// - /// The new size (distance from origin to each vertex). - /// The origin to scale about. - /// A new with the updated size, or null if invalid. - public new Polygon? SetSizeCopy(float size, Vector2 origin) + /// The destination triangle mesh that receives the generated vertices and indices. + /// Whether to apply Delaunay refinement when creating the triangulation. + /// + /// This method delegates to the clipping/triangulation backend and does not modify the polygon itself. + /// + public void Triangulate(TriMesh result, bool useDelaunay = false) { - if (Count < 3) return null; - var newPolygon = new Polygon(this.Count); + ClipperImmediate2D.CreatePolygonTriangulation(this, useDelaunay, result); + } - for (var i = 0; i < Count; i++) + #endregion + + #region Outline Triangulation + /// + /// Triangulates the polygon's outline as a stroked path and writes the generated triangles into the provided . + /// + /// The destination triangulation that receives the generated triangles. + /// The outline thickness to triangulate. + /// The maximum miter length factor used for joins. + /// Whether sharp joins that exceed the miter limit should be beveled. + /// Whether to apply Delaunay refinement when creating the triangulation. + /// + /// This method triangulates only the polygon outline, not the filled interior. + /// + public void TriangulateOutline(Triangulation result, float thickness, float miterLimit = 2f, bool beveled = false, bool useDelaunay = false) + { + ClipperImmediate2D.CreatePolygonOutlineTriangulation(this, thickness, miterLimit, beveled, useDelaunay, result); + } + + /// + /// Triangulates the polygon's outline as a stroked path and writes the generated triangles into the provided . + /// + /// The destination triangle mesh that receives the generated vertices and indices. + /// The outline thickness to triangulate. + /// The maximum miter length factor used for joins. + /// Whether sharp joins that exceed the miter limit should be beveled. + /// Whether to apply Delaunay refinement when creating the triangulation. + /// + /// This method triangulates only the polygon outline, not the filled interior. + /// + public void TriangulateOutline(TriMesh result, float thickness, float miterLimit = 2f, bool beveled = false, bool useDelaunay = false) + { + ClipperImmediate2D.CreatePolygonOutlineTriangulation(this, thickness, miterLimit, beveled, useDelaunay, result); + } + #endregion + + #region Outline Perimeter Triangulation + /// + /// Builds an open that follows a portion of this polygon's perimeter starting at the specified vertex index. + /// + /// The positive perimeter length to trace along the polygon. + /// The starting vertex index. The value is wrapped to the polygon's valid index range. + /// The destination polyline that will be cleared and populated with the traced perimeter points. + /// + /// If is less than or equal to zero, or the polygon contains fewer than two vertices, the method returns without modifying . + /// + public void PolygonToPolylinePerimeter(float perimeterToDraw, int startIndex, Polyline result) + { + if (perimeterToDraw <= 0f) return; + + if (Count <= 1) return; + + result.Clear(); + + bool ccw = perimeterToDraw > 0; + float absPerimeterToDraw = MathF.Abs(perimeterToDraw); + float accumulatedPerimeter = 0f; + int currentIndex = ShapeMath.WrapIndex(Count, startIndex); + + //create polyline based on perimeter & start index + while (absPerimeterToDraw > accumulatedPerimeter) { - var w = this[i] - origin; - newPolygon.Add(origin + w.SetLength(size)); + int nextIndex = ShapeMath.WrapIndex(Count, currentIndex + (ccw ? 1 : -1)); + var cur = this[currentIndex]; + var next = this[nextIndex]; + currentIndex = nextIndex; + result.Add(cur); + float segmentLength = (next - cur).Length(); + + if (accumulatedPerimeter + segmentLength >= absPerimeterToDraw) + { + float f = (perimeterToDraw - accumulatedPerimeter) / segmentLength; + var end = cur.Lerp(next, f); + result.Add(end); + break; + } + + accumulatedPerimeter += segmentLength; } + } + + /// + /// Triangulates a stroked portion of the polygon perimeter and writes the generated triangles into the provided . + /// + /// The destination triangulation that receives the generated triangles. + /// The positive perimeter length to trace before triangulating the outline. + /// The starting vertex index for tracing the perimeter. The value is wrapped to the polygon's valid index range. + /// The outline thickness to triangulate. + /// The maximum miter length factor used for joins. + /// Whether sharp joins that exceed the miter limit should be beveled. + /// The end-cap style to use for the generated open polyline outline. + /// Whether to apply Delaunay refinement when creating the triangulation. + /// + /// This method first converts the requested perimeter segment into a temporary , then triangulates that stroked path. + /// + public void TriangulateOutlinePerimeter(Triangulation result, float perimeterToDraw, int startIndex, float thickness, + float miterLimit = 2f, bool beveled = false, ShapeClipperEndType endType = ShapeClipperEndType.Butt, bool useDelaunay = false) + { + PolygonToPolylinePerimeter(perimeterToDraw, startIndex, polylinePerimeterBuffer); + ClipperImmediate2D.CreatePolylineTriangulation(polylinePerimeterBuffer, thickness, miterLimit, beveled, endType, useDelaunay, result); + } + + /// + /// Triangulates a stroked portion of the polygon perimeter and writes the generated triangles into the provided . + /// + /// The destination triangle mesh that receives the generated vertices and indices. + /// The positive perimeter length to trace before triangulating the outline. + /// The starting vertex index for tracing the perimeter. The value is wrapped to the polygon's valid index range. + /// The outline thickness to triangulate. + /// The maximum miter length factor used for joins. + /// Whether sharp joins that exceed the miter limit should be beveled. + /// The end-cap style to use for the generated open polyline outline. + /// Whether to apply Delaunay refinement when creating the triangulation. + /// + /// This method first converts the requested perimeter segment into a temporary , then triangulates that stroked path. + /// + public void TriangulateOutlinePerimeter(TriMesh result, float perimeterToDraw, int startIndex, float thickness, + float miterLimit = 2f, bool beveled = false, ShapeClipperEndType endType = ShapeClipperEndType.Butt, bool useDelaunay = false) + { + PolygonToPolylinePerimeter(perimeterToDraw, startIndex, polylinePerimeterBuffer); + ClipperImmediate2D.CreatePolylineTriangulation(polylinePerimeterBuffer, thickness, miterLimit, beveled, endType, useDelaunay, result); + } + #endregion - return newPolygon; + #region Outline Percentage Triangulation + /// + /// Builds an open that follows a percentage of this polygon's perimeter starting at the specified vertex index. + /// + /// The fraction of the total perimeter to trace. Values greater than or equal to 1 copy the full polygon perimeter. + /// The starting vertex index. The value is wrapped to the polygon's valid index range. + /// The destination polyline that receives the traced perimeter points. + /// + /// If is less than or equal to zero, or the polygon contains fewer than two vertices, the method returns without modifying . + /// + public void PolygonToPolylinePercentage(float f, int startIndex, Polyline result) + { + if (f <= 0) return; + + if (Count <= 1) return; + + if (f >= 1f) + { + result.Clear(); + foreach (var p in this) + { + result.Add(p); + } + return; + } + + float totalPerimeter = 0f; + + for (var i = 0; i < Count; i++) + { + var start = this[i]; + var end = this[(i + 1) % Count]; + float l = (end - start).Length(); + totalPerimeter += l; + } + PolygonToPolylinePerimeter(totalPerimeter * f, startIndex, result); } + /// - /// Returns a copy of the polygon with its size set to a given value about its centroid. + /// Triangulates a stroked portion of the polygon perimeter defined by a percentage of its total length and writes the generated triangles into the provided . /// - /// The new size (distance from centroid to each vertex). - /// A new with the updated size, or null if invalid. - public Polygon? SetSizeCopy(float size) + /// Unused polygon parameter retained for API compatibility; the current polygon instance is used to generate the perimeter segment. + /// The destination triangulation that receives the generated triangles. + /// The fraction of the total perimeter to trace before triangulating the outline. + /// The starting vertex index for tracing the perimeter. The value is wrapped to the polygon's valid index range. + /// The outline thickness to triangulate. + /// The maximum miter length factor used for joins. + /// Whether sharp joins that exceed the miter limit should be beveled. + /// The end-cap style to use for the generated open polyline outline. + /// Whether to apply Delaunay refinement when creating the triangulation. + /// + /// This method first converts the requested perimeter fraction into a temporary , then triangulates that stroked path. + /// + public void TriangulateOutlinePercentage(IReadOnlyList polygon, Triangulation result, float f, int startIndex, float thickness, + float miterLimit = 2f, bool beveled = false, ShapeClipperEndType endType = ShapeClipperEndType.Butt, bool useDelaunay = false) { - if (Count < 3) return null; - return SetSizeCopy(size, GetCentroid()); + PolygonToPolylinePercentage(f, startIndex, polylinePerimeterBuffer); + ClipperImmediate2D.CreatePolylineTriangulation(polylinePerimeterBuffer, thickness, miterLimit, beveled, endType, useDelaunay, result); } + /// - /// Returns a copy of the polygon with a transform applied, using a given origin. + /// Triangulates a stroked portion of the polygon perimeter defined by a percentage of its total length and writes the generated triangles into the provided . /// - /// The transform to apply. - /// The origin for rotation and scaling. - /// A new with the transform applied, or null if invalid. - public new Polygon? SetTransformCopy(Transform2D transform, Vector2 origin) + /// Unused polygon parameter retained for API compatibility; the current polygon instance is used to generate the perimeter segment. + /// The destination triangle mesh that receives the generated vertices and indices. + /// The fraction of the total perimeter to trace before triangulating the outline. + /// The starting vertex index for tracing the perimeter. The value is wrapped to the polygon's valid index range. + /// The outline thickness to triangulate. + /// The maximum miter length factor used for joins. + /// Whether sharp joins that exceed the miter limit should be beveled. + /// The end-cap style to use for the generated open polyline outline. + /// Whether to apply Delaunay refinement when creating the triangulation. + /// + /// This method first converts the requested perimeter fraction into a temporary , then triangulates that stroked path. + /// + public void TriangulateOutlinePercentage(IReadOnlyList polygon, TriMesh result, float f, int startIndex, float thickness, + float miterLimit = 2f, bool beveled = false, ShapeClipperEndType endType = ShapeClipperEndType.Butt, bool useDelaunay = false) { - if (Count < 3) return null; - var newPolygon = SetPositionCopy(transform.Position); - if (newPolygon == null) return null; - newPolygon.SetRotation(transform.RotationRad, origin); - newPolygon.SetSize(transform.ScaledSize.Length, origin); - return newPolygon; + PolygonToPolylinePercentage(f, startIndex, polylinePerimeterBuffer); + ClipperImmediate2D.CreatePolylineTriangulation(polylinePerimeterBuffer, thickness, miterLimit, beveled, endType, useDelaunay, result); } + #endregion + + #region Cut Ray + /// - /// Returns a copy of the polygon with an offset transform applied, using a given origin. + /// Intersects a ray with this polygon and stores all segments of the ray that lie inside the polygon in the provided result list. /// - /// The offset transform to apply. - /// The origin for rotation and scaling. - /// A new with the offset applied, or null if invalid. - public new Polygon? ApplyOffsetCopy(Transform2D offset, Vector2 origin) + /// The origin point of the ray. + /// The direction vector of the ray. Must not be zero. + /// A list to store the resulting segments inside the polygon. + /// The number of segments added to the result list. + /// Segments are sorted by distance from the ray origin. + public int CutWithRay(Vector2 rayPoint, Vector2 rayDirection, ref List result) { - if (Count < 3) return null; + if (Count < 3) return 0; + if (rayDirection.X == 0 && rayDirection.Y == 0) return 0; - var newPolygon = ChangePositionCopy(offset.Position); - if (newPolygon == null) return null; - newPolygon.ChangeRotation(offset.RotationRad, origin); - newPolygon.ChangeSize(offset.ScaledSize.Length, origin); - return newPolygon; + rayDirection = rayDirection.Normalize(); + var intersectionPoints = IntersectPolygonRay(this, rayPoint, rayDirection, ref intersectionPointsReference); + if (intersectionPoints < 2) return 0; + + int count = result.Count; + intersectionPointsReference.SortClosestFirst(rayPoint); + + for (int i = 0; i < intersectionPointsReference.Count - 1; i += 2) + { + var segmentStart = intersectionPointsReference[i].Point; + var segmentEnd = intersectionPointsReference[i + 1].Point; + var segment = new Segment(segmentStart, segmentEnd); + result.Add(segment); + } + + intersectionPointsReference.Clear(); + return result.Count - count; } #endregion diff --git a/ShapeEngine/Geometry/PolygonDef/PolygonStatic.cs b/ShapeEngine/Geometry/PolygonDef/PolygonStatic.cs deleted file mode 100644 index 5b9d0ee7..00000000 --- a/ShapeEngine/Geometry/PolygonDef/PolygonStatic.cs +++ /dev/null @@ -1,393 +0,0 @@ -using System.Numerics; -using ShapeEngine.Core.Structs; -using ShapeEngine.Geometry.PointsDef; -using ShapeEngine.Geometry.RectDef; -using ShapeEngine.Geometry.SegmentDef; -using ShapeEngine.Geometry.SegmentsDef; -using ShapeEngine.Geometry.TriangleDef; -using ShapeEngine.Geometry.TriangulationDef; -using ShapeEngine.Random; -using ShapeEngine.StaticLib; - -namespace ShapeEngine.Geometry.PolygonDef; - -public partial class Polygon -{ - /// - /// Intersects a ray with this polygon and returns all segments of the ray that lie inside the polygon. - /// - /// The origin point of the ray. - /// The direction vector of the ray. Must not be zero. - /// A list of segments inside the polygon, or null if there are fewer than 3 vertices or no valid intersections. - /// Segments are sorted by distance from the ray origin. - public List? CutRayWithPolygon(Vector2 rayPoint, Vector2 rayDirection) - { - if (Count < 3) return null; - if (rayDirection.X == 0 && rayDirection.Y == 0) return null; - - rayDirection = rayDirection.Normalize(); - var intersectionPoints = IntersectPolygonRay(this, rayPoint, rayDirection); - if (intersectionPoints == null || intersectionPoints.Count < 2) return null; - - intersectionPoints.SortClosestFirst(rayPoint); - - var segments = new List(); - for (int i = 0; i < intersectionPoints.Count - 1; i += 2) - { - var segmentStart = intersectionPoints[i].Point; - var segmentEnd = intersectionPoints[i + 1].Point; - var segment = new Segment(segmentStart, segmentEnd); - segments.Add(segment); - } - - return segments; - } - - /// - /// Intersects a ray with this polygon and stores all segments of the ray that lie inside the polygon in the provided result list. - /// - /// The origin point of the ray. - /// The direction vector of the ray. Must not be zero. - /// A list to store the resulting segments inside the polygon. - /// The number of segments added to the result list. - /// Segments are sorted by distance from the ray origin. - public int CutRayWithPolygon(Vector2 rayPoint, Vector2 rayDirection, ref List result) - { - if (Count < 3) return 0; - if (rayDirection.X == 0 && rayDirection.Y == 0) return 0; - - rayDirection = rayDirection.Normalize(); - var intersectionPoints = IntersectPolygonRay(this, rayPoint, rayDirection, ref intersectionPointsReference); - if (intersectionPoints < 2) return 0; - - int count = result.Count; - intersectionPointsReference.SortClosestFirst(rayPoint); - - for (int i = 0; i < intersectionPointsReference.Count - 1; i += 2) - { - var segmentStart = intersectionPointsReference[i].Point; - var segmentEnd = intersectionPointsReference[i + 1].Point; - var segment = new Segment(segmentStart, segmentEnd); - result.Add(segment); - } - - intersectionPointsReference.Clear(); - return result.Count - count; - } - - internal static bool ContainsPointCheck(Vector2 a, Vector2 b, Vector2 pointToCheck) - { - if (a.Y < pointToCheck.Y && b.Y >= pointToCheck.Y || b.Y < pointToCheck.Y && a.Y >= pointToCheck.Y) - { - if (a.X + (pointToCheck.Y - a.Y) / (b.Y - a.Y) * (b.X - a.X) < pointToCheck.X) - { - return true; - } - } - - return false; - } - - /// - /// Triangulates a set of points using Delaunay triangulation. Only works with non-self-intersecting shapes. - /// - /// The points to triangulate. Can be any set of points, including polygons. - /// A triangulation of the input points. - public static Triangulation TriangulateDelaunay(IEnumerable points) - { - var enumerable = points.ToList(); - var supraTriangle = GetBoundingTriangle(enumerable, 2f); - return TriangulateDelaunay(enumerable, supraTriangle); - } - - /// - /// Triangulates a set of points using Delaunay triangulation, with a custom bounding triangle. Only works with non-self-intersecting shapes. - /// - /// The points to triangulate. Can be any set of points, including polygons. - /// A triangle that encapsulates all the points. - /// A triangulation of the input points. - public static Triangulation TriangulateDelaunay(IEnumerable points, Triangle supraTriangle) - { - Triangulation triangles = new() { supraTriangle }; - - foreach (var p in points) - { - Triangulation badTriangles = new(); - - //Identify 'bad triangles' - for (int triIndex = triangles.Count - 1; triIndex >= 0; triIndex--) - { - Triangle triangle = triangles[triIndex]; - - //A 'bad triangle' is defined as a triangle who's CircumCentre contains the current point - var circumCircle = triangle.GetCircumCircle(); - float distSq = Vector2.DistanceSquared(p, circumCircle.Center); - if (distSq < circumCircle.Radius * circumCircle.Radius) - { - badTriangles.Add(triangle); - triangles.RemoveAt(triIndex); - } - } - - Segments allEdges = new(); - foreach (var badTriangle in badTriangles) - { - allEdges.AddRange(badTriangle.GetEdges()); - } - - Segments uniqueEdges = GetUniqueSegmentsDelaunay(allEdges); - //Create new triangles - for (int i = 0; i < uniqueEdges.Count; i++) - { - var edge = uniqueEdges[i]; - triangles.Add(new(p, edge)); - } - } - - //Remove all triangles that share a vertex with the supra triangle to recieve the final triangulation - for (int i = triangles.Count - 1; i >= 0; i--) - { - var t = triangles[i]; - if (t.SharesVertex(supraTriangle)) triangles.RemoveAt(i); - } - - - return triangles; - } - - private static Segments GetUniqueSegmentsDelaunay(Segments segments) - { - Segments uniqueEdges = new(); - for (int i = segments.Count - 1; i >= 0; i--) - { - var edge = segments[i]; - if (IsSimilar(segments, edge)) - { - uniqueEdges.Add(edge); - } - } - - return uniqueEdges; - } - - private static bool IsSimilar(Segments segments, Segment seg) - { - var counter = 0; - foreach (var segment in segments) - { - if (segment.IsSimilar(seg)) counter++; - if (counter > 1) return false; - } - - return true; - } - - /// - /// Gets a bounding rectangle that encapsulates all points. - /// - /// The points to encapsulate. - /// A rectangle that contains all the points, or an empty rectangle if fewer than 2 points. - public static Rect GetBoundingBox(IEnumerable points) - { - var enumerable = points as Vector2[] ?? points.ToArray(); - if (enumerable.Length < 2) return new(); - var start = enumerable.First(); - Rect r = new(start.X, start.Y, 0, 0); - - foreach (var p in enumerable) - { - r = r.Enlarge(p); - } - - return r; - } - - /// - /// Gets a triangle that encapsulates all points, with an optional margin factor. - /// - /// The points to encapsulate. - /// A factor for scaling the final triangle. Default is 1. - /// A triangle that contains all the points. - public static Triangle GetBoundingTriangle(IEnumerable points, float marginFactor = 1f) - { - var bounds = GetBoundingBox(points); - float dMax = bounds.Size.Max() * marginFactor; - var center = bounds.Center; - var a = new Vector2(center.X, bounds.BottomLeft.Y + dMax); - var b = new Vector2(center.X - dMax * 1.25f, bounds.TopLeft.Y - dMax / 4); - var c = new Vector2(center.X + dMax * 1.25f, bounds.TopLeft.Y - dMax / 4); - - return new Triangle(a, b, c); - } - - /// - /// Gets the axis vectors (edge directions) of a polygon's segments. - /// - /// The polygon to analyze. - /// If true, returns normalized direction vectors; - /// A list of axis vectors for each segment. - public static List GetSegmentAxis(Polygon p, bool normalized = false) - { - if (p.Count <= 1) return new(); - if (p.Count == 2) - { - return new() { p[1] - p[0] }; - } - - List axis = []; - for (var i = 0; i < p.Count; i++) - { - var start = p[i]; - var end = p[(i + 1) % p.Count]; - var a = end - start; - axis.Add(normalized ? a.Normalize() : a); - } - - return axis; - } - - /// - /// Gets the axis vectors (edge directions) of a collection of segments. - /// - /// The segments to analyze. - /// If true, returns normalized direction vectors; - /// A list of axis vectors for each segment. - public static List GetSegmentAxis(Segments edges, bool normalized = false) - { - List axis = []; - foreach (var seg in edges) - { - axis.Add(normalized ? seg.Dir : seg.Displacement); - } - - return axis; - } - - /// - /// Creates a polygon from a set of relative points and a transform. - /// - /// The relative points defining the shape. - /// The transform to apply to each point. - /// A polygon with transformed points, or an empty polygon if fewer than 3 points. - public static Polygon GetShape(Points relative, Transform2D transform) - { - if (relative.Count < 3) return []; - Polygon shape = []; - for (var i = 0; i < relative.Count; i++) - { - shape.Add(transform.ApplyTransformTo(relative[i])); - } - - return shape; - } - - /// - /// Generates a random polygon in relative space. - /// - /// Number of points (vertices) in the polygon. Must be at least 3. - /// Minimum distance from the origin for each point. - /// Maximum distance from the origin for each point. - /// A randomly generated polygon. - public static Polygon? GenerateRelative(int pointCount, float minLength, float maxLength) - { - if (pointCount < 3) return null; - if (Math.Abs(minLength - maxLength) < ShapeMath.Epsilon) return null; - if (minLength > maxLength) - { - //swap - (minLength, maxLength) = (maxLength, minLength); - } - Polygon poly = []; - float angleStep = ShapeMath.PI * 2.0f / pointCount; - - for (var i = 0; i < pointCount; i++) - { - float randLength = Rng.Instance.RandF(minLength, maxLength); - var p = ShapeVec.Right().Rotate(-angleStep * i) * randLength; - poly.Add(p); - } - - return poly; - } - - /// - /// Generates a random polygon centered at a given position. - /// - /// The center position of the polygon. - /// Number of points (vertices) in the polygon. Must be at least 3. - /// Minimum distance from the center for each point. - /// Maximum distance from the center for each point. - /// A randomly generated polygon centered at the specified position. - public static Polygon? Generate(Vector2 center, int pointCount, float minLength, float maxLength) - { - if (pointCount < 3) return null; - if (Math.Abs(minLength - maxLength) < ShapeMath.Epsilon) return null; - if (minLength > maxLength) - { - //swap - (minLength, maxLength) = (maxLength, minLength); - } - Polygon points = []; - float angleStep = ShapeMath.PI * 2.0f / pointCount; - - for (int i = 0; i < pointCount; i++) - { - float randLength = Rng.Instance.RandF(minLength, maxLength); - var p = ShapeVec.Right().Rotate(-angleStep * i) * randLength; - p += center; - points.Add(p); - } - - return points; - } - - /// - /// Generates a polygon around the given segment. Points are generated ccw around the segment beginning with the segment start. - /// - /// The segment to build a polygon around. - /// The minimum perpendicular magnitude factor for generating a point. (0-1) - /// The maximum perpendicular magnitude factor for generating a point. (0-1) - /// The minimum factor of the length between points along the line.(0-1) - /// The maximum factor of the length between points along the line.(0-1) - /// Returns the generated polygon. - public static Polygon? Generate(Segment segment, float magMin = 0.1f, float magMax = 0.25f, float minSectionLength = 0.025f, float maxSectionLength = 0.1f) - { - if (segment.LengthSquared <= 0) return null; - if (magMin <= 0 || magMax <= 0) return null; - if (minSectionLength <= 0 || maxSectionLength <= 0) return null; - if (magMin > magMax) - { - (magMin, magMax) = (magMax, magMin); - } - - if (minSectionLength > maxSectionLength) - { - (minSectionLength, maxSectionLength) = (maxSectionLength, minSectionLength); - } - Polygon poly = [segment.Start]; - var dir = segment.Dir; - var dirRight = dir.GetPerpendicularRight(); - var dirLeft = dir.GetPerpendicularLeft(); - float len = segment.Length; - float minSectionLengthSq = (minSectionLength * len) * (minSectionLength * len); - var cur = segment.Start; - while (true) - { - cur += dir * Rng.Instance.RandF(minSectionLength, maxSectionLength) * len; - if ((cur - segment.End).LengthSquared() < minSectionLengthSq) break; - poly.Add(cur + dirRight * Rng.Instance.RandF(magMin, magMax)); - } - - cur = segment.End; - poly.Add(cur); - while (true) - { - cur -= dir * Rng.Instance.RandF(minSectionLength, maxSectionLength) * len; - if ((cur - segment.Start).LengthSquared() < minSectionLengthSq) break; - poly.Add(cur + dirLeft * Rng.Instance.RandF(magMin, magMax)); - } - - return poly; - } - -} \ No newline at end of file diff --git a/ShapeEngine/Geometry/PolygonDef/Polygons.cs b/ShapeEngine/Geometry/PolygonDef/Polygons.cs new file mode 100644 index 00000000..943c5200 --- /dev/null +++ b/ShapeEngine/Geometry/PolygonDef/Polygons.cs @@ -0,0 +1,66 @@ +using System.Numerics; +using ShapeEngine.Geometry.PointsDef; + +namespace ShapeEngine.Geometry.PolygonDef; + +/// +/// Represents a collection of objects. +/// +public class Polygons : List +{ + /// + /// Initializes a new instance of the class. + /// + public Polygons() { } + /// + /// Initializes a new instance of the class with the specified capacity. + /// + /// The number of elements that the new list can initially store. + public Polygons(int capacity) : base(capacity) { } + /// + /// Initializes a new instance of the class containing the specified polygons. + /// + /// An array of polygons to add. + public Polygons(params Polygon[] polygons) { AddRange(polygons); } + /// + /// Initializes a new instance of the class containing the specified polygons. + /// + /// An enumerable collection of polygons to add. + public Polygons(IEnumerable polygons) { AddRange(polygons); } + + #region Convex Hull + + private static Points pointsBuffer = new(); + + /// + /// Computes the convex hull from all points contained in the polygons in this collection. + /// + /// + /// A instance containing the convex hull points. + /// + public Points FindConvexHull() + { + pointsBuffer.Clear(); + foreach(var poly in this) + { + pointsBuffer.AddRange(poly); + } + return pointsBuffer.FindConvexHull(); + } + + /// + /// Computes the convex hull from all points contained in the polygons in this collection + /// and stores the resulting hull points in the provided list. + /// + /// The list to populate with the convex hull points. + public void FindConvexHull(List result) + { + pointsBuffer.Clear(); + foreach(var poly in this) + { + pointsBuffer.AddRange(poly); + } + pointsBuffer.FindConvexHull(result); + } + #endregion +} \ No newline at end of file diff --git a/ShapeEngine/Geometry/PolylineDef/Polyline.cs b/ShapeEngine/Geometry/PolylineDef/Polyline.cs index b81aec0e..3263ff75 100644 --- a/ShapeEngine/Geometry/PolylineDef/Polyline.cs +++ b/ShapeEngine/Geometry/PolylineDef/Polyline.cs @@ -23,6 +23,12 @@ namespace ShapeEngine.Geometry.PolylineDef; /// public partial class Polyline : Points, IEquatable, IShapeTypeProvider { + #region Helper + + private static Polyline buffer = new(); + + #endregion + #region Constructors /// /// Initializes a new instance of the class with no points. @@ -125,6 +131,7 @@ public Circle GetBoundingCircle() return new Circle(origin, MathF.Sqrt(maxD)); } + /// /// Calculates the axis-aligned bounding box that contains all points of the polyline. /// @@ -142,12 +149,12 @@ public Rect GetBoundingBox() return r; } - /// - /// Returns the segments (edges) of the polyline as a collection. - /// If the points are in counter-clockwise (CCW) order, the segment normals face to the right of the segment direction. - /// If InsideNormals is true, the normals face to the left of the segment direction. - /// - /// A collection representing the polyline's edges. + /// + /// Returns the segments (edges) of the polyline as a collection. + /// If the points are in counter-clockwise (CCW) order, the segment normals face to the right of the segment direction. + /// If InsideNormals is true, the normals face to the left of the segment direction. + /// + /// A collection representing the polyline's edges. public Segments GetEdges() { if (Count <= 1) return new(); @@ -160,12 +167,32 @@ public Segments GetEdges() } return segments; } + /// /// Converts this to a collection containing the same points. /// /// A new instance with the points from this polyline. public Points ToPoints() { return new(this); } + public bool GetEdgeDirections(List result, bool normalized = false) + { + if (Count <= 1) return false; + result.Clear(); + if (Count == 2) + { + result.Add(this[1] - this[0]); + return true; + } + for (var i = 0; i < Count - 1; i++) + { + var start = this[i]; + var end = this[i + 1]; + var a = end - start; + result.Add(normalized ? a.Normalize() : a); + } + + return true; + } #endregion #region Points & Vertex @@ -175,6 +202,7 @@ public Segments GetEdges() /// the polyline will be in clockwise (CW) order if it was counter-clockwise (CCW) before and vice versa. /// public void ReverseOrder() => Reverse(); + /// /// Gets the segment (edge) between the two points at the specified index. /// @@ -194,6 +222,7 @@ public Segment GetSegment(int index) /// /// A random vertex from the polyline. public Vector2 GetRandomVertex() { return Rng.Instance.RandCollection(this); } + /// /// Gets a random edge (segment) from the polyline. /// @@ -202,63 +231,67 @@ public Segment GetSegment(int index) #endregion #region Interpolated Edge Points - /// - /// Interpolate the edge(segment) between each pair of points using t and return the new interpolated points. + /// Computes one interpolated point along each edge of the polyline and writes the results into . /// - /// The value t for interpolation. Should be between 0 - 1. - /// - public Points? InterpolatedEdgePoints(float t) + /// The interpolation factor used for each edge. Values between 0 and 1 produce points between each vertex and the next vertex. + /// The destination collection that will be cleared and populated with the interpolated points. + /// + /// If the collection contains fewer than two points, the method returns without modifying . + /// + public new void GetInterpolatedEdgePoints(float t, Points result) { - if (Count < 2) return null; + if (Count < 2) return; - var result = new Points(); + result.Clear(); + result.EnsureCapacity(Count); for (int i = 0; i < Count - 1; i++) { var cur = this[i]; var next = this[i + 1]; - var interpolated = cur.Lerp(next, t);// Vector2.Lerp(cur, next, t); + var interpolated = cur.Lerp(next, t); result.Add(interpolated); } - - return result; } + /// - /// Interpolate the edge(segment) between each pair of points using t and return the new interpolated points. + /// Repeatedly computes interpolated edge points for the polyline and writes the final result into . /// - /// The value t for interpolation. Should be between 0 - 1. - /// Recursive steps. The number of times the result of InterpolatedEdgesPoints will be run through InterpolateEdgePoints. - /// - public Points? InterpolatedEdgePoints(float t, int steps) + /// The interpolation factor used for each edge on every pass. Values between 0 and 1 produce points between each vertex and the next vertex. + /// The number of interpolation passes to perform. Values less than or equal to 1 perform a single pass. + /// The destination collection that will receive the interpolated points. + /// + /// If the collection contains fewer than two points, the method returns without modifying . + /// + public new void GetInterpolatedEdgePoints(float t, int steps, Points result) { - if (Count < 2) return null; - if (steps <= 1) return InterpolatedEdgePoints(t); + if (Count < 2 || steps <= 0) return; + + if (steps == 1) + { + GetInterpolatedEdgePoints(t, result); + return; + } + steps = ShapeMath.MinInt(steps, Count - 1); + int remainingSteps = steps; - var result = new Points(); - var buffer = new Points(); + result.Clear(); + while (remainingSteps > 0) { var target = result.Count <= 0 ? this : result; - for (int i = 0; i < target.Count; i++) - { - var cur = target[i]; - var next = target[i + 1]; - var interpolated = cur.Lerp(next, t); - buffer.Add(interpolated); - } - - (result, buffer) = (buffer, result);//switch buffer and result - buffer.Clear(); + target.GetInterpolatedEdgePoints(t, pointsBuffer); + result.Clear(); + result.AddRange(pointsBuffer); + pointsBuffer.Clear(); remainingSteps--; } - - - return result; } #endregion #region Static + /// /// Creates a polyline shape from a set of relative points and a transformation. /// @@ -276,7 +309,6 @@ public static Polyline GetShape(Points relative, Transform2D transform) } return shape; } - #endregion } diff --git a/ShapeEngine/Geometry/PolylineDef/PolylineDrawing.cs b/ShapeEngine/Geometry/PolylineDef/PolylineDrawing.cs index 8d05eba6..3b43a112 100644 --- a/ShapeEngine/Geometry/PolylineDef/PolylineDrawing.cs +++ b/ShapeEngine/Geometry/PolylineDef/PolylineDrawing.cs @@ -20,126 +20,14 @@ namespace ShapeEngine.Geometry.PolylineDef; /// public static class PolylineDrawing { - #region Draw Masked - /// - /// Draws each segment of the polyline using a triangular mask. - /// - /// The polyline whose segments will be drawn. - /// The used as the clipping mask. - /// Drawing parameters such as thickness, color and cap type. - /// If true, draws the parts inside the mask instead of outside. - public static void DrawMasked(this Polyline polyline, Triangle mask, LineDrawingInfo lineInfo, bool reversedMask = false) - { - if (polyline.Count < 2) return; - - for (var i = 0; i < polyline.Count - 1; i++) - { - var start = polyline[i]; - var end = polyline[i + 1]; - var segment = new Segment(start, end); - segment.DrawMasked(mask, lineInfo, reversedMask); - } - } - /// - /// Draws each segment of the polyline using a circular mask. - /// - /// The polyline whose segments will be drawn. - /// The used as the clipping mask. - /// Drawing parameters such as thickness, color and cap type. - /// If true, draws the parts inside the mask instead of outside. - public static void DrawMasked(this Polyline polyline, Circle mask, LineDrawingInfo lineInfo, bool reversedMask = false) - { - if (polyline.Count < 2) return; - - for (var i = 0; i < polyline.Count - 1; i++) - { - var start = polyline[i]; - var end = polyline[i + 1]; - var segment = new Segment(start, end); - segment.DrawMasked(mask, lineInfo, reversedMask); - } - } - /// - /// Draws each segment of the polyline using a rectangular mask. - /// - /// The polyline whose segments will be drawn. - /// The used as the clipping mask. - /// Drawing parameters such as thickness, color and cap type. - /// If true, draws the parts inside the mask instead of outside. - public static void DrawMasked(this Polyline polyline, Rect mask, LineDrawingInfo lineInfo, bool reversedMask = false) - { - if (polyline.Count < 2) return; - - for (var i = 0; i < polyline.Count - 1; i++) - { - var start = polyline[i]; - var end = polyline[i + 1]; - var segment = new Segment(start, end); - segment.DrawMasked(mask, lineInfo, reversedMask); - } - } - /// - /// Draws each segment of the polyline using a quadrilateral mask. - /// - /// The polyline whose segments will be drawn. - /// The used as the clipping mask. - /// Drawing parameters such as thickness, color and cap type. - /// If true, draws the parts inside the mask instead of outside. - public static void DrawMasked(this Polyline polyline, Quad mask, LineDrawingInfo lineInfo, bool reversedMask = false) - { - if (polyline.Count < 2) return; - - for (var i = 0; i < polyline.Count - 1; i++) - { - var start = polyline[i]; - var end = polyline[i + 1]; - var segment = new Segment(start, end); - segment.DrawMasked(mask, lineInfo, reversedMask); - } - } + //TODO: + // - Clean up and remove unused methods (relative polyline for instance) + // - Add regions + // - Copy structure of PolygonDrawing + // - Use PolygonDrawing for functions that need to be overhauled here + // - Add DrawFast methods that dont work with alpha color (just draw segments between vertices with endcaps) - /// - /// Draws each segment of the polyline using a as the clipping mask. - /// - /// The polyline whose segments will be drawn. - /// The used as the clipping mask. - /// Drawing parameters such as thickness, color and cap type. - /// If true, draws the parts inside the mask instead of outside. - public static void DrawMasked(this Polyline polyline, Polygon mask, LineDrawingInfo lineInfo, bool reversedMask = false) - { - if (polyline.Count < 2) return; - - for (var i = 0; i < polyline.Count - 1; i++) - { - var start = polyline[i]; - var end = polyline[i + 1]; - var segment = new Segment(start, end); - segment.DrawMasked(mask, lineInfo, reversedMask); - } - } - /// - /// Draws each segment of the polyline using a mask of a generic closed shape type. - /// - /// - /// The mask type that implements (for example , , , or ). - /// - /// The polyline whose segments will be drawn. - /// The mask instance used for clipping each segment. - /// Drawing parameters such as thickness, color and cap type. - /// If true, draws the parts inside the mask instead of outside. - public static void DrawMasked(this Polyline polyline, T mask, LineDrawingInfo lineInfo, bool reversedMask = false) where T : IClosedShapeTypeProvider - { - if (polyline.Count < 2) return; - - for (var i = 0; i < polyline.Count - 1; i++) - { - var start = polyline[i]; - var end = polyline[i + 1]; - var segment = new Segment(start, end); - segment.DrawMasked(mask, lineInfo, reversedMask); - } - } - #endregion + //TODO: Rework all below with new ClipperImmediate2d system! /// /// Draws the polyline using a single color and specified thickness. @@ -505,24 +393,7 @@ public static void DrawLinesScaled(this Polyline relative, Transform2D transform { DrawLinesScaled(relative, transform.Position, transform.ScaledSize.Length, transform.RotationDeg, lineInfo, sideScaleFactor, sideScaleOrigin); } - - /// - /// Draws circles at each vertex of the polyline. - /// - /// The polyline whose vertices to draw. - /// The radius of each vertex circle. - /// The color of the vertex circles. - /// The number of segments to use for each circle. - /// - /// Useful for debugging or highlighting polyline vertices. - /// - public static void DrawVertices(this Polyline polyline, float vertexRadius, ColorRgba color, int circleSegments) - { - foreach (var p in polyline) - { - CircleDrawing.DrawCircle(p, vertexRadius, color, circleSegments); - } - } + /// /// Draws the polyline with a glow effect, interpolating width and color along each segment. @@ -563,4 +434,270 @@ public static void DrawGlow(this Polyline polyline, float width, float endWidth, Draw(polyline, currentWidth, currentColor, capType, capPoints); } } -} \ No newline at end of file + + + #region Draw Masked + /// + /// Draws each segment of the polyline using a triangular mask. + /// + /// The polyline whose segments will be drawn. + /// The used as the clipping mask. + /// Drawing parameters such as thickness, color and cap type. + /// If true, draws the parts inside the mask instead of outside. + public static void DrawMasked(this Polyline polyline, Triangle mask, LineDrawingInfo lineInfo, bool reversedMask = false) + { + if (polyline.Count < 2) return; + + for (var i = 0; i < polyline.Count - 1; i++) + { + var start = polyline[i]; + var end = polyline[i + 1]; + var segment = new Segment(start, end); + segment.DrawMasked(mask, lineInfo, reversedMask); + } + } + /// + /// Draws each segment of the polyline using a circular mask. + /// + /// The polyline whose segments will be drawn. + /// The used as the clipping mask. + /// Drawing parameters such as thickness, color and cap type. + /// If true, draws the parts inside the mask instead of outside. + public static void DrawMasked(this Polyline polyline, Circle mask, LineDrawingInfo lineInfo, bool reversedMask = false) + { + if (polyline.Count < 2) return; + + for (var i = 0; i < polyline.Count - 1; i++) + { + var start = polyline[i]; + var end = polyline[i + 1]; + var segment = new Segment(start, end); + segment.DrawMasked(mask, lineInfo, reversedMask); + } + } + /// + /// Draws each segment of the polyline using a rectangular mask. + /// + /// The polyline whose segments will be drawn. + /// The used as the clipping mask. + /// Drawing parameters such as thickness, color and cap type. + /// If true, draws the parts inside the mask instead of outside. + public static void DrawMasked(this Polyline polyline, Rect mask, LineDrawingInfo lineInfo, bool reversedMask = false) + { + if (polyline.Count < 2) return; + + for (var i = 0; i < polyline.Count - 1; i++) + { + var start = polyline[i]; + var end = polyline[i + 1]; + var segment = new Segment(start, end); + segment.DrawMasked(mask, lineInfo, reversedMask); + } + } + /// + /// Draws each segment of the polyline using a quadrilateral mask. + /// + /// The polyline whose segments will be drawn. + /// The used as the clipping mask. + /// Drawing parameters such as thickness, color and cap type. + /// If true, draws the parts inside the mask instead of outside. + public static void DrawMasked(this Polyline polyline, Quad mask, LineDrawingInfo lineInfo, bool reversedMask = false) + { + if (polyline.Count < 2) return; + + for (var i = 0; i < polyline.Count - 1; i++) + { + var start = polyline[i]; + var end = polyline[i + 1]; + var segment = new Segment(start, end); + segment.DrawMasked(mask, lineInfo, reversedMask); + } + } + + /// + /// Draws each segment of the polyline using a as the clipping mask. + /// + /// The polyline whose segments will be drawn. + /// The used as the clipping mask. + /// Drawing parameters such as thickness, color and cap type. + /// If true, draws the parts inside the mask instead of outside. + public static void DrawMasked(this Polyline polyline, Polygon mask, LineDrawingInfo lineInfo, bool reversedMask = false) + { + if (polyline.Count < 2) return; + + for (var i = 0; i < polyline.Count - 1; i++) + { + var start = polyline[i]; + var end = polyline[i + 1]; + var segment = new Segment(start, end); + segment.DrawMasked(mask, lineInfo, reversedMask); + } + } + /// + /// Draws each segment of the polyline using a mask of a generic closed shape type. + /// + /// + /// The mask type that implements (for example , , , or ). + /// + /// The polyline whose segments will be drawn. + /// The mask instance used for clipping each segment. + /// Drawing parameters such as thickness, color and cap type. + /// If true, draws the parts inside the mask instead of outside. + public static void DrawMasked(this Polyline polyline, T mask, LineDrawingInfo lineInfo, bool reversedMask = false) where T : IClosedShapeTypeProvider + { + if (polyline.Count < 2) return; + + for (var i = 0; i < polyline.Count - 1; i++) + { + var start = polyline[i]; + var end = polyline[i + 1]; + var segment = new Segment(start, end); + segment.DrawMasked(mask, lineInfo, reversedMask); + } + } + #endregion + + #region Gapped + /// + /// Draws a gapped outline for a polyline (open or closed), creating a dashed or segmented effect along the polyline's length. + /// + /// The polyline to draw. + /// + /// The total length of the polyline. + /// If zero or negative, the method calculates it automatically. + /// Providing a known length avoids redundant calculations and improves performance, especially for static segments. + /// + /// Parameters describing how to draw the polyline. + /// Parameters describing the gap configuration. + /// + /// The perimeter of the polyline if positive; otherwise, -1. + /// If the shape does not change, the valid length can be reused in subsequent frames to avoid recalculating. + /// + /// + /// - If is 0 or is 0, the polyline is drawn solid. + /// - If is 1 or greater, no polyline is drawn. + /// + public static float DrawGappedOutline(this Polyline polyline, float perimeter, LineDrawingInfo lineInfo, GappedOutlineDrawingInfo gapDrawingInfo) + { + if (gapDrawingInfo.Gaps <= 0 || gapDrawingInfo.GapPerimeterPercentage <= 0f) + { + polyline.Draw(lineInfo); + return perimeter > 0f ? perimeter : -1f; + } + + if (gapDrawingInfo.GapPerimeterPercentage >= 1f) return perimeter > 0f ? perimeter : -1f; + + var nonGapPercentage = 1f - gapDrawingInfo.GapPerimeterPercentage; + + var gapPercentageRange = gapDrawingInfo.GapPerimeterPercentage / gapDrawingInfo.Gaps; + var nonGapPercentageRange = nonGapPercentage / gapDrawingInfo.Gaps; + + if (perimeter <= 0f) + { + perimeter = polyline.GetLength(); + } + + var startDistance = perimeter * gapDrawingInfo.StartOffset; + var curDistance = 0f; + var nextDistance = startDistance; + + + var curIndex = 0; + var curPoint = polyline[0]; + var nextPoint= polyline[1]; + var curW = nextPoint - curPoint; + var curDis = curW.Length(); + + var points = new List(3); + + int whileCounter = gapDrawingInfo.Gaps; + + while (whileCounter > 0) + { + if (curDistance + curDis >= nextDistance) //as long as next distance in smaller than the distance to the next polyline point + { + var p = curPoint + (curW / curDis) * (nextDistance - curDistance); + + + if (points.Count == 0) + { + // var prevDistance = nextDistance; + nextDistance += nonGapPercentageRange * perimeter; + points.Add(p); + + } + else + { + // var prevDistance = nextDistance; + nextDistance += gapPercentageRange * perimeter; + points.Add(p); + + if (points.Count == 2) + { + SegmentDrawing.DrawSegment(points[0], points[1], lineInfo); + } + else + { + for (var i = 0; i < points.Count - 1; i++) + { + var p1 = points[i]; + var p2 = points[(i + 1) % points.Count]; + SegmentDrawing.DrawSegment(p1, p2, lineInfo); + } + } + points.Clear(); + whileCounter--; + } + + } + else + { + if (curIndex >= polyline.Count - 2) //last point + { + if (points.Count > 0) + { + points.Add(nextPoint); + if (points.Count == 2) + { + SegmentDrawing.DrawSegment(points[0], points[1], lineInfo); + } + else + { + for (var i = 0; i < points.Count - 1; i++) + { + var p1 = points[i]; + var p2 = points[(i + 1) % points.Count]; + SegmentDrawing.DrawSegment(p1, p2, lineInfo); + } + } + points.Clear(); + points.Add(polyline[0]); + } + + curDistance += curDis; + curIndex = 0; + curPoint = polyline[curIndex]; + nextPoint = polyline[(curIndex + 1) % polyline.Count]; + curW = nextPoint - curPoint; + curDis = curW.Length(); + } + else + { + if(points.Count > 0) points.Add(nextPoint); + + curDistance += curDis; + curIndex += 1;// (curIndex + 1) % polyline.Count; + curPoint = polyline[curIndex]; + nextPoint = polyline[(curIndex + 1) % polyline.Count]; + curW = nextPoint - curPoint; + curDis = curW.Length(); + } + } + + } + + return perimeter; + } + #endregion +} + diff --git a/ShapeEngine/Geometry/PolylineDef/PolylineMath.cs b/ShapeEngine/Geometry/PolylineDef/PolylineMath.cs index e6f005ab..71d2c610 100644 --- a/ShapeEngine/Geometry/PolylineDef/PolylineMath.cs +++ b/ShapeEngine/Geometry/PolylineDef/PolylineMath.cs @@ -2,6 +2,8 @@ using ShapeEngine.Core.Structs; using ShapeEngine.Geometry.PointsDef; using ShapeEngine.Geometry.PolygonDef; +using ShapeEngine.Geometry.TriangulationDef; +using ShapeEngine.ShapeClipper; using ShapeEngine.StaticLib; namespace ShapeEngine.Geometry.PolylineDef; @@ -10,51 +12,6 @@ public partial class Polyline { #region Math - /// - /// Returns a set of points representing the projection of the polyline along a given vector. - /// - /// The vector along which to project each point of the polyline. - /// - /// A collection containing the original and projected points, or null if the vector is zero. - /// - /// - /// Each point in the polyline is duplicated and offset by the vector . - /// - public Points? GetProjectedShapePoints(Vector2 v) - { - if (v.LengthSquared() <= 0f) return null; - var points = new Points(Count); - for (var i = 0; i < Count; i++) - { - points.Add(this[i]); - points.Add(this[i] + v); - } - - return points; - } - - /// - /// Projects the polyline along a given vector and returns the convex hull of the resulting points as a polygon. - /// - /// The vector along which to project each point of the polyline. - /// - /// A representing the convex hull of the projected points, - /// or null if the vector is zero. - /// - public Polygon? ProjectShape(Vector2 v) - { - if (v.LengthSquared() <= 0f || Count < 2) return null; - - var points = new Points(Count * 2); - for (var i = 0; i < Count; i++) - { - points.Add(this[i]); - points.Add(this[i] + v); - } - - return Polygon.FindConvexHull(points); - } - /// /// Gets the centroid point along the polyline, based on its length. /// @@ -83,28 +40,6 @@ public Vector2 GetCentroidOnLine() // return new Vector2(); } - /// - /// Calculates the mean centroid (arithmetic average) of all points in the polyline. - /// - /// - /// The mean centroid as a . Returns (0,0) if the polyline is empty, or the single point if only one exists. - /// - /// - /// This method averages all point positions. For a geometric centroid along the line, use . - /// - public Vector2 GetCentroidMean() - { - if (Count <= 0) return new(0f); - else if (Count == 1) return this[0]; - Vector2 total = new(0f); - foreach (Vector2 p in this) - { - total += p; - } - - return total / Count; - } - /// /// Gets a point along the polyline at a normalized position. /// @@ -295,288 +230,223 @@ public void SetSize(float size) } } + #endregion + + #region Outline Triangulation /// - /// Returns a new polyline translated so its centroid is at the specified position. + /// Triangulates this polyline's stroked outline and writes the generated triangles into the provided . /// - /// The new centroid position for the copy. - /// A new with the centroid at , - /// or null if fewer than 2 points. + /// The destination triangulation that receives the generated triangles. + /// The stroke thickness to triangulate. + /// The maximum miter length factor used for stroke joins. + /// Whether joins that exceed the miter limit should be beveled. + /// The end-cap style to use for the open polyline. + /// Whether to apply Delaunay refinement when creating the triangulation. /// - /// The original polyline is not modified. + /// This method triangulates only the stroked outline of the polyline. It does not modify the polyline itself. /// - public Polyline? SetPositionCopy(Vector2 newPosition) + public void TriangulateOutline(Triangulation result, float thickness, float miterLimit = 2f, bool beveled = false, ShapeClipperEndType endType = ShapeClipperEndType.Butt, bool useDelaunay = false) { - if (Count < 2) return null; - var centroid = GetCentroidMean(); - var delta = newPosition - centroid; - return ChangePositionCopy(delta); + ClipperImmediate2D.CreatePolylineTriangulation(this, thickness, miterLimit, beveled, endType, useDelaunay, result); } - + /// - /// Returns a new polyline translated by the specified offset. + /// Triangulates this polyline's stroked outline and writes the generated triangles into the provided . /// - /// The vector by which to offset all points. - /// A new translated by , - /// or null if fewer than 2 points. + /// The destination triangle mesh that receives the generated vertices and indices. + /// The stroke thickness to triangulate. + /// The maximum miter length factor used for stroke joins. + /// Whether joins that exceed the miter limit should be beveled. + /// The end-cap style to use for the open polyline. + /// Whether to apply Delaunay refinement when creating the triangulation. /// - /// The original polyline is not modified. + /// This method triangulates only the stroked outline of the polyline. It does not modify the polyline itself. /// - public new Polyline? ChangePositionCopy(Vector2 offset) + public void TriangulateOutline(TriMesh result, float thickness, float miterLimit = 2f, bool beveled = false, ShapeClipperEndType endType = ShapeClipperEndType.Butt, bool useDelaunay = false) { - if (Count < 2) return null; - var newPolygon = new Polyline(this.Count); - for (int i = 0; i < Count; i++) - { - newPolygon.Add(this[i] + offset); - } - - return newPolygon; + ClipperImmediate2D.CreatePolylineTriangulation(this, thickness, miterLimit, beveled, endType, useDelaunay, result); } - + #endregion + + #region Outline Perimeter Triangulation + /// - /// Returns a new polyline rotated by the specified angle around the given origin. + /// Builds a new open representing the first portion of this polyline up to the specified traveled distance. /// - /// The rotation angle in radians. - /// The origin point to rotate around. - /// A new rotated by around , or null if fewer than 2 points. + /// The distance to trace along the polyline. Values less than or equal to zero are ignored. + /// The destination polyline that will be cleared and populated with the traced points. /// - /// The original polyline is not modified. + /// The resulting polyline follows the original vertex order from the first point toward the last point and does not wrap. + /// If the requested distance ends in the middle of a segment, an interpolated endpoint is added. /// - public new Polyline? ChangeRotationCopy(float rotRad, Vector2 origin) + public void ToPolylinePerimeter(float perimeterToDraw, Polyline result) { - if (Count < 2) return null; - var newPolygon = new Polyline(this.Count); - for (var i = 0; i < Count; i++) + if (perimeterToDraw <= 0f) return; + + if (Count <= 1) return; + + result.Clear(); + + bool ccw = perimeterToDraw > 0; + float absPerimeterToDraw = MathF.Abs(perimeterToDraw); + float accumulatedPerimeter = 0f; + int currentIndex = ccw ? 0 : Count - 1; + + + // Create polyline based on perimeter. + // // Positive walks forward from 0. + // // Negative walks backward from Count - 1. + // // No wrapping: polyline stays open. + while (absPerimeterToDraw > accumulatedPerimeter) { - var w = this[i] - origin; - newPolygon.Add(origin + w.Rotate(rotRad)); - } - - return newPolygon; - } - - /// - /// Returns a new polyline rotated by the specified angle around its centroid. - /// - /// The rotation angle in radians. - /// A new rotated by around the centroid, or null if fewer than 2 points. - /// - /// The original polyline is not modified. - /// - public Polyline? ChangeRotationCopy(float rotRad) - { - if (Count < 2) return null; - return ChangeRotationCopy(rotRad, GetCentroidMean()); - } - - /// - /// Returns a new polyline rotated so that the vector from the origin to the first point matches the specified angle. - /// - /// The target angle in radians. - /// The origin point for the rotation. - /// A new with the specified absolute rotation, or null if fewer than 2 points. - /// - /// The original polyline is not modified. - /// - public new Polyline? SetRotationCopy(float angleRad, Vector2 origin) - { - if (Count < 2) return null; - - var curAngle = (this[0] - origin).AngleRad(); - var rotRad = ShapeMath.GetShortestAngleRad(curAngle, angleRad); - return ChangeRotationCopy(rotRad, origin); - } - - /// - /// Returns a new polyline rotated so that the vector from the centroid to the first point matches the specified angle. - /// - /// The target angle in radians. - /// A new with the specified absolute rotation, or null if fewer than 2 points. - /// - /// The original polyline is not modified. - /// - public Polyline? SetRotationCopy(float angleRad) - { - if (Count < 2) return null; + int nextIndex = currentIndex + (ccw ? 1 : -1); + if (nextIndex < 0 || nextIndex >= Count) break; //safety + + var cur = this[currentIndex]; + var next = this[nextIndex]; + result.Add(cur); + + float segmentLength = (next - cur).Length(); + + if (accumulatedPerimeter + segmentLength >= absPerimeterToDraw) + { + float remainingPerimeter = absPerimeterToDraw - accumulatedPerimeter; + float f = segmentLength <= 0f ? 0f : remainingPerimeter / segmentLength; + var end = cur.Lerp(next, f); + result.Add(end); + break; + } - var origin = GetCentroidMean(); - var curAngle = (this[0] - origin).AngleRad(); - var rotRad = ShapeMath.GetShortestAngleRad(curAngle, angleRad); - return ChangeRotationCopy(rotRad, origin); - } + accumulatedPerimeter += segmentLength; + currentIndex = nextIndex; - /// - /// Returns a new polyline scaled uniformly about its centroid. - /// - /// The scale factor to apply. - /// A new scaled by , or null if fewer than 2 points. - /// - /// The original polyline is not modified. - /// - public Polyline? ScaleSizeCopy(float scale) - { - if (Count < 2) return null; - return ScaleSizeCopy(scale, GetCentroidMean()); + // Reached the open end of the polyline; add the endpoint and stop. + if ((ccw && currentIndex == Count - 1) || (!ccw && currentIndex == 0)) + { + result.Add(this[currentIndex]); + break; + } + } } - + /// - /// Returns a new polyline scaled uniformly about the given origin. + /// Triangulates the stroked outline of the initial portion of this polyline up to the specified traveled distance and writes the result into the provided . /// - /// The scale factor to apply. - /// The origin point for scaling. - /// A new scaled by about , or null if fewer than 2 points. + /// The destination triangulation that receives the generated triangles. + /// The distance to trace along the polyline before triangulating the stroke. + /// The stroke thickness to triangulate. + /// The maximum miter length factor used for stroke joins. + /// Whether joins that exceed the miter limit should be beveled. + /// The end-cap style to use for the generated partial polyline. + /// Whether to apply Delaunay refinement when creating the triangulation. /// - /// The original polyline is not modified. + /// This method first builds a temporary partial polyline with , then triangulates that stroked path. /// - public new Polyline? ScaleSizeCopy(float scale, Vector2 origin) + public void TriangulateOutlinePerimeter(Triangulation result, float perimeterToDraw, float thickness, + float miterLimit = 2f, bool beveled = false, ShapeClipperEndType endType = ShapeClipperEndType.Butt, bool useDelaunay = false) { - if (Count < 2) return null; - var newPolyline = new Polyline(this.Count); - - for (var i = 0; i < Count; i++) - { - var w = this[i] - origin; - newPolyline.Add(origin + w * scale); - } - - return newPolyline; + ToPolylinePerimeter(perimeterToDraw, buffer); + ClipperImmediate2D.CreatePolylineTriangulation(buffer, thickness, miterLimit, beveled, endType, useDelaunay, result); } - + /// - /// Returns a new polyline scaled non-uniformly about the given origin. + /// Triangulates the stroked outline of the initial portion of this polyline up to the specified traveled distance and writes the result into the provided . /// - /// The scale vector to apply to each axis. - /// The origin point for scaling. - /// A new scaled by about , or null if fewer than 2 points. + /// The destination triangle mesh that receives the generated vertices and indices. + /// The distance to trace along the polyline before triangulating the stroke. + /// The stroke thickness to triangulate. + /// The maximum miter length factor used for stroke joins. + /// Whether joins that exceed the miter limit should be beveled. + /// The end-cap style to use for the generated partial polyline. + /// Whether to apply Delaunay refinement when creating the triangulation. /// - /// The original polyline is not modified. + /// This method first builds a temporary partial polyline with , then triangulates that stroked path. /// - public new Polyline? ScaleSizeCopy(Vector2 scale, Vector2 origin) + public void TriangulateOutlinePerimeter(TriMesh result, float perimeterToDraw, float thickness, + float miterLimit = 2f, bool beveled = false, ShapeClipperEndType endType = ShapeClipperEndType.Butt, bool useDelaunay = false) { - if (Count < 2) return null; - var newPolyline = new Polyline(this.Count); - - for (var i = 0; i < Count; i++) - { - var w = this[i] - origin; - newPolyline.Add(origin + w * scale); - } - - return newPolyline; + ToPolylinePerimeter(perimeterToDraw, buffer); + ClipperImmediate2D.CreatePolylineTriangulation(buffer, thickness, miterLimit, beveled, endType, useDelaunay, result); } + #endregion + #region Outline Percentage Triangulation /// - /// Returns a new polyline with each vector from the origin to each point changed in length by the specified amount. + /// Builds a new open representing the first portion of this polyline up to the specified fraction of its total length. /// - /// The amount to change the length of each vector from the origin. - /// The origin point for the size change. - /// A new with changed size, or null if fewer than 2 points. + /// The fraction of the total polyline length to include. Values less than or equal to zero are ignored, and values greater than or equal to one copy the full polyline. + /// The destination polyline that receives the traced points. /// - /// The original polyline is not modified. + /// This method converts the requested fraction into an absolute length along the polyline, then delegates to . /// - public new Polyline? ChangeSizeCopy(float amount, Vector2 origin) + public void ToPolylinePercentage(float f, Polyline result) { - if (Count < 2) return null; - var newPolyline = new Polyline(this.Count); + if (f <= 0) return; + + if (Count <= 1) return; - for (var i = 0; i < Count; i++) + if (f >= 1f) { - var w = this[i] - origin; - newPolyline.Add(origin + w.ChangeLength(amount)); + result.Clear(); + foreach (var p in this) + { + result.Add(p); + } + return; } - - return newPolyline; - } - - /// - /// Returns a new polyline with each vector from the centroid to each point changed in length by the specified amount. - /// - /// The amount to change the length of each vector from the centroid. - /// A new with changed size, or null if fewer than 3 points. - /// - /// The original polyline is not modified. - /// - public Polyline? ChangeSizeCopy(float amount) - { - if (Count < 3) return null; - return ChangeSizeCopy(amount, GetCentroidMean()); - } - - /// - /// Returns a new polyline with each vector from the origin to each point set to the specified length. - /// - /// The new length for each vector from the origin. - /// The origin point for the size set. - /// A new with set size, or null if fewer than 2 points. - /// - /// The original polyline is not modified. - /// - public new Polyline? SetSizeCopy(float size, Vector2 origin) - { - if (Count < 2) return null; - var newPolyline = new Polyline(this.Count); - - for (var i = 0; i < Count; i++) + + float totalPerimeter = 0f; + + for (var i = 0; i < Count - 1; i++) { - var w = this[i] - origin; - newPolyline.Add(origin + w.SetLength(size)); + var start = this[i]; + var end = this[i + 1]; + float l = (end - start).Length(); + totalPerimeter += l; } - - return newPolyline; - } - - /// - /// Returns a new polyline with each vector from the centroid to each point set to the specified length. - /// - /// The new length for each vector from the centroid. - /// A new with set size, or null if fewer than 2 points. - /// - /// The original polyline is not modified. - /// - public Polyline? SetSizeCopy(float size) - { - if (Count < 2) return null; - return SetSizeCopy(size, GetCentroidMean()); + ToPolylinePerimeter(totalPerimeter * f, result); } - + /// - /// Returns a new polyline with the specified transform applied, using the given origin. + /// Triangulates the stroked outline of the initial portion of this polyline defined by a fraction of its total length and writes the result into the provided . /// - /// The transform to apply. - /// The origin point for the transform. - /// A new with the transform applied, or null if fewer than 2 points. + /// Unused parameter retained for API compatibility. + /// The destination triangulation that receives the generated triangles. + /// The fraction of the total polyline length to include before triangulating the stroke. + /// The stroke thickness to triangulate. + /// The maximum miter length factor used for stroke joins. + /// Whether joins that exceed the miter limit should be beveled. + /// The end-cap style to use for the generated partial polyline. + /// Whether to apply Delaunay refinement when creating the triangulation. /// - /// The original polyline is not modified. + /// This method first builds a temporary partial polyline with , then triangulates that stroked path. /// - public new Polyline? SetTransformCopy(Transform2D transform, Vector2 origin) + public void TriangulateOutlinePercentage(IReadOnlyList polygon, Triangulation result, float f, float thickness, + float miterLimit = 2f, bool beveled = false, ShapeClipperEndType endType = ShapeClipperEndType.Butt, bool useDelaunay = false) { - if (Count < 2) return null; - var newPolyline = SetPositionCopy(transform.Position); - if (newPolyline == null) return null; - newPolyline.SetRotation(transform.RotationRad, origin); - newPolyline.SetSize(transform.ScaledSize.Length, origin); - return newPolyline; + ToPolylinePercentage(f, buffer); + ClipperImmediate2D.CreatePolylineTriangulation(buffer, thickness, miterLimit, beveled, endType, useDelaunay, result); } - + /// - /// Returns a new polyline with the specified offset applied, using the given origin. + /// Triangulates the stroked outline of the initial portion of this polyline defined by a fraction of its total length and writes the result into the provided . /// - /// The offset to apply. - /// The origin point for the offset. - /// A new with the offset applied, or null if fewer than 2 points. + /// Unused parameter retained for API compatibility. + /// The destination triangle mesh that receives the generated vertices and indices. + /// The fraction of the total polyline length to include before triangulating the stroke. + /// The stroke thickness to triangulate. + /// The maximum miter length factor used for stroke joins. + /// Whether joins that exceed the miter limit should be beveled. + /// The end-cap style to use for the generated partial polyline. + /// Whether to apply Delaunay refinement when creating the triangulation. /// - /// The original polyline is not modified. + /// This method first builds a temporary partial polyline with , then triangulates that stroked path. /// - public new Polyline? ApplyOffsetCopy(Transform2D offset, Vector2 origin) + public void TriangulateOutlinePercentage(IReadOnlyList polygon, TriMesh result, float f, float thickness, + float miterLimit = 2f, bool beveled = false, ShapeClipperEndType endType = ShapeClipperEndType.Butt, bool useDelaunay = false) { - if (Count < 2) return null; - - var newPolyline = ChangePositionCopy(offset.Position); - if (newPolyline == null) return null; - newPolyline.ChangeRotation(offset.RotationRad, origin); - newPolyline.ChangeSize(offset.ScaledSize.Length, origin); - return newPolyline; + ToPolylinePercentage(f, buffer); + ClipperImmediate2D.CreatePolylineTriangulation(buffer, thickness, miterLimit, beveled, endType, useDelaunay, result); } - #endregion } \ No newline at end of file diff --git a/ShapeEngine/Geometry/PolylineDef/Polylines.cs b/ShapeEngine/Geometry/PolylineDef/Polylines.cs new file mode 100644 index 00000000..b9ad46ba --- /dev/null +++ b/ShapeEngine/Geometry/PolylineDef/Polylines.cs @@ -0,0 +1,22 @@ +namespace ShapeEngine.Geometry.PolylineDef; + +/// +/// Represents a collection of objects. +/// +public class Polylines : List +{ + /// + /// Initializes a new instance of the class. + /// + public Polylines() { } + /// + /// Initializes a new instance of the class containing the specified polylines. + /// + /// An array of polylines to add. + public Polylines(params Polyline[] polylines) { AddRange(polylines); } + /// + /// Initializes a new instance of the class containing the specified polylines. + /// + /// An enumerable collection of polylines to add. + public Polylines(IEnumerable polylines) { AddRange(polylines); } +} \ No newline at end of file diff --git a/ShapeEngine/Geometry/QuadDef/Quad.cs b/ShapeEngine/Geometry/QuadDef/Quad.cs index 364ca0cc..2d2101eb 100644 --- a/ShapeEngine/Geometry/QuadDef/Quad.cs +++ b/ShapeEngine/Geometry/QuadDef/Quad.cs @@ -40,13 +40,30 @@ namespace ShapeEngine.Geometry.QuadDef; #region Getters /// - /// Gets the center point of the quad. + /// Gets the top-left vertex of the quad (alias for A). + /// Points are expected in counter-clockwise order. + /// + public Vector2 TopLeft => A; + /// + /// Gets the top-right vertex of the quad (alias for D). + /// Points are expected in counter-clockwise order. + /// + public Vector2 TopRight => D; + /// + /// Gets the bottom-right vertex of the quad (alias for C). + /// Points are expected in counter-clockwise order. /// - public Vector2 Center => GetPoint(0.5f); + public Vector2 BottomRight => C; /// - /// Gets the angle in radians of the BC edge. + /// Gets the bottom-left vertex of the quad (alias for B). + /// Points are expected in counter-clockwise order. /// - public float AngleRad => BC.AngleRad(); + public Vector2 BottomLeft => B; + + /// + /// Gets the center point of the quad. + /// + public Vector2 Center => (A + C) * 0.5f; /// /// Gets the vector from A to B. /// @@ -63,6 +80,23 @@ namespace ShapeEngine.Geometry.QuadDef; /// Gets the vector from D to A. /// public Vector2 DA => A - D; + + /// + /// Gets the midpoint of the edge from A to B. + /// + public Vector2 ABCenter => (A + B) * 0.5f; + /// + /// Gets the midpoint of the edge from B to C. + /// + public Vector2 BCCenter => (B + C) * 0.5f; + /// + /// Gets the midpoint of the edge from C to D. + /// + public Vector2 CDCenter => (C + D) * 0.5f; + /// + /// Gets the midpoint of the edge from D to A. + /// + public Vector2 DACenter => (D + A) * 0.5f; /// /// Gets the segment from A to B. @@ -150,6 +184,20 @@ public Quad(Rect rect) /// Rotates the rectangle around the specified pivot to form the quad. public Quad(Rect rect, float rotRad, AnchorPoint pivot) { + var sin = MathF.Sin(rotRad); + var cos = MathF.Cos(rotRad); + + var right = new Vector2(cos * rect.Width, sin * rect.Width); + var down = new Vector2(-sin * rect.Height, cos * rect.Height); + var topLeft = -right * pivot.X - down * pivot.Y; + + A = topLeft; + B = topLeft + down; + C = topLeft + right + down; + D = topLeft + right; + + /* + //OLD var pivotPoint = rect.GetPoint(pivot); var topLeft = rect.TopLeft; var bottomRight = rect.BottomRight; @@ -158,6 +206,41 @@ public Quad(Rect rect, float rotRad, AnchorPoint pivot) B = (new Vector2(topLeft.X, bottomRight.Y) - pivotPoint).Rotate(rotRad); C = (bottomRight - pivotPoint).Rotate(rotRad); D = (new Vector2(bottomRight.X, topLeft.Y) - pivotPoint).Rotate(rotRad); + */ + + } + /// + /// Initializes a new from a , applying a rotation around the specified pivot. + /// The rectangle's corner positions are translated so the pivot is the rotation origin, rotated by , + /// and the resulting corner positions are used as the quad vertices. + /// + /// The rectangle to convert to a quad. + /// Rotation angle in radians to apply around . + /// The point used as the rotation origin. + public Quad(Rect rect, float rotRad, Vector2 pivot) + { + var sin = MathF.Sin(rotRad); + var cos = MathF.Cos(rotRad); + + var right = new Vector2(cos * rect.Width, sin * rect.Width); + var down = new Vector2(-sin * rect.Height, cos * rect.Height); + var topLeft = (rect.TopLeft - pivot).Rotate(rotRad); + + A = topLeft; + B = topLeft + down; + C = topLeft + right + down; + D = topLeft + right; + + /* + //OLD + var topLeft = rect.TopLeft; + var bottomRight = rect.BottomRight; + + A = (topLeft - pivot).Rotate(rotRad); + B = (new Vector2(topLeft.X, bottomRight.Y) - pivot).Rotate(rotRad); + C = (bottomRight - pivot).Rotate(rotRad); + D = (new Vector2(bottomRight.X, topLeft.Y) - pivot).Rotate(rotRad); + */ } /// /// Initializes a new from a position, size, rotation, and alignment. @@ -169,6 +252,20 @@ public Quad(Rect rect, float rotRad, AnchorPoint pivot) /// Creates a quad with the specified alignment and rotation. public Quad(Vector2 pos, Size size, float rotRad, AnchorPoint alignment) { + var sin = MathF.Sin(rotRad); + var cos = MathF.Cos(rotRad); + + var right = new Vector2(cos * size.Width, sin * size.Width); + var down = new Vector2(-sin * size.Height, cos * size.Height); + var topLeft = pos - right * alignment.X - down * alignment.Y; + + A = topLeft; + B = topLeft + down; + C = topLeft + right + down; + D = topLeft + right; + + /* + //Old var offset = size * alignment.ToVector2(); var topLeft = pos - offset; @@ -180,7 +277,7 @@ public Quad(Vector2 pos, Size size, float rotRad, AnchorPoint alignment) A = pos + (a - pos).Rotate(rotRad); B = pos + (b - pos).Rotate(rotRad); C = pos + (c - pos).Rotate(rotRad); - D = pos + (d - pos).Rotate(rotRad); + D = pos + (d - pos).Rotate(rotRad);*/ } #endregion @@ -394,70 +491,6 @@ public Points GetRandomPointsOnEdge(int amount) #region Operators /// - /// Adds two instances component-wise. - /// - /// The first quad. - /// The second quad. - /// A new with each vertex being the sum of the corresponding vertices. - public static Quad operator +(Quad left, Quad right) - { - return new - ( - left.A + right.A, - left.B + right.B, - left.C + right.C, - left.D + right.D - ); - } - /// - /// Subtracts one from another component-wise. - /// - /// The first quad. - /// The quad to subtract. - /// A new with each vertex being the difference of the corresponding vertices. - public static Quad operator -(Quad left, Quad right) - { - return new - ( - left.A - right.A, - left.B - right.B, - left.C - right.C, - left.D - right.D - ); - } - /// - /// Multiplies two instances component-wise. - /// - /// The first quad. - /// The second quad. - /// A new with each vertex being the product of the corresponding vertices. - public static Quad operator *(Quad left, Quad right) - { - return new - ( - left.A * right.A, - left.B * right.B, - left.C * right.C, - left.D * right.D - ); - } - /// - /// Divides one by another component-wise. - /// - /// The numerator quad. - /// The denominator quad. - /// A new with each vertex being the quotient of the corresponding vertices. - public static Quad operator /(Quad left, Quad right) - { - return new - ( - left.A / right.A, - left.B / right.B, - left.C / right.C, - left.D / right.D - ); - } - /// /// Adds a to each vertex of the . /// /// The quad. @@ -521,40 +554,96 @@ public Points GetRandomPointsOnEdge(int amount) left.D / right ); } + /// - /// Multiplies each vertex of the by a scalar. + /// Increases the size of the uniformly by the specified amount. /// - /// The quad. - /// The scalar value. - /// A new with each vertex multiplied by the scalar. + /// The quad to resize. + /// The amount to add to the quad's size. + /// A new with the modified size. + public static Quad operator +(Quad left, float right) + { + return left.ChangeSize(right); + } + /// + /// Decreases the size of the uniformly by the specified amount. + /// + /// The quad to resize. + /// The amount to subtract from the quad's size. + /// A new with the modified size. + public static Quad operator -(Quad left, float right) + { + return left.ChangeSize(-right); + } + /// + /// Scales the size of the uniformly by the specified factor. + /// + /// The quad to scale. + /// The scale factor. + /// A new with the scaled size. public static Quad operator *(Quad left, float right) { - return new - ( - left.A * right, - left.B * right, - left.C * right, - left.D * right - ); + return left.ScaleSize(right); } /// - /// Divides each vertex of the by a scalar. + /// Scales the size of the uniformly by the inverse of the specified factor. /// - /// The quad. - /// The scalar value. - /// A new with each vertex divided by the scalar. - /// Returns an empty Quad if right is 0. + /// The quad to scale. + /// The divisor. + /// + /// A new with the scaled size, or a quad scaled to zero if + /// is 0. + /// public static Quad operator /(Quad left, float right) { - if (right == 0) return new(); - return new - ( - left.A / right, - left.B / right, - left.C / right, - left.D / right - ); + if (right == 0) + { + return left.ScaleSize(0f); + } + return left.ScaleSize(1f / right); } + + /// + /// Adds a to the quad, increasing its size accordingly. + /// + /// The quad to modify. + /// The size to add. + /// A new with the increased size. + public static Quad operator +(Quad left, Size right) + { + return left.ChangeSize(right); + } + /// + /// Subtracts a from the quad, decreasing its size accordingly. + /// + /// The quad to modify. + /// The size to subtract. + /// A new with the decreased size. + public static Quad operator -(Quad left, Size right) + { + return left.ChangeSize(-right); + } + /// + /// Scales the quad´s size by as factor. (component-wise multiplication). + /// + /// The quad to scale. + /// The size to scale by. + /// A new scaled by the given size. + public static Quad operator *(Quad left, Size right) + { + return left.ScaleSize(right); + } + /// + /// Divides the quad's size by as a factor (component-wise division). + /// + /// The quad to scale. + /// The size to divide by. + /// A new scaled by the inverse of the given size. + public static Quad operator /(Quad left, Size right) + { + return left.ScaleSize(right.Inverse()); + } + /// /// Determines whether two instances are equal. /// diff --git a/ShapeEngine/Geometry/QuadDef/QuadDrawing.cs b/ShapeEngine/Geometry/QuadDef/QuadDrawing.cs index 387a492e..70c5d551 100644 --- a/ShapeEngine/Geometry/QuadDef/QuadDrawing.cs +++ b/ShapeEngine/Geometry/QuadDef/QuadDrawing.cs @@ -1,3 +1,4 @@ + using System.Numerics; using Raylib_cs; using ShapeEngine.Color; @@ -19,509 +20,2874 @@ namespace ShapeEngine.Geometry.QuadDef; /// public static class QuadDrawing { - #region Draw Masked - /// - /// Draws the quad's outline segments constrained by a mask. - /// - /// The quad whose sides will be drawn (extension receiver). - /// Triangle mask used to clip each segment's drawing. - /// Line drawing parameters (thickness, color, cap type, etc.). - /// If true, draws the parts inside the mask instead of outside. - /// - /// This extension method forwards the draw call to each segment's DrawMasked overload, - /// allowing per-segment clipping by the provided triangle mask. - /// - public static void DrawLinesMasked(this Quad quad, Triangle mask, LineDrawingInfo lineInfo, bool reversedMask = false) - { - quad.SegmentAToB.DrawMasked(mask, lineInfo, reversedMask); - quad.SegmentBToC.DrawMasked(mask, lineInfo, reversedMask); - quad.SegmentCToD.DrawMasked(mask, lineInfo, reversedMask); - quad.SegmentDToA.DrawMasked(mask, lineInfo, reversedMask); - } - /// - /// Draws the quad's outline segments constrained by a mask. - /// - /// The quad whose sides will be drawn (extension receiver). - /// Circle mask used to clip each segment's drawing. - /// Line drawing parameters (thickness, color, cap type, etc.). - /// If true, draws the parts inside the mask instead of outside. - /// - /// This extension method forwards the draw call to each segment's DrawMasked overload, - /// allowing per-segment clipping by the provided circle mask. - /// - public static void DrawLinesMasked(this Quad quad, Circle mask, LineDrawingInfo lineInfo, bool reversedMask = false) - { - quad.SegmentAToB.DrawMasked(mask, lineInfo, reversedMask); - quad.SegmentBToC.DrawMasked(mask, lineInfo, reversedMask); - quad.SegmentCToD.DrawMasked(mask, lineInfo, reversedMask); - quad.SegmentDToA.DrawMasked(mask, lineInfo, reversedMask); - } - /// - /// Draws the quad's outline segments constrained by a mask. - /// - /// The quad whose sides will be drawn (extension receiver). - /// Rect mask used to clip each segment's drawing. - /// Line drawing parameters (thickness, color, cap type, etc.). - /// If true, draws the parts inside the mask instead of outside. - /// - /// This extension method forwards the draw call to each segment's DrawMasked overload, - /// allowing per-segment clipping by the provided rectangle mask. - /// - public static void DrawLinesMasked(this Quad quad, Rect mask, LineDrawingInfo lineInfo, bool reversedMask = false) - { - quad.SegmentAToB.DrawMasked(mask, lineInfo, reversedMask); - quad.SegmentBToC.DrawMasked(mask, lineInfo, reversedMask); - quad.SegmentCToD.DrawMasked(mask, lineInfo, reversedMask); - quad.SegmentDToA.DrawMasked(mask, lineInfo, reversedMask); - } - /// - /// Draws the quad's outline segments constrained by a mask. - /// - /// The quad whose sides will be drawn (extension receiver). - /// Quad mask used to clip each segment's drawing. - /// Line drawing parameters (thickness, color, cap type, etc.). - /// If true, draws the parts inside the mask instead of outside. - /// - /// Forwards the draw call to each segment's DrawMasked overload, - /// allowing per-segment clipping by the provided quad mask. - /// - public static void DrawLinesMasked(this Quad quad, Quad mask, LineDrawingInfo lineInfo, bool reversedMask = false) - { - quad.SegmentAToB.DrawMasked(mask, lineInfo, reversedMask); - quad.SegmentBToC.DrawMasked(mask, lineInfo, reversedMask); - quad.SegmentCToD.DrawMasked(mask, lineInfo, reversedMask); - quad.SegmentDToA.DrawMasked(mask, lineInfo, reversedMask); - } - /// - /// Draws the quad's outline segments constrained by a mask. - /// - /// The quad whose sides will be drawn (extension receiver). - /// Polygon mask used to clip each segment's drawing. - /// Line drawing parameters (thickness, color, cap type, etc.). - /// If true, draws the parts inside the mask instead of outside. - /// - /// Forwards the draw call to each segment's DrawMasked overload, - /// allowing per-segment clipping by the provided polygon mask. - /// - public static void DrawLinesMasked(this Quad quad, Polygon mask, LineDrawingInfo lineInfo, bool reversedMask = false) + #region Draw + + public static void Draw(this Quad q, ColorRgba color) { - quad.SegmentAToB.DrawMasked(mask, lineInfo, reversedMask); - quad.SegmentBToC.DrawMasked(mask, lineInfo, reversedMask); - quad.SegmentCToD.DrawMasked(mask, lineInfo, reversedMask); - quad.SegmentDToA.DrawMasked(mask, lineInfo, reversedMask); + Raylib.DrawTriangle(q.A, q.B, q.C, color.ToRayColor()); + Raylib.DrawTriangle(q.A, q.C, q.D, color.ToRayColor()); } + + #endregion + + #region Draw Scaled /// - /// Draws the quad's outline segments constrained by a generic closed-shape mask. + /// Draws a quad with scaled sides based on a specific draw type. /// - /// - /// The mask type. Must implement to provide closed-shape semantics - /// required for segment clipping. - /// - /// The quad whose sides will be drawn (extension receiver). - /// Mask used to clip each segment's drawing. - /// Line drawing parameters (thickness, color, cap type, etc.). - /// If true, draws the parts inside the mask instead of outside. - /// - /// Forwards the draw call to each segment's DrawMasked overload, allowing per-segment clipping - /// by the provided mask. This generic overload enables using any closed-shape provider without - /// adding a separate overload for each concrete shape type. - /// - public static void DrawLinesMasked(this Quad quad, T mask, LineDrawingInfo lineInfo, bool reversedMask = false) where T : IClosedShapeTypeProvider + /// The quad to draw. + /// The color of the drawn shape. + /// The scale factor of the sides (0 to 1). If >= 1, the full quad is drawn. If <= 0, nothing is drawn. + /// The origin point for scaling the sides (0 = start, 1 = end, 0.5 = center). + /// + /// The style of drawing: + /// + /// 0: [Filled] Drawn as 6 filled triangles, effectivly cutting of corners. + /// 1: [Sides] Each side is connected to the quad's center. + /// 2: [Sides Inverse] The start of 1 side is connected to the end of the next side and is connected to the quad's center. + /// + /// + public static void DrawScaled(this Quad q, ColorRgba color, float sideScaleFactor, float sideScaleOrigin, int drawType) { - quad.SegmentAToB.DrawMasked(mask, lineInfo, reversedMask); - quad.SegmentBToC.DrawMasked(mask, lineInfo, reversedMask); - quad.SegmentCToD.DrawMasked(mask, lineInfo, reversedMask); - quad.SegmentDToA.DrawMasked(mask, lineInfo, reversedMask); + if (sideScaleFactor <= 0) return; + if (sideScaleFactor >= 1) + { + q.Draw(color); + return; + } + + var s1 = new Segment(q.A, q.B).ScaleSegment(sideScaleFactor, sideScaleOrigin); + var s2 = new Segment(q.B, q.C).ScaleSegment(sideScaleFactor, sideScaleOrigin); + var s3 = new Segment(q.C, q.D).ScaleSegment(sideScaleFactor, sideScaleOrigin); + var s4 = new Segment(q.D, q.A).ScaleSegment(sideScaleFactor, sideScaleOrigin); + + var rayColor = color.ToRayColor(); + if (drawType == 0) + { + Raylib.DrawTriangle(s1.Start, s1.End, s2.Start, rayColor); + Raylib.DrawTriangle(s4.End, s1.Start, s2.Start, rayColor); + + Raylib.DrawTriangle(s4.End, s2.Start, s4.Start, rayColor); + Raylib.DrawTriangle(s4.Start, s2.Start, s2.End, rayColor); + + Raylib.DrawTriangle(s4.Start, s2.End, s3.End, rayColor); + Raylib.DrawTriangle(s3.End, s2.End, s3.Start, rayColor); + + } + else if (drawType == 1) + { + var center = q.Center; + Raylib.DrawTriangle(s1.Start, s1.End, center, rayColor); + Raylib.DrawTriangle(s2.Start, s2.End, center, rayColor); + Raylib.DrawTriangle(s3.Start, s3.End, center, rayColor); + Raylib.DrawTriangle(s4.Start, s4.End, center, rayColor); + } + else + { + var center = q.Center; + Raylib.DrawTriangle(s4.End, s1.Start, center, rayColor); + Raylib.DrawTriangle(s1.End, s2.Start, center, rayColor); + Raylib.DrawTriangle(s2.End, s3.Start, center, rayColor); + Raylib.DrawTriangle(s3.End, s4.Start, center, rayColor); + } } #endregion - /// - /// Draws a filled quadrilateral using four vertices. - /// - /// The first vertex of the quad. - /// The second vertex of the quad. - /// The third vertex of the quad. - /// The fourth vertex of the quad. - /// The color to fill the quad. - /// Fills the quad by drawing two triangles. - public static void DrawQuad(Vector2 a, Vector2 b, Vector2 c, Vector2 d, ColorRgba color) + #region Draw Lines + + public static void DrawLines(this Quad q, LineDrawingInfo lineInfo) { - Raylib.DrawTriangle(a, b, c, color.ToRayColor()); - Raylib.DrawTriangle(a, c, d, color.ToRayColor()); + q.DrawLines(lineInfo.Thickness, lineInfo.Color); } + + public static void DrawLines(this Quad q, float lineThickness, ColorRgba color) + { + + var horizontal = q.C - q.B; + var vertical = q.A - q.B; + var w = horizontal.Length(); + var h = vertical.Length(); + if (w <= 0 || h <= 0) return; + var bA = horizontal / w; + var bC = vertical / h; - /// - /// Draws the outline of a quadrilateral with specified line thickness and style. - /// - /// The first vertex of the quad. - /// The second vertex of the quad. - /// The third vertex of the quad. - /// The fourth vertex of the quad. - /// The thickness of the outline. - /// The color of the outline. - /// The style of the line caps. - /// The number of points used for the cap style. - /// Draws each side of the quad as a separate segment. - public static void DrawQuadLines(Vector2 a, Vector2 b, Vector2 c, Vector2 d, float lineThickness, ColorRgba color, LineCapType capType = LineCapType.CappedExtended, int capPoints = 2) - { - SegmentDrawing.DrawSegment(a, b, lineThickness, color, capType, capPoints); - SegmentDrawing.DrawSegment(b, c, lineThickness, color, capType, capPoints); - SegmentDrawing.DrawSegment(c, d, lineThickness, color, capType, capPoints); - SegmentDrawing.DrawSegment(d, a, lineThickness, color, capType, capPoints); - } + lineThickness = MathF.Min(lineThickness, MathF.Min(w, h) * 0.5f); + + var offsetDistance = MathF.Sqrt(2f * lineThickness * lineThickness); + + var internalBisectorB = bA + bC; + if (internalBisectorB.LengthSquared() < 1e-8f) + { + // edges are colinear; pick a perpendicular as fallback + internalBisectorB = new Vector2(-bA.Y, bA.X); + } + else + { + internalBisectorB = Vector2.Normalize(internalBisectorB); + } + + var aB = Vector2.Normalize(q.B - q.A); + var aD = Vector2.Normalize(q.D - q.A); - /// - /// Draws the outline of a quadrilateral, scaling each side by a specified factor. - /// - /// The first vertex of the quad. - /// The second vertex of the quad. - /// The third vertex of the quad. - /// The fourth vertex of the quad. - /// The thickness of the outline. - /// The color of the outline. - /// The factor by which to scale each side (0 = no line, 1 = full length). - /// The style of the line caps. - /// The number of points used for the cap style. - /// - /// Each side is drawn from its starting vertex towards its ending vertex, scaled by . - /// - public static void DrawQuadLines(Vector2 a, Vector2 b, Vector2 c, Vector2 d, float lineThickness, ColorRgba color, float sideLengthFactor, LineCapType capType = LineCapType.CappedExtended, int capPoints = 2) - { - var side1 = b - a; - var end1 = a + side1 * sideLengthFactor; + var internalBisectorA = aB + aD; + if (internalBisectorA.LengthSquared() < 1e-8f) + { + // edges are colinear; pick a perpendicular as fallback + internalBisectorA = new Vector2(-aB.Y, aB.X); + } + else + { + internalBisectorA = Vector2.Normalize(internalBisectorA); + } + + var outsideA = q.A - internalBisectorA * offsetDistance; + var insideA = q.A + internalBisectorA * offsetDistance; - var side2 = c - b; - var end2 = b + side2 * sideLengthFactor; + var outsideB = q.B - internalBisectorB * offsetDistance; + var insideB = q.B + internalBisectorB * offsetDistance; - var side3 = d - c; - var end3 = c + side3 * sideLengthFactor; + var outsideC = q.C + internalBisectorA * offsetDistance; + var insideC = q.C - internalBisectorA * offsetDistance; - var side4 = a - d; - var end4 = d + side4 * sideLengthFactor; + var outsideD = q.D + internalBisectorB * offsetDistance; + var insideD = q.D - internalBisectorB * offsetDistance; - SegmentDrawing.DrawSegment(a, end1, lineThickness, color, capType, capPoints); - SegmentDrawing.DrawSegment(b, end2, lineThickness, color, capType, capPoints); - SegmentDrawing.DrawSegment(c, end3, lineThickness, color, capType, capPoints); - SegmentDrawing.DrawSegment(d, end4, lineThickness, color, capType, capPoints); + TriangleDrawing.DrawTriangle(outsideA, outsideB, insideA, color); + TriangleDrawing.DrawTriangle(insideA, outsideB, insideB, color); + + TriangleDrawing.DrawTriangle(outsideB, outsideC, insideB, color); + TriangleDrawing.DrawTriangle(insideB, outsideC, insideC, color); + + TriangleDrawing.DrawTriangle(outsideC, outsideD, insideC, color); + TriangleDrawing.DrawTriangle(insideC, outsideD, insideD, color); + + TriangleDrawing.DrawTriangle(outsideD, outsideA, insideD, color); + TriangleDrawing.DrawTriangle(insideD, outsideA, insideA, color); } - /// - /// Draws a specified percentage of the outline of a quadrilateral. - /// - /// The first vertex of the quad. - /// The second vertex of the quad. - /// The third vertex of the quad. - /// The fourth vertex of the quad. - /// - /// The percentage of the outline to draw. - /// - /// Negative value reverses the direction (clockwise). - /// Integer part changes the starting corner (0 = a, 1 = b, etc.). - /// Fractional part is the percentage of the outline to draw. - /// Example: 0.35 starts at corner a, goes counter-clockwise, and draws 35% of the outline. - /// Example: -2.7 starts at b (third corner in cw direction), draws 70% of the outline in cw direction. - /// - /// - /// The thickness of the outline. - /// The color of the outline. - /// The style of the line caps. - /// The number of points used for the cap style. - /// - /// Useful for animating outlines or highlighting portions of a quad. - /// - public static void DrawQuadLinesPercentage(Vector2 a, Vector2 b, Vector2 c, Vector2 d, float f, float lineThickness, ColorRgba color, LineCapType capType = LineCapType.CappedExtended, int capPoints = 2) + #endregion + + #region Draw Lines Scaled + + public static void DrawLinesScaled(this Quad q, LineDrawingInfo lineInfo, float sideScaleFactor, float sideScaleOrigin = 0.5f) { - if (f == 0) return; + if (sideScaleFactor <= 0) return; + + if (sideScaleFactor >= 1) + { + q.DrawLines(lineInfo); + return; + } + + lineInfo = lineInfo.SetThickness(MathF.Min(lineInfo.Thickness, q.GetSize().Min() * 0.5f)); + + SegmentDrawing.DrawSegment(q.A, q.B, lineInfo, sideScaleFactor, sideScaleOrigin); + SegmentDrawing.DrawSegment(q.B, q.C, lineInfo, sideScaleFactor, sideScaleOrigin); + SegmentDrawing.DrawSegment(q.C, q.D, lineInfo, sideScaleFactor, sideScaleOrigin); + SegmentDrawing.DrawSegment(q.D, q.A, lineInfo, sideScaleFactor, sideScaleOrigin); + + } + + #endregion + + #region Draw Lines Percentage - bool negative = false; - if (f < 0) + public static void DrawLinesPercentage(this Quad quad, float f, int startIndex, LineDrawingInfo lineInfo) + { + quad.DrawLinesPercentage(f, startIndex, lineInfo.Thickness, lineInfo.Color); + } + + public static void DrawLinesPercentage(this Quad quad, float f, int startIndex, float lineThickness, ColorRgba color) + { + if (f == 0f || lineThickness <= 0) return; + var order = GetDrawLinePercentageOrder(quad, f, startIndex); + if(order.p <= 0f) return; + if(order.p >= 1f) { - negative = true; - f *= -1; + quad.DrawLines(lineThickness, color); + return; } - int startCorner = (int)f; - float percentage = f - startCorner; - if (percentage <= 0) return; + var p1 = order.a; + var p2 = order.b; + var p3 = order.c; + var p4 = order.d; + var percentage = order.p; + bool ccw = order.ccw; + var edge1 = p2 - p1; + var edge4 = p1 - p4; + float size1 = edge1.Length(); + float size2 = edge4.Length(); + + var rayColor = color.ToRayColor(); + lineThickness = MathF.Min(lineThickness, MathF.Min(size1, size2) * 0.5f); + float offsetDistance = MathF.Sqrt(2f * lineThickness * lineThickness); + float totalPerimeter = (size1 + size2) * 2f; + float perimeter = totalPerimeter * percentage; + float perimeterRemaining = perimeter; - startCorner = ShapeMath.Clamp(startCorner, 0, 3); + var edge2 = p3 - p2; + var edge3 = p4 - p3; + var n1 = edge1.Normalize(); + var n2 = edge2.Normalize(); + var n3 = edge3.Normalize(); + var n4 = edge4.Normalize(); - if (startCorner == 0) + var dir1 = (n1 - n4).Normalize(); + var dir2 = (n2 - n1).Normalize(); + var dir3 = (n3 - n2).Normalize(); + var dir4 = (n4 - n3).Normalize(); + + var curPoint = p1; + var curInner = curPoint + dir1 * offsetDistance; + var curOuter = curPoint- dir1 * offsetDistance; + + var nextPoint = p2; + var nextInner= nextPoint + dir2 * offsetDistance; + var nextOuter = nextPoint - dir2 * offsetDistance; + + if (perimeterRemaining >= size1) { - if (negative) + perimeterRemaining -= size1; + if (ccw) { - DrawQuadLinesPercentageHelper(a, d, c, b, percentage, lineThickness, color, capType, capPoints); + Raylib.DrawTriangle(curOuter, nextInner, curInner, rayColor); + Raylib.DrawTriangle(curOuter, nextOuter, nextInner, rayColor); } else { - DrawQuadLinesPercentageHelper(a, b, c, d, percentage, lineThickness, color, capType, capPoints); + Raylib.DrawTriangle(nextInner, curOuter, curInner, rayColor); + Raylib.DrawTriangle(nextOuter, curOuter, nextInner, rayColor); } } - else if (startCorner == 1) + else { - if (negative) + float factor = perimeterRemaining / size1; + var innerEnd = Vector2.Lerp(curInner, nextInner, factor); + var outerEnd = Vector2.Lerp(curOuter, nextOuter, factor); + if (ccw) { - DrawQuadLinesPercentageHelper(d, c, b, a, percentage, lineThickness, color, capType, capPoints); + Raylib.DrawTriangle(curOuter, innerEnd, curInner, rayColor); + Raylib.DrawTriangle(curOuter, outerEnd, innerEnd, rayColor); } else { - DrawQuadLinesPercentageHelper(b, c, d, a, percentage, lineThickness, color, capType, capPoints); + Raylib.DrawTriangle( innerEnd,curOuter, curInner, rayColor); + Raylib.DrawTriangle( outerEnd,curOuter, innerEnd, rayColor); } + return; } - else if (startCorner == 2) + + curInner = nextInner; + curOuter = nextOuter; + nextPoint = p3; + nextInner= nextPoint + dir3 * offsetDistance; + nextOuter = nextPoint - dir3 * offsetDistance; + + if (perimeterRemaining >= size2) { - if (negative) + perimeterRemaining -= size2; + if (ccw) { - DrawQuadLinesPercentageHelper(c, b, a, d, percentage, lineThickness, color, capType, capPoints); + Raylib.DrawTriangle(curOuter, nextInner, curInner, rayColor); + Raylib.DrawTriangle(curOuter, nextOuter, nextInner, rayColor); } else { - DrawQuadLinesPercentageHelper(c, d, a, b, percentage, lineThickness, color, capType, capPoints); + Raylib.DrawTriangle( nextInner,curOuter, curInner, rayColor); + Raylib.DrawTriangle( nextOuter,curOuter, nextInner, rayColor); } } - else if (startCorner == 3) + else { - if (negative) + float factor = perimeterRemaining / size2; + var innerEnd = Vector2.Lerp(curInner, nextInner, factor); + var outerEnd = Vector2.Lerp(curOuter, nextOuter, factor); + if (ccw) { - DrawQuadLinesPercentageHelper(b, a, d, c, percentage, lineThickness, color, capType, capPoints); + Raylib.DrawTriangle(curOuter, innerEnd, curInner, rayColor); + Raylib.DrawTriangle(curOuter, outerEnd, innerEnd, rayColor); } else { - DrawQuadLinesPercentageHelper(d, a, b, c, percentage, lineThickness, color, capType, capPoints); + Raylib.DrawTriangle( innerEnd,curOuter, curInner, rayColor); + Raylib.DrawTriangle( outerEnd,curOuter, innerEnd, rayColor); } + + return; } - } - - /// - /// Draws the outline of a quadrilateral using a structure. - /// - /// The first vertex of the quad. - /// The second vertex of the quad. - /// The third vertex of the quad. - /// The fourth vertex of the quad. - /// The line drawing information (thickness, color, cap type, etc.). - public static void DrawQuadLines(Vector2 a, Vector2 b, Vector2 c, Vector2 d, LineDrawingInfo lineInfo) - { - SegmentDrawing.DrawSegment(a, b, lineInfo.Thickness, lineInfo.Color, lineInfo.CapType, lineInfo.CapPoints); - SegmentDrawing.DrawSegment(b, c, lineInfo.Thickness, lineInfo.Color, lineInfo.CapType, lineInfo.CapPoints); - SegmentDrawing.DrawSegment(c, d, lineInfo.Thickness, lineInfo.Color, lineInfo.CapType, lineInfo.CapPoints); - SegmentDrawing.DrawSegment(d, a, lineInfo.Thickness, lineInfo.Color, lineInfo.CapType, lineInfo.CapPoints); - } + + curInner = nextInner; + curOuter = nextOuter; + nextPoint = p4; + nextInner= nextPoint + dir4 * offsetDistance; + nextOuter = nextPoint - dir4 * offsetDistance; - /// - /// Draws a filled quadrilateral using the vertices of a . - /// - /// The quad to draw. - /// The color to fill the quad. - public static void Draw(this Quad q, ColorRgba color) => DrawQuad(q.A, q.B, q.C, q.D, color); + if (perimeterRemaining >= size1) + { + perimeterRemaining -= size1; + if (ccw) + { + Raylib.DrawTriangle(curOuter, nextInner, curInner, rayColor); + Raylib.DrawTriangle(curOuter, nextOuter, nextInner, rayColor); + } + else + { + Raylib.DrawTriangle( nextInner,curOuter, curInner, rayColor); + Raylib.DrawTriangle( nextOuter,curOuter, nextInner, rayColor); + } + + } + else + { + float factor = perimeterRemaining / size1; + var innerEnd = Vector2.Lerp(curInner, nextInner, factor); + var outerEnd = Vector2.Lerp(curOuter, nextOuter, factor); + if (ccw) + { + Raylib.DrawTriangle(curOuter, innerEnd, curInner, rayColor); + Raylib.DrawTriangle(curOuter, outerEnd, innerEnd, rayColor); + } + else + { + Raylib.DrawTriangle( innerEnd,curOuter, curInner, rayColor); + Raylib.DrawTriangle( outerEnd,curOuter, innerEnd, rayColor); + } + + return; + } + + curInner = nextInner; + curOuter = nextOuter; + nextPoint = p1; + nextInner= nextPoint + dir1 * offsetDistance; + nextOuter = nextPoint - dir1 * offsetDistance; - /// - /// Draws the outline of a with specified line thickness and style. - /// - /// The quad to outline. - /// The thickness of the outline. - /// The color of the outline. - /// The style of the line caps. - /// The number of points used for the cap style. - public static void DrawLines(this Quad q, float lineThickness, ColorRgba color, LineCapType capType = LineCapType.CappedExtended, int capPoints = 2) - { - DrawQuadLines(q.A, q.B, q.C, q.D, lineThickness, color, capType, capPoints); + if (perimeterRemaining >= size2) + { + if (ccw) + { + Raylib.DrawTriangle(curOuter, nextInner, curInner, rayColor); + Raylib.DrawTriangle(curOuter, nextOuter, nextInner, rayColor); + } + else + { + Raylib.DrawTriangle(nextInner,curOuter, curInner, rayColor); + Raylib.DrawTriangle(nextOuter,curOuter, nextInner, rayColor); + } + + } + else + { + float factor = perimeterRemaining / size2; + var innerEnd = Vector2.Lerp(curInner, nextInner, factor); + var outerEnd = Vector2.Lerp(curOuter, nextOuter, factor); + if (ccw) + { + Raylib.DrawTriangle(curOuter, innerEnd, curInner, rayColor); + Raylib.DrawTriangle(curOuter, outerEnd, innerEnd, rayColor); + } + else + { + Raylib.DrawTriangle( innerEnd,curOuter, curInner, rayColor); + Raylib.DrawTriangle( outerEnd,curOuter, innerEnd, rayColor); + } + } } - + #endregion + + #region Draw Vignette /// - /// Draws the outline of a , scaling each side by a specified factor. + /// Draws a "vignette" effect inside the quad, creating a circular hole in the center. + /// The area between the inner circle and the quad's outer edges is filled with the specified color. /// - /// The quad to outline. - /// The thickness of the outline. - /// The color of the outline. - /// The factor by which to scale each side (0 = no line, 1 = full length). - /// The style of the line caps. - /// The number of points used for the cap style. - /// - /// Each side is drawn from its starting vertex towards its ending vertex, scaled by . - /// - public static void DrawLines(this Quad q, float lineThickness, ColorRgba color, float sideLengthFactor, LineCapType capType = LineCapType.CappedExtended, int capPoints = 2) + /// The quad to draw the vignette within. + /// The radius of the inner circular hole. + /// The starting rotation angle of the inner circle in degrees. + /// The color of the filled area. + /// + /// Determines the smoothness of the inner circle (0.0 to 1.0). + /// Higher values result in more segments and a smoother circle. + /// + public static void DrawVignette(this Quad q, float circleRadius, float circleRotDeg, ColorRgba color, float circleSmoothness = 0.5f) { - DrawQuadLines(q.A, q.B, q.C, q.D, lineThickness, color, sideLengthFactor, capType, capPoints); - } + if (circleRadius <= 0) + { + q.Draw(color); + return; + } - /// - /// Draws the outline of a using a structure. - /// - /// The quad to outline. - /// The line drawing information (thickness, color, cap type, etc.). - public static void DrawLines(this Quad q, LineDrawingInfo lineInfo) => DrawQuadLines(q.A, q.B, q.C, q.D, lineInfo); + // Clamp radius to ensure at least some vignette is drawn + var minDimension = q.GetSize().Min(); + var maxRadius = minDimension * 0.5f - 1f; + if (circleRadius > maxRadius) + { + circleRadius = maxRadius; + } + if (!CircleDrawing.CalculateCircleDrawingParameters(circleRadius, circleSmoothness, out float angleStepRad, out int segments, true)) return; + + var center = q.Center; + + // 1. Calculate Basis Vectors for local coordinate projection + // Assuming A=TopLeft, B=BottomLeft -> A->B is Down + // Assuming B=BottomLeft, C=BottomRight -> B->C is Right + Vector2 rightVec = q.C - q.B; + float width = rightVec.Length(); + if (width <= 0) return; + Vector2 rightAxis = rightVec / width; + + Vector2 downVec = q.B - q.A; + float height = downVec.Length(); + if (height <= 0) return; + Vector2 downAxis = downVec / height; + + float halfWidth = width * 0.5f; + float halfHeight = height * 0.5f; + + // Map side indices to actual Quad corner vertices for filling gaps. + // Side 0 (Right) -> Side 1 (Bottom) crosses Corner BR (C) + // Side 1 (Bottom) -> Side 2 (Left) crosses Corner BL (B) + // Side 2 (Left) -> Side 3 (Top) crosses Corner TL (A) + // Side 3 (Top) -> Side 0 (Right) crosses Corner TR (D) + Vector2[] cornerVertices = { q.C, q.B, q.A, q.D }; + + var rayColor = color.ToRayColor(); + var circleRotRad = circleRotDeg * ShapeMath.DEGTORAD; + + // Helper to project a direction vector onto the Quad's outer boundary + // Returns the world position on the edge and the side index (0=Right, 1=Bottom, 2=Left, 3=Top) + (Vector2 point, int side) ProjectToEdge(Vector2 direction) + { + float dotRight = Vector2.Dot(direction, rightAxis); + float dotDown = Vector2.Dot(direction, downAxis); + + // Avoid division by zero + float denomX = MathF.Abs(dotRight) > 1e-6f ? dotRight : 1e-6f; + float denomY = MathF.Abs(dotDown) > 1e-6f ? dotDown : 1e-6f; + + // Calculate distance to vertical (X) and horizontal (Y) boundaries + float tX = MathF.Abs(halfWidth / denomX); + float tY = MathF.Abs(halfHeight / denomY); + + if (tX < tY) + { + // Hits vertical side + int side = dotRight > 0 ? 0 : 2; // 0: Right, 2: Left + return (center + direction * tX, side); + } + else + { + // Hits horizontal side + int side = dotDown > 0 ? 1 : 3; // 1: Bottom, 3: Top + return (center + direction * tY, side); + } + } + + // 2. Initialize Starting Point + Vector2 currentDir = new Vector2(MathF.Cos(circleRotRad), MathF.Sin(circleRotRad)); + Vector2 startInner = center + currentDir * circleRadius; + var startProjection = ProjectToEdge(currentDir); + + Vector2 currentInner = startInner; + Vector2 currentOuter = startProjection.point; + int currentSide = startProjection.side; + + // 3. Iterate Segments + for (int i = 0; i < segments; i++) + { + float nextAngle = circleRotRad + angleStepRad * (i + 1); + Vector2 nextDir = new Vector2(MathF.Cos(nextAngle), MathF.Sin(nextAngle)); + + // Calculate Next Vertices + Vector2 nextInner = center + nextDir * circleRadius; + var nextProjection = ProjectToEdge(nextDir); + Vector2 nextOuter = nextProjection.point; + int nextSide = nextProjection.side; + + // Draw Vignette Segment (2 Triangles forming a quad) + // CCW Order: InnerStart -> EndOuter -> StartOuter + Raylib.DrawTriangle(currentInner, nextOuter, currentOuter, rayColor); + // CCW Order: InnerStart -> EndInner -> EndOuter + Raylib.DrawTriangle(currentInner, nextInner, nextOuter, rayColor); + + // Fill Corner Gap if we transitioned between sides (e.g., Right to Bottom) + if (currentSide != nextSide) + { + // The corner to fill corresponds to the current side index before the switch + Raylib.DrawTriangle(currentOuter, nextOuter, cornerVertices[currentSide], rayColor); + } + + // Advance + currentInner = nextInner; + currentOuter = nextOuter; + currentSide = nextSide; + } + } + #endregion + + #region Draw Corners /// - /// Draws a specified percentage of the outline of a . + /// Draws the corners of the quad with independent lengths for each corner. /// - /// The quad to outline. - /// - /// The percentage of the outline to draw. - /// - /// Negative value reverses the direction (clockwise). - /// Integer part changes the starting corner (0 = a, 1 = b, etc.). - /// Fractional part is the percentage of the outline to draw. - /// Example: 0.35 starts at corner a, goes counter-clockwise, and draws 35% of the outline. - /// Example: -2.7 starts at b (third corner in cw direction), draws 70% of the outline in cw direction. - /// - /// - /// The thickness of the outline. - /// The color of the outline. - /// The style of the line caps. - /// The number of points used for the cap style. - /// - /// Useful for animating outlines or highlighting portions of a quad. - /// - public static void DrawLinesPercentage(this Quad q, float f, float lineThickness, ColorRgba color, LineCapType capType = LineCapType.CappedExtended, int capPoints = 2) + /// The quad to draw corners for. + /// The thickness of the corner lines. + /// The color of the corner lines. + /// The length of the top-left corner. + /// The length of the top-right corner. + /// The length of the bottom-right corner. + /// The length of the bottom-left corner. + public static void DrawCorners(this Quad quad, float lineThickness, ColorRgba color, float tlCorner, float trCorner, float brCorner, float blCorner) { - DrawQuadLinesPercentage(q.A, q.B, q.C, q.D, f, lineThickness, color, capType, capPoints); + if (lineThickness <= 0f || color.A <= 0) return; + if(tlCorner <= 0 && trCorner <= 0 && brCorner <= 0 && blCorner <= 0) return; + + var a = quad.A; + var b = quad.B; + var c = quad.C; + var d = quad.D; + + float w = (d - a).Length(); + float h = (b - a).Length(); + if(w <= 0 || h <= 0) return; + + var nL = (a - d).Normalize(); + var nD = (b - a).Normalize(); + var nR = (c - b).Normalize(); + var nU = (d - c).Normalize(); + + float halfWidth = w * 0.5f; + float halfHeight = h * 0.5f; + + bool lineThicknessBiggerThanWidthOrHeight = lineThickness >= halfWidth || lineThickness >= halfHeight; + var rayColor = color.ToRayColor(); + + if (tlCorner > 0f) + { + var cornerLength = tlCorner; + if (lineThicknessBiggerThanWidthOrHeight) + { + var innerW = MathF.Min(MathF.Max(cornerLength, lineThickness), halfWidth); + var innerH = MathF.Min(MathF.Max(cornerLength, lineThickness), halfHeight); + + var newA = a + nU * lineThickness + nL * lineThickness; + var newB = a + nD * innerH + nL * lineThickness; + var newC = a + nD * innerH + nR * innerW; + var newD = a + nU * lineThickness + nR * innerW; + Raylib.DrawTriangle(newA, newB, newC, rayColor); + Raylib.DrawTriangle(newA, newC, newD, rayColor); + } + else if (cornerLength < lineThickness) + { + //just draw a square over the corner + var tl = a + nU * lineThickness + nL * lineThickness; + var bl = a + nD * lineThickness + nL * lineThickness; + var br = a + nD * lineThickness + nR * lineThickness; + var tr = a + nU * lineThickness + nR * lineThickness; + TriangleDrawing.DrawTriangle(tl, bl, tr, rayColor); + TriangleDrawing.DrawTriangle(tr, bl, br, rayColor); + + } + else + { + var cornerLengthH = MathF.Min(cornerLength, halfWidth); + var cornerLengthV = MathF.Min(cornerLength, halfHeight); + + var outer = a + nU * lineThickness + nL * lineThickness; + var outerH = a + nU * lineThickness + nR * cornerLengthH; + var outerV = a + nD * cornerLengthV + nL * lineThickness; + var inner = a + nD * lineThickness + nR * lineThickness; + var innerH = a + nD * lineThickness + nR * cornerLengthH; + var innerV = a + nD * cornerLengthV + nR * lineThickness; + + TriangleDrawing.DrawTriangle(outer, inner, outerH, rayColor); + TriangleDrawing.DrawTriangle(inner, innerH, outerH, rayColor); + TriangleDrawing.DrawTriangle(outer, outerV, inner, rayColor); + TriangleDrawing.DrawTriangle(inner, outerV, innerV, rayColor); + + } + } + + if (trCorner > 0f) + { + var cornerLength = trCorner; + if (lineThicknessBiggerThanWidthOrHeight) + { + var innerW = MathF.Min(MathF.Max(cornerLength, lineThickness), halfWidth); + var innerH = MathF.Min(MathF.Max(cornerLength, lineThickness), halfHeight); + + var newA = d + nU * lineThickness + nL * innerW; + var newB = d + nD * innerH + nL * innerW; + var newC = d + nD * innerH + nR * lineThickness; + var newD = d + nU * lineThickness + nR * lineThickness; + Raylib.DrawTriangle(newA, newB, newC, rayColor); + Raylib.DrawTriangle(newA, newC, newD, rayColor); + } + else if (cornerLength < lineThickness) + { + //just draw a square over the corner + var tl = d + nU * lineThickness + nL * lineThickness; + var bl = d + nD * lineThickness + nL * lineThickness; + var br = d + nD * lineThickness + nR * lineThickness; + var tr = d + nU * lineThickness + nR * lineThickness; + TriangleDrawing.DrawTriangle(tl, bl, tr, rayColor); + TriangleDrawing.DrawTriangle(tr, bl, br, rayColor); + + } + else + { + var cornerLengthH = MathF.Min(cornerLength, halfWidth); + var cornerLengthV = MathF.Min(cornerLength, halfHeight); + + var outer = d + nU * lineThickness + nR * lineThickness; + var outerH = d + nU * lineThickness + nL * cornerLengthH; + var outerV = d + nD * cornerLengthV + nR * lineThickness; + var inner = d + nD * lineThickness + nL * lineThickness; + var innerH = d + nD * lineThickness + nL * cornerLengthH; + var innerV = d + nD * cornerLengthV + nL * lineThickness; + + TriangleDrawing.DrawTriangle(outerH, inner, outer, rayColor); + TriangleDrawing.DrawTriangle(outerH, innerH, inner, rayColor); + TriangleDrawing.DrawTriangle(outer, inner, outerV, rayColor); + TriangleDrawing.DrawTriangle(inner, innerV, outerV, rayColor); + + } + } + + if (brCorner > 0f) + { + var cornerLength = brCorner; + if (lineThicknessBiggerThanWidthOrHeight) + { + var innerW = MathF.Min(MathF.Max(cornerLength, lineThickness), halfWidth); + var innerH = MathF.Min(MathF.Max(cornerLength, lineThickness), halfHeight); + + var newA = c + nU * innerH + nL * innerW; + var newB = c + nD * lineThickness + nL * innerW; + var newC = c + nD * lineThickness + nR * lineThickness; + var newD = c + nU * innerH + nR * lineThickness; + Raylib.DrawTriangle(newA, newB, newC, rayColor); + Raylib.DrawTriangle(newA, newC, newD, rayColor); + } + else if (cornerLength < lineThickness) + { + //just draw a square over the corner + var tl = c + nU * lineThickness + nL * lineThickness; + var bl = c + nD * lineThickness + nL * lineThickness; + var br = c + nD * lineThickness + nR * lineThickness; + var tr = c + nU * lineThickness + nR * lineThickness; + TriangleDrawing.DrawTriangle(tl, bl, tr, rayColor); + TriangleDrawing.DrawTriangle(tr, bl, br, rayColor); + + } + else + { + var cornerLengthH = MathF.Min(cornerLength, halfWidth); + var cornerLengthV = MathF.Min(cornerLength, halfHeight); + + var outer = c + nD * lineThickness + nR * lineThickness; + var outerH = c + nD * lineThickness + nL * cornerLengthH; + var outerV = c + nU * cornerLengthV + nR * lineThickness; + var inner = c + nU * lineThickness + nL * lineThickness; + var innerH = c + nU * lineThickness + nL * cornerLengthH; + var innerV = c + nU * cornerLengthV + nL * lineThickness; + + TriangleDrawing.DrawTriangle(outerV, inner, outer, rayColor); + TriangleDrawing.DrawTriangle(outerV, innerV, inner, rayColor); + TriangleDrawing.DrawTriangle(outerH, outer, inner, rayColor); + TriangleDrawing.DrawTriangle(inner, innerH, outerH, rayColor); + + } + } + + if (blCorner > 0f) + { + var cornerLength = blCorner; + + if (lineThicknessBiggerThanWidthOrHeight) + { + var innerW = MathF.Min(MathF.Max(cornerLength, lineThickness), halfWidth); + var innerH = MathF.Min(MathF.Max(cornerLength, lineThickness), halfHeight); + + var newA = b + nU * innerH + nL * lineThickness; + var newB = b + nD * lineThickness + nL * lineThickness; + var newC = b + nD * lineThickness + nR * innerW; + var newD = b + nU * innerH + nR * innerW; + Raylib.DrawTriangle(newA, newB, newC, rayColor); + Raylib.DrawTriangle(newA, newC, newD, rayColor); + } + else if (cornerLength < lineThickness) + { + //just draw a square over the corner + var tl = b + nU * lineThickness + nL * lineThickness; + var bl = b + nD * lineThickness + nL * lineThickness; + var br = b + nD * lineThickness + nR * lineThickness; + var tr = b + nU * lineThickness + nR * lineThickness; + TriangleDrawing.DrawTriangle(tl, bl, tr, rayColor); + TriangleDrawing.DrawTriangle(tr, bl, br, rayColor); + + } + else + { + var cornerLengthH = MathF.Min(cornerLength, halfWidth); + var cornerLengthV = MathF.Min(cornerLength, halfHeight); + + var outer = b + nD * lineThickness + nL * lineThickness; + var outerH = b + nD * lineThickness + nR * cornerLengthH; + var outerV = b + nU * cornerLengthV + nL * lineThickness; + var inner = b + nU * lineThickness + nR * lineThickness; + var innerH = b + nU * lineThickness + nR * cornerLengthH; + var innerV = b + nU * cornerLengthV + nR * lineThickness; + + TriangleDrawing.DrawTriangle(outerV, outer, inner, rayColor); + TriangleDrawing.DrawTriangle(outerV, inner, innerV, rayColor); + TriangleDrawing.DrawTriangle(outer, outerH, inner, rayColor); + TriangleDrawing.DrawTriangle(inner, outerH, innerH, rayColor); + + } + } } + + /// + /// Draws all corners of the quad with the same length. + /// + /// The quad to draw corners for. + /// The thickness of the corner lines. + /// The color of the corner lines. + /// The length of the corner segments. + public static void DrawCorners(this Quad quad, float lineThickness, ColorRgba color, float cornerLength) + { + if (lineThickness <= 0f || color.A <= 0 || cornerLength <= 0) return; + + var a = quad.A; + var b = quad.B; + var c = quad.C; + var d = quad.D; + + float w = (d - a).Length(); + float h = (b - a).Length(); + if(w <= 0 || h <= 0) return; + var nL = (a - d).Normalize(); + var nD = (b - a).Normalize(); + var nR = (c - b).Normalize(); + var nU = (d - c).Normalize(); + + float halfWidth = w * 0.5f; + float halfHeight = h * 0.5f; + bool widthDominant = w > h; + float minHalf = widthDominant ? halfHeight : halfWidth; + float maxHalf = widthDominant ? halfWidth : halfHeight; + float maxCorner = MathF.Max(lineThickness, cornerLength); + var rayColor = color.ToRayColor(); + + if (lineThickness >= minHalf) + { + if (lineThickness >= maxHalf) + { + var newA = a + nU * lineThickness + nL * lineThickness; + var newB = b + nD * lineThickness + nL * lineThickness; + var newC = c + nD * lineThickness + nR * lineThickness; + var newD = d + nU * lineThickness + nR * lineThickness; + Raylib.DrawTriangle(newA, newB, newC, rayColor); + Raylib.DrawTriangle(newA, newC, newD, rayColor); + } + else + { + if (widthDominant) + { + if (maxCorner >= halfWidth) + { + var newA = a + nU * lineThickness + nL * lineThickness; + var newB = b + nD * lineThickness + nL * lineThickness; + var newC = c + nD * lineThickness + nR * lineThickness; + var newD = d + nU * lineThickness + nR * lineThickness; + Raylib.DrawTriangle(newA, newB, newC, rayColor); + Raylib.DrawTriangle(newA, newC, newD, rayColor); + } + else + { + var newA = a + nU * lineThickness + nL * lineThickness; + var newB = b + nD * lineThickness + nL * lineThickness; + var newC = newB + nR * (lineThickness + maxCorner); + var newD = newA + nR * (lineThickness + maxCorner); + + Raylib.DrawTriangle(newA, newB, newC, rayColor); + Raylib.DrawTriangle(newA, newC, newD, rayColor); + + newC = c + nD * lineThickness + nR * lineThickness; + newD = d + nU * lineThickness + nR * lineThickness; + newA = newD + nL * (lineThickness + maxCorner); + newB = newC + nL * (lineThickness + maxCorner); + + Raylib.DrawTriangle(newA, newB, newC, rayColor); + Raylib.DrawTriangle(newA, newC, newD, rayColor); + } + } + else + { + if (maxCorner >= halfHeight) + { + var newA = a + nU * lineThickness + nL * lineThickness; + var newB = b + nD * lineThickness + nL * lineThickness; + var newC = c + nD * lineThickness + nR * lineThickness; + var newD = d + nU * lineThickness + nR * lineThickness; + Raylib.DrawTriangle(newA, newB, newC, rayColor); + Raylib.DrawTriangle(newA, newC, newD, rayColor); + } + else + { + var newA = a + nU * lineThickness + nL * lineThickness; + var newD = d + nU * lineThickness + nR * lineThickness; + var newB = newA + nD * (lineThickness + maxCorner); + var newC = newD + nD * (lineThickness + maxCorner); + + Raylib.DrawTriangle(newA, newB, newC, rayColor); + Raylib.DrawTriangle(newA, newC, newD, rayColor); + + newB = b + nD * lineThickness + nL * lineThickness; + newC = c + nD * lineThickness + nR * lineThickness; + newA = newB + nU * (lineThickness + maxCorner); + newD = newC + nU * (lineThickness + maxCorner); + + Raylib.DrawTriangle(newA, newB, newC, rayColor); + Raylib.DrawTriangle(newA, newC, newD, rayColor); + } + } + } + } + else + { + if (cornerLength < lineThickness) + { + //just draw a squares over each corner + + //tl + var tl = a + nU * lineThickness + nL * lineThickness; + var bl = a + nD * lineThickness + nL * lineThickness; + var br = a + nD * lineThickness + nR * lineThickness; + var tr = a + nU * lineThickness + nR * lineThickness; + TriangleDrawing.DrawTriangle(tl, bl, tr, rayColor); + TriangleDrawing.DrawTriangle(tr, bl, br, rayColor); + + //tr + tl = d + nU * lineThickness + nL * lineThickness; + bl = d + nD * lineThickness + nL * lineThickness; + br = d + nD * lineThickness + nR * lineThickness; + tr = d + nU * lineThickness + nR * lineThickness; + TriangleDrawing.DrawTriangle(tl, bl, tr, rayColor); + TriangleDrawing.DrawTriangle(tr, bl, br, rayColor); + + //br + tl = c + nU * lineThickness + nL * lineThickness; + bl = c + nD * lineThickness + nL * lineThickness; + br = c + nD * lineThickness + nR * lineThickness; + tr = c + nU * lineThickness + nR * lineThickness; + TriangleDrawing.DrawTriangle(tl, bl, tr, rayColor); + TriangleDrawing.DrawTriangle(tr, bl, br, rayColor); + + //bl + tl = b + nU * lineThickness + nL * lineThickness; + bl = b + nD * lineThickness + nL * lineThickness; + br = b + nD * lineThickness + nR * lineThickness; + tr = b + nU * lineThickness + nR * lineThickness; + TriangleDrawing.DrawTriangle(tl, bl, tr, rayColor); + TriangleDrawing.DrawTriangle(tr, bl, br, rayColor); + + } + else + { + //tl + var cornerLengthH = MathF.Min(cornerLength, halfWidth); + var cornerLengthV = MathF.Min(cornerLength, halfHeight); + + var outer = a + nU * lineThickness + nL * lineThickness; + var outerH = a + nU * lineThickness + nR * cornerLengthH; + var outerV = a + nD * cornerLengthV + nL * lineThickness; + var inner = a + nD * lineThickness + nR * lineThickness; + var innerH = a + nD * lineThickness + nR * cornerLengthH; + var innerV = a + nD * cornerLengthV + nR * lineThickness; + + TriangleDrawing.DrawTriangle(outer, inner, outerH, rayColor); + TriangleDrawing.DrawTriangle(inner, innerH, outerH, rayColor); + TriangleDrawing.DrawTriangle(outer, outerV, inner, rayColor); + TriangleDrawing.DrawTriangle(inner, outerV, innerV, rayColor); + + //tr + outer = d + nU * lineThickness + nR * lineThickness; + outerH = d + nU * lineThickness + nL * cornerLengthH; + outerV = d + nD * cornerLengthV + nR * lineThickness; + inner = d + nD * lineThickness + nL * lineThickness; + innerH = d + nD * lineThickness + nL * cornerLengthH; + innerV = d + nD * cornerLengthV + nL * lineThickness; + + TriangleDrawing.DrawTriangle(outerH, inner, outer, rayColor); + TriangleDrawing.DrawTriangle(outerH, innerH, inner, rayColor); + TriangleDrawing.DrawTriangle(outer, inner, outerV, rayColor); + TriangleDrawing.DrawTriangle(inner, innerV, outerV, rayColor); + + //br + outer = c + nD * lineThickness + nR * lineThickness; + outerH = c + nD * lineThickness + nL * cornerLengthH; + outerV = c + nU * cornerLengthV + nR * lineThickness; + inner = c + nU * lineThickness + nL * lineThickness; + innerH = c + nU * lineThickness + nL * cornerLengthH; + innerV = c + nU * cornerLengthV + nL * lineThickness; + + TriangleDrawing.DrawTriangle(outerV, inner, outer, rayColor); + TriangleDrawing.DrawTriangle(outerV, innerV, inner, rayColor); + TriangleDrawing.DrawTriangle(outerH, outer, inner, rayColor); + TriangleDrawing.DrawTriangle(inner, innerH, outerH, rayColor); + + //bl + outer = b + nD * lineThickness + nL * lineThickness; + outerH = b + nD * lineThickness + nR * cornerLengthH; + outerV = b + nU * cornerLengthV + nL * lineThickness; + inner = b + nU * lineThickness + nR * lineThickness; + innerH = b + nU * lineThickness + nR * cornerLengthH; + innerV = b + nU * cornerLengthV + nR * lineThickness; + + TriangleDrawing.DrawTriangle(outerV, outer, inner, rayColor); + TriangleDrawing.DrawTriangle(outerV, inner, innerV, rayColor); + TriangleDrawing.DrawTriangle(outer, outerH, inner, rayColor); + TriangleDrawing.DrawTriangle(inner, outerH, innerH, rayColor); + + } + } + } + #endregion + + #region Draw Corners Relative /// - /// Draws a specified percentage of the outline of a using a structure. + /// Draws the corners of the quad with independent lengths relative to the quad's minimum dimension. /// - /// The quad to outline. - /// - /// The percentage of the outline to draw. - /// - /// Negative value reverses the direction (clockwise). - /// Integer part changes the starting corner (0 = a, 1 = b, etc.). - /// Fractional part is the percentage of the outline to draw. - /// Example: 0.35 starts at corner a, goes counter-clockwise, and draws 35% of the outline. - /// Example: -2.7 starts at b (third corner in cw direction), draws 70% of the outline in cw direction. - /// - /// - /// The line drawing information (thickness, color, cap type, etc.). - /// - /// Useful for animating outlines or highlighting portions of a quad. - /// - public static void DrawLinesPercentage(this Quad q, float f, LineDrawingInfo lineInfo) + /// The quad to draw corners for. + /// The thickness of the corner lines. + /// The color of the corner lines. + /// Factor (0-1) for the top-left corner length relative to the quad's minimum size. + /// Factor (0-1) for the top-right corner length relative to the quad's minimum size. + /// Factor (0-1) for the bottom-right corner length relative to the quad's minimum size. + /// Factor (0-1) for the bottom-left corner length relative to the quad's minimum size. + public static void DrawCornersRelative(this Quad quad, float lineThickness, ColorRgba color, float tlCornerFactor, float trCornerFactor, float brCornerFactor, float blCornerFactor) { - DrawQuadLinesPercentage(q.A, q.B, q.C, q.D, f, lineInfo.Thickness, lineInfo.Color, lineInfo.CapType, lineInfo.CapPoints); + float minSize = quad.GetSize().Min(); + quad.DrawCorners(lineThickness, color, tlCornerFactor * minSize, trCornerFactor * minSize, brCornerFactor * minSize, blCornerFactor * minSize); } - + /// - /// Draws circles at each vertex of a . + /// Draws all corners of the quad with the same length relative to the quad's minimum dimension. /// - /// The quad whose vertices to draw. - /// The radius of each vertex circle. - /// The color of the vertex circles. - /// The number of segments to use for each circle (default is 8). - /// - /// Useful for visualizing or highlighting the corners of a quad. - /// - public static void DrawVertices(this Quad q, float vertexRadius, ColorRgba color, int circleSegments = 8) + /// The quad to draw corners for. + /// The thickness of the corner lines. + /// The color of the corner lines. + /// Factor (0-1) for the corner length relative to the quad's minimum size. + public static void DrawCornersRelative(this Quad quad, float lineThickness, ColorRgba color, float cornerLengthFactor) { - CircleDrawing.DrawCircle(q.A, vertexRadius, color, circleSegments); - CircleDrawing.DrawCircle(q.B, vertexRadius, color, circleSegments); - CircleDrawing.DrawCircle(q.C, vertexRadius, color, circleSegments); - CircleDrawing.DrawCircle(q.D, vertexRadius, color, circleSegments); + quad.DrawCornersRelative(lineThickness, color, cornerLengthFactor, cornerLengthFactor, cornerLengthFactor, cornerLengthFactor); } - + #endregion + + #region Draw Chamfered Corners /// - /// Draws the outline of a where each side can be scaled towards the origin of the side. + /// Draws the quad with chamfered corners of equal length. /// - /// The quad to outline. - /// The line drawing information (thickness, color, cap type, etc.). - /// The rotation of the quad in degrees. - /// The anchor point for rotation alignment. - /// - /// The scale factor for each side. - /// - /// 0: No quad is drawn. - /// 1: The normal quad is drawn. - /// 0.5: Each side is half as long. - /// - /// - /// - /// The point along the line to scale from, in both directions (0 to 1). - /// - /// 0: Start of Segment - /// 0.5: Center of Segment - /// 1: End of Segment - /// - /// - /// - /// Allows for dynamic scaling and rotation of quad outlines, useful for effects and animations. - /// - public static void DrawLinesScaled(this Quad q, LineDrawingInfo lineInfo, float rotDeg, AnchorPoint alignment, float sideScaleFactor, float sideScaleOrigin = 0.5f) + /// The quad to draw. + /// The fill color of the shape. + /// The length of the slant for all corners. + public static void DrawChamferedCorners(this Quad quad, ColorRgba color, float cornerLength) { - if (sideScaleFactor <= 0) return; + DrawChamferedCorners(quad, color, cornerLength, cornerLength); + } + + /// + /// Draws the quad with chamfered corners with separate horizontal and vertical slant lengths. + /// + /// The quad to draw. + /// The fill color of the shape. + /// The horizontal length of the slant. + /// The vertical length of the slant. + public static void DrawChamferedCorners(this Quad quad, ColorRgba color, float cornerLengthHorizontal, float cornerLengthVertical) + { + var size = quad.GetSize(); + if(size.Width <= 0 || size.Height <= 0) return; + if (cornerLengthHorizontal <= 0 && cornerLengthVertical <= 0) + { + quad.Draw(color); + return; + } + + float halfWidth = size.Width / 2f; + float halfHeight = size.Height / 2f; - if(rotDeg != 0) q = q.ChangeRotation(rotDeg * ShapeMath.DEGTORAD, alignment); + var nD = quad.NormalDown; + var nR = quad.NormalRight; + var nL = -nR; + var nU = -nD; - if (sideScaleFactor >= 1) + var tl = quad.TopLeft; + var br = quad.BottomRight; + + if (cornerLengthHorizontal >= halfWidth && cornerLengthVertical >= halfHeight) { - q.DrawLines(lineInfo); + var p1 = tl + nR * halfWidth; + var p2 = tl + nD * halfHeight; + var p3 = br + nL * halfWidth; + var p4 = br + nU * halfHeight; + TriangleDrawing.DrawTriangle(p1, p2, p3, color); + TriangleDrawing.DrawTriangle(p1, p3, p4, color); return; } - SegmentDrawing.DrawSegment(q.A, q.B, lineInfo, sideScaleFactor, sideScaleOrigin); - SegmentDrawing.DrawSegment(q.B, q.C, lineInfo, sideScaleFactor, sideScaleOrigin); - SegmentDrawing.DrawSegment(q.C, q.D, lineInfo, sideScaleFactor, sideScaleOrigin); - SegmentDrawing.DrawSegment(q.D, q.A, lineInfo, sideScaleFactor, sideScaleOrigin); + var bl = quad.BottomLeft; + var tr = quad.TopRight; + + + if (cornerLengthHorizontal >= halfWidth) + { + var top = tl + nR * halfWidth; + var bottom = bl + nR * halfWidth; + var tlV = tl + nD * cornerLengthVertical; + var blV = bl + nU * cornerLengthVertical; + var brV = br + nU * cornerLengthVertical; + var trV = tr + nD * cornerLengthVertical; + + TriangleDrawing.DrawTriangle(top, tlV, blV, color); + TriangleDrawing.DrawTriangle(top, blV, bottom, color); + + TriangleDrawing.DrawTriangle(top, bottom, brV, color); + TriangleDrawing.DrawTriangle(top, brV, trV, color); + } + else if (cornerLengthVertical >= halfHeight) + { + var left = tl + nD * halfHeight; + var right = tr + nD * halfHeight; + var tlH = tl + nR * cornerLengthHorizontal; + var blH = bl + nR * cornerLengthHorizontal; + var brH = br + nL * cornerLengthHorizontal; + var trH = tr + nL * cornerLengthHorizontal; + + TriangleDrawing.DrawTriangle(tlH, left, blH, color); + TriangleDrawing.DrawTriangle(tlH, blH, trH, color); + + TriangleDrawing.DrawTriangle(trH, blH, brH, color); + TriangleDrawing.DrawTriangle(trH, brH, right, color); + } + else + { + + var tlH = tl + nR * cornerLengthHorizontal; + var tlV = tl + nD * cornerLengthVertical; + + var blV = bl + nU * cornerLengthVertical; + var blH = bl + nR * cornerLengthHorizontal; + + var brH = br + nL * cornerLengthHorizontal; + var brV = br + nU * cornerLengthVertical; + + var trV = tr + nD * cornerLengthVertical; + var trH = tr + nL * cornerLengthHorizontal; + + //left triangles + TriangleDrawing.DrawTriangle(tlV, blV, tlH, color); + TriangleDrawing.DrawTriangle(tlH, blV, blH, color); + //center triangles + TriangleDrawing.DrawTriangle(tlH, blH, trH, color); + TriangleDrawing.DrawTriangle(trH, blH, brH, color); + + //right triangles + TriangleDrawing.DrawTriangle(trH, brH, trV, color); + TriangleDrawing.DrawTriangle(trV, brH, brV, color); + } } - private static void DrawQuadLinesPercentageHelper(Vector2 p1, Vector2 p2, Vector2 p3, Vector2 p4, float percentage, float lineThickness, ColorRgba color, LineCapType capType = LineCapType.CappedExtended, int capPoints = 2) + + /// + /// Draws the quad with independently chamfered corners. + /// + /// The quad to draw. + /// The fill color of the shape. + /// The slant length for the top-left corner. + /// The slant length for the top-right corner. + /// The slant length for the bottom-right corner. + /// The slant length for the bottom-left corner. + public static void DrawChamferedCorners(this Quad quad, ColorRgba color, float tlCorner, float blCorner, float brCorner, float trCorner) { - var l1 = (p2 - p1).Length(); - var l2 = (p3 - p2).Length(); - var l3 = (p4 - p3).Length(); - var l4 = (p1 - p4).Length(); - var perimeterToDraw = (l1 + l2 + l3 + l4) * percentage; - - // Draw first segment - var curP = p1; - var nextP = p2; - if (perimeterToDraw < l1) + if (tlCorner <= 0 && trCorner <= 0 && brCorner <= 0 && blCorner <= 0) { - float p = perimeterToDraw / l1; - nextP = curP.Lerp(nextP, p); - SegmentDrawing.DrawSegment(curP, nextP, lineThickness, color); + quad.Draw(color); return; } - - SegmentDrawing.DrawSegment(curP, nextP, lineThickness, color, capType, capPoints); - perimeterToDraw -= l1; - - // Draw second segment - curP = nextP; - nextP = p3; - if (perimeterToDraw < l2) - { - float p = perimeterToDraw / l2; - nextP = curP.Lerp(nextP, p); - SegmentDrawing.DrawSegment(curP, nextP, lineThickness, color, capType, capPoints); - return; + + Vector2 nU = quad.NormalUp; + Vector2 nR = quad.NormalRight; + + Vector2 center = quad.Center; + + var size = quad.GetSize(); + var halfWidth = size.Width * 0.5f; + var halfHeight = size.Height * 0.5f; + + Vector2 prev, start; + bool startMaxed, prevMaxed; + + var rayColor = color.ToRayColor(); + + if (tlCorner > 0) + { + var cornerLengthH = MathF.Min(tlCorner, halfWidth); + var cornerLengthV = MathF.Min(tlCorner, halfHeight); + Vector2 a = quad.TopLeft + nR * cornerLengthH; + Vector2 b = quad.TopLeft - nU * cornerLengthV; + + start = a; + prev = b; + + Raylib.DrawTriangle(a, b, center, rayColor); + + startMaxed = tlCorner >= halfWidth; + prevMaxed = tlCorner >= halfHeight; } - - SegmentDrawing.DrawSegment(curP, nextP, lineThickness, color, capType, capPoints); - perimeterToDraw -= l2; - - // Draw third segment - curP = nextP; - nextP = p4; - if (perimeterToDraw < l3) - { - float p = perimeterToDraw / l3; - nextP = curP.Lerp(nextP, p); - SegmentDrawing.DrawSegment(curP, nextP, lineThickness, color, capType, capPoints); - return; + else + { + Vector2 b = quad.TopLeft; + Vector2 a = b + nR * halfWidth; + Vector2 c = b - nU * halfHeight; + + start = a; + prev = c; + + Raylib.DrawTriangle(a, b, c, rayColor); + Raylib.DrawTriangle(a, c, center, rayColor); + + startMaxed = true; + prevMaxed = true; + } + + if (blCorner > 0) + { + var cornerLengthH = MathF.Min(blCorner, halfWidth); + var cornerLengthV = MathF.Min(blCorner, halfHeight); + Vector2 a = quad.BottomLeft + nU * cornerLengthV; + Vector2 b = quad.BottomLeft + nR * cornerLengthH; + + if (!prevMaxed || blCorner < halfHeight) + { + Raylib.DrawTriangle(prev, a, center, rayColor); + } + + prevMaxed = blCorner >= halfWidth; + + prev = b; + + Raylib.DrawTriangle(a, b, center, rayColor); + } + else + { + Vector2 b = quad.BottomLeft; + Vector2 a = b + nU * halfHeight; + Vector2 c = b + nR * halfWidth; + + if (!prevMaxed) + { + Raylib.DrawTriangle(prev, a, center, rayColor); + } + + prevMaxed = true; + + prev = c; + + Raylib.DrawTriangle(a, b, c, rayColor); + Raylib.DrawTriangle(a, c, center, rayColor); + } + + if (brCorner > 0) + { + var cornerLengthH = MathF.Min(brCorner, halfWidth); + var cornerLengthV = MathF.Min(brCorner, halfHeight); + Vector2 a = quad.BottomRight - nR * cornerLengthH; + Vector2 b = quad.BottomRight + nU * cornerLengthV; + + if (!prevMaxed || brCorner < halfWidth) + { + Raylib.DrawTriangle(prev, a, center, rayColor); + } + + prevMaxed = brCorner >= halfHeight; + + prev = b; + + Raylib.DrawTriangle(a, b, center, rayColor); + } + else + { + Vector2 b = quad.BottomRight; + Vector2 a = b - nR * halfWidth; + Vector2 c = b + nU * halfHeight; + + if (!prevMaxed) + { + Raylib.DrawTriangle(prev, a, center, rayColor); + } + + prevMaxed = true; + + prev = c; + + Raylib.DrawTriangle(a, b, c, rayColor); + Raylib.DrawTriangle(a, c, center, rayColor); } - SegmentDrawing.DrawSegment(curP, nextP, lineThickness, color, capType, capPoints); - perimeterToDraw -= l3; - - // Draw fourth segment - curP = nextP; - nextP = p1; - if (perimeterToDraw < l4) + if (trCorner > 0) + { + var cornerLengthH = MathF.Min(trCorner, halfWidth); + var cornerLengthV = MathF.Min(trCorner, halfHeight); + Vector2 a = quad.TopRight - nU * cornerLengthV; + Vector2 b = quad.TopRight - nR * cornerLengthH; + + if (!prevMaxed || trCorner < halfHeight) + { + Raylib.DrawTriangle(prev, a, center, rayColor); + } + + Raylib.DrawTriangle(a, b, center, rayColor); + + if(!startMaxed || trCorner < halfWidth) + { + Raylib.DrawTriangle(b, start, center, rayColor); + } + } + else + { + Vector2 b = quad.TopRight; + Vector2 a = b - nU * halfHeight; + Vector2 c = b - nR * halfWidth; + + if (!prevMaxed) + { + Raylib.DrawTriangle(prev, a, center, rayColor); + } + + Raylib.DrawTriangle(a, b, c, rayColor); + Raylib.DrawTriangle(a, c, center, rayColor); + + if (!startMaxed) + { + Raylib.DrawTriangle(c, start, center, rayColor); + } + } + } + + #endregion + + #region Draw Chamfered Corners Relative + /// + /// Draws the quad with slanted (chamfered) corners relative to the quad's dimensions. + /// + /// The quad to draw. + /// The fill color of the shape. + /// The slant factor (0-1) relative to half the quad's size. + public static void DrawChamferedCornersRelative(this Quad quad, ColorRgba color, float cornerLengthFactor) + { + var size = quad.GetSize(); + if(size.Width <= 0 || size.Height <= 0) return; + if (cornerLengthFactor <= 0) { - float p = perimeterToDraw / l4; - nextP = curP.Lerp(nextP, p); + quad.Draw(color); + return; } - SegmentDrawing.DrawSegment(curP, nextP, lineThickness, color, capType, capPoints); + + float halfWidth = size.Width / 2f; + float halfHeight = size.Height / 2f; + + if (cornerLengthFactor >= 1f) cornerLengthFactor = 1f; + DrawChamferedCorners(quad, color, halfWidth * cornerLengthFactor, halfHeight * cornerLengthFactor); } - + + /// + /// Draws the quad with slanted (chamfered) corners relative to the quad's dimensions, specifying horizontal and vertical factors. + /// + /// The quad to draw. + /// The fill color of the shape. + /// The horizontal slant factor (0-1) relative to half the quad's width. + /// The vertical slant factor (0-1) relative to half the quad's height. + public static void DrawChamferedCornersRelative(this Quad quad, ColorRgba color, float cornerLengthFactorHorizontal, float cornerLengthFactorVertical) + { + var size = quad.GetSize(); + if(size.Width <= 0 || size.Height <= 0) return; + if (cornerLengthFactorHorizontal <= 0 && cornerLengthFactorVertical <= 0) + { + quad.Draw(color); + return; + } + + if (cornerLengthFactorHorizontal >= 1f) cornerLengthFactorHorizontal = 1f; + if(cornerLengthFactorVertical >= 1f) cornerLengthFactorVertical = 1f; + + float cornerLengthH = cornerLengthFactorHorizontal * size.Width * 0.5f; + float cornerLengthV = cornerLengthFactorVertical * size.Height * 0.5f; + DrawChamferedCorners(quad, color, cornerLengthH, cornerLengthV); + } + + /// + /// Draws the quad with independently slanted (chamfered) corners relative to the quad's dimensions. + /// + /// The quad to draw. + /// The fill color of the shape. + /// Top-left corner slant factor (0-1) relative to half the quad's width. + /// Top-right corner slant factor (0-1) relative to half the quad's width. + /// Bottom-right corner slant factor (0-1) relative to half the quad's width. + /// Bottom-left corner slant factor (0-1) relative to half the quad's width. + public static void DrawChamferedCornersRelative(this Quad quad, ColorRgba color, float tlCornerFactor, float blCornerFactor, float brCornerFactor, float trCornerFactor) + { + if (tlCornerFactor <= 0 && trCornerFactor <= 0 && brCornerFactor <= 0 && blCornerFactor <= 0) + { + quad.Draw(color); + return; + } + + if(tlCornerFactor >= 1f) tlCornerFactor = 1f; + if(trCornerFactor >= 1f) trCornerFactor = 1f; + if(brCornerFactor >= 1f) brCornerFactor = 1f; + if(blCornerFactor >= 1f) blCornerFactor = 1f; + + Vector2 nU = quad.NormalUp; + Vector2 nR = quad.NormalRight; + + Vector2 center = quad.Center; + + var size = quad.GetSize(); + var halfWidth = size.Width * 0.5f; + var halfHeight = size.Height * 0.5f; + + Vector2 prev, start; + bool startMaxed, prevMaxed; + + var rayColor = color.ToRayColor(); + + if (tlCornerFactor > 0) + { + var cornerLengthH = tlCornerFactor * halfWidth; + var cornerLengthV = tlCornerFactor * halfHeight; + Vector2 a = quad.TopLeft + nR * cornerLengthH; + Vector2 b = quad.TopLeft - nU * cornerLengthV; + + start = a; + prev = b; + + Raylib.DrawTriangle(a, b, center, rayColor); + + startMaxed = prevMaxed = tlCornerFactor >= 1f; + } + else + { + Vector2 b = quad.TopLeft; + Vector2 a = b + nR * halfWidth; + Vector2 c = b - nU * halfHeight; + + start = a; + prev = c; + + Raylib.DrawTriangle(a, b, c, rayColor); + Raylib.DrawTriangle(a, c, center, rayColor); + + startMaxed = true; + prevMaxed = true; + } + + if (blCornerFactor > 0) + { + var cornerLengthH = blCornerFactor * halfWidth; + var cornerLengthV = blCornerFactor * halfHeight; + Vector2 a = quad.BottomLeft + nU * cornerLengthV; + Vector2 b = quad.BottomLeft + nR * cornerLengthH; + + if (!prevMaxed || blCornerFactor < 1f) + { + Raylib.DrawTriangle(prev, a, center, rayColor); + } + + prevMaxed = blCornerFactor >= 1f; + + prev = b; + + Raylib.DrawTriangle(a, b, center, rayColor); + } + else + { + Vector2 b = quad.BottomLeft; + Vector2 a = b + nU * halfHeight; + Vector2 c = b + nR * halfWidth; + + if (!prevMaxed) + { + Raylib.DrawTriangle(prev, a, center, rayColor); + } + + prevMaxed = true; + + prev = c; + + Raylib.DrawTriangle(a, b, c, rayColor); + Raylib.DrawTriangle(a, c, center, rayColor); + } + + if (brCornerFactor > 0) + { + var cornerLengthH = brCornerFactor * halfWidth; + var cornerLengthV = brCornerFactor * halfHeight; + Vector2 a = quad.BottomRight - nR * cornerLengthH; + Vector2 b = quad.BottomRight + nU * cornerLengthV; + + if (!prevMaxed || brCornerFactor < 1f) + { + Raylib.DrawTriangle(prev, a, center, rayColor); + } + + prevMaxed = brCornerFactor >= 1f; + + prev = b; + + Raylib.DrawTriangle(a, b, center, rayColor); + } + else + { + Vector2 b = quad.BottomRight; + Vector2 a = b - nR * halfWidth; + Vector2 c = b + nU * halfHeight; + + if (!prevMaxed) + { + Raylib.DrawTriangle(prev, a, center, rayColor); + } + + prevMaxed = true; + + prev = c; + + Raylib.DrawTriangle(a, b, c, rayColor); + Raylib.DrawTriangle(a, c, center, rayColor); + } + + if (trCornerFactor > 0) + { + var cornerLengthH = trCornerFactor * halfWidth; + var cornerLengthV = trCornerFactor * halfHeight; + Vector2 a = quad.TopRight - nU * cornerLengthV; + Vector2 b = quad.TopRight - nR * cornerLengthH; + + if (!prevMaxed || trCornerFactor < 1f) + { + Raylib.DrawTriangle(prev, a, center, rayColor); + } + + Raylib.DrawTriangle(a, b, center, rayColor); + + if(!startMaxed || trCornerFactor < 1f) + { + Raylib.DrawTriangle(b, start, center, rayColor); + } + } + else + { + Vector2 b = quad.TopRight; + Vector2 a = b - nU * halfHeight; + Vector2 c = b - nR * halfWidth; + + if (!prevMaxed) + { + Raylib.DrawTriangle(prev, a, center, rayColor); + } + + Raylib.DrawTriangle(a, b, c, rayColor); + Raylib.DrawTriangle(a, c, center, rayColor); + + if (!startMaxed) + { + Raylib.DrawTriangle(c, start, center, rayColor); + } + } + } + #endregion + + #region Draw Chamfered Corners Lines + /// + /// Draws the outline of a quad with equally chamfered corners using the specified line thickness. + /// + /// The quad whose chamfered outline is drawn. + /// + /// The thickness of the outline. If less than or equal to zero, the chamfered quad is filled instead of outlined. + /// + /// The color used to draw the outline. + /// + /// The chamfer length applied uniformly to all four corners. If less than or equal to zero, the regular quad outline is drawn. + /// + /// + /// The chamfer length and line thickness are clamped to the quad's minimum half-size to avoid invalid geometry. + /// Internally, this method builds inner and outer chamfer polygons and triangulates the outline area. + /// + public static void DrawChamferedCornersLines(this Quad quad, float lineThickness, ColorRgba color, float cornerLength) + { + if (lineThickness <= 0) + { + quad.DrawChamferedCorners(color, cornerLength); + return; + } + + if (cornerLength <= 0) + { + quad.DrawLines(lineThickness, color); + return; + } + + var size = quad.GetSize(); + if(size.Width <= 0 || size.Height <= 0) return; + + float halfWidth = size.Width * 0.5f; + float halfHeight = size.Height * 0.5f; + var minHalfSize = MathF.Min(halfWidth, halfHeight); + lineThickness = MathF.Min(lineThickness, minHalfSize); + cornerLength = MathF.Min(cornerLength, minHalfSize); + + var nR = quad.NormalRight; + var nD = quad.NormalDown; + var nL = -nR; + var nU = -nD; + + var chamferA = new ChamferedCorner(quad.A, cornerLength, cornerLength, nR, nD); + var chamferB = new ChamferedCorner(quad.B, cornerLength, cornerLength, nU, nR); + var chamferC = new ChamferedCorner(quad.C, cornerLength, cornerLength, nL, nU, -chamferA.ChamferDirPrev, -chamferA.ChamferDirNext); + var chamferD = new ChamferedCorner(quad.D, cornerLength, cornerLength, nD, nL, -chamferB.ChamferDirPrev, -chamferB.ChamferDirNext); + + var chamferLength = chamferA.GetChamferLength(nL, lineThickness); + + outerChamferPointsHelper.Clear(); + innerChamferPointsHelper.Clear(); + + chamferA.AddToList(innerChamferPointsHelper); + chamferB.AddToList(innerChamferPointsHelper); + chamferC.AddToList(innerChamferPointsHelper); + chamferD.AddToList(innerChamferPointsHelper); + + chamferA.AddOuterToList(outerChamferPointsHelper, chamferLength); + chamferB.AddOuterToList(outerChamferPointsHelper, chamferLength); + chamferC.AddOuterToList(outerChamferPointsHelper, chamferLength); + chamferD.AddOuterToList(outerChamferPointsHelper, chamferLength); + + DrawChamferedOutline(lineThickness, color); + } + + /// + /// Draws the outline of a quad with chamfered corners using separate horizontal and vertical chamfer lengths. + /// + /// The quad whose chamfered outline is drawn. + /// + /// The thickness of the outline. If less than or equal to zero, the chamfered quad is filled instead of outlined. + /// + /// The color used to draw the outline. + /// + /// The chamfer length measured along horizontal edges. If less than or equal to zero while the vertical value is positive, + /// the method falls back to the uniform chamfer overload using the vertical value. + /// + /// + /// The chamfer length measured along vertical edges. If less than or equal to zero while the horizontal value is positive, + /// the method falls back to the uniform chamfer overload using the horizontal value. + /// + /// + /// When both chamfer lengths are effectively equal, this method forwards to the single-length overload. + /// Chamfer lengths are clamped against the corresponding quad half-dimensions, and line thickness is clamped + /// against the minimum half-size of the quad. + /// + public static void DrawChamferedCornersLines(this Quad quad, float lineThickness, ColorRgba color, float cornerLengthHorizontal, float cornerLengthVertical) + { + if(lineThickness <= 0 && (cornerLengthHorizontal <= 0 || cornerLengthVertical <= 0)) + { + quad.Draw(color); + return; + } + + if (cornerLengthHorizontal <= 0 || cornerLengthVertical <= 0) + { + quad.DrawLines(lineThickness, color); + return; + } + // else if (cornerLengthHorizontal <= 0f) + // { + // quad.DrawChamferedCornersLines(lineThickness, color, cornerLengthVertical); + // return; + // } + // else if (cornerLengthVertical <= 0f) + // { + // quad.DrawChamferedCornersLines(lineThickness, color, cornerLengthHorizontal); + // return; + // } + // if (Math.Abs(cornerLengthHorizontal - cornerLengthVertical) < 0.0001f) + // { + // quad.DrawChamferedCornersLines(lineThickness, color, cornerLengthHorizontal); + // return; + // } + + if (lineThickness <= 0) + { + quad.DrawChamferedCorners(color, cornerLengthHorizontal, cornerLengthVertical); + return; + } + + var size = quad.GetSize(); + if(size.Width <= 0 || size.Height <= 0) return; + + float halfWidth = size.Width * 0.5f; + float halfHeight = size.Height * 0.5f; + var minHalfSize = MathF.Min(halfWidth, halfHeight); + lineThickness = MathF.Min(lineThickness, minHalfSize); + + var cornerLengthW = MathF.Min(cornerLengthHorizontal, halfWidth); + var cornerLengthH = MathF.Min(cornerLengthVertical, halfHeight); + + var nR = quad.NormalRight; + var nD = quad.NormalDown; + var nL = -nR; + var nU = -nD; + + var chamferA = new ChamferedCorner(quad.A, cornerLengthW, cornerLengthH, nR, nD); + var chamferB = new ChamferedCorner(quad.B, cornerLengthH, cornerLengthW, nU, nR); + var chamferC = new ChamferedCorner(quad.C, cornerLengthW, cornerLengthH, nL, nU); + var chamferD = new ChamferedCorner(quad.D, cornerLengthH, cornerLengthW, nD, nL); + + var chamferLengthV = chamferA.GetChamferLength(nL, lineThickness); + var chamferLengthH = chamferB.GetChamferLength(nD, lineThickness); + + outerChamferPointsHelper.Clear(); + innerChamferPointsHelper.Clear(); + + chamferA.AddToList(innerChamferPointsHelper); + chamferB.AddToList(innerChamferPointsHelper); + chamferC.AddToList(innerChamferPointsHelper); + chamferD.AddToList(innerChamferPointsHelper); + + chamferA.AddOuterToList(outerChamferPointsHelper, chamferLengthH, chamferLengthV); + chamferB.AddOuterToList(outerChamferPointsHelper, chamferLengthV, chamferLengthH); + chamferC.AddOuterToList(outerChamferPointsHelper, chamferLengthH, chamferLengthV); + chamferD.AddOuterToList(outerChamferPointsHelper, chamferLengthV, chamferLengthH); + + DrawChamferedOutline(lineThickness, color); + } + + /// + /// Draws the outline of a quad with independently chamfered corners. + /// + /// The quad whose chamfered outline is drawn. + /// + /// The thickness of the outline. If less than or equal to zero, the chamfered quad is filled instead of outlined. + /// + /// The color used to draw the outline. + /// Chamfer length for the top-left corner. + /// Chamfer length for the bottom-left corner. + /// Chamfer length for the bottom-right corner. + /// Chamfer length for the top-right corner. + /// + /// Negative corner values are clamped to zero. Each corner length is also clamped to the quad's minimum half-size + /// before geometry is generated. If all corner lengths are zero, the method falls back to drawing a regular quad outline. + /// + public static void DrawChamferedCornersLines(this Quad quad, float lineThickness, ColorRgba color, float tlCorner, float blCorner, float brCorner, float trCorner) + { + tlCorner = MathF.Max(0, tlCorner); + blCorner = MathF.Max(0, blCorner); + brCorner = MathF.Max(0, brCorner); + trCorner = MathF.Max(0, trCorner); + + var cornerSum = tlCorner + blCorner + brCorner + trCorner; + + if(lineThickness <= 0 && cornerSum <= 0) + { + quad.Draw(color); + return; + } + + if (cornerSum <= 0) + { + quad.DrawLines(lineThickness, color); + return; + } + + if (lineThickness <= 0) + { + quad.DrawChamferedCorners(color, tlCorner, blCorner, brCorner, trCorner); + return; + } + + var size = quad.GetSize(); + if(size.Width <= 0 || size.Height <= 0) return; + + float halfWidth = size.Width * 0.5f; + float halfHeight = size.Height * 0.5f; + var minHalfSize = MathF.Min(halfWidth, halfHeight); + lineThickness = MathF.Min(lineThickness, minHalfSize); + + var cornerLengthTl = MathF.Min(tlCorner, minHalfSize); + var cornerLengthBl = MathF.Min(blCorner, minHalfSize); + var cornerLengthBr = MathF.Min(brCorner, minHalfSize); + var cornerLengthTr = MathF.Min(trCorner, minHalfSize); + + var nR = quad.NormalRight; + var nD = quad.NormalDown; + var nL = -nR; + var nU = -nD; + + + var chamferA = new ChamferedCorner(quad.A, cornerLengthTl, nR, nD); + var chamferB = new ChamferedCorner(quad.B, cornerLengthBl, nU, nR); + var chamferC = new ChamferedCorner(quad.C, cornerLengthBr, nL, nU); + var chamferD = new ChamferedCorner(quad.D, cornerLengthTr, nD, nL); + + var chamferLengthA = chamferA.GetChamferLength(nL, lineThickness); + var chamferLengthB = chamferB.GetChamferLength(nD, lineThickness); + var chamferLengthC = chamferC.GetChamferLength(nR, lineThickness); + var chamferLengthD = chamferD.GetChamferLength(nU, lineThickness); + + outerChamferPointsHelper.Clear(); + innerChamferPointsHelper.Clear(); + + chamferA.AddToList(innerChamferPointsHelper); + chamferB.AddToList(innerChamferPointsHelper); + chamferC.AddToList(innerChamferPointsHelper); + chamferD.AddToList(innerChamferPointsHelper); + + chamferA.AddOuterToList(outerChamferPointsHelper, chamferLengthA); + chamferB.AddOuterToList(outerChamferPointsHelper, chamferLengthB); + chamferC.AddOuterToList(outerChamferPointsHelper, chamferLengthC); + chamferD.AddOuterToList(outerChamferPointsHelper, chamferLengthD); + + DrawChamferedOutline(lineThickness, color); + } + + #endregion + + #region Draw Chamfered Corners Relative Lines + /// + /// Draws the outline of a quad with uniformly chamfered corners specified as a relative factor of the quad size. + /// + /// The quad whose chamfered outline is drawn. + /// + /// The thickness of the outline. If less than or equal to zero, the filled chamfered quad is drawn instead. + /// + /// The color used to draw the outline. + /// + /// A normalized factor in the range [0, 1] that determines the chamfer amount relative to the quad half-size. + /// + /// + /// The factor is clamped to the range [0, 1]. The resulting horizontal and vertical chamfer lengths are derived + /// from half the quad width and half the quad height respectively. + /// + public static void DrawChamferedCornersLinesRelative(this Quad quad, float lineThickness, ColorRgba color, float cornerLengthFactor) + { + var size = quad.GetSize(); + float halfWidth = size.Width * 0.5f; + float halfHeight = size.Height * 0.5f; + cornerLengthFactor = ShapeMath.Clamp(cornerLengthFactor, 0f, 1f); + + DrawChamferedCornersLines(quad, lineThickness, color, halfWidth * cornerLengthFactor, halfHeight * cornerLengthFactor); + } + + /// + /// Draws the outline of a quad with chamfered corners specified by separate relative horizontal and vertical factors. + /// + /// The quad whose chamfered outline is drawn. + /// + /// The thickness of the outline. If less than or equal to zero, the filled chamfered quad is drawn instead. + /// + /// The color used to draw the outline. + /// + /// A normalized factor in the range [0, 1] that scales the chamfer amount relative to half the quad width. + /// + /// + /// A normalized factor in the range [0, 1] that scales the chamfer amount relative to half the quad height. + /// + /// + /// Both factors are clamped independently to the range [0, 1] before being converted into absolute chamfer lengths. + /// + public static void DrawChamferedCornersLinesRelative(this Quad quad, float lineThickness, ColorRgba color, float cornerLengthFactorHorizontal, float cornerLengthFactorVertical) + { + var size = quad.GetSize(); + float halfWidth = size.Width * 0.5f; + float halfHeight = size.Height * 0.5f; + cornerLengthFactorHorizontal = ShapeMath.Clamp(cornerLengthFactorHorizontal, 0f, 1f); + cornerLengthFactorVertical = ShapeMath.Clamp(cornerLengthFactorVertical, 0f, 1f); + float cornerLengthH = cornerLengthFactorHorizontal * halfWidth; + float cornerLengthV = cornerLengthFactorVertical * halfHeight; + DrawChamferedCornersLines(quad, lineThickness, color, cornerLengthH, cornerLengthV); + } + + /// + /// Draws the outline of a quad with independently chamfered corners specified as relative factors. + /// + /// The quad whose chamfered outline is drawn. + /// + /// The thickness of the outline. If less than or equal to zero, the filled chamfered quad is drawn instead. + /// + /// The color used to draw the outline. + /// Normalized chamfer factor for the top-left corner. + /// Normalized chamfer factor for the bottom-left corner. + /// Normalized chamfer factor for the bottom-right corner. + /// Normalized chamfer factor for the top-right corner. + /// + /// Each factor is clamped to the range [0, 1]. The effective chamfer distances are derived from the quad half-width + /// or half-height depending on the corner edge direction used to construct the chamfer geometry. + /// + public static void DrawChamferedCornersLinesRelative(this Quad quad, float lineThickness, ColorRgba color, float tlCornerFactor, float blCornerFactor, float brCornerFactor, float trCornerFactor) + { + tlCornerFactor = ShapeMath.Clamp(tlCornerFactor, 0f, 1f); + blCornerFactor = ShapeMath.Clamp(blCornerFactor, 0f, 1f); + brCornerFactor = ShapeMath.Clamp(brCornerFactor, 0f, 1f); + trCornerFactor = ShapeMath.Clamp(trCornerFactor, 0f, 1f); + + var cornerFactorSum = tlCornerFactor + blCornerFactor + brCornerFactor + trCornerFactor; + + if(lineThickness <= 0 && cornerFactorSum <= 0) + { + quad.Draw(color); + return; + } + + if (cornerFactorSum <= 0) + { + quad.DrawLines(lineThickness, color); + return; + } + + if (lineThickness <= 0) + { + quad.DrawChamferedCornersRelative(color, tlCornerFactor, blCornerFactor, brCornerFactor, trCornerFactor); + return; + } + + var size = quad.GetSize(); + if(size.Width <= 0 || size.Height <= 0) return; + + float halfWidth = size.Width * 0.5f; + float halfHeight = size.Height * 0.5f; + var minHalfSize = MathF.Min(halfWidth, halfHeight); + lineThickness = MathF.Min(lineThickness, minHalfSize); + + var cornerLengthPrevTl = MathF.Min(halfWidth * tlCornerFactor, halfWidth); + var cornerLengthNextTl = MathF.Min(halfHeight * tlCornerFactor, halfHeight); + + var cornerLengthPrevBl = MathF.Min(halfHeight * blCornerFactor, halfHeight); + var cornerLengthNextBl = MathF.Min(halfWidth * blCornerFactor, halfWidth); + + var cornerLengthPrevBr = MathF.Min(halfWidth * brCornerFactor, halfWidth); + var cornerLengthNextBr = MathF.Min(halfHeight * brCornerFactor, halfHeight); + + var cornerLengthPrevTr = MathF.Min(halfHeight * trCornerFactor, halfHeight); + var cornerLengthNextTr = MathF.Min(halfWidth * trCornerFactor, halfWidth); + + var nR = quad.NormalRight; + var nD = quad.NormalDown; + var nL = -nR; + var nU = -nD; + + + var chamferA = new ChamferedCorner(quad.A, cornerLengthPrevTl, cornerLengthNextTl, nR, nD); + var chamferB = new ChamferedCorner(quad.B, cornerLengthPrevBl, cornerLengthNextBl, nU, nR); + var chamferC = new ChamferedCorner(quad.C, cornerLengthPrevBr, cornerLengthNextBr, nL, nU); + var chamferD = new ChamferedCorner(quad.D, cornerLengthPrevTr, cornerLengthNextTr, nD, nL); + + var chamferLengthA = chamferA.GetChamferLength(nL, lineThickness); + var chamferLengthB = chamferB.GetChamferLength(nD, lineThickness); + var chamferLengthC = chamferC.GetChamferLength(nR, lineThickness); + var chamferLengthD = chamferD.GetChamferLength(nU, lineThickness); + + outerChamferPointsHelper.Clear(); + innerChamferPointsHelper.Clear(); + + chamferA.AddToList(innerChamferPointsHelper); + chamferB.AddToList(innerChamferPointsHelper); + chamferC.AddToList(innerChamferPointsHelper); + chamferD.AddToList(innerChamferPointsHelper); + + chamferA.AddOuterToList(outerChamferPointsHelper, chamferLengthA); + chamferB.AddOuterToList(outerChamferPointsHelper, chamferLengthB); + chamferC.AddOuterToList(outerChamferPointsHelper, chamferLengthC); + chamferD.AddOuterToList(outerChamferPointsHelper, chamferLengthD); + + DrawChamferedOutline(lineThickness, color); + + } + #endregion + + #region Draw Vertices + /// + /// Draws circles at each vertex of a . + /// + /// The quad whose vertices to draw. + /// The radius of each vertex circle. + /// The color of the vertex circles. + /// + /// The smoothness value (0-1). This controls the visual quality of the circle by inversely interpolating the current . + /// A value of 0 uses the maximum side length (fewer sides, less smooth), while 1 uses the minimum side length (more sides, smoother). + /// The resulting side length determines the number of polygon sides used to approximate the circle. + /// + /// + /// Useful for visualizing or highlighting the corners of a quad. + /// + public static void DrawVertices(this Quad q, float vertexRadius, ColorRgba color, float smoothness = 0.5f) + { + var circle = new Circle(q.A, vertexRadius); + circle.Draw(color, smoothness); + circle = circle.SetPosition(q.B); + circle.Draw(color, smoothness); + circle = circle.SetPosition(q.C); + circle.Draw(color, smoothness); + circle = circle.SetPosition(q.D); + circle.Draw(color, smoothness); + } + #endregion + + #region Draw Masked + /// + /// Draws the quad's outline segments constrained by a mask. + /// + /// The quad whose sides will be drawn (extension receiver). + /// Triangle mask used to clip each segment's drawing. + /// Line drawing parameters (thickness, color, cap type, etc.). + /// If true, draws the parts inside the mask instead of outside. + /// + /// This extension method forwards the draw call to each segment's DrawMasked overload, + /// allowing per-segment clipping by the provided triangle mask. + /// + public static void DrawLinesMasked(this Quad quad, Triangle mask, LineDrawingInfo lineInfo, bool reversedMask = false) + { + quad.SegmentAToB.DrawMasked(mask, lineInfo, reversedMask); + quad.SegmentBToC.DrawMasked(mask, lineInfo, reversedMask); + quad.SegmentCToD.DrawMasked(mask, lineInfo, reversedMask); + quad.SegmentDToA.DrawMasked(mask, lineInfo, reversedMask); + } + /// + /// Draws the quad's outline segments constrained by a mask. + /// + /// The quad whose sides will be drawn (extension receiver). + /// Circle mask used to clip each segment's drawing. + /// Line drawing parameters (thickness, color, cap type, etc.). + /// If true, draws the parts inside the mask instead of outside. + /// + /// This extension method forwards the draw call to each segment's DrawMasked overload, + /// allowing per-segment clipping by the provided circle mask. + /// + public static void DrawLinesMasked(this Quad quad, Circle mask, LineDrawingInfo lineInfo, bool reversedMask = false) + { + quad.SegmentAToB.DrawMasked(mask, lineInfo, reversedMask); + quad.SegmentBToC.DrawMasked(mask, lineInfo, reversedMask); + quad.SegmentCToD.DrawMasked(mask, lineInfo, reversedMask); + quad.SegmentDToA.DrawMasked(mask, lineInfo, reversedMask); + } + /// + /// Draws the quad's outline segments constrained by a mask. + /// + /// The quad whose sides will be drawn (extension receiver). + /// Rect mask used to clip each segment's drawing. + /// Line drawing parameters (thickness, color, cap type, etc.). + /// If true, draws the parts inside the mask instead of outside. + /// + /// This extension method forwards the draw call to each segment's DrawMasked overload, + /// allowing per-segment clipping by the provided rectangle mask. + /// + public static void DrawLinesMasked(this Quad quad, Rect mask, LineDrawingInfo lineInfo, bool reversedMask = false) + { + quad.SegmentAToB.DrawMasked(mask, lineInfo, reversedMask); + quad.SegmentBToC.DrawMasked(mask, lineInfo, reversedMask); + quad.SegmentCToD.DrawMasked(mask, lineInfo, reversedMask); + quad.SegmentDToA.DrawMasked(mask, lineInfo, reversedMask); + } + /// + /// Draws the quad's outline segments constrained by a mask. + /// + /// The quad whose sides will be drawn (extension receiver). + /// Quad mask used to clip each segment's drawing. + /// Line drawing parameters (thickness, color, cap type, etc.). + /// If true, draws the parts inside the mask instead of outside. + /// + /// Forwards the draw call to each segment's DrawMasked overload, + /// allowing per-segment clipping by the provided quad mask. + /// + public static void DrawLinesMasked(this Quad quad, Quad mask, LineDrawingInfo lineInfo, bool reversedMask = false) + { + quad.SegmentAToB.DrawMasked(mask, lineInfo, reversedMask); + quad.SegmentBToC.DrawMasked(mask, lineInfo, reversedMask); + quad.SegmentCToD.DrawMasked(mask, lineInfo, reversedMask); + quad.SegmentDToA.DrawMasked(mask, lineInfo, reversedMask); + } + /// + /// Draws the quad's outline segments constrained by a mask. + /// + /// The quad whose sides will be drawn (extension receiver). + /// Polygon mask used to clip each segment's drawing. + /// Line drawing parameters (thickness, color, cap type, etc.). + /// If true, draws the parts inside the mask instead of outside. + /// + /// Forwards the draw call to each segment's DrawMasked overload, + /// allowing per-segment clipping by the provided polygon mask. + /// + public static void DrawLinesMasked(this Quad quad, Polygon mask, LineDrawingInfo lineInfo, bool reversedMask = false) + { + quad.SegmentAToB.DrawMasked(mask, lineInfo, reversedMask); + quad.SegmentBToC.DrawMasked(mask, lineInfo, reversedMask); + quad.SegmentCToD.DrawMasked(mask, lineInfo, reversedMask); + quad.SegmentDToA.DrawMasked(mask, lineInfo, reversedMask); + } + /// + /// Draws the quad's outline segments constrained by a generic closed-shape mask. + /// + /// + /// The mask type. Must implement to provide closed-shape semantics + /// required for segment clipping. + /// + /// The quad whose sides will be drawn (extension receiver). + /// Mask used to clip each segment's drawing. + /// Line drawing parameters (thickness, color, cap type, etc.). + /// If true, draws the parts inside the mask instead of outside. + /// + /// Forwards the draw call to each segment's DrawMasked overload, allowing per-segment clipping + /// by the provided mask. This generic overload enables using any closed-shape provider without + /// adding a separate overload for each concrete shape type. + /// + public static void DrawLinesMasked(this Quad quad, T mask, LineDrawingInfo lineInfo, bool reversedMask = false) where T : IClosedShapeTypeProvider + { + quad.SegmentAToB.DrawMasked(mask, lineInfo, reversedMask); + quad.SegmentBToC.DrawMasked(mask, lineInfo, reversedMask); + quad.SegmentCToD.DrawMasked(mask, lineInfo, reversedMask); + quad.SegmentDToA.DrawMasked(mask, lineInfo, reversedMask); + } + #endregion + + #region Gapped + + /// + /// Draws a gapped outline for a quadrilateral, creating a dashed or segmented effect along the quad's perimeter. + /// + /// The quadrilateral to draw. + /// + /// The total length of the quad's perimeter. + /// If zero or negative, the method calculates it automatically. + /// Providing a known length avoids redundant calculations and improves performance, especially for static segments. + /// + /// Parameters describing how to draw the outline. + /// Parameters describing the gap configuration. + /// + /// The perimeter of the quad if positive; otherwise, -1. + /// If the shape does not change, the valid length can be reused in subsequent frames to avoid recalculating. + /// + /// + /// - If is 0 or is 0, the outline is drawn solid. + /// - If is 1 or greater, no outline is drawn. + /// + public static float DrawGappedOutline(this Quad quad, float perimeter, LineDrawingInfo lineInfo, GappedOutlineDrawingInfo gapDrawingInfo) + { + if (gapDrawingInfo.Gaps <= 0 || gapDrawingInfo.GapPerimeterPercentage <= 0f) + { + quad.DrawLines(lineInfo); + return perimeter > 0f ? perimeter : -1f; + } + + if (gapDrawingInfo.GapPerimeterPercentage >= 1f) return perimeter > 0f ? perimeter : -1f; + + var nonGapPercentage = 1f - gapDrawingInfo.GapPerimeterPercentage; + + var gapPercentageRange = gapDrawingInfo.GapPerimeterPercentage / gapDrawingInfo.Gaps; + var nonGapPercentageRange = nonGapPercentage / gapDrawingInfo.Gaps; + + + var shapePoints = new[] {quad.A, quad.B, quad.C, quad.D}; + int sides = shapePoints.Length; + + if (perimeter <= 0f) + { + perimeter = 0f; + for (int i = 0; i < sides; i++) + { + var curP = shapePoints[i]; + var nextP = shapePoints[(i + 1) % sides]; + perimeter += (nextP - curP).Length(); + } + } + + + var startDistance = perimeter * gapDrawingInfo.StartOffset; + var curDistance = 0f; + var nextDistance = startDistance; + + var curIndex = 0; + var curPoint = shapePoints[0]; + var nextPoint= shapePoints[1]; + var curW = nextPoint - curPoint; + var curDis = curW.Length(); + + var points = new List(3); + + int whileCounter = gapDrawingInfo.Gaps; + + while (whileCounter > 0) + { + if (curDistance + curDis >= nextDistance) + { + var p = curPoint + (curW / curDis) * (nextDistance - curDistance); + + + if (points.Count == 0) + { + nextDistance += nonGapPercentageRange * perimeter; + points.Add(p); + + } + else + { + nextDistance += gapPercentageRange * perimeter; + points.Add(p); + if (points.Count == 2) + { + SegmentDrawing.DrawSegment(points[0], points[1], lineInfo); + } + else + { + for (var i = 0; i < points.Count - 1; i++) + { + var p1 = points[i]; + var p2 = points[(i + 1) % points.Count]; + SegmentDrawing.DrawSegment(p1, p2, lineInfo); + } + } + + points.Clear(); + whileCounter--; + } + + } + else + { + + if(points.Count > 0) points.Add(nextPoint); + + curDistance += curDis; + curIndex = (curIndex + 1) % sides; + curPoint = shapePoints[curIndex]; + nextPoint = shapePoints[(curIndex + 1) % sides]; + curW = nextPoint - curPoint; + curDis = curW.Length(); + } + + } + + return perimeter; + } + + #endregion + + #region Helper + private static (Vector2 a, Vector2 b, Vector2 c, Vector2 d, float p, bool ccw) GetDrawLinePercentageOrder(Quad quad, float percentage, int startIndex) + { + if (percentage == 0f) return (quad.A, quad.B, quad.C, quad.D, 0f, true); + bool ccw = true; + if (percentage < 0f) + { + percentage *= -1f; + ccw = false; + } + float perc = ShapeMath.Clamp(percentage, 0f, 1f); + var corner = ShapeMath.WrapI(startIndex, 0, 4); + + if (corner == 0) + { + return ccw ? (quad.A, quad.B, quad.C, quad.D, perc, ccw) : (quad.A, quad.D, quad.C, quad.B, perc, ccw); + } + if (corner == 1) + { + return ccw ? (quad.B, quad.C, quad.D, quad.A, perc, ccw) : (quad.B, quad.A, quad.D, quad.C, perc, ccw); + } + if (corner == 2) + { + return ccw ? (quad.C, quad.D, quad.A, quad.B, perc, ccw) : (quad.C, quad.B, quad.A, quad.D, perc, ccw); + } + + return ccw ? (quad.D, quad.A, quad.B, quad.C, perc, ccw) : (quad.D, quad.C, quad.B, quad.A, perc, ccw); + } + #endregion + + //TODO: Rework with ClipperImmediate2d + + #region Helper Chamfered Corner Lines + private static List innerChamferPointsHelper = new(16); + private static List outerChamferPointsHelper = new(16); + private static List innerChamferPointsInsetResult = new(16); + private static List innerChamferPointsInsetBuffer = new(16); + private static List chamferedOutlineTriangulationResultVertices = new(64); + private static List chamferedOutlineTriangulationResultIndices = new(64); + + private readonly struct ChamferedCorner + { + #region Fields + + public readonly bool SharpCorner; + public readonly Vector2 Corner; + public readonly Vector2 Prev; + public readonly Vector2 Next; + public readonly Vector2 ChamferDir; + public readonly Vector2 ChamferDirPrev; + public readonly Vector2 ChamferDirNext; + // public readonly Vector2 ChamferEdgeDir; + #endregion + + #region Constructors + public ChamferedCorner(Vector2 p, float cornerLength, Vector2 normalPrev, Vector2 normalNext) + { + Corner = p; + if (cornerLength <= 0f) + { + Prev = p; + Next = p; + // ChamferEdgeDir = Vector2.Zero; + ChamferDir = -(normalPrev + normalNext).Normalize(); + ChamferDirPrev = ChamferDir; + ChamferDirNext = ChamferDir; + SharpCorner = true; + } + else + { + Prev = p + normalPrev * cornerLength; + Next = p + normalNext * cornerLength; + // ChamferEdgeDir = (Next - Prev).Normalize(); + var chamferEdgeDir = (Next - Prev).Normalize(); + ChamferDir = chamferEdgeDir.GetPerpendicularRight(); + ChamferDirPrev = (ChamferDir + (-normalNext)).Normalize(); + ChamferDirNext = (ChamferDir + (-normalPrev)).Normalize(); + SharpCorner = false; + } + + } + public ChamferedCorner(Vector2 p, float cornerLengthPrev, float cornerLengthNext, Vector2 normalPrev, Vector2 normalNext) + { + Corner = p; + Prev = p + normalPrev * cornerLengthPrev; + Next = p + normalNext * cornerLengthNext; + // ChamferEdgeDir = (Next - Prev).Normalize();# + var chamferEdgeDir = (Next - Prev).Normalize(); + ChamferDir = chamferEdgeDir.GetPerpendicularRight(); + ChamferDirPrev = (ChamferDir + (-normalNext)).Normalize(); + ChamferDirNext = (ChamferDir + (-normalPrev)).Normalize(); + SharpCorner = false; + } + public ChamferedCorner(Vector2 p, float cornerLengthPrev, float cornerLengthNext, Vector2 normalPrev, Vector2 normalNext, Vector2 chamferDirPrev, Vector2 chamferDirNext) + { + Corner = p; + Prev = p + normalPrev * cornerLengthPrev; + Next = p + normalNext * cornerLengthNext; + // ChamferEdgeDir = (Next - Prev).Normalize(); + var chamferEdgeDir = (Next - Prev).Normalize(); + ChamferDir = chamferEdgeDir.GetPerpendicularRight(); + ChamferDirPrev = chamferDirPrev; + ChamferDirNext = chamferDirNext; + SharpCorner = false; + } + #endregion + + #region Functions + public float GetChamferLength(Vector2 normal, float lineThickness) + { + if (SharpCorner) + { + return MathF.Sqrt(2f * lineThickness * lineThickness); + } + var angleRad = ShapeVec.AngleRad(ChamferDirNext, normal); + return Triangle.RightTriangleGetHypotenuseFromAdjacent(angleRad, lineThickness); + } + public void AddToList(List list) + { + if(SharpCorner) list.Add(Corner); + else + { + list.Add(Prev); + list.Add(Next); + } + } + public void AddOuterToList(List list, float chamferLength) + { + if (SharpCorner) + { + var outer = Prev + ChamferDir * chamferLength; + list.Add(outer); + return; + } + var outerPrev = Prev + ChamferDirPrev * chamferLength; + var outerNext = Next + ChamferDirNext * chamferLength; + list.Add(outerPrev); + list.Add(outerNext); + } + public void AddOuterToList(List list, float chamferLengthPrev, float chamferLengthNext) + { + var outerPrev = Prev + ChamferDirPrev * chamferLengthPrev; + var outerNext = Next + ChamferDirNext * chamferLengthNext; + list.Add(outerPrev); + list.Add(outerNext); + } + #endregion + } + + private static void DrawChamferedOutline(float lineThickness, ColorRgba color) + { + CleanInPlace(innerChamferPointsHelper, 0.01f, 0.01f); + CleanInPlace(outerChamferPointsHelper, 0.01f, 0.01f); + + innerChamferPointsInsetResult.Clear(); + InsetConvex(innerChamferPointsHelper, lineThickness, innerChamferPointsInsetResult, innerChamferPointsInsetBuffer); + + if (innerChamferPointsInsetResult.Count <= 0) return; + + var rayColor = color.ToRayColor(); + if (innerChamferPointsInsetResult.Count < 3) + { + var center = innerChamferPointsInsetResult.Count == 1 ? innerChamferPointsInsetResult[0] : (innerChamferPointsHelper[0] + innerChamferPointsHelper[1]) * 0.5f; + for (int i = 0; i < outerChamferPointsHelper.Count; i++) + { + var p1 = outerChamferPointsHelper[i]; + var p2 = outerChamferPointsHelper[(i + 1) % outerChamferPointsHelper.Count]; + Raylib.DrawTriangle(p1, p2, center, rayColor); + } + + return; + } + + chamferedOutlineTriangulationResultIndices.Clear(); + chamferedOutlineTriangulationResultVertices.Clear(); + TriangulateConvexOutline(outerChamferPointsHelper, innerChamferPointsInsetResult, chamferedOutlineTriangulationResultVertices, chamferedOutlineTriangulationResultIndices); + + for (int t = 0; t < chamferedOutlineTriangulationResultIndices.Count; t += 3) + { + int i0 = chamferedOutlineTriangulationResultIndices[t + 0]; + int i1 = chamferedOutlineTriangulationResultIndices[t + 1]; + int i2 = chamferedOutlineTriangulationResultIndices[t + 2]; + + Vector2 v0 = chamferedOutlineTriangulationResultVertices[i0]; + Vector2 v1 = chamferedOutlineTriangulationResultVertices[i1]; + Vector2 v2 = chamferedOutlineTriangulationResultVertices[i2]; + + Raylib.DrawTriangle(v0, v1, v2, rayColor); + } + } + private static bool CleanInPlace(List poly, float mergeEps = 1e-3f, float collinearEps = 1e-5f) + { + if (poly.Count < 3) return false; + + float mergeEps2 = mergeEps * mergeEps; + + // 1) remove consecutive near-duplicates + for (int i = poly.Count - 1; i > 0; i--) + { + if (Vector2.DistanceSquared(poly[i], poly[i - 1]) <= mergeEps2) + poly.RemoveAt(i); + } + + // 2) remove closing near-duplicate + if (poly.Count > 1 && Vector2.DistanceSquared(poly[0], poly[^1]) <= mergeEps2) + poly.RemoveAt(poly.Count - 1); + + if (poly.Count < 3) return false; + + // 3) remove near-collinear points + for (int i = poly.Count - 1; poly.Count >= 3 && i >= 0; i--) + { + int i0 = (i - 1 + poly.Count) % poly.Count; + int i1 = i; + int i2 = (i + 1) % poly.Count; + + Vector2 a = poly[i0]; + Vector2 b = poly[i1]; + Vector2 c = poly[i2]; + + Vector2 ab = b - a; + Vector2 bc = c - b; + + // If angle is ~180°, cross ~ 0 + float cross = ab.X * bc.Y - ab.Y * bc.X; + if (MathF.Abs(cross) <= collinearEps) + poly.RemoveAt(i1); + } + + return poly.Count >= 3; + } + + #endregion + + //TODO: Remvoe - Use new ClipperImmediate2d class instead of those functions! + + #region Inset Convex + /// + /// Normal convenience function: allocates and returns a new list. + /// Thread-safe. + /// + private static List InsetConvex(IReadOnlyList poly, float inset, float eps = 1e-6f) + { + if (poly == null) throw new ArgumentNullException(nameof(poly)); + // Allocate output (garbage-producing variant) + var result = new List(Math.Max(4, poly.Count)); + // Use low-gc core with temporary buffers allocated here (still allocations, but contained) + var tmp = new List(Math.Max(4, poly.Count)); + InsetConvex(poly, inset, result, tmp, eps); + return result; + } + + /// + /// Lowest-garbage variant: + /// - Writes output into 'result' (cleared first). + /// - Uses 'temp' as scratch (cleared first). + /// - Thread-safe because caller owns buffers. + /// + /// After the first time the lists grow to sufficient capacity, this produces zero GC. + /// + private static bool InsetConvex(IReadOnlyList poly, float inset, List result, List temp, float eps = 1e-6f) + { + if (poly == null) throw new ArgumentNullException(nameof(poly)); + if (result == null) throw new ArgumentNullException(nameof(result)); + if (temp == null) throw new ArgumentNullException(nameof(temp)); + + int n = poly.Count; + if (n < 3) return false; + if (inset < 0) return false; + + result.Clear(); + temp.Clear(); + + if (inset <= eps) + { + EnsureCapacity(result, n); + for (int i = 0; i < n; i++) result.Add(poly[i]); + return true; + } + + bool ccw = SignedArea2(poly) > 0f; + + float scale = EstimateScale(poly); + float big = MathF.Max(1f, scale) * 10000f; + + // We'll clip a big box by each inset half-plane. + // Use result/temp alternately as buffers 'a' and 'b'. + List a = result; + List b = temp; + + a.Clear(); + EnsureCapacity(a, 4); + a.Add(new Vector2(-big, -big)); + a.Add(new Vector2( big, -big)); + a.Add(new Vector2( big, big)); + a.Add(new Vector2(-big, big)); + + b.Clear(); + + for (int i = 0; i < n; i++) + { + Vector2 p0 = poly[i]; + Vector2 p1 = poly[(i + 1) % n]; + Vector2 e = p1 - p0; + + if (e.LengthSquared() <= eps * eps) + continue; + + Vector2 leftN = NormalizeSafe(new Vector2(-e.Y, e.X), eps); + Vector2 inwardN = ccw ? leftN : -leftN; + float c = Vector2.Dot(inwardN, p0) + inset; + + ClipByHalfPlane(input: a, output: b, n: inwardN, c: c, eps: eps); + + // swap buffers (no allocations) + var t = a; a = b; b = t; + b.Clear(); + + if (a.Count == 0) + { + // Collapsed -> single point + a.Add(PolygonAreaCentroid(poly, eps)); + break; + } + } + + // Degenerate/tiny outcomes -> collapse to single point. + if (a.Count >= 3) + { + float a2 = MathF.Abs(SignedArea2(a)); + if (a2 <= eps) + { + Vector2 p = CentroidOfVertices(a); + a.Clear(); + a.Add(p); + } + } + else if (a.Count == 2) + { + Vector2 mid = 0.5f * (a[0] + a[1]); + a.Clear(); + a.Add(mid); + } + else if (a.Count == 0) + { + a.Add(PolygonAreaCentroid(poly, eps)); + } + + // Preserve original winding (only meaningful for polygons with 3+ vertices) + if (a.Count >= 3) + { + bool outCcw = SignedArea2(a) > 0f; + if (outCcw != ccw) a.Reverse(); + } + + // If final buffer is temp, copy it back into result. + // (This copy is still allocation-free; it's just element copies.) + if (!ReferenceEquals(a, result)) + { + result.Clear(); + EnsureCapacity(result, a.Count); + for (int i = 0; i < a.Count; i++) result.Add(a[i]); + } + + return true; + } + + // ------------------- Clipping core ------------------- + + // Clips 'input' by half-plane dot(n, p) >= c into 'output' + private static void ClipByHalfPlane(List input, List output, Vector2 n, float c, float eps) + { + output.Clear(); + int count = input.Count; + if (count == 0) return; + + Vector2 prev = input[count - 1]; + float prevD = Vector2.Dot(n, prev) - c; + bool prevInside = prevD >= -eps; + + for (int i = 0; i < count; i++) + { + Vector2 curr = input[i]; + float currD = Vector2.Dot(n, curr) - c; + bool currInside = currD >= -eps; + + if (currInside) + { + if (!prevInside) + output.Add(IntersectSegmentWithLine(prev, curr, n, c, eps)); + output.Add(curr); + } + else if (prevInside) + { + output.Add(IntersectSegmentWithLine(prev, curr, n, c, eps)); + } + + prev = curr; + prevInside = currInside; + } + + Cleanup(output, eps); + } + + private static Vector2 IntersectSegmentWithLine(Vector2 p0, Vector2 p1, Vector2 n, float c, float eps) + { + Vector2 d = p1 - p0; + float denom = Vector2.Dot(n, d); + + if (MathF.Abs(denom) <= eps) + return p0; // nearly parallel fallback + + float t = (c - Vector2.Dot(n, p0)) / denom; + t = Clamp01(t); + return p0 + d * t; + } + + private static void Cleanup(List poly, float eps) + { + if (poly.Count == 0) return; + float eps2 = eps * eps; + + // Remove consecutive near-duplicates + for (int i = poly.Count - 1; i > 0; i--) + { + if ((poly[i] - poly[i - 1]).LengthSquared() <= eps2) + poly.RemoveAt(i); + } + + // Remove closing near-duplicate + if (poly.Count > 1 && (poly[0] - poly[poly.Count - 1]).LengthSquared() <= eps2) + poly.RemoveAt(poly.Count - 1); + } + + // ------------------- Geometry helpers ------------------- + + private static float SignedArea2(IReadOnlyList poly) + { + double sum = 0; + for (int i = 0, n = poly.Count; i < n; i++) + { + Vector2 a = poly[i]; + Vector2 b = poly[(i + 1) % n]; + sum += (double)a.X * b.Y - (double)a.Y * b.X; + } + return (float)sum; // 2*area + } + + private static float EstimateScale(IReadOnlyList poly) + { + float minX = poly[0].X, maxX = poly[0].X; + float minY = poly[0].Y, maxY = poly[0].Y; + + for (int i = 1; i < poly.Count; i++) + { + var p = poly[i]; + if (p.X < minX) minX = p.X; + if (p.X > maxX) maxX = p.X; + if (p.Y < minY) minY = p.Y; + if (p.Y > maxY) maxY = p.Y; + } + + float dx = maxX - minX; + float dy = maxY - minY; + return MathF.Sqrt(dx * dx + dy * dy); + } + + private static Vector2 NormalizeSafe(Vector2 v, float eps) + { + float len = v.Length(); + if (len <= eps) return Vector2.Zero; + return v / len; + } + + private static float Clamp01(float x) => x < 0f ? 0f : (x > 1f ? 1f : x); + + private static void EnsureCapacity(List list, int needed) + { + if (list.Capacity < needed) list.Capacity = needed; + } + + private static Vector2 CentroidOfVertices(IReadOnlyList poly) + { + Vector2 c = Vector2.Zero; + for (int i = 0; i < poly.Count; i++) c += poly[i]; + return c / Math.Max(1, poly.Count); + } + + // Area-weighted centroid (guaranteed inside for convex polygons) + private static Vector2 PolygonAreaCentroid(IReadOnlyList poly, float eps) + { + double a2 = 0; // 2*area + double cx6a = 0; // 6A*Cx + double cy6a = 0; // 6A*Cy + + int n = poly.Count; + for (int i = 0; i < n; i++) + { + Vector2 p = poly[i]; + Vector2 q = poly[(i + 1) % n]; + double cross = (double)p.X * q.Y - (double)q.X * p.Y; + + a2 += cross; + cx6a += (p.X + q.X) * cross; + cy6a += (p.Y + q.Y) * cross; + } + + if (Math.Abs(a2) <= eps) + return CentroidOfVertices(poly); + + double A = a2 / 2.0; + double cx = cx6a / (6.0 * A); + double cy = cy6a / (6.0 * A); + return new Vector2((float)cx, (float)cy); + } + #endregion + + #region Triangulate Convex Outline + private static bool TriangulateConvexOutline(IReadOnlyList outerCCW, IReadOnlyList innerCCW, List vertices, List indices, float eps = 1e-6f) + { + int nO = outerCCW.Count; + int nI = innerCCW.Count; + + if (nO < 3 || nI < 3) + { + Console.WriteLine("[TriangulateConvexOutlineYDown] Warning: outer/inner must have at least 3 points."); + return false; + } + + vertices.Clear(); + indices.Clear(); + + // Pack vertices as [outer..., inner...] + int outerBase = 0; + EnsureCapacity(vertices, nO + nI); + for (int i = 0; i < nO; i++) vertices.Add(outerCCW[i]); + int innerBase = vertices.Count; + for (int i = 0; i < nI; i++) vertices.Add(innerCCW[i]); + + // Screen-space CCW (Y-down) means SignedArea2 (math Y-up) will be NEGATIVE. + // We'll just detect and if either is not in the expected orientation, warn and proceed anyway. + // If winding is wrong, triangles may be flipped; caller can fix their input order. + float outerArea2 = SignedArea2_MathYUp(outerCCW); + float innerArea2 = SignedArea2_MathYUp(innerCCW); + if (outerArea2 >= 0 || innerArea2 >= 0) + { + Console.WriteLine("[TriangulateConvexOutlineYDown] Warning: expected CCW in Y-down. (SignedArea2 should be < 0 in math-Y-up test). Proceeding anyway."); + } + + // Start at rightmost points (max X, tie max Y) on both loops. + int iO = IndexOfRightmost(outerCCW); + int iI = IndexOfRightmost(innerCCW); + + // We will advance exactly nO + nI times, producing that many triangles. + // This is the correct triangle count for a convex ring: nO + nI triangles. + // (Think of it as triangulating a convex polygon with a convex hole -> nO + nI triangles.) + int stepsO = 0, stepsI = 0; + + // Pre-ensure index capacity (avoid growth GC in hot loops) + EnsureCapacity(indices, (nO + nI) * 3); + + // Helper local to map loop index -> vertex buffer index + int O(int idx) => outerBase + idx; + int I(int idx) => innerBase + idx; + + for (int guard = 0; guard < nO + nI; guard++) + { + int nextO = (iO + 1) % nO; + int nextI = (iI + 1) % nI; + + Vector2 o = outerCCW[iO]; + Vector2 oN = outerCCW[nextO]; + Vector2 i = innerCCW[iI]; + Vector2 iN = innerCCW[nextI]; + + Vector2 dO = oN - o; + Vector2 dI = iN - i; + + // Decide which boundary to advance. + // We need an ordering of edge directions around the loop. + // In Y-down coordinates, the sign of cross is flipped relative to math Y-up. + // We'll compute cross in math sense (X right, Y up) by negating Y to get Y-up vectors. + // Equivalent: crossYDown(a,b) = -crossMath(a,b). + float crossMath = Cross(new Vector2(dO.X, -dO.Y), new Vector2(dI.X, -dI.Y)); + + bool advanceOuter; + if (MathF.Abs(crossMath) <= eps) + { + // Nearly parallel; advance shorter edge for stability + advanceOuter = dO.LengthSquared() <= dI.LengthSquared(); + } + else + { + // If dO comes "before" dI in CCW (Y-down), advance outer. + // Because we converted to math-Y-up via (x, -y), we can use crossMath > 0 as CCW ordering there. + advanceOuter = crossMath > 0; + } + + if (advanceOuter) + { + if (stepsO >= nO) advanceOuter = false; // outer already fully advanced + } + else + { + if (stepsI >= nI) advanceOuter = true; // inner already fully advanced + } + + if (advanceOuter) + { + // Emit triangle between inner current and outer edge (iI, iO, nextO) + // Must be CCW in Y-down. We'll enforce by checking signed area in Y-down and swapping if needed. + int a = I(iI); + int b = O(iO); + int c = O(nextO); + AddTriangleCCW_YDown(vertices, indices, a, b, c, eps); + + iO = nextO; + stepsO++; + } + else + { + // Emit triangle between outer current and inner edge (iO, nextI, iI) or similar. + int a = O(iO); + int b = I(nextI); + int c = I(iI); + AddTriangleCCW_YDown(vertices, indices, a, b, c, eps); + + iI = nextI; + stepsI++; + } + } + + // Validate we produced a multiple of 3 indices + if ((indices.Count % 3) != 0) + { + Console.WriteLine("[TriangulateConvexOutlineYDown] Warning: produced non-multiple-of-3 indices. Clearing."); + indices.Clear(); + return false; + } + + return true; + } + + // --- Triangle winding enforcement (Y-down CCW) --- + + private static void AddTriangleCCW_YDown(List v, List idx, int i0, int i1, int i2, float eps) + { + Vector2 a = v[i0]; + Vector2 b = v[i1]; + Vector2 c = v[i2]; + + // In Y-down, "CCW" corresponds to negative cross in math coordinates. + // Compute signed area *2 in Y-down directly: + // crossYDown = crossMath( (b-a) with Y flipped? ) but easiest: + // Use math cross with actual coordinates and then negate. + float crossMath = Cross(b - a, c - a); + float crossYDown = -crossMath; + + if (crossYDown <= eps) + { + // swap to flip winding + idx.Add(i0); + idx.Add(i2); + idx.Add(i1); + } + else + { + idx.Add(i0); + idx.Add(i1); + idx.Add(i2); + } + } + + // --- Helpers --- + + private static int IndexOfRightmost(IReadOnlyList poly) + { + int best = 0; + for (int i = 1; i < poly.Count; i++) + { + var p = poly[i]; + var b = poly[best]; + if (p.X > b.X || (p.X == b.X && p.Y > b.Y)) + best = i; + } + return best; + } + + // Signed area *2 in standard math coords (Y-up). + private static float SignedArea2_MathYUp(IReadOnlyList poly) + { + double sum = 0; + for (int i = 0; i < poly.Count; i++) + { + Vector2 a = poly[i]; + Vector2 b = poly[(i + 1) % poly.Count]; + sum += (double)a.X * b.Y - (double)a.Y * b.X; + } + return (float)sum; + } + + private static float Cross(Vector2 a, Vector2 b) => a.X * b.Y - a.Y * b.X; + + private static void EnsureCapacity(List list, int needed) + { + if (list.Capacity < needed) list.Capacity = needed; + } + #endregion + + //TODO: Remove End + } \ No newline at end of file diff --git a/ShapeEngine/Geometry/QuadDef/QuadMath.cs b/ShapeEngine/Geometry/QuadDef/QuadMath.cs index 3f8159c4..97719106 100644 --- a/ShapeEngine/Geometry/QuadDef/QuadMath.cs +++ b/ShapeEngine/Geometry/QuadDef/QuadMath.cs @@ -11,6 +11,30 @@ public readonly partial struct Quad { #region Math + /// + /// Gets the unit normal vector pointing to the left side of the quad. + /// Computed by normalizing the vector from D to A. + /// + public Vector2 NormalLeft => DA.Normalize(); + + /// + /// Gets the unit normal vector pointing downward along the quad. + /// Computed by normalizing the vector from A to B. + /// + public Vector2 NormalDown => AB.Normalize(); + + /// + /// Gets the unit normal vector pointing to the right side of the quad. + /// Computed by normalizing the vector from B to C. + /// + public Vector2 NormalRight => BC.Normalize(); + + /// + /// Gets the unit normal vector pointing upward along the quad. + /// Computed by normalizing the vector from C to D. + /// + public Vector2 NormalUp => CD.Normalize(); + /// /// Gets the projected points of the quad along a given vector. /// @@ -31,6 +55,35 @@ public readonly partial struct Quad }; return points; } + + /// + /// Populates the provided collection with the quad's vertices and their projected positions along a given vector. + /// + /// + /// The collection to clear and fill with the original and projected vertices. + /// + /// The vector along which to project the quad's vertices. + /// + /// if the projection vector is non-zero and the points were added; otherwise, . + /// + public bool GetProjectedShapePoints(Points result, Vector2 v) + { + if (v.LengthSquared() <= 0f) return false; + + result.Clear(); + result.EnsureCapacity(8); + + result.Add(A); + result.Add(B); + result.Add(C); + result.Add(D); + result.Add(A + v); + result.Add(B + v); + result.Add(C + v); + result.Add(D + v); + + return true; + } /// /// Projects the quad along a given vector and returns the convex hull as a polygon. @@ -50,7 +103,36 @@ public readonly partial struct Quad C + v, D + v }; - return Polygon.FindConvexHull(points); + + Polygon result = new Polygon(8); + points.FindConvexHull(result); + return result; + } + + /// + /// Projects the quad along a given vector and stores the convex hull in the provided . + /// + /// + /// The instance to populate with the convex hull of the projected shape. + /// + /// The vector along which to project the quad's vertices. + /// + /// if the projection vector is non\-zero and the polygon was populated; otherwise, . + /// + public bool ProjectShape(Polygon result, Vector2 v) + { + if (v.LengthSquared() <= 0f) return false; + var points = new Points + { + A, B, C, D, + A + v, + B + v, + C + v, + D + v + }; + + points.FindConvexHull(result); + return true; } /// @@ -133,53 +215,19 @@ public float GetArea() return abc.GetArea() + cda.GetArea(); } - #endregion - - #region Transform - - /// - /// Rotates the quad by a specified angle in radians around a given anchor point. - /// - /// The angle in radians to rotate. - /// The anchor point for rotation. - /// A new rotated by the specified angle. - public Quad ChangeRotation(float rad, AnchorPoint alignment) - { - var pivotPoint = GetPoint(alignment); - var a = pivotPoint + (A - pivotPoint).Rotate(rad); - var b = pivotPoint + (B - pivotPoint).Rotate(rad); - var c = pivotPoint + (C - pivotPoint).Rotate(rad); - var d = pivotPoint + (D - pivotPoint).Rotate(rad); - return new(a, b, c, d); - } - - /// - /// Rotates the quad to a specific angle in radians around a given anchor point. - /// - /// The target angle in radians. - /// The anchor point for rotation. - /// A new with the specified rotation. - /// Uses the shortest rotation path to reach the target angle. - public Quad SetRotation(float angleRad, AnchorPoint alignment) - { - float amount = ShapeMath.GetShortestAngleRad(AngleRad, angleRad); - return ChangeRotation(amount, alignment); - } - /// - /// Rotates the quad by a specified angle in radians around its center. + /// Gets the length of the diagonal connecting corner A and corner C. /// - /// The angle in radians to rotate. - /// A new rotated by the specified angle. - public Quad ChangeRotation(float rad) => ChangeRotation(rad, AnchorPoint.Center); + public float GetDiagonalLengt() => (A - C).Length(); /// - /// Rotates the quad to a specific angle in radians around its center. + /// Gets the squared length of the diagonal connecting corner A and corner C. /// - /// The target angle in radians. - /// A new with the specified rotation. - public Quad SetRotation(float angleRad) => SetRotation(angleRad, AnchorPoint.Center); + public float GetDiagonalLengthSquare() => (A - C).LengthSquared(); + #endregion + + #region Position Methods /// /// Moves the quad by the specified offset vector. /// @@ -221,71 +269,159 @@ public Quad SetPosition(Vector2 newPosition, AnchorPoint alignment) /// The new position for the center. /// A new with its center at the new position. public Quad SetPosition(Vector2 newPosition) => SetPosition(newPosition, AnchorPoint.Center); + #endregion + + #region Rotation Methods + /// + /// Gets the current rotation of the quad in radians. + /// + /// + /// The angle, in radians, derived from the vector between the quad center and the center of the CD edge. + /// + /// + /// A quad with zero rotation matches the orientation of an axis-aligned rectangle. + /// The rotation is inferred from the direction from Center to CDCenter. + /// + public float GetRotationRad() + { + //A quad with 0 rotation should equal a standard rect. + //If the vector from the center to the right side center is 0 the quad is not rotated + return ShapeVec.AngleRad(CDCenter - Center); + } + + /// + /// Gets the current rotation of the quad in degrees. + /// + /// The quad rotation converted from radians to degrees. + public float GetRotationDeg() => GetRotationRad() * ShapeMath.RADTODEG; + + /// + /// Rotates the quad by a specified angle in radians around a given anchor point. + /// + /// The angle in radians to rotate. + /// The anchor point for rotation. + /// A new rotated by the specified angle. + public Quad ChangeRotation(float amountRad, AnchorPoint alignment) + { + var pivotPoint = GetPoint(alignment); + var a = pivotPoint + (A - pivotPoint).Rotate(amountRad); + var b = pivotPoint + (B - pivotPoint).Rotate(amountRad); + var c = pivotPoint + (C - pivotPoint).Rotate(amountRad); + var d = pivotPoint + (D - pivotPoint).Rotate(amountRad); + return new(a, b, c, d); + } + + /// + /// Rotates the quad to a specific angle in radians around a given anchor point. + /// + /// The target angle in radians. + /// The anchor point for rotation. + /// A new with the specified rotation. + /// Uses the shortest rotation path to reach the target angle. + public Quad SetRotation(float angleRad, AnchorPoint alignment) + { + float amount = ShapeMath.GetShortestAngleRad(GetRotationRad(), angleRad); + return ChangeRotation(amount, alignment); + } /// - /// Scales the size of the quad uniformly by a scalar value, relative to the origin. + /// Rotates the quad by a specified angle in radians around its center. /// - /// The scale factor. - /// A new scaled by the given factor. - public Quad ScaleSize(float scale) => this * scale; + /// The angle in radians to rotate. + /// A new rotated by the specified angle. + public Quad ChangeRotation(float rad) => ChangeRotation(rad, AnchorPoint.Center); /// - /// Scales the size of the quad component-wise by a , relative to the origin. + /// Rotates the quad to a specific angle in radians around its center. + /// + /// The target angle in radians. + /// A new with the specified rotation. + public Quad SetRotation(float angleRad) => SetRotation(angleRad, AnchorPoint.Center); + #endregion + + #region Size Methods + /// + /// Gets the current size of the quad. /// - /// The scale factors for width and height. - /// A new scaled by the given size. - public Quad ScaleSize(Size scale) => new Quad(A * scale, B * scale, C * scale, D * scale); + /// + /// A whose width is the length of edge DA and whose height is the length of edge AB. + /// + /// + /// This assumes the quad is being treated as a rectangle-like shape where adjacent edge lengths + /// represent width and height regardless of rotation. + /// + public Size GetSize() + { + var e1 = B - A; + var e2 = D - A; + float height = e1.Length(); + float width = e2.Length(); + return new Size(width, height); + } + + /// + /// Scales the quad uniformly relative to its center. + /// + /// The uniform scale factor to apply to both width and height. + /// A new with its size scaled around the center. + public Quad ScaleSize(float scale) + { + return ScaleSize(scale, AnchorPoint.Center); + } /// - /// Scales the size of the quad uniformly by a scalar value, relative to a specified anchor point. + /// Scales the quad non-uniformly relative to its center. /// - /// The scale factor. - /// The anchor point for scaling. - /// A new scaled by the given factor around the anchor point. + /// + /// A containing the width and height scale factors. + /// + /// A new with its size scaled around the center. + public Quad ScaleSize(Size scale) + { + return ScaleSize(scale, AnchorPoint.Center); + } + + /// + /// Scales the quad uniformly around the specified anchor point. + /// + /// The uniform scale factor to apply to both width and height. + /// The anchor point that remains fixed during scaling. + /// A new with its size scaled around the given anchor. public Quad ScaleSize(float scale, AnchorPoint alignment) { var p = GetPoint(alignment); - return new - ( - A + (A - p) * scale, - B + (B - p) * scale, - C + (C - p) * scale, - D + (D - p) * scale - ); + return new(p, GetSize() * scale, GetRotationRad(), alignment); } /// - /// Scales the size of the quad component-wise by a , relative to a specified anchor point. + /// Scales the quad non-uniformly around the specified anchor point. /// - /// The scale factors for width and height. - /// The anchor point for scaling. - /// A new scaled by the given size around the anchor point. + /// + /// A containing the width and height scale factors. + /// + /// The anchor point that remains fixed during scaling. + /// A new with its size scaled around the given anchor. public Quad ScaleSize(Size scale, AnchorPoint alignment) { var p = GetPoint(alignment); - return new - ( - A + (A - p) * scale, - B + (B - p) * scale, - C + (C - p) * scale, - D + (D - p) * scale - ); + return new(p, GetSize() * scale, GetRotationRad(), alignment); } /// - /// Changes the size of the quad by a specified amount, relative to its center. + /// Changes the quad by moving each corner farther from or closer to the specified anchor point + /// along the corner's current diagonal direction. /// - /// The amount to change the size by. - /// A new with the size changed by the specified amount. - public Quad ChangeSize(float amount) => ChangeSize(amount, AnchorPoint.Center); - - /// - /// Changes the size of the quad by a specified amount, relative to a specified anchor point. - /// - /// The amount to change the size by. - /// The anchor point for resizing. - /// A new with the size changed by the specified amount around the anchor point. - public Quad ChangeSize(float amount, AnchorPoint alignment) + /// + /// The distance to add to each corner's current distance from the anchor point. + /// Positive values expand the quad; negative values contract it. + /// + /// The anchor point used as the origin for the diagonal resize. + /// A new with all corners adjusted radially from the anchor point. + /// + /// Each vertex is processed independently. If a vertex already lies exactly on the anchor point, + /// that vertex remains unchanged to avoid division by zero. + /// + public Quad ChangeDiagonalSize(float amount, AnchorPoint alignment) { Vector2 newA, newB, newC, newD; @@ -333,133 +469,192 @@ public Quad ChangeSize(float amount, AnchorPoint alignment) return new(newA, newB, newC, newD); } + + /// + /// Changes the quad size uniformly by the same amount on both axes around its center. + /// + /// + /// The amount to add to both width and height. + /// + /// A new with the adjusted size. + public Quad ChangeSize(float amount) => ChangeSize(amount, AnchorPoint.Center); /// - /// Sets the size of the quad to a specific value, relative to its center. + /// Changes the quad size uniformly by the same amount on both axes around the specified anchor point. /// - /// The new size for the quad. - /// A new with the specified size. + /// The amount to add to both width and height. + /// The anchor point that remains fixed during the resize. + /// A new with the adjusted size. + public Quad ChangeSize(float amount, AnchorPoint alignment) + { + var p = GetPoint(alignment); + return new(p, GetSize() + amount, GetRotationRad(), alignment); + } + + /// + /// Changes the quad size by independent width and height amounts around its center. + /// + /// The amount to add to the current width. + /// The amount to add to the current height. + /// A new with the adjusted width and height. + public Quad ChangeSize(float widthAmount, float heightAmount) + { + return ChangeSize(new Size(widthAmount, heightAmount), AnchorPoint.Center); + } + + /// + /// Changes the quad size by independent width and height amounts around the specified anchor point. + /// + /// The amount to add to the current width. + /// The amount to add to the current height. + /// The anchor point that remains fixed during the resize. + /// A new with the adjusted width and height. + public Quad ChangeSize(float widthAmount, float heightAmount, AnchorPoint alignment) + { + return ChangeSize(new Size(widthAmount, heightAmount), alignment); + } + + /// + /// Changes the quad size by the specified width and height amounts around its center. + /// + /// + /// A whose components are added to the current width and height. + /// + /// A new with the adjusted size. + public Quad ChangeSize(Size amount) => ChangeSize(amount, AnchorPoint.Center); + + /// + /// Changes the quad size by the specified width and height amounts around the given anchor point. + /// + /// + /// A whose components are added to the current width and height. + /// + /// The anchor point that remains fixed during the resize. + /// A new with the adjusted size. + public Quad ChangeSize(Size amount, AnchorPoint alignment) + { + var p = GetPoint(alignment); + return new(p, GetSize() + amount, GetRotationRad(), alignment); + } + + /// + /// Sets the quad to a uniform square size around its center. + /// + /// The target width and height value. + /// A new with both width and height set to . public Quad SetSize(float size) => SetSize(size, AnchorPoint.Center); - + /// - /// Sets the size of the quad to a specific value, relative to a specified anchor point. + /// Sets the quad to a uniform square size around the specified anchor point. /// - /// The new size for the quad. - /// The anchor point for resizing. - /// A new with the specified size around the anchor point. + /// The target width and height value. + /// The anchor point that remains fixed during the resize. + /// A new with both width and height set to . public Quad SetSize(float size, AnchorPoint alignment) { - Vector2 newA, newB, newC, newD; - - var origin = GetPoint(alignment); - - var wA = (A - origin); - var lSqA = wA.LengthSquared(); - if (lSqA <= 0f) newA = A; - else - { - var l = MathF.Sqrt(lSqA); - var dir = wA / l; - newA = origin + dir * size; - } - - var wB = (B - origin); - var lSqB = wB.LengthSquared(); - if (lSqB <= 0f) newB = B; - else - { - var l = MathF.Sqrt(lSqB); - var dir = wB / l; - newB = origin + dir * size; - } - - var wC = (C - origin); - var lSqC = wC.LengthSquared(); - if (lSqC <= 0f) newC = C; - else - { - var l = MathF.Sqrt(lSqC); - var dir = wC / l; - newC = origin + dir * size; - } - - var wD = (D - origin); - var lSqD = wD.LengthSquared(); - if (lSqD <= 0f) newD = D; - else - { - var l = MathF.Sqrt(lSqD); - var dir = wD / l; - newD = origin + dir * size; - } + var p = GetPoint(alignment); + return new(p, new Size(size, size), GetRotationRad(), alignment); + } - return new(newA, newB, newC, newD); + /// + /// Sets the quad size around its center. + /// + /// The target width and height. + /// A new with the specified size. + public Quad SetSize(Size size) => SetSize(size, AnchorPoint.Center); + + /// + /// Sets the quad size around the specified anchor point. + /// + /// The target width and height. + /// The anchor point that remains fixed during the resize. + /// A new with the specified size. + public Quad SetSize(Size size, AnchorPoint alignment) + { + var p = GetPoint(alignment); + return new(p, size, GetRotationRad(), alignment); } + #endregion + + #region Transform Methods - /// - /// Applies a transform to the quad by: - /// - /// Moving it by .Position - /// Rotating the moved quad by .RotationRad - /// Changing the size of the rotated quad by .ScaledSize.Length - /// - /// - /// The transform to apply to the quad. - /// A new with the applied transform. + /// + /// Applies a relative transform offset to the quad using its center as the anchor point. + /// + /// + /// The transform offset containing positional, rotational, and size adjustments. + /// + /// + /// A new translated, rotated, and resized by the provided offset. + /// + /// + /// The operations are applied in the following order: position, rotation, then size. + /// public Quad ApplyOffset(Transform2D offset) { var newQuad = ChangePosition(offset.Position); newQuad = newQuad.ChangeRotation(offset.RotationRad); - return newQuad.ChangeSize(offset.ScaledSize.Length); + return newQuad.ChangeSize(offset.ScaledSize); } /// - /// Sets the transform of the quad to match the given . + /// Replaces the quad transform using its center as the anchor point. /// - /// The transform to set. - /// A new with the specified transform. - /// Moves, rotates, and sets the size of the quad in sequence. + /// + /// The target transform containing the desired position, rotation, and size. + /// + /// + /// A new whose transform matches the provided values. + /// + /// + /// The operations are applied in the following order: position, rotation, then size. + /// public Quad SetTransform(Transform2D transform) { var newQuad = SetPosition(transform.Position); newQuad = newQuad.SetRotation(transform.RotationRad); - return newQuad.SetSize(transform.ScaledSize.Length); + return newQuad.SetSize(transform.ScaledSize); } - + /// - /// Applies a transform to the quad by: - /// - /// Moving it by .Position - /// Rotating the moved quad by .RotationRad around the specified - /// Changing the size of the rotated quad by .ScaledSize.Length, relative to - /// + /// Applies a relative transform offset to the quad using the specified anchor point. /// - /// The transform to apply to the quad. - /// The anchor point for rotation and scaling. - /// A new with the applied transform. + /// + /// The transform offset containing positional, rotational, and size adjustments. + /// + /// The anchor point used for rotation and resizing. + /// + /// A new translated, rotated, and resized by the provided offset. + /// + /// + /// The translation is applied directly, while rotation and size changes are performed relative + /// to . + /// public Quad ApplyOffset(Transform2D offset, AnchorPoint alignment) { var newQuad = ChangePosition(offset.Position); newQuad = newQuad.ChangeRotation(offset.RotationRad, alignment); - return newQuad.ChangeSize(offset.ScaledSize.Length, alignment); + return newQuad.ChangeSize(offset.ScaledSize, alignment); } /// - /// Sets the transform of the quad by: - /// - /// Moving it to .Position, aligning - /// Rotating the moved quad to .RotationRad around - /// Setting the size of the rotated quad to .ScaledSize.Length, relative to - /// + /// Replaces the quad transform using the specified anchor point. /// - /// The transform to set. - /// The anchor point for alignment, rotation, and scaling. - /// A new with the specified transform. + /// + /// The target transform containing the desired position, rotation, and size. + /// + /// The anchor point used for positioning, rotation, and resizing. + /// + /// A new whose transform matches the provided values. + /// + /// + /// The operations are applied in the following order: position, rotation, then size. + /// public Quad SetTransform(Transform2D transform, AnchorPoint alignment) { var newQuad = SetPosition(transform.Position, alignment); newQuad = newQuad.SetRotation(transform.RotationRad, alignment); - return newQuad.SetSize(transform.ScaledSize.Length, alignment); + return newQuad.SetSize(transform.ScaledSize, alignment); } #endregion diff --git a/ShapeEngine/Geometry/RectDef/Rect.cs b/ShapeEngine/Geometry/RectDef/Rect.cs index 07a6d312..a9aec934 100644 --- a/ShapeEngine/Geometry/RectDef/Rect.cs +++ b/ShapeEngine/Geometry/RectDef/Rect.cs @@ -5,6 +5,7 @@ using ShapeEngine.Geometry.PointsDef; using ShapeEngine.Geometry.PolygonDef; using ShapeEngine.Geometry.PolylineDef; +using ShapeEngine.Geometry.QuadDef; using ShapeEngine.Geometry.SegmentDef; using ShapeEngine.Geometry.SegmentsDef; using ShapeEngine.Geometry.TriangleDef; @@ -25,26 +26,41 @@ namespace ShapeEngine.Geometry.RectDef; /// Gets the X-coordinate of the top-left corner of the rectangle. /// public readonly float X; + /// /// Gets the Y-coordinate of the top-left corner of the rectangle. /// public readonly float Y; + /// /// Gets the width of the rectangle. /// public readonly float Width; + /// /// Gets the height of the rectangle. /// public readonly float Height; + /// + /// Returns a string that describes this rectangle's position and size. + /// + /// A string in the form Rect[X: ..., Y: ..., Width: ..., Height: ...]. public override string ToString() { return $"Rect[X: {X}, Y: {Y}, Width: {Width}, Height: {Height}]"; } + /// + /// Gets the closed-shape type represented by this instance. + /// + /// . public ClosedShapeType GetClosedShapeType() => ClosedShapeType.Rect; + /// + /// Gets the general shape type represented by this instance. + /// + /// . public ShapeType GetShapeType() => ShapeType.Rect; #endregion @@ -54,18 +70,22 @@ public override string ToString() /// Gets the top-left corner of the rectangle as a . /// public Vector2 TopLeft => new(X, Y); + /// /// Gets the top-right corner of the rectangle as a . /// public Vector2 TopRight => new(X + Width, Y); + /// /// Gets the bottom-right corner of the rectangle as a . /// public Vector2 BottomRight => new(X + Width, Y + Height); + /// /// Gets the bottom-left corner of the rectangle as a . /// public Vector2 BottomLeft => new(X, Y + Height); + /// /// Gets the center point of the rectangle as a . /// @@ -75,14 +95,17 @@ public override string ToString() /// Gets the top-left corner of the rectangle (alias for ). /// public Vector2 A => TopLeft; + /// /// Gets the bottom-left corner of the rectangle (alias for ). /// public Vector2 B => BottomLeft; + /// /// Gets the bottom-right corner of the rectangle (alias for ). /// public Vector2 C => BottomRight; + /// /// Gets the top-right corner of the rectangle (alias for ). /// @@ -98,14 +121,17 @@ public override string ToString() /// Gets the Y-coordinate of the top edge of the rectangle. /// public float Top => Y; + /// /// Gets the Y-coordinate of the bottom edge of the rectangle. /// public float Bottom => Y + Height; + /// /// Gets the X-coordinate of the left edge of the rectangle. /// public float Left => X; + /// /// Gets the X-coordinate of the right edge of the rectangle. /// @@ -115,22 +141,27 @@ public override string ToString() /// Gets the left edge of the rectangle as a . /// public Segment LeftSegment => new(TopLeft, BottomLeft); + /// /// Gets the bottom edge of the rectangle as a . /// public Segment BottomSegment => new(BottomLeft, BottomRight); + /// /// Gets the right edge of the rectangle as a . /// public Segment RightSegment => new(BottomRight, TopRight); + /// /// Gets the top edge of the rectangle as a . /// public Segment TopSegment => new(TopRight, TopLeft); + /// /// Gets the size of the rectangle as a . /// public Size Size => new(Width, Height); + /// /// Gets the rectangle as a structure. /// @@ -146,13 +177,21 @@ public override string ToString() /// The Y-coordinate of the top-left corner. /// The width of the rectangle. /// The height of the rectangle. - /// Use this constructor to create a rectangle by specifying its position and size directly. + /// Use this constructor to create a rectangle by specifying its position and size directly. + /// Negative width/height mirrors the rect. public Rect(float x, float y, float width, float height) { - this.X = x; - this.Y = y; - this.Width = width; - this.Height = height; + // X = x; + // Y = y; + // Width = width; + // Height = height; + float right = x + width; + float bottom = y + height; + + X = MathF.Min(x, right); + Y = MathF.Min(y, bottom); + Width = MathF.Abs(width); + Height = MathF.Abs(height); } /// @@ -164,10 +203,10 @@ public Rect(float x, float y, float width, float height) public Rect(Vector2 topLeft, Vector2 bottomRight) { var final = Fix(topLeft, bottomRight); - this.X = final.topLeft.X; - this.Y = final.topLeft.Y; - this.Width = final.bottomRight.X - this.X; - this.Height = final.bottomRight.Y - this.Y; + X = final.topLeft.X; + Y = final.topLeft.Y; + Width = final.bottomRight.X - this.X; + Height = final.bottomRight.Y - this.Y; } /// @@ -175,29 +214,43 @@ public Rect(Vector2 topLeft, Vector2 bottomRight) /// /// The top-left corner of the rectangle. /// The size of the rectangle. - /// Use this constructor to create a rectangle by specifying its top-left corner and size. + /// Use this constructor to create a rectangle by specifying its top-left corner and size. + /// Negative width/height mirrors the rect. public Rect(Vector2 topLeft, Size size) { - this.X = topLeft.X; - this.Y = topLeft.Y; - this.Width = size.Width; - this.Height = size.Height; + // X = topLeft.X; + // Y = topLeft.Y; + // Width = size.Width; + // Height = size.Height; + float right = topLeft.X + size.Width; + float bottom = topLeft.Y + size.Height; + + X = MathF.Min(topLeft.X, right); + Y = MathF.Min(topLeft.Y, bottom); + Width = MathF.Abs(size.Width); + Height = MathF.Abs(size.Height); } + /// /// Initializes a new instance of the struct from a position, size, and alignment anchor. /// /// The reference position for the rectangle. /// The size of the rectangle. /// The anchor point used to align the rectangle relative to the position. - /// The anchor point determines how the rectangle is positioned relative to the given position. + /// The anchor point determines how the rectangle is positioned relative to the given position. + /// Negative width/height mirrors the rect. public Rect(Vector2 position, Size size, AnchorPoint alignment) { var offset = size * alignment.ToVector2(); var topLeft = position - offset; - this.X = topLeft.X; - this.Y = topLeft.Y; - this.Width = size.Width; - this.Height = size.Height; + + float right = topLeft.X + size.Width; + float bottom = topLeft.Y + size.Height; + + X = MathF.Min(topLeft.X, right); + Y = MathF.Min(topLeft.Y, bottom); + Width = MathF.Abs(size.Width); + Height = MathF.Abs(size.Height); } /// @@ -206,10 +259,10 @@ public Rect(Vector2 position, Size size, AnchorPoint alignment) /// The rectangle structure. public Rect(Rectangle rect) { - this.X = rect.X; - this.Y = rect.Y; - this.Width = rect.Width; - this.Height = rect.Height; + X = rect.X; + Y = rect.Y; + Width = rect.Width; + Height = rect.Height; } #endregion @@ -282,11 +335,38 @@ public override int GetHashCode() #region Shapes /// - /// Points are ordered in ccw order starting with top left (tl, bl, br, tr). + /// Rotates this rectangle by around a pivot computed from the given + /// and returns the resulting quadrilateral as a . + /// + /// Rotation angle in degrees. + /// Anchor point that determines the rotation pivot relative to the rectangle's top-left and size. + /// + /// A representing the four corners of the rotated rectangle in the same corner ordering as the source rect. + /// + public Quad RotateToQuad(float angleDeg, AnchorPoint pivot) + { + return new Quad(this, angleDeg, pivot); + } + + /// + /// Rotates the rectangle's four corners around the specified pivot by the provided angle (in degrees) + /// and returns the resulting quadrilateral as a . + /// Points in the returned preserve the source rectangle's corner ordering (top-left, bottom-left, bottom-right, top-right). + /// + /// Rotation angle in degrees. + /// Pivot point to rotate around. + /// A representing the rotated rectangle. + public Quad RotateToQuad(float angleDeg, Vector2 pivot) + { + return new Quad(this, angleDeg, pivot); + } + + /// + /// Rotates this rectangle around the pivot defined by and returns the resulting corners as a . /// /// The angle in degrees to rotate. /// The anchor point for rotation. - /// + /// A containing the rotated rectangle corners in counter-clockwise order starting at the top-left corner before rotation. public Polygon Rotate(float angleDeg, AnchorPoint alignment) { var poly = ToPolygon(); @@ -295,6 +375,22 @@ public Polygon Rotate(float angleDeg, AnchorPoint alignment) return poly; } + /// + /// Rotates this rectangle around the pivot defined by and writes the resulting vertices into . + /// + /// The destination polygon that will be cleared and populated with the rotated rectangle corners. + /// The rotation angle in degrees. + /// The anchor point used to derive the rotation pivot relative to the rectangle. + /// + /// The resulting polygon stores the rectangle corners in counter-clockwise order starting at the top-left corner before rotation. + /// + public void Rotate(Polygon result, float angleDeg, AnchorPoint alignment) + { + ToPolygon(result); + var pivot = TopLeft + (Size * alignment.ToVector2()).ToVector2(); + result.ChangeRotation(angleDeg * ShapeMath.DEGTORAD, pivot); + } + /// /// Rotates the corners of the rectangle and returns the resulting points as a list. /// @@ -309,11 +405,46 @@ public Points RotateList(float angleDeg, AnchorPoint alignment) return points; } + /// + /// Rotates this rectangle around the pivot defined by and writes the resulting corner points into . + /// + /// The destination collection that will be cleared and populated with the rotated rectangle corners. + /// The rotation angle in degrees. + /// The anchor point used to derive the rotation pivot relative to the rectangle. + /// + /// The resulting points are written in counter-clockwise order starting at the top-left corner before rotation. + /// + public void RotateList(Points result, float angleDeg, AnchorPoint alignment) + { + ToPoints(result); + var pivot = TopLeft + (Size * alignment.ToVector2()).ToVector2(); + result.ChangeRotation(angleDeg * ShapeMath.DEGTORAD, pivot); + } + /// /// Converts the rectangle to a list of points representing its corners. /// /// A object containing the corners of the rectangle. public Points ToPoints() { return [TopLeft, BottomLeft, BottomRight, TopRight]; } + + /// + /// Writes this rectangle's four corners into . + /// + /// The destination collection that will be cleared and populated with the rectangle corners. + /// + /// Points are written in counter-clockwise order starting at the top-left corner: top-left, bottom-left, bottom-right, top-right. + /// + public void ToPoints(Points result) + { + result.Clear(); + result.EnsureCapacity(4); + + result.Add(A); + result.Add(B); + result.Add(C); + result.Add(D); + } + /// /// Converts the rectangle to a polygon representing its shape. /// @@ -325,20 +456,51 @@ public Points RotateList(float angleDeg, AnchorPoint alignment) /// The corners are added in counter-clockwise order: top-left, bottom-left, bottom-right, top-right. /// /// A reference to a that will be populated with the rectangle's corners. - public void ToPolygon(ref Polygon result) + public void ToPolygon(Polygon result) { - if(result.Count > 0) result.Clear(); + result.Clear(); + result.EnsureCapacity(4); + result.Add(A); result.Add(B); result.Add(C); result.Add(D); } + + /// + /// Converts the rectangle to a quadrilateral (). + /// + /// A representing the rectangle. + public Quad ToQuad() + { + return new Quad(this); + } /// /// Converts the rectangle to a polyline representing its outline. /// /// A object representing the rectangle's outline. public Polyline ToPolyline() { return [TopLeft, BottomLeft, BottomRight, TopRight]; } + + /// + /// Writes this rectangle's outline vertices into as an open . + /// + /// The destination polyline that will be cleared and populated with the rectangle corners. + /// + /// Points are written in counter-clockwise order starting at the top-left corner: top-left, bottom-left, bottom-right, top-right. + /// The first point is not repeated at the end. + /// + public void ToPolyline(Polyline result) + { + result.Clear(); + result.EnsureCapacity(4); + + result.Add(A); + result.Add(B); + result.Add(C); + result.Add(D); + } + /// /// Gets the edges of the rectangle as segments. /// @@ -357,6 +519,34 @@ public Segments GetEdges() return [left, bottom, right, top]; } + /// + /// Writes this rectangle's four edges into . + /// + /// The destination collection that will be cleared and populated with the rectangle edges. + /// + /// Segments are written in counter-clockwise order: left, bottom, right, top. + /// + public void GetEdges(Segments segments) + { + var a = TopLeft; + var b = BottomLeft; + var c = BottomRight; + var d = TopRight; + + Segment left = new(a, b); + Segment bottom = new(b, c); + Segment right = new(c, d); + Segment top = new(d, a); + + segments.Clear(); + segments.EnsureCapacity(4); + + segments.Add(left); + segments.Add(bottom); + segments.Add(right); + segments.Add(top); + } + /// /// Triangulates the rectangle into two triangles. /// @@ -368,82 +558,34 @@ public Triangulation Triangulate() Triangle b = new(TopLeft, BottomRight, TopRight); return new Triangulation() { a, b }; } - - /// - /// Gets points for slanted corners of the rectangle. - /// - /// Top-left corner slant amount. - /// Top-right corner slant amount. - /// Bottom-right corner slant amount. - /// Bottom-left corner slant amount. - /// A object containing the slanted corner points. - public Polygon GetSlantedCornerPoints(float tlCorner, float trCorner, float brCorner, float blCorner) - { - Polygon points = []; - - var tl = TopLeft; - tlCorner = MathF.Max(tlCorner, 0); - points.Add(tl + new Vector2(MathF.Min(tlCorner, Width), 0f)); - points.Add(tl + new Vector2(0f, MathF.Min(tlCorner, Height))); - - var bl = BottomLeft; - blCorner = MathF.Max(blCorner, 0); - points.Add(bl - new Vector2(0f, MathF.Min(blCorner, Height))); - points.Add(bl + new Vector2(MathF.Min(blCorner, Width), 0f)); - - var br = BottomRight; - brCorner = MathF.Max(brCorner, 0); - points.Add(br - new Vector2(MathF.Min(brCorner, Width), 0f)); - points.Add(br - new Vector2(0f, MathF.Min(brCorner, Height))); - - var tr = TopRight; - trCorner = MathF.Max(trCorner, 0); - points.Add(tr + new Vector2(0f, MathF.Min(trCorner, Height))); - points.Add(tr - new Vector2(MathF.Min(trCorner, Width), 0f)); - - return points; - } + /// - /// Get the points to draw a rectangle with slanted corners. The corner values are the percentage of the width/height of the rectange the should be used for the slant. + /// Triangulates this rectangle into two triangles and writes them into . /// - /// Should be between 0-1 - /// Should be between 0-1 - /// Should be between 0-1 - /// Should be between 0-1 - /// Returns points in ccw order. - public Polygon GetSlantedCornerPointsRelative(float tlCorner, float trCorner, float brCorner, float blCorner) + /// The destination triangulation that will be cleared and populated with the generated triangles. + /// + /// The triangles are written in this order: (TopLeft, BottomLeft, BottomRight) and (TopLeft, BottomRight, TopRight). + /// + public void Triangulate(Triangulation result) { - Polygon points = []; - - var tl = TopLeft; - tlCorner = ShapeMath.Clamp(tlCorner, 0f, 1f); - points.Add(tl + new Vector2(tlCorner * Width, 0f)); - points.Add(tl + new Vector2(0f, tlCorner * Height)); - - var bl = BottomLeft; - blCorner = ShapeMath.Clamp(blCorner, 0f, 1f); - points.Add(bl - new Vector2(0f, blCorner * Height)); - points.Add(bl + new Vector2(blCorner * Width, 0f)); - - var br = BottomRight; - brCorner = ShapeMath.Clamp(brCorner, 0f, 1f); - points.Add(br - new Vector2(brCorner * Width, 0f)); - points.Add(br - new Vector2(0f, brCorner * Height)); - - var tr = TopRight; - trCorner = ShapeMath.Clamp(trCorner, 0f, 1f); - points.Add(tr + new Vector2(0f, trCorner * Height)); - points.Add(tr - new Vector2(trCorner * Width, 0f)); + Triangle a = new(TopLeft, BottomLeft, BottomRight); + Triangle b = new(TopLeft, BottomRight, TopRight); - return points; + result.Clear(); + result.Add(a); + result.Add(b); } #endregion #region Union & Difference /// - /// Creates a rect that represents the intersection between a and b. If there is no intersection, an - /// empty rect is returned. + /// Computes the overlap rectangle between this rectangle and . /// + /// The rectangle to test against this rectangle. + /// The intersecting region as a , or an empty rectangle if the two rectangles do not overlap. + /// + /// Despite the method name, this operation returns the geometric intersection of the two rectangles. + /// public Rect Difference(Rect rect) { @@ -459,10 +601,15 @@ public Rect Difference(Rect rect) return new(); } + /// - /// Creates a rect that represents the intersection between a and b. If there is no intersection, an - /// empty rect is returned. + /// Computes the overlap rectangle between this rectangle and . /// + /// The rectangle to test against this rectangle. + /// The intersecting region as a , or an empty rectangle if the two rectangles do not overlap. + /// + /// Despite the method name, this operation returns the geometric intersection of the two rectangles. + /// public Rect Difference2(Rect other) { if (OverlapShape(other)) @@ -488,6 +635,7 @@ public Rect Union(Rect rect) return new Rect(x1, y1, x2 - x1, y2 - y1); } + /// /// Creates a rectangle that represents the union between a and b. /// @@ -514,6 +662,7 @@ public Segment GetSegment(int index) if(i == 2) return new Segment(C, D); return new Segment(D, A); } + /// /// Gets a point on the rectangle based on the specified alignment anchor. /// @@ -524,6 +673,7 @@ public Vector2 GetPoint(AnchorPoint alignment) var offset = Size * alignment.ToVector2(); return TopLeft + offset; } + /// /// Rotates the corners of the rectangle around a pivot point by the specified angle. /// @@ -532,10 +682,28 @@ public Vector2 GetPoint(AnchorPoint alignment) /// A tuple containing the rotated corners of the rectangle. public (Vector2 tl, Vector2 bl, Vector2 br, Vector2 tr) RotateCorners(Vector2 pivot, float angleDeg) { - var poly = ToPolygon(); - poly.ChangeRotation(angleDeg * ShapeMath.DEGTORAD, pivot); - return new(poly[0], poly[1], poly[2], poly[3]); + var a = TopLeft; + var b = BottomLeft; + var c = BottomRight; + var d = TopRight; + + var rotRad = angleDeg * ShapeMath.DEGTORAD; + + var w = a - pivot; + a = pivot + w.Rotate(rotRad); + + w = b - pivot; + b = pivot + w.Rotate(rotRad); + + w = c - pivot; + c = pivot + w.Rotate(rotRad); + + w = d - pivot; + d = pivot + w.Rotate(rotRad); + + return (a, b, c, d); } + /// /// Gets a random point inside the rectangle. /// @@ -556,6 +724,27 @@ public Points GetRandomPointsInside(int amount) } return points; } + + /// + /// Writes a specified number of random points sampled from inside this rectangle into . + /// + /// The destination collection that will be cleared and populated with the generated points. + /// The number of random interior points to generate. + /// + /// If is less than or equal to zero, the method returns without modifying . + /// + public void GetRandomPointsInside(Points result, int amount) + { + if (amount <= 0) return; + + result.Clear(); + result.EnsureCapacity(amount); + + for (int i = 0; i < amount; i++) + { + result.Add(GetRandomPointInside()); + } + } /// /// Gets a random vertex (corner) of the rectangle. @@ -569,34 +758,88 @@ public Vector2 GetRandomVertex() else if (randIndex == 2) return BottomRight; else return TopRight; } + /// /// Gets a random edge of the rectangle as a . /// /// A representing a random edge of the rectangle. public Segment GetRandomEdge() => GetEdges().GetRandomSegment(); + /// /// Gets a random point on the perimeter of the rectangle. /// /// A representing a random point on the rectangle's edge. public Vector2 GetRandomPointOnEdge() => GetRandomEdge().GetRandomPoint(); + /// /// Gets a specified number of random points on the perimeter of the rectangle. /// /// The number of random points to generate on the edge. /// A object containing the random points on the rectangle's edge. - public Points GetRandomPointsOnEdge(int amount) => GetEdges().GetRandomPoints(amount); + public Points GetRandomPointsOnEdge(int amount) + { + var edges = GetEdges(); + var points = new Points(); + edges.GetRandomPoints(amount, points); + return points; + } + + /// + /// Writes a specified number of random points sampled from this rectangle's perimeter into . + /// + /// The destination collection that will be cleared and populated with the generated edge points. + /// The number of random perimeter points to generate. + /// + /// Edge selection is delegated to using the rectangle's four edges. + /// + public void GetRandomPointsOnEdge(Points result, int amount) + { + var edges = GetEdges(); + edges.GetRandomPoints(amount, result); + } + + /// + /// Gets a random triangle contained inside this rectangle. + /// Attempts up to 100 random samples of three points inside the rectangle and returns the first triangle + /// whose area is greater than . If is greater than or + /// equal to half the rectangle area or sampling fails, a fallback triangle formed by the rect's left edge + /// (TopLeft, BottomLeft, BottomRight) is returned. + /// + /// Minimum required triangle area. Defaults to 1e-6f. + /// A that lies inside the rectangle. + public Triangle GetRandomTriangleInside(float minArea = 1e-6f) + { + const int maxAttempts = 100; + if(minArea >= GetArea() * 0.5f) return new Triangle(TopLeft, BottomLeft, BottomRight); + for (int i = 0; i < maxAttempts; i++) + { + var a = GetRandomPointInside(); + var b = GetRandomPointInside(); + var c = GetRandomPointInside(); + float area = MathF.Abs((b.X - a.X) * (c.Y - a.Y) - (c.X - a.X) * (b.Y - a.Y)) * 0.5f; + if (area > minArea) return new Triangle(a, b, c); + } + return new Triangle(TopLeft, BottomLeft, BottomRight); + } #endregion #region Corners /// - /// Corners a numbered in ccw order starting from the top left. (tl, bl, br, tr) + /// Gets one of the rectangle's corners by index. /// - /// Corner Index from 0 to 3 - /// - public Vector2 GetCorner(int corner) => ToPolygon()[corner % 4]; + /// The corner index. Values are wrapped modulo 4 in counter-clockwise order starting at the top-left corner: 0 = top-left, 1 = bottom-left, 2 = bottom-right, 3 = top-right. + /// The corner position corresponding to the wrapped index. + public Vector2 GetCorner(int corner) + { + var index = corner % 4; + if(index == 0) return TopLeft; + else if(index == 1) return BottomLeft; + else if(index == 2) return BottomRight; + else return TopRight; + } /// /// Gets the corners of the rectangle relative to a given position. Points are ordered in counter-clockwise order starting from the top left. @@ -605,12 +848,33 @@ public Vector2 GetRandomVertex() /// A containing the relative corner points. public Polygon GetPointsRelative(Vector2 pos) { - var points = ToPolygon(); //GetPoints(rect); - for (int i = 0; i < points.Count; i++) - { - points[i] -= pos; - } - return points; + var result = new Polygon(4); + + result.Add(TopLeft - pos); + result.Add(BottomLeft - pos); + result.Add(BottomRight - pos); + result.Add(TopRight - pos); + + return result; + } + + /// + /// Writes this rectangle's corners relative to into . + /// + /// The destination polygon that will be cleared and populated with the relative corner points. + /// The position to subtract from each corner. + /// + /// Points are written in counter-clockwise order starting at the top-left corner. + /// + public void GetPointsRelative(Polygon result, Vector2 pos) + { + result.Clear(); + result.EnsureCapacity(4); + + result.Add(TopLeft - pos); + result.Add(BottomLeft - pos); + result.Add(BottomRight - pos); + result.Add(TopRight - pos); } #endregion @@ -637,6 +901,7 @@ public static (Vector2 topLeft, Vector2 bottomRight) Fix(Vector2 topLeft, Vector return (newTopLeft, newBottomRight); } + /// /// Constructs 9 rectangles out of an outer and inner rectangle. /// @@ -709,17 +974,19 @@ public static Segments GetEdges(Vector2 tl, Vector2 bl, Vector2 br, Vector2 tr) return segments; } + /// /// Creates a rectangle from a circle by using the circle's center and radius. /// /// The circle to create the rectangle from. /// A representing the bounding rectangle of the circle. - public static Rect FromCircle(Circle c) => new(c.Center, new Size(c.Radius, c.Radius), new (0.5f, 0.5f)); + public static Rect FromCircle(Circle c) => new(c.Center, new Size(c.Diameter), new (0.5f, 0.5f)); /// /// Gets an empty rectangle with zero size. /// public static Rect Empty => new(); + private static ValueRange RangeHull(ValueRange a, ValueRange b) { return new @@ -748,6 +1015,7 @@ private static ValueRange RangeHull(ValueRange a, ValueRange b) left.Height + right.Height ); } + /// /// Subtracts one rectangle from another component-wise. /// @@ -764,6 +1032,7 @@ private static ValueRange RangeHull(ValueRange a, ValueRange b) left.Height - right.Height ); } + /// /// Multiplies two rectangles component-wise. /// @@ -780,6 +1049,7 @@ private static ValueRange RangeHull(ValueRange a, ValueRange b) left.Height * right.Height ); } + /// /// Divides one rectangle by another component-wise. /// @@ -796,6 +1066,7 @@ private static ValueRange RangeHull(ValueRange a, ValueRange b) left.Height / right.Height ); } + /// /// Adds a vector to a rectangle's position. /// @@ -812,6 +1083,7 @@ private static ValueRange RangeHull(ValueRange a, ValueRange b) left.Height ); } + /// /// Subtracts a vector from a rectangle's position. /// @@ -828,6 +1100,7 @@ private static ValueRange RangeHull(ValueRange a, ValueRange b) left.Height ); } + /// /// Multiplies a rectangle's position and size by a vector component-wise. /// @@ -844,6 +1117,7 @@ private static ValueRange RangeHull(ValueRange a, ValueRange b) left.Height * right.Y ); } + /// /// Divides a rectangle's position and size by a vector component-wise. /// @@ -860,6 +1134,7 @@ private static ValueRange RangeHull(ValueRange a, ValueRange b) left.Height / right.Y ); } + /// /// Adds a scalar to a rectangle's position and size. /// @@ -876,6 +1151,7 @@ private static ValueRange RangeHull(ValueRange a, ValueRange b) left.Height + right ); } + /// /// Subtracts a scalar from a rectangle's position and size. /// @@ -892,6 +1168,7 @@ private static ValueRange RangeHull(ValueRange a, ValueRange b) left.Height - right ); } + /// /// Multiplies a rectangle's position and size by a scalar. /// @@ -908,6 +1185,7 @@ private static ValueRange RangeHull(ValueRange a, ValueRange b) left.Height * right ); } + /// /// Divides a rectangle's position and size by a scalar. /// @@ -941,6 +1219,26 @@ public Points GetInterpolatedEdgePoints(float t) return new Points(4){a1, b1, c1, d1}; } + + /// + /// Writes one interpolated point per rectangle edge into . + /// + /// The destination collection that will be cleared and populated with the interpolated edge points. + /// The interpolation factor used on each edge. + /// + /// The returned points correspond to interpolation on edges A→B, B→C, C→D, and D→A in that order. + /// + public void GetInterpolatedEdgePoints(Points result, float t) + { + result.Clear(); + result.EnsureCapacity(4); + + result.Add(A.Lerp(B, t)); + result.Add(B.Lerp(C, t)); + result.Add(C.Lerp(D, t)); + result.Add(D.Lerp(A, t)); + } + /// /// Gets interpolated points along the edges of the rectangle with specified steps. /// @@ -950,6 +1248,28 @@ public Points GetInterpolatedEdgePoints(float t) public Points GetInterpolatedEdgePoints(float t, int steps) { if(steps <= 1) return GetInterpolatedEdgePoints(t); + + var result = new Points(4); + GetInterpolatedEdgePoints(result, t, steps); + return result; + } + + /// + /// Repeatedly interpolates the rectangle edges and writes the final four points into . + /// + /// The destination collection that receives the final interpolated points. + /// The interpolation factor used at each step. + /// The number of interpolation iterations to perform. Values less than or equal to one fall back to a single interpolation pass. + /// + /// Each iteration interpolates between the points produced by the previous iteration, always preserving four output points in edge order. + /// + public void GetInterpolatedEdgePoints(Points result, float t, int steps) + { + if (steps <= 1) + { + GetInterpolatedEdgePoints(result, t); + return; + } var a1 = A.Lerp(B, t); var b1 = B.Lerp(C, t); @@ -971,7 +1291,13 @@ public Points GetInterpolatedEdgePoints(float t, int steps) remainingSteps--; } - return new Points(4){a1, b1, c1, d1}; + result.Clear(); + result.EnsureCapacity(4); + + result.Add(a1); + result.Add(b1); + result.Add(c1); + result.Add(d1); } #endregion } \ No newline at end of file diff --git a/ShapeEngine/Geometry/RectDef/RectDrawing.cs b/ShapeEngine/Geometry/RectDef/RectDrawing.cs index ca8b91ab..84bd4513 100644 --- a/ShapeEngine/Geometry/RectDef/RectDrawing.cs +++ b/ShapeEngine/Geometry/RectDef/RectDrawing.cs @@ -1,3 +1,4 @@ + using System.Numerics; using Raylib_cs; using ShapeEngine.Color; @@ -11,6 +12,7 @@ namespace ShapeEngine.Geometry.RectDef; + /// /// Provides static extension methods for drawing rectangles and grids, including advanced features such as nine-patch, rounded corners, slanted corners, partial outlines, and more. /// @@ -20,908 +22,1022 @@ namespace ShapeEngine.Geometry.RectDef; /// public static class RectDrawing { - #region Draw Masked + #region Draw /// - /// Draws the rectangle's four side segments, but only where they intersect the given triangular mask. - /// Each side is drawn by forwarding the call to the segment-level masked draw method. + /// Draws a filled axis-aligned rectangle from two corner points. /// - /// The rectangle whose sides will be drawn. - /// The triangular mask used to clip each side. - /// Line drawing parameters (thickness, color, cap style, etc.). - /// If true, draws the parts inside the mask instead of outside. - public static void DrawLinesMasked(this Rect rect, Triangle mask, LineDrawingInfo lineInfo, bool reversedMask = false) + /// The rectangle's top-left corner. + /// The rectangle's bottom-right corner. + /// The fill color. + public static void DrawRect(Vector2 topLeft, Vector2 bottomRight, ColorRgba color) { - rect.TopSegment.DrawMasked(mask, lineInfo, reversedMask); - rect.LeftSegment.DrawMasked(mask, lineInfo, reversedMask); - rect.BottomSegment.DrawMasked(mask, lineInfo, reversedMask); - rect.RightSegment.DrawMasked(mask, lineInfo, reversedMask); + Raylib.DrawRectangleV(topLeft, bottomRight - topLeft, color.ToRayColor()); } + /// - /// Draws the rectangle's four side segments, but only where they intersect the given circular mask. - /// Each side is drawn by forwarding the call to the segment-level masked draw method. + /// Draws a filled rectangle. /// - /// The rectangle whose sides will be drawn. - /// The circular mask used to clip each side. - /// Line drawing parameters (thickness, color, cap style, etc.). - /// If true, draws the parts inside the mask instead of outside. - public static void DrawLinesMasked(this Rect rect, Circle mask, LineDrawingInfo lineInfo, bool reversedMask = false) + /// The rectangle to draw. + /// The fill color. + public static void Draw(this Rect rect, ColorRgba color) { - rect.TopSegment.DrawMasked(mask, lineInfo, reversedMask); - rect.LeftSegment.DrawMasked(mask, lineInfo, reversedMask); - rect.BottomSegment.DrawMasked(mask, lineInfo, reversedMask); - rect.RightSegment.DrawMasked(mask, lineInfo, reversedMask); + Raylib.DrawRectangleV(rect.TopLeft, rect.BottomRight - rect.TopLeft, color.ToRayColor()); } + #endregion + + #region Draw Rounded /// - /// Draws the rectangle's four side segments, but only where they intersect the given rectangular mask. - /// Each side is drawn by forwarding the call to the segment-level masked draw method which performs - /// clipping against the provided mask. + /// Draws a filled rectangle with rounded corners. /// - /// The rectangle whose sides will be drawn. - /// The rectangular mask used to clip each side. - /// Line drawing parameters (thickness, color, cap style, etc.). - /// If true, draws the parts inside the mask instead of outside. - public static void DrawLinesMasked(this Rect rect, Rect mask, LineDrawingInfo lineInfo, bool reversedMask = false) + /// The rectangle to draw. + /// The fill color. + /// The normalized corner roundness passed to Raylib. + /// The number of segments used for each rounded corner. + /// + /// This method forwards to Raylib's rounded rectangle drawing API. + /// + public static void DrawRounded(this Rect rect, ColorRgba color, float roundness, int segments) { - rect.TopSegment.DrawMasked(mask, lineInfo, reversedMask); - rect.LeftSegment.DrawMasked(mask, lineInfo, reversedMask); - rect.BottomSegment.DrawMasked(mask, lineInfo, reversedMask); - rect.RightSegment.DrawMasked(mask, lineInfo, reversedMask); + Raylib.DrawRectangleRounded(rect.Rectangle, roundness, segments, color); } + + #endregion + + #region Draw Scaled /// - /// Draws the rectangle's four side segments, but only where they intersect the given quadrilateral mask. - /// Each side is forwarded to the corresponding segment-level masked draw method which handles clipping against the provided mask. + /// Draws a rect with scaled sides based on a specific draw type. /// - /// The rectangle whose sides will be drawn. - /// The quadrilateral mask used to clip each side. - /// Line drawing parameters (thickness, color, cap style, etc.). - /// If true, draws the parts inside the mask instead of outside. - public static void DrawLinesMasked(this Rect rect, Quad mask, LineDrawingInfo lineInfo, bool reversedMask = false) + /// The rect to draw. + /// The color of the drawn shape. + /// The scale factor of the sides (0 to 1). If >= 1, the full quad is drawn. If <= 0, nothing is drawn. + /// The origin point for scaling the sides (0 = start, 1 = end, 0.5 = center). + /// + /// The style of drawing: + /// + /// 0: [Filled] Drawn as 6 filled triangles, effectivly cutting of corners. + /// 1: [Sides] Each side is connected to the quad's center. + /// 2: [Sides Inverse] The start of 1 side is connected to the end of the next side and is connected to the quad's center. + /// + /// + public static void DrawScaled(this Rect r, ColorRgba color, float sideScaleFactor, float sideScaleOrigin, int drawType) { - rect.TopSegment.DrawMasked(mask, lineInfo, reversedMask); - rect.LeftSegment.DrawMasked(mask, lineInfo, reversedMask); - rect.BottomSegment.DrawMasked(mask, lineInfo, reversedMask); - rect.RightSegment.DrawMasked(mask, lineInfo, reversedMask); + var q = r.ToQuad(); + q.DrawScaled(color, sideScaleFactor, sideScaleOrigin, drawType); } + #endregion + + #region Draw Lines + /// - /// Draws the rectangle's four side segments, but only where they intersect the given polygon mask. - /// Each side is drawn by forwarding the call to the segment-level masked draw method which performs - /// clipping against the provided mask. + /// Draws the outline of a rectangle. /// - /// The rectangle whose sides will be drawn. - /// The polygonal mask used to clip each side. - /// Line drawing parameters (thickness, color, cap style, etc.). - /// If true, draws the parts inside the mask instead of outside. - public static void DrawLinesMasked(this Rect rect, Polygon mask, LineDrawingInfo lineInfo, bool reversedMask = false) + /// The rectangle to outline. + /// The outline thickness. + /// The outline color. + /// + /// The rectangle is converted to a and drawn using quad line rendering. + /// + public static void DrawLines(this Rect rect, float lineThickness, ColorRgba color) { - rect.TopSegment.DrawMasked(mask, lineInfo, reversedMask); - rect.LeftSegment.DrawMasked(mask, lineInfo, reversedMask); - rect.BottomSegment.DrawMasked(mask, lineInfo, reversedMask); - rect.RightSegment.DrawMasked(mask, lineInfo, reversedMask); + var q = rect.ToQuad(); + q.DrawLines(lineThickness, color); + // var thickness = MathF.Min(lineThickness, rect.Size.Min() * 0.5f); + // rect = rect.ChangeSize(thickness * 2f, AnchorPoint.Center); + // Raylib.DrawRectangleLinesEx(rect.Rectangle, thickness * 2f, color); } + /// - /// Draws the rectangle's four side segments clipped against a generic closed-shape mask. + /// Draws the outline of a rectangle using a configuration. /// - /// - /// The mask type implementing (for example , , , etc.). - /// - /// The rectangle whose sides will be drawn. - /// The mask used to clip each side. - /// Line drawing parameters (thickness, color, cap style, etc.). - /// If true, draws the parts inside the mask instead of outside. - public static void DrawLinesMasked(this Rect rect, T mask, LineDrawingInfo lineInfo, bool reversedMask = false) where T : IClosedShapeTypeProvider + /// The rectangle to outline. + /// The line drawing settings to use. + /// + /// The rectangle is converted to a and drawn using quad line rendering. + /// + public static void DrawLines(this Rect rect, LineDrawingInfo lineInfo) { - rect.TopSegment.DrawMasked(mask, lineInfo, reversedMask); - rect.LeftSegment.DrawMasked(mask, lineInfo, reversedMask); - rect.BottomSegment.DrawMasked(mask, lineInfo, reversedMask); - rect.RightSegment.DrawMasked(mask, lineInfo, reversedMask); + var q = rect.ToQuad(); + q.DrawLines(lineInfo); + // var thickness = MathF.Min(lineInfo.Thickness, rect.Size.Min() * 0.5f); + // rect = rect.ChangeSize(thickness * 2f, AnchorPoint.Center); + // Raylib.DrawRectangleLinesEx(rect.Rectangle, thickness, lineInfo.Color); } #endregion + #region Draw Rounded Lines /// - /// Draws a using a single color for all patches. - /// - /// The nine-patch rectangle to draw. - /// The color to use for all patches. - public static void Draw(this NinePatchRect npr, ColorRgba color) - { - var rects = npr.Rects; - foreach (var r in rects) - { - r.Draw(color); - } - } - - /// - /// Draws a using separate colors for the source and patch rectangles. + /// Draws a rounded rectangle outline. /// - /// The nine-patch rectangle to draw. - /// The color for the source rectangle. - /// The color for the patch rectangles. - public static void Draw(this NinePatchRect npr, ColorRgba sourceColorRgba, ColorRgba patchColorRgba) + /// The rectangle to outline. + /// The outline thickness. + /// The outline color. + /// The normalized corner roundness passed to Raylib. + /// The number of segments used for each rounded corner. + /// + /// If or is not positive, this falls back to . + /// + public static void DrawLinesRounded(this Rect rect, float lineThickness, ColorRgba color, float roundness, int segments) { - npr.Source.Draw(sourceColorRgba); - var rects = npr.Rects; - foreach (var r in rects) + if (roundness <= 0f || segments <= 0) { - r.Draw(patchColorRgba); + rect.DrawLines(lineThickness, color); + return; } + var thickness = MathF.Min(lineThickness, rect.Size.Min() * 0.5f); + rect = rect.ChangeSize(-thickness * 1.99f, AnchorPoint.Center); + Raylib.DrawRectangleRoundedLinesEx(rect.Rectangle, roundness, segments, thickness * 2f, color); } - + #endregion + + #region Draw Lines Scaled + /// - /// Draws the outlines of a using the specified line thickness and color. + /// Draws a rectangle outline where each side can be scaled towards the origin of the side. /// - /// The nine-patch rectangle to draw. - /// The thickness of the outline lines. - /// The color of the outline lines. - public static void DrawLines(this NinePatchRect npr, float lineThickness, ColorRgba color) + /// The rectangle to draw. + /// The line drawing information (thickness, color, etc.). + /// + /// The scale factor for each side. + /// + /// 0: No Rect is drawn. + /// 1: The normal Rect is drawn. + /// 0.5: Each side is half as long. + /// + /// + /// + /// The point along each side to scale from, in both directions (0 to 1). + /// + /// 0: Start of Side + /// 0.5: Center of Side + /// 1: End of Side + /// + /// + /// + /// Useful for creating stylized or animated rectangles. + /// + public static void DrawLinesScaled(this Rect r, LineDrawingInfo lineInfo, float sideScaleFactor, float sideScaleOrigin = 0.5f) { - var rects = npr.Rects; - foreach (var r in rects) + if (sideScaleFactor <= 0f) return; + if (sideScaleFactor >= 1f) { - r.DrawLines(lineThickness, color); + r.DrawLines(lineInfo); + return; } + + lineInfo = lineInfo.SetThickness(MathF.Min(lineInfo.Thickness, r.Size.Min() * 0.5f)); + + SegmentDrawing.DrawSegment(r.TopLeft, r.BottomLeft, lineInfo, sideScaleFactor, sideScaleOrigin); + SegmentDrawing.DrawSegment(r.BottomLeft, r.BottomRight, lineInfo, sideScaleFactor, sideScaleOrigin); + SegmentDrawing.DrawSegment(r.BottomRight, r.TopRight, lineInfo, sideScaleFactor, sideScaleOrigin); + SegmentDrawing.DrawSegment(r.TopRight, r.TopLeft, lineInfo, sideScaleFactor, sideScaleOrigin); } + #endregion + + #region Draw Lines Percentage + /// - /// Draws the outlines of a using separate line thickness and color for the source and patch rectangles. + /// Draws part of the rectangle outline based on a perimeter percentage. /// - /// The nine-patch rectangle to draw. - /// The line thickness for the source rectangle. - /// The line thickness for the patch rectangles. - /// The color for the source rectangle outline. - /// The color for the patch rectangles outlines. - public static void DrawLines(this NinePatchRect npr, float sourceLineThickness, float patchLineThickness, ColorRgba sourceColorRgba, ColorRgba patchColorRgba) + /// The rectangle whose outline is drawn. + /// The fraction of the perimeter to draw. + /// The starting edge index used by the underlying quad draw order. + /// The outline thickness. + /// The outline color. + /// + /// This method converts the rectangle to a and delegates to quad percentage line drawing. + /// + public static void DrawLinesPercentage(this Rect rect, float f, int startIndex, float lineThickness, ColorRgba color) { - npr.Source.DrawLines(sourceLineThickness, sourceColorRgba); - var rects = npr.Rects; - foreach (var r in rects) - { - r.DrawLines(patchLineThickness, patchColorRgba); - } + var quad = new Quad(rect); + quad.DrawLinesPercentage(f, startIndex, new LineDrawingInfo(lineThickness, color)); } - + /// - /// Draws a grid within the specified bounds using the given line thickness and color. + /// Draws part of the rectangle outline based on a perimeter percentage using line settings. /// - /// The grid definition specifying the number of rows and columns. - /// The rectangle bounds in which to draw the grid. - /// The thickness of the grid lines. - /// The color of the grid lines. + /// The rectangle whose outline is drawn. + /// The fraction of the perimeter to draw. + /// The starting edge index used by the underlying quad draw order. + /// The line drawing settings to use. /// - /// The grid is drawn using horizontal and vertical lines spaced according to the number of rows and columns. + /// This method converts the rectangle to a and delegates to quad percentage line drawing. /// - public static void Draw(this Grid grid, Rect bounds, float lineThickness, ColorRgba color) + public static void DrawLinesPercentage(this Rect rect, float f, int startIndex, LineDrawingInfo lineInfo) { - Vector2 rowSpacing = new(0f, bounds.Height / grid.Rows); - for (int row = 0; row < grid.Rows + 1; row++) - { - SegmentDrawing.DrawSegment(bounds.TopLeft + rowSpacing * row, bounds.TopRight + rowSpacing * row, lineThickness, color); - } - Vector2 colSpacing = new(bounds.Width / grid.Cols, 0f); - for (int col = 0; col < grid.Cols + 1; col++) - { - SegmentDrawing.DrawSegment(bounds.TopLeft + colSpacing * col, bounds.BottomLeft + colSpacing * col, lineThickness, color); - } + var quad = new Quad(rect); + quad.DrawLinesPercentage(f, startIndex, lineInfo); } - + + #endregion + + #region Draw Corners /// - /// Draws a grid inside a rectangle, with the specified number of lines and line drawing information. + /// Draws the corners of the rectangle with independent lengths for each corner. /// - /// The rectangle in which to draw the grid. - /// The number of grid lines (both horizontal and vertical). - /// The line drawing information (thickness, color, etc.). - public static void DrawGrid(this Rect r, int lines, LineDrawingInfo lineInfo) + /// The rectangle to draw corners for. + /// The thickness of the corner lines. + /// The color of the corner lines. + /// The length of the top-left corner. + /// The length of the top-right corner. + /// The length of the bottom-right corner. + /// The length of the bottom-left corner. + public static void DrawCorners(this Rect rect, float lineThickness, ColorRgba color, float tlCorner, float trCorner, float brCorner, float blCorner) { - var xOffset = new Vector2(r.Width / lines, 0f);// * i; - var yOffset = new Vector2(0f, r.Height / lines);// * i; - - var tl = r.TopLeft; - var tr = tl + new Vector2(r.Width, 0); - var bl = tl + new Vector2(0, r.Height); - - for (var i = 0; i < lines; i++) - { - SegmentDrawing.DrawSegment(tl + xOffset * i, bl + xOffset * i, lineInfo); - SegmentDrawing.DrawSegment(tl + yOffset * i, tr + yOffset * i, lineInfo); - } + var quad = rect.ToQuad(); + quad.DrawCorners(lineThickness, color, tlCorner, trCorner, brCorner, blCorner); } - + /// - /// Draws a filled rectangle using the specified top-left and bottom-right coordinates and color. + /// Draws all corners of the rectangle with the same length. /// - /// The top-left corner of the rectangle. - /// The bottom-right corner of the rectangle. - /// The fill color. - public static void DrawRect(Vector2 topLeft, Vector2 bottomRight, ColorRgba color) + /// The rectangle to draw corners for. + /// The thickness of the corner lines. + /// The color of the corner lines. + /// The length of the corner segments. + public static void DrawCorners(this Rect rect, float lineThickness, ColorRgba color, float cornerLength) { - Raylib.DrawRectangleV(topLeft, bottomRight - topLeft, color.ToRayColor()); + rect.DrawCorners(lineThickness, color, cornerLength, cornerLength, cornerLength, cornerLength); } - + + #endregion + + #region Draw Corners Relative /// - /// Draws a filled, rotated rectangle using the specified corners, pivot, rotation, and color. + /// Draws the corners of the rectangle with independent lengths relative to the rectangle's minimum dimension. /// - /// The top-left corner of the rectangle. - /// The bottom-right corner of the rectangle. - /// The pivot point for rotation. - /// The rotation in degrees. - /// The fill color. - public static void DrawRect(Vector2 topLeft, Vector2 bottomRight, Vector2 pivot, float rotDeg, ColorRgba color) + /// The rectangle to draw corners for. + /// The thickness of the corner lines. + /// The color of the corner lines. + /// Factor (0-1) for the top-left corner length relative to the rectangle's minimum size. + /// Factor (0-1) for the top-right corner length relative to the rectangle's minimum size. + /// Factor (0-1) for the bottom-right corner length relative to the rectangle's minimum size. + /// Factor (0-1) for the bottom-left corner length relative to the rectangle's minimum size. + public static void DrawCornersRelative(this Rect rect, float lineThickness, ColorRgba color, float tlCornerFactor, float trCornerFactor, float brCornerFactor, float blCornerFactor) { - var a = pivot + (topLeft - pivot).RotateDeg(rotDeg); - var b = pivot + (new Vector2(topLeft.X, bottomRight.Y) - pivot).RotateDeg(rotDeg); - var c = pivot + (bottomRight - pivot).RotateDeg(rotDeg); - var d = pivot + (new Vector2(bottomRight.X, topLeft.Y) - pivot).RotateDeg(rotDeg); - QuadDrawing.DrawQuad(a, b, c, d, color); + float minSize = MathF.Min(rect.Width, rect.Height); + rect.DrawCorners(lineThickness, color, tlCornerFactor * minSize, trCornerFactor * minSize, brCornerFactor * minSize, blCornerFactor * minSize); } - + /// - /// Draws the outline of a rectangle using the specified corners, line thickness, and color. + /// Draws all corners of the rectangle with the same length relative to the rectangle's minimum dimension. /// - /// The top-left corner of the rectangle. - /// The bottom-right corner of the rectangle. - /// The thickness of the outline. - /// The color of the outline. - public static void DrawRectLines(Vector2 topLeft, Vector2 bottomRight, float lineThickness, ColorRgba color) => DrawLines(new Rect(topLeft, bottomRight), lineThickness, color); + /// The rectangle to draw corners for. + /// The thickness of the corner lines. + /// The color of the corner lines. + /// Factor (0-1) for the corner length relative to the rectangle's minimum size. + public static void DrawCornersRelative(this Rect rect, float lineThickness, ColorRgba color, float cornerLengthFactor) + { + rect.DrawCornersRelative(lineThickness, color, cornerLengthFactor, cornerLengthFactor, cornerLengthFactor, cornerLengthFactor); + } + #endregion + + #region Draw Chamfered Corners + /// - /// Draws the outline of a rectangle with each side scaled by a factor, - /// using the specified line thickness, color, and cap style. + /// Draws a filled rectangle with equally chamfered corners. /// - /// The top-left corner of the rectangle. - /// The bottom-right corner of the rectangle. - /// The thickness of the outline. - /// The color of the outline. - /// The factor by which to scale each side (0 to 1). - /// The type of line cap to use. - /// The number of points for the cap. + /// The rectangle to draw. + /// The fill color. + /// The chamfer length applied to all corners. /// - /// Useful for drawing partial outlines or stylized rectangles. + /// The rectangle is converted to a and drawn using quad chamfer rendering. /// - public static void DrawRectLines(Vector2 topLeft, Vector2 bottomRight, float lineThickness, ColorRgba color, float sideLengthFactor, - LineCapType capType = LineCapType.Extended, int capPoints = 0) + public static void DrawChamferedCorners(this Rect rect, ColorRgba color, float cornerLength) { - var a = topLeft; - var b = new Vector2(topLeft.X, bottomRight.Y); - var c = bottomRight; - var d = new Vector2(bottomRight.X, topLeft.Y); - - var side1 = b - a; - var end1 = a + side1 * sideLengthFactor; - - var side2 = c - b; - var end2 = b + side2 * sideLengthFactor; - - var side3 = d - c; - var end3 = c + side3 * sideLengthFactor; - - var side4 = a - d; - var end4 = d + side4 * sideLengthFactor; - - SegmentDrawing.DrawSegment(a, end1, lineThickness, color, capType, capPoints); - SegmentDrawing.DrawSegment(b, end2, lineThickness, color, capType, capPoints); - SegmentDrawing.DrawSegment(c, end3, lineThickness, color, capType, capPoints); - SegmentDrawing.DrawSegment(d, end4, lineThickness, color, capType, capPoints); + var q = rect.ToQuad(); + q.DrawChamferedCorners(color, cornerLength); } - + /// - /// Draws the outline of a rectangle using the specified line drawing information. + /// Draws a filled rectangle with chamfered corners using separate horizontal and vertical chamfer lengths. /// - /// The top-left corner of the rectangle. - /// The bottom-right corner of the rectangle. - /// The line drawing information (thickness, color, etc.). - public static void DrawRectLines(Vector2 topLeft, Vector2 bottomRight, LineDrawingInfo lineInfo) + /// The rectangle to draw. + /// The fill color. + /// The chamfer length measured along horizontal edges. + /// The chamfer length measured along vertical edges. + /// + /// The rectangle is converted to a and drawn using quad chamfer rendering. + /// + public static void DrawChamferedCorners(this Rect rect, ColorRgba color, float cornerLengthHorizontal, float cornerLengthVertical) { - var a = topLeft; - var b = new Vector2(topLeft.X, bottomRight.Y); - var c = bottomRight; - var d = new Vector2(bottomRight.X, topLeft.Y); - - SegmentDrawing.DrawSegment(a, b, lineInfo); - SegmentDrawing.DrawSegment(b, c, lineInfo); - SegmentDrawing.DrawSegment(c, d, lineInfo); - SegmentDrawing.DrawSegment(d, a, lineInfo); - } - - /// - /// Draws the outline of a rotated rectangle using the specified corners, - /// pivot, rotation, line thickness, color, and cap style. - /// - /// The top-left corner of the rectangle. - /// The bottom-right corner of the rectangle. - /// The pivot point for rotation. - /// The rotation in degrees. - /// The thickness of the outline. - /// The color of the outline. - /// The type of line cap to use. - /// The number of points for the cap. - public static void DrawRectLines(Vector2 topLeft, Vector2 bottomRight, Vector2 pivot, float rotDeg, float lineThickness, ColorRgba color, LineCapType capType = LineCapType.Extended, int capPoints = 0) - { - var a = pivot + (topLeft - pivot).RotateDeg(rotDeg); - var b = pivot + (new Vector2(topLeft.X, bottomRight.Y) - pivot).RotateDeg(rotDeg); - var c = pivot + (bottomRight - pivot).RotateDeg(rotDeg); - var d = pivot + (new Vector2(bottomRight.X, topLeft.Y) - pivot).RotateDeg(rotDeg); - QuadDrawing.DrawQuadLines(a, b, c, d, lineThickness, color, capType, capPoints); + var q = rect.ToQuad(); + q.DrawChamferedCorners(color, cornerLengthHorizontal, cornerLengthVertical); } - - /// - /// Draws the outline of a rotated rectangle using the specified line drawing information. - /// - /// The top-left corner of the rectangle. - /// The bottom-right corner of the rectangle. - /// The pivot point for rotation. - /// The rotation in degrees. - /// The line drawing information (thickness, color, etc.). - public static void DrawRectLines(Vector2 topLeft, Vector2 bottomRight, Vector2 pivot, float rotDeg, LineDrawingInfo lineInfo) - => DrawRectLines(topLeft, bottomRight, pivot, rotDeg, lineInfo.Thickness, lineInfo.Color, lineInfo.CapType, lineInfo.CapPoints); - + /// - /// Draws a filled rectangle using the specified color. + /// Draws a filled rectangle with independent chamfer lengths for each corner. /// /// The rectangle to draw. /// The fill color. - public static void Draw(this Rect rect, ColorRgba color) => Raylib.DrawRectangleRec(rect.Rectangle, color.ToRayColor()); - + /// The chamfer length for the top-left corner. + /// The chamfer length for the bottom-left corner. + /// The chamfer length for the bottom-right corner. + /// The chamfer length for the top-right corner. + /// + /// The rectangle is converted to a and drawn using quad chamfer rendering. + /// + public static void DrawChamferedCorners(this Rect rect, ColorRgba color, float tlCorner, float blCorner, float brCorner, float trCorner) + { + var q = rect.ToQuad(); + q.DrawChamferedCorners(color, tlCorner, blCorner, brCorner, trCorner); + } + #endregion + + #region Draw Chamfered Corners Relative /// - /// Draws a filled, rotated rectangle using the specified pivot, rotation, and color. + /// Draws a filled rectangle with equally chamfered corners using a relative factor. /// /// The rectangle to draw. - /// The pivot point for rotation. - /// The rotation in degrees. /// The fill color. - public static void Draw(this Rect rect, Vector2 pivot, float rotDeg, ColorRgba color) => DrawRect(rect.TopLeft, rect.BottomRight, pivot, rotDeg, color); - + /// The normalized chamfer factor applied to all corners. + /// + /// The factor is interpreted by the underlying implementation relative to the rectangle size. + /// + public static void DrawChamferedCornersRelative(this Rect rect, ColorRgba color, float cornerLengthFactor) + { + var q = rect.ToQuad(); + q.DrawChamferedCornersRelative(color, cornerLengthFactor); + } + /// - /// Draws the outline of a rectangle using the specified line thickness and color. + /// Draws a filled rectangle with chamfered corners using separate relative horizontal and vertical factors. /// /// The rectangle to draw. - /// The thickness of the outline. - /// The color of the outline. - public static void DrawLines(this Rect rect, float lineThickness, ColorRgba color) => Raylib.DrawRectangleLinesEx(rect.Rectangle, lineThickness * 2, color.ToRayColor()); - + /// The fill color. + /// The normalized chamfer factor relative to the rectangle width. + /// The normalized chamfer factor relative to the rectangle height. + /// + /// The factors are interpreted by the underlying implementation relative to the rectangle size. + /// + public static void DrawChamferedCornersRelative(this Rect rect, ColorRgba color, float cornerLengthFactorHorizontal, float cornerLengthFactorVertical) + { + var q = rect.ToQuad(); + q.DrawChamferedCornersRelative(color, cornerLengthFactorHorizontal, cornerLengthFactorVertical); + } + /// - /// Draws the outline of a rectangle using the specified line drawing information. + /// Draws a filled rectangle with independent relative chamfer factors for each corner. /// /// The rectangle to draw. - /// The line drawing information (thickness, color, etc.). - public static void DrawLines(this Rect rect, LineDrawingInfo lineInfo) + /// The fill color. + /// The normalized chamfer factor for the top-left corner. + /// The normalized chamfer factor for the bottom-left corner. + /// The normalized chamfer factor for the bottom-right corner. + /// The normalized chamfer factor for the top-right corner. + /// + /// The factors are interpreted by the underlying implementation relative to the rectangle size. + /// + public static void DrawChamferedCornersRelative(this Rect rect, ColorRgba color,float tlCornerFactor, float blCornerFactor, float brCornerFactor, float trCornerFactor) { - SegmentDrawing.DrawSegment(rect.TopLeft, rect.BottomLeft, lineInfo); - SegmentDrawing.DrawSegment(rect.BottomLeft, rect.BottomRight, lineInfo); - SegmentDrawing.DrawSegment(rect.BottomRight, rect.TopRight, lineInfo); - SegmentDrawing.DrawSegment(rect.TopRight, rect.TopLeft, lineInfo); + var q = rect.ToQuad(); + q.DrawChamferedCornersRelative(color, tlCornerFactor, blCornerFactor, brCornerFactor, trCornerFactor); } - + #endregion + + #region Draw Chamfered Corners Lines + /// - /// Draws the outline of a rotated rectangle using the specified pivot, - /// rotation, line thickness, color, and cap style. + /// Draws a rectangle outline with equally chamfered corners. /// - /// The rectangle to draw. - /// The pivot point for rotation. - /// The rotation in degrees. - /// The thickness of the outline. - /// The color of the outline. - /// The type of line cap to use. - /// The number of points for the cap. - public static void DrawLines(this Rect rect, Vector2 pivot, float rotDeg, float lineThickness, ColorRgba color, LineCapType capType = LineCapType.Extended, int capPoints = 0) + /// The rectangle whose outline is drawn. + /// The outline thickness. + /// The outline color. + /// The chamfer length applied to all corners. + /// + /// The rectangle is converted to a and drawn using quad chamfer outline rendering. + /// + public static void DrawChamferedCornersLines(this Rect rect, float lineThickness, ColorRgba color, float cornerLength) { - DrawRectLines(rect.TopLeft, rect.BottomRight, pivot, rotDeg, lineThickness, color, capType, capPoints); + var q = rect.ToQuad(); + q.DrawChamferedCornersLines(lineThickness, color, cornerLength); } - + /// - /// Draws the outline of a rectangle with each side scaled by a factor, - /// using the specified line thickness, color, and cap style. + /// Draws a rectangle outline with chamfered corners using separate horizontal and vertical chamfer lengths. /// - /// The rectangle to draw. - /// The thickness of the outline. - /// The color of the outline. - /// The factor by which to scale each side (0 to 1). - /// The type of line cap to use. - /// The number of points for the cap. - public static void DrawLines(this Rect rect, float lineThickness, ColorRgba color, float sideLengthFactor, LineCapType capType = LineCapType.Extended, int capPoints = 0) + /// The rectangle whose outline is drawn. + /// The outline thickness. + /// The outline color. + /// The chamfer length measured along horizontal edges. + /// The chamfer length measured along vertical edges. + /// + /// The rectangle is converted to a and drawn using quad chamfer outline rendering. + /// + public static void DrawChamferedCornersLines(this Rect rect, float lineThickness, ColorRgba color, float cornerLengthHorizontal, float cornerLengthVertical) { - DrawRectLines(rect.TopLeft, rect.BottomRight, lineThickness, color, sideLengthFactor, capType, capPoints); + var q = rect.ToQuad(); + q.DrawChamferedCornersLines(lineThickness, color, cornerLengthHorizontal, cornerLengthVertical); } - + /// - /// Draws the outline of a rotated rectangle using the specified line drawing information. + /// Draws a rectangle outline with independent chamfer lengths for each corner. /// - /// The rectangle to draw. - /// The pivot point for rotation. - /// The rotation in degrees. - /// The line drawing information (thickness, color, etc.). - public static void DrawLines(this Rect rect, Vector2 pivot, float rotDeg, LineDrawingInfo lineInfo) + /// The rectangle whose outline is drawn. + /// The outline thickness. + /// The outline color. + /// The chamfer length for the top-left corner. + /// The chamfer length for the bottom-left corner. + /// The chamfer length for the bottom-right corner. + /// The chamfer length for the top-right corner. + /// + /// The rectangle is converted to a and drawn using quad chamfer outline rendering. + /// + public static void DrawChamferedCornersLines(this Rect rect, float lineThickness, ColorRgba color, float tlCorner, float blCorner, float brCorner, float trCorner) { - DrawRectLines(rect.TopLeft, rect.BottomRight, pivot, rotDeg, lineInfo); + var q = rect.ToQuad(); + q.DrawChamferedCornersLines(lineThickness, color, tlCorner, blCorner, brCorner, trCorner); } - + + #endregion + + #region Draw Chamfered Corners Relative Lines + /// - /// Draws a certain percentage of a rectangle's outline, starting at a specified corner and direction. - /// - /// The top-left corner of the rectangle. - /// The bottom-right corner of the rectangle. - /// - /// Specifies which portion of the rectangle's outline to draw, as well as the starting corner and direction. - /// - /// The integer part (0-3) selects the starting corner: - /// - /// Counter-Clockwise -> 0 = top-left, 1 = bottom-left, 2 = bottom-right, 3 = top-right. - /// Clockwise -> 0 = top-left, 1 = top-right, 2 = bottom-right, 3 = bottom-left. - /// - /// - /// The fractional part (0.0-1.0) determines the percentage of the outline to draw, relative to the full perimeter. - /// A negative value reverses the drawing direction (clockwise instead of counter-clockwise). - /// - /// Examples: - /// - /// 0.35 - Start at top-left, draw 35% of the outline counter-clockwise. - /// -2.7 - Start at bottom-right, draw 70% of the outline clockwise. - /// - /// - /// The thickness of the outline. - /// The color of the outline. - /// The type of line cap to use. - /// The number of points for the cap. + /// Draws a rectangle outline with equally chamfered corners using a relative factor. + /// + /// The rectangle whose outline is drawn. + /// The outline thickness. + /// The outline color. + /// The normalized chamfer factor applied to all corners. /// - /// Useful for progress indicators or animated outlines. + /// The factor is interpreted by the underlying implementation relative to the rectangle size. /// - public static void DrawRectLinesPercentage(Vector2 topLeft, Vector2 bottomRight, float f, float lineThickness, ColorRgba color, LineCapType capType = LineCapType.CappedExtended, int capPoints = 2) + public static void DrawChamferedCornersLinesRelative(this Rect rect, float lineThickness, ColorRgba color, float cornerLengthFactor) { - if (f == 0) return; - var r = new Rect(topLeft, bottomRight); - if (r.Width <= 0 || r.Height <= 0) return; - - bool negative = false; - if (f < 0) - { - negative = true; - f *= -1; - } - - int startCorner = (int)f; - float percentage = f - startCorner; - if (percentage <= 0) return; - - startCorner = ShapeMath.Clamp(startCorner, 0, 3); - - var perimeter = r.Width * 2 + r.Height * 2; - var perimeterToDraw = perimeter * percentage; + var q = rect.ToQuad(); + q.DrawChamferedCornersLinesRelative(lineThickness, color, cornerLengthFactor); + } + + /// + /// Draws a rectangle outline with chamfered corners using separate relative horizontal and vertical factors. + /// + /// The rectangle whose outline is drawn. + /// The outline thickness. + /// The outline color. + /// The normalized chamfer factor relative to the rectangle width. + /// The normalized chamfer factor relative to the rectangle height. + /// + /// The factors are interpreted by the underlying implementation relative to the rectangle size. + /// + public static void DrawChamferedCornersLinesRelative(this Rect rect, float lineThickness, ColorRgba color, float cornerLengthFactorHorizontal, float cornerLengthFactorVertical) + { + var q = rect.ToQuad(); + q.DrawChamferedCornersLinesRelative(lineThickness, color, cornerLengthFactorHorizontal, cornerLengthFactorVertical); + } + + /// + /// Draws a rectangle outline with independent relative chamfer factors for each corner. + /// + /// The rectangle whose outline is drawn. + /// The outline thickness. + /// The outline color. + /// The normalized chamfer factor for the top-left corner. + /// The normalized chamfer factor for the bottom-left corner. + /// The normalized chamfer factor for the bottom-right corner. + /// The normalized chamfer factor for the top-right corner. + /// + /// The factors are interpreted by the underlying implementation relative to the rectangle size. + /// + public static void DrawChamferedCornersLinesRelative(this Rect rect, float lineThickness, ColorRgba color, float tlCornerFactor, float blCornerFactor, float brCornerFactor, float trCornerFactor) + { + var q = rect.ToQuad(); + q.DrawChamferedCornersLinesRelative(lineThickness, color, tlCornerFactor, blCornerFactor, brCornerFactor, trCornerFactor); + } + #endregion + + #region Draw Grid - if (startCorner == 0) - { - if (negative) - { - DrawRectLinesPercentageHelper(r.TopLeft, r.TopRight, r.BottomRight, r.BottomLeft, perimeterToDraw, r.Width, r.Height, lineThickness, color, capType, capPoints); - } - else - { - DrawRectLinesPercentageHelper(r.TopLeft, r.BottomLeft, r.BottomRight, r.TopRight, perimeterToDraw, r.Height, r.Width, lineThickness, color, capType, capPoints); - } - } - else if (startCorner == 1) - { - if (negative) - { - DrawRectLinesPercentageHelper(r.TopRight, r.BottomRight, r.BottomLeft, r.TopLeft, perimeterToDraw, r.Height, r.Width, lineThickness, color, capType, capPoints); - } - else - { - DrawRectLinesPercentageHelper(r.BottomLeft, r.BottomRight, r.TopRight, r.TopLeft, perimeterToDraw, r.Width, r.Height, lineThickness, color, capType, capPoints); - } - } - else if (startCorner == 2) + /// + /// Draws a grid within the specified bounds using the given line thickness and color. + /// + /// The grid definition specifying the number of rows and columns. + /// The rectangle bounds in which to draw the grid. + /// The thickness of the grid lines. + /// The color of the grid lines. + /// + /// The grid is drawn using horizontal and vertical lines spaced according to the number of rows and columns. + /// + public static void Draw(this Grid grid, Rect bounds, float lineThickness, ColorRgba color) + { + Vector2 rowSpacing = new(0f, bounds.Height / grid.Rows); + for (int row = 0; row < grid.Rows + 1; row++) { - if (negative) - { - DrawRectLinesPercentageHelper(r.BottomRight, r.BottomLeft, r.TopLeft, r.TopRight, perimeterToDraw, r.Width, r.Height, lineThickness, color, capType, capPoints); - } - else - { - DrawRectLinesPercentageHelper(r.BottomRight, r.TopRight, r.TopLeft, r.BottomLeft, perimeterToDraw, r.Height, r.Width, lineThickness, color, capType, capPoints); - } + SegmentDrawing.DrawSegment(bounds.TopLeft + rowSpacing * row, bounds.TopRight + rowSpacing * row, lineThickness, color); } - else if (startCorner == 3) + Vector2 colSpacing = new(bounds.Width / grid.Cols, 0f); + for (int col = 0; col < grid.Cols + 1; col++) { - if (negative) - { - DrawRectLinesPercentageHelper(r.BottomLeft, r.TopLeft, r.TopRight, r.BottomRight, perimeterToDraw, r.Height, r.Width, lineThickness, color, capType, capPoints); - } - else - { - DrawRectLinesPercentageHelper(r.TopRight, r.TopLeft, r.BottomLeft, r.BottomRight, perimeterToDraw, r.Width, r.Height, lineThickness, color, capType, capPoints); - } + SegmentDrawing.DrawSegment(bounds.TopLeft + colSpacing * col, bounds.BottomLeft + colSpacing * col, lineThickness, color); } - } /// - /// Draws a certain percentage of a rotated rectangle's outline, starting at a specified corner and direction. - /// - /// The top-left corner of the rectangle. - /// The bottom-right corner of the rectangle. - /// See for details. - /// The pivot point for rotation. - /// The rotation in degrees. - /// The thickness of the outline. - /// The color of the outline. - /// The type of line cap to use. - /// The number of points for the cap. - public static void DrawRectLinesPercentage(Vector2 topLeft, Vector2 bottomRight, float f, Vector2 pivot, float rotDeg, float lineThickness, ColorRgba color, LineCapType capType = LineCapType.Extended, int capPoints = 0) - { - var a = pivot + (topLeft - pivot).RotateDeg(rotDeg); - var b = pivot + (new Vector2(topLeft.X, bottomRight.Y) - pivot).RotateDeg(rotDeg); - var c = pivot + (bottomRight - pivot).RotateDeg(rotDeg); - var d = pivot + (new Vector2(bottomRight.X, topLeft.Y) - pivot).RotateDeg(rotDeg); + /// Draws a grid inside a rectangle, with the specified number of lines and line drawing information. + /// + /// The rectangle in which to draw the grid. + /// The number of grid lines (both horizontal and vertical). + /// The line drawing information (thickness, color, etc.). + public static void DrawGrid(this Rect r, int lines, LineDrawingInfo lineInfo) + { + var w = r.Width; + var h = r.Height; + var xOffset = new Vector2(w / lines, 0f); + var yOffset = new Vector2(0f, h / lines); - QuadDrawing.DrawQuadLinesPercentage(a, b, c, d, f, lineThickness, color, capType, capPoints); + var tl = r.TopLeft; + var tr = tl + new Vector2(w, 0); + var bl = tl + new Vector2(0, h); + + var thickness = lineInfo.Thickness; + var maxThicknessH = MathF.Min((w / lines) * 0.5f, thickness); + var maxThicknessV = MathF.Min((h / lines) * 0.5f, thickness); + var lineInfoH = lineInfo.SetThickness(maxThicknessH); + var lineInfoV = lineInfo.SetThickness(maxThicknessV); + + for (var i = 1; i < lines; i++) + { + SegmentDrawing.DrawSegment(tl + xOffset * i, bl + xOffset * i, lineInfoH); + SegmentDrawing.DrawSegment(tl + yOffset * i, tr + yOffset * i, lineInfoV); + } } + #endregion + + #region Draw Nine Patch Rect /// - /// Draws a certain percentage of a rotated rectangle's outline using the specified line drawing information. + /// Draws a using a single color for all patches. /// - /// The rectangle to draw. - /// See for details. - /// The pivot point for rotation. - /// The rotation in degrees. - /// The thickness of the outline. - /// The color of the outline. - /// The type of line cap to use. - /// The number of points for the cap. - public static void DrawLinesPercentage(this Rect rect, float f, Vector2 pivot, float rotDeg, float lineThickness, ColorRgba color, LineCapType capType = LineCapType.Extended, int capPoints = 0) + /// The nine-patch rectangle to draw. + /// The color to use for all patches. + public static void Draw(this NinePatchRect npr, ColorRgba color) { - DrawRectLinesPercentage(rect.TopLeft, rect.BottomRight, f, pivot, rotDeg, lineThickness, color, capType, capPoints); + var rects = npr.Rects; + foreach (var r in rects) + { + r.Draw(color); + } } /// - /// Draws a certain percentage of a rotated rectangle's outline using the specified line drawing information. + /// Draws a using separate colors for the source and patch rectangles. /// - /// The rectangle to draw. - /// See for details. - /// The pivot point for rotation. - /// The rotation in degrees. - /// The line drawing information (thickness, color, etc.). - public static void DrawLinesPercentage(this Rect rect, float f, Vector2 pivot, float rotDeg, LineDrawingInfo lineInfo) + /// The nine-patch rectangle to draw. + /// The color for the source rectangle. + /// The color for the patch rectangles. + public static void Draw(this NinePatchRect npr, ColorRgba sourceColorRgba, ColorRgba patchColorRgba) { - DrawRectLinesPercentage(rect.TopLeft, rect.BottomRight, f, pivot, rotDeg, lineInfo); + npr.Source.Draw(sourceColorRgba); + var rects = npr.Rects; + foreach (var r in rects) + { + r.Draw(patchColorRgba); + } } - + /// - /// Draws a certain percentage of a rotated rectangle's outline using the specified line drawing information. + /// Draws the outlines of a using the specified line thickness and color. /// - /// The top-left corner of the rectangle. - /// The bottom-right corner of the rectangle. - /// See for details. - /// The pivot point for rotation. - /// The rotation in degrees. - /// The line drawing information (thickness, color, etc.). - public static void DrawRectLinesPercentage(Vector2 topLeft, Vector2 bottomRight, float f, Vector2 pivot, float rotDeg, LineDrawingInfo lineInfo) - => DrawRectLinesPercentage(topLeft, bottomRight, f, pivot, rotDeg, lineInfo.Thickness, lineInfo.Color, lineInfo.CapType, lineInfo.CapPoints); + /// The nine-patch rectangle to draw. + /// The thickness of the outline lines. + /// The color of the outline lines. + public static void DrawLines(this NinePatchRect npr, float lineThickness, ColorRgba color) + { + var rects = npr.Rects; + foreach (var r in rects) + { + r.DrawLines(lineThickness, color); + } + } /// - /// Draws a rectangle outline where each side can be scaled towards the origin of the side. + /// Draws the outlines of a using separate line thickness and color for the source and patch rectangles. /// - /// The rectangle to draw. - /// The line drawing information (thickness, color, etc.). - /// The rotation in degrees. - /// The pivot point for rotation. - /// - /// The scale factor for each side. - /// - /// 0: No Rect is drawn. - /// 1: The normal Rect is drawn. - /// 0.5: Each side is half as long. - /// - /// - /// - /// The point along each side to scale from, in both directions (0 to 1). - /// - /// 0: Start of Side - /// 0.5: Center of Side - /// 1: End of Side - /// - /// - /// - /// Useful for creating stylized or animated rectangles. - /// - public static void DrawLinesScaled(this Rect r, LineDrawingInfo lineInfo, float rotDeg, Vector2 pivot, float sideScaleFactor, float sideScaleOrigin = 0.5f) + /// The nine-patch rectangle to draw. + /// The line thickness for the source rectangle. + /// The line thickness for the patch rectangles. + /// The color for the source rectangle outline. + /// The color for the patch rectangles outlines. + public static void DrawLines(this NinePatchRect npr, float sourceLineThickness, float patchLineThickness, ColorRgba sourceColorRgba, ColorRgba patchColorRgba) { - if (sideScaleFactor <= 0f) return; - if (sideScaleFactor >= 1f) - { - r.DrawLines(pivot, rotDeg, lineInfo); - return; - } - if (rotDeg == 0f) - { - SegmentDrawing.DrawSegment(r.TopLeft, r.BottomLeft, lineInfo, sideScaleFactor, sideScaleOrigin); - SegmentDrawing.DrawSegment(r.BottomLeft, r.BottomRight, lineInfo, sideScaleFactor, sideScaleOrigin); - SegmentDrawing.DrawSegment(r.BottomRight, r.TopRight, lineInfo, sideScaleFactor, sideScaleOrigin); - SegmentDrawing.DrawSegment(r.TopRight, r.TopLeft, lineInfo, sideScaleFactor, sideScaleOrigin); - } - else + npr.Source.DrawLines(sourceLineThickness, sourceColorRgba); + var rects = npr.Rects; + foreach (var r in rects) { - var corners = r.RotateCorners(pivot, rotDeg); - SegmentDrawing.DrawSegment(corners.tl, corners.bl, lineInfo, sideScaleFactor, sideScaleOrigin); - SegmentDrawing.DrawSegment(corners.bl, corners.br, lineInfo, sideScaleFactor, sideScaleOrigin); - SegmentDrawing.DrawSegment(corners.br, corners.tr, lineInfo, sideScaleFactor, sideScaleOrigin); - SegmentDrawing.DrawSegment(corners.tr, corners.tl, lineInfo, sideScaleFactor, sideScaleOrigin); + r.DrawLines(patchLineThickness, patchColorRgba); } } + #endregion + + #region Draw Vertices /// /// Draws circles at each vertex of the rectangle. /// /// The rectangle whose vertices to draw. /// The radius of each vertex circle. /// The color of the vertex circles. - /// The number of segments for each circle (default: 8). - public static void DrawVertices(this Rect rect, float vertexRadius, ColorRgba color, int circleSegments = 8) + /// + /// The smoothness value (0-1). This controls the visual quality of the circle by inversely interpolating the current . + /// A value of 0 uses the maximum side length (fewer sides, less smooth), while 1 uses the minimum side length (more sides, smoother). + /// The resulting side length determines the number of polygon sides used to approximate the circle. + /// + public static void DrawVertices(this Rect rect, float vertexRadius, ColorRgba color, float smoothness = 0.5f) { - CircleDrawing.DrawCircle(rect.TopLeft, vertexRadius, color, circleSegments); - CircleDrawing.DrawCircle(rect.TopRight, vertexRadius, color, circleSegments); - CircleDrawing.DrawCircle(rect.BottomLeft, vertexRadius, color, circleSegments); - CircleDrawing.DrawCircle(rect.BottomRight, vertexRadius, color, circleSegments); + var circle = new Circle(rect.TopLeft, vertexRadius); + circle.Draw(color, smoothness); + circle = circle.SetPosition(rect.TopRight); + circle.Draw(color, smoothness); + circle = circle.SetPosition(rect.BottomLeft); + circle.Draw(color, smoothness); + circle = circle.SetPosition(rect.BottomRight); + circle.Draw(color, smoothness); } - + #endregion + + #region Draw Masked + /// - /// Draws a filled rectangle with rounded corners. + /// Draws the rectangle's four side segments, but only where they intersect the given triangular mask. + /// Each side is drawn by forwarding the call to the segment-level masked draw method. /// - /// The rectangle to draw. - /// The roundness of the corners (0 to 1). - /// The number of segments to approximate the roundness. - /// The fill color. - public static void DrawRounded(this Rect rect, float roundness, int segments, ColorRgba color) => Raylib.DrawRectangleRounded(rect.Rectangle, roundness, segments, color.ToRayColor()); - + /// The rectangle whose sides will be drawn. + /// The triangular mask used to clip each side. + /// Line drawing parameters (thickness, color, cap style, etc.). + /// If true, draws the parts inside the mask instead of outside. + public static void DrawLinesMasked(this Rect rect, Triangle mask, LineDrawingInfo lineInfo, bool reversedMask = false) + { + rect.TopSegment.DrawMasked(mask, lineInfo, reversedMask); + rect.LeftSegment.DrawMasked(mask, lineInfo, reversedMask); + rect.BottomSegment.DrawMasked(mask, lineInfo, reversedMask); + rect.RightSegment.DrawMasked(mask, lineInfo, reversedMask); + } /// - /// Draws the outline of a rectangle with rounded corners. + /// Draws the rectangle's four side segments, but only where they intersect the given circular mask. + /// Each side is drawn by forwarding the call to the segment-level masked draw method. /// - /// The rectangle to draw. - /// The roundness of the corners (0 to 1). - /// The thickness of the outline. - /// The number of segments to approximate the roundness. - /// The color of the outline. - public static void DrawRoundedLines(this Rect rect, float roundness, float lineThickness, int segments, ColorRgba color) - => Raylib.DrawRectangleRoundedLinesEx(rect.Rectangle, roundness, segments, lineThickness * 2, color.ToRayColor()); - + /// The rectangle whose sides will be drawn. + /// The circular mask used to clip each side. + /// Line drawing parameters (thickness, color, cap style, etc.). + /// If true, draws the parts inside the mask instead of outside. + public static void DrawLinesMasked(this Rect rect, Circle mask, LineDrawingInfo lineInfo, bool reversedMask = false) + { + rect.TopSegment.DrawMasked(mask, lineInfo, reversedMask); + rect.LeftSegment.DrawMasked(mask, lineInfo, reversedMask); + rect.BottomSegment.DrawMasked(mask, lineInfo, reversedMask); + rect.RightSegment.DrawMasked(mask, lineInfo, reversedMask); + } /// - /// Draws a filled rectangle with slanted corners. + /// Draws the rectangle's four side segments, but only where they intersect the given rectangular mask. + /// Each side is drawn by forwarding the call to the segment-level masked draw method which performs + /// clipping against the provided mask. /// - /// The rectangle to draw. - /// The fill color. - /// The slant amount for the top-left corner. - /// The slant amount for the top-right corner. - /// The slant amount for the bottom-right corner. - /// The slant amount for the bottom-left corner. - /// - /// Uses absolute values for corner values from 0 to Min(width,height) of the rect. - /// Therefore, the corner values should be positive and not exceed the smallest dimension of the rect. - /// - public static void DrawSlantedCorners(this Rect rect, ColorRgba color, float tlCorner, float trCorner, float brCorner, float blCorner) + /// The rectangle whose sides will be drawn. + /// The rectangular mask used to clip each side. + /// Line drawing parameters (thickness, color, cap style, etc.). + /// If true, draws the parts inside the mask instead of outside. + public static void DrawLinesMasked(this Rect rect, Rect mask, LineDrawingInfo lineInfo, bool reversedMask = false) { - var points = rect.GetSlantedCornerPoints(tlCorner, trCorner, brCorner, blCorner); - points.DrawPolygonConvex(rect.Center, color); + rect.TopSegment.DrawMasked(mask, lineInfo, reversedMask); + rect.LeftSegment.DrawMasked(mask, lineInfo, reversedMask); + rect.BottomSegment.DrawMasked(mask, lineInfo, reversedMask); + rect.RightSegment.DrawMasked(mask, lineInfo, reversedMask); } - /// - /// Draws a filled, rotated rectangle with slanted corners. + /// Draws the rectangle's four side segments, but only where they intersect the given quadrilateral mask. + /// Each side is forwarded to the corresponding segment-level masked draw method which handles clipping against the provided mask. /// - /// The rectangle to draw. - /// The pivot point for rotation. - /// The rotation in degrees. - /// The fill color. - /// The slant amount for the top-left corner. - /// The slant amount for the top-right corner. - /// The slant amount for the bottom-right corner. - /// The slant amount for the bottom-left corner. - /// - /// Uses absolute values for corner values from 0 to Min(width,height) of the rect. - /// Therefore, the corner values should be positive and not exceed the smallest dimension of the rect. - /// - public static void DrawSlantedCorners(this Rect rect, Vector2 pivot, float rotDeg, ColorRgba color, float tlCorner, float trCorner, float brCorner, float blCorner) + /// The rectangle whose sides will be drawn. + /// The quadrilateral mask used to clip each side. + /// Line drawing parameters (thickness, color, cap style, etc.). + /// If true, draws the parts inside the mask instead of outside. + public static void DrawLinesMasked(this Rect rect, Quad mask, LineDrawingInfo lineInfo, bool reversedMask = false) { - var poly = rect.GetSlantedCornerPoints(tlCorner, trCorner, brCorner, blCorner); - poly.ChangeRotation(rotDeg * ShapeMath.DEGTORAD, pivot); - poly.DrawPolygonConvex(rect.Center, color); + rect.TopSegment.DrawMasked(mask, lineInfo, reversedMask); + rect.LeftSegment.DrawMasked(mask, lineInfo, reversedMask); + rect.BottomSegment.DrawMasked(mask, lineInfo, reversedMask); + rect.RightSegment.DrawMasked(mask, lineInfo, reversedMask); } - /// - /// Draws the outline of a rectangle with slanted corners. + /// Draws the rectangle's four side segments, but only where they intersect the given polygon mask. + /// Each side is drawn by forwarding the call to the segment-level masked draw method which performs + /// clipping against the provided mask. /// - /// The rectangle to draw. - /// The line drawing information (thickness, color, etc.). - /// The slant amount for the top-left corner. - /// The slant amount for the top-right corner. - /// The slant amount for the bottom-right corner. - /// The slant amount for the bottom-left corner. - /// - /// Uses absolute values for corner values from 0 to Min(width,height) of the rect. - /// Therefore, the corner values should be positive and not exceed the smallest dimension of the rect. - /// - public static void DrawSlantedCornersLines(this Rect rect, LineDrawingInfo lineInfo, float tlCorner, float trCorner, float brCorner, float blCorner) + /// The rectangle whose sides will be drawn. + /// The polygonal mask used to clip each side. + /// Line drawing parameters (thickness, color, cap style, etc.). + /// If true, draws the parts inside the mask instead of outside. + public static void DrawLinesMasked(this Rect rect, Polygon mask, LineDrawingInfo lineInfo, bool reversedMask = false) { - var points = rect.GetSlantedCornerPoints(tlCorner, trCorner, brCorner, blCorner); - points.DrawLines(lineInfo); + rect.TopSegment.DrawMasked(mask, lineInfo, reversedMask); + rect.LeftSegment.DrawMasked(mask, lineInfo, reversedMask); + rect.BottomSegment.DrawMasked(mask, lineInfo, reversedMask); + rect.RightSegment.DrawMasked(mask, lineInfo, reversedMask); } - /// - /// Draws the outline of a rotated rectangle with slanted corners. + /// Draws the rectangle's four side segments clipped against a generic closed-shape mask. /// - /// The rectangle to draw. - /// The pivot point for rotation. - /// The rotation in degrees. - /// The line drawing information (thickness, color, etc.). - /// The slant amount for the top-left corner. - /// The slant amount for the top-right corner. - /// The slant amount for the bottom-right corner. - /// The slant amount for the bottom-left corner. - /// - /// Uses absolute values for corner values from 0 to Min(width,height) of the rect. - /// Therefore, the corner values should be positive and not exceed the smallest dimension of the rect. - /// - public static void DrawSlantedCornersLines(this Rect rect, Vector2 pivot, float rotDeg, LineDrawingInfo lineInfo, float tlCorner, float trCorner, float brCorner, float blCorner) + /// + /// The mask type implementing (for example , , , etc.). + /// + /// The rectangle whose sides will be drawn. + /// The mask used to clip each side. + /// Line drawing parameters (thickness, color, cap style, etc.). + /// If true, draws the parts inside the mask instead of outside. + public static void DrawLinesMasked(this Rect rect, T mask, LineDrawingInfo lineInfo, bool reversedMask = false) where T : IClosedShapeTypeProvider { - var poly = rect.GetSlantedCornerPoints(tlCorner, trCorner, brCorner, blCorner); - poly.ChangeRotation(rotDeg * ShapeMath.DEGTORAD, pivot); - poly.DrawLines(lineInfo); + rect.TopSegment.DrawMasked(mask, lineInfo, reversedMask); + rect.LeftSegment.DrawMasked(mask, lineInfo, reversedMask); + rect.BottomSegment.DrawMasked(mask, lineInfo, reversedMask); + rect.RightSegment.DrawMasked(mask, lineInfo, reversedMask); } + #endregion + + #region Draw Vignette /// - /// Draws corner lines for a rectangle, with each corner having a specified length. + /// Draws a "vignette" effect inside the rect, creating a circular hole in the center. + /// The area between the inner circle and the rect's outer edges is filled with the specified color. + /// + /// The rect to draw the vignette within. + /// The radius of the inner circular hole. + /// The starting rotation angle of the inner circle in degrees. + /// The color of the filled area. + /// + /// Determines the smoothness of the inner circle (0.0 to 1.0). + /// Higher values result in more segments and a smoother circle. + /// + public static void DrawVignette(this Rect r, float circleRadius, float circleRotDeg, ColorRgba color, float circleSmoothness = 0.5f) + { + var q = r.ToQuad(); + q.DrawVignette(circleRadius, circleRotDeg, color, circleSmoothness); + } + #endregion + + #region Gapped + /// + /// Draws a gapped outline for a rectangle, creating a dashed or segmented effect along the rectangle's perimeter. /// /// The rectangle to draw. - /// The line drawing information (thickness, color, etc.). - /// The length of the top-left corner lines. - /// The length of the top-right corner lines. - /// The length of the bottom-right corner lines. - /// The length of the bottom-left corner lines. + /// + /// The total length of the rectangle's perimeter. + /// If zero or negative, the method calculates it automatically. + /// Providing a known length avoids redundant calculations and improves performance, especially for static segments. + /// + /// Parameters describing how to draw the outline. + /// Parameters describing the gap configuration. + /// + /// The perimeter of the rectangle if positive; otherwise, -1. + /// If the shape does not change, the valid length can be reused in subsequent frames to avoid recalculating. + /// /// - /// Uses absolute values for corner values from 0 to Min(width,height) of the rect. - /// Therefore, the corner values should be positive and not exceed the smallest dimension of the rect. + /// - If is 0 or is 0, the outline is drawn solid. + /// - If is 1 or greater, no outline is drawn. /// - public static void DrawCorners(this Rect rect, LineDrawingInfo lineInfo, float tlCorner, float trCorner, float brCorner, float blCorner) + public static float DrawGappedOutline(this Rect rect, float perimeter, LineDrawingInfo lineInfo, GappedOutlineDrawingInfo gapDrawingInfo) { - var tl = rect.TopLeft; - var tr = rect.TopRight; - var br = rect.BottomRight; - var bl = rect.BottomLeft; - - if (tlCorner > 0f) - { - SegmentDrawing.DrawSegment(tl, tl + new Vector2(MathF.Min(tlCorner, rect.Width), 0f), lineInfo); - SegmentDrawing.DrawSegment(tl, tl + new Vector2(0f, MathF.Min(tlCorner, rect.Height)), lineInfo); - } - if (trCorner > 0f) + if (gapDrawingInfo.Gaps <= 0 || gapDrawingInfo.GapPerimeterPercentage <= 0f) { - SegmentDrawing.DrawSegment(tr, tr - new Vector2(MathF.Min(trCorner, rect.Width), 0f), lineInfo); - SegmentDrawing.DrawSegment(tr, tr + new Vector2(0f, MathF.Min(trCorner, rect.Height)), lineInfo); + rect.DrawLines(lineInfo); + return perimeter > 0f ? perimeter : -1f; } - if (brCorner > 0f) + + if (gapDrawingInfo.GapPerimeterPercentage >= 1f) return perimeter > 0f ? perimeter : -1f; + + var nonGapPercentage = 1f - gapDrawingInfo.GapPerimeterPercentage; + + var gapPercentageRange = gapDrawingInfo.GapPerimeterPercentage / gapDrawingInfo.Gaps; + var nonGapPercentageRange = nonGapPercentage / gapDrawingInfo.Gaps; + + + var shapePoints = new[] {rect.A, rect.B, rect.C, rect.D}; + int sides = shapePoints.Length; + + if (perimeter <= 0f) { - SegmentDrawing.DrawSegment(br, br - new Vector2(MathF.Min(brCorner, rect.Width), 0f), lineInfo); - SegmentDrawing.DrawSegment(br, br - new Vector2(0f, MathF.Min(brCorner, rect.Height)), lineInfo); + perimeter = 0f; + for (int i = 0; i < sides; i++) + { + var curP = shapePoints[i]; + var nextP = shapePoints[(i + 1) % sides]; + perimeter += (nextP - curP).Length(); + } } - if (blCorner > 0f) + + var startDistance = perimeter * gapDrawingInfo.StartOffset; + var curDistance = 0f; + var nextDistance = startDistance; + + var curIndex = 0; + var curPoint = shapePoints[0]; + var nextPoint= shapePoints[1]; + var curW = nextPoint - curPoint; + var curDis = curW.Length(); + + var points = new List(3); + + int whileCounter = gapDrawingInfo.Gaps; + + while (whileCounter > 0) { - SegmentDrawing.DrawSegment(bl, bl + new Vector2(MathF.Min(blCorner, rect.Width), 0f), lineInfo); - SegmentDrawing.DrawSegment(bl, bl - new Vector2(0f, MathF.Min(blCorner, rect.Height)), lineInfo); + if (curDistance + curDis >= nextDistance) + { + var p = curPoint + (curW / curDis) * (nextDistance - curDistance); + + + if (points.Count == 0) + { + nextDistance += nonGapPercentageRange * perimeter; + points.Add(p); + + } + else + { + nextDistance += gapPercentageRange * perimeter; + points.Add(p); + if (points.Count == 2) + { + SegmentDrawing.DrawSegment(points[0], points[1], lineInfo); + } + else + { + for (var i = 0; i < points.Count - 1; i++) + { + var p1 = points[i]; + var p2 = points[(i + 1) % points.Count]; + SegmentDrawing.DrawSegment(p1, p2, lineInfo); + } + } + + points.Clear(); + whileCounter--; + } + + } + else + { + + if(points.Count > 0) points.Add(nextPoint); + + curDistance += curDis; + curIndex = (curIndex + 1) % sides; + curPoint = shapePoints[curIndex]; + nextPoint = shapePoints[(curIndex + 1) % sides]; + curW = nextPoint - curPoint; + curDis = curW.Length(); + } + } - } + return perimeter; + } + + #endregion + + #region UI /// - /// Draws corner lines for a rectangle, with all corners having the same length. + /// Draws a progress-style outline along the rectangle perimeter. /// - /// The rectangle to draw. - /// The line drawing information (thickness, color, etc.). - /// - /// The length of the corner lines for all corners. - /// Uses an absolute value from 0 to Min(width,height) of the rect. - /// Therefore, the corner value should be positive and not exceed the smallest dimension of the rect. - /// - public static void DrawCorners(this Rect rect, LineDrawingInfo lineInfo, float cornerLength) - => DrawCorners(rect, lineInfo, cornerLength, cornerLength, cornerLength, cornerLength); + /// The rectangle whose outline is drawn. + /// The fraction of the perimeter to draw. + /// The starting edge index for the outline progress. + /// The outline thickness. + /// The outline color. + /// + /// This is a convenience wrapper around . + /// + public static void DrawOutlineBar(this Rect rect, float f, int startIndex, float lineThickness, ColorRgba color) + { + rect.DrawLinesPercentage(f, startIndex, lineThickness, color); + } /// - /// Draws corner lines for a rectangle, with each corner having a specified relative length (0 to 1). + /// Draws a rotated progress-style outline along the rectangle perimeter. /// - /// The rectangle to draw. - /// The line drawing information (thickness, color, etc.). - /// The relative length of the top-left corner lines (0-1). - /// The relative length of the top-right corner lines (0-1). - /// The relative length of the bottom-right corner lines (0-1). - /// The relative length of the bottom-left corner lines (0-1). - public static void DrawCornersRelative(this Rect rect, LineDrawingInfo lineInfo, float tlCorner, float trCorner, float brCorner, float blCorner) - { - var tl = rect.TopLeft; - var tr = rect.TopRight; - var br = rect.BottomRight; - var bl = rect.BottomLeft; - - if (tlCorner > 0f && tlCorner < 1f) - { - SegmentDrawing.DrawSegment(tl, tl + new Vector2(tlCorner * rect.Width, 0f), lineInfo); - SegmentDrawing.DrawSegment(tl, tl + new Vector2(0f, tlCorner * rect.Height), lineInfo); - } - if (trCorner > 0f && trCorner < 1f) - { - SegmentDrawing.DrawSegment(tr, tr - new Vector2(tlCorner * rect.Width, 0f), lineInfo); - SegmentDrawing.DrawSegment(tr, tr + new Vector2(0f, tlCorner * rect.Height), lineInfo); - } - if (brCorner > 0f && brCorner < 1f) - { - SegmentDrawing.DrawSegment(br, br - new Vector2(tlCorner * rect.Width, 0f), lineInfo); - SegmentDrawing.DrawSegment(br, br - new Vector2(0f, tlCorner * rect.Height), lineInfo); - } - if (blCorner > 0f && blCorner < 1f) - { - SegmentDrawing.DrawSegment(bl, bl + new Vector2(tlCorner * rect.Width, 0f), lineInfo); - SegmentDrawing.DrawSegment(bl, bl - new Vector2(0f, tlCorner * rect.Height), lineInfo); - } + /// The rectangle whose outline is drawn. + /// The fraction of the perimeter to draw. + /// The starting edge index for the outline progress. + /// The rotation angle in degrees. + /// The anchor point used as the rotation pivot. + /// The outline thickness. + /// The outline color. + /// + /// The rectangle is converted to a rotated before drawing the partial outline. + /// + public static void DrawOutlineBar(this Rect rect, float f, int startIndex, float angleDeg, AnchorPoint pivot, float lineThickness, ColorRgba color) + { + var q = new Quad(rect, angleDeg, pivot); + q.DrawLinesPercentage(f, startIndex, lineThickness, color); } - + /// - /// Draws corner lines for a rectangle, with all corners having the same relative length (0 to 1). + /// Draws a rotated progress-style outline along the rectangle perimeter. /// - /// The rectangle to draw. - /// The line drawing information (thickness, color, etc.). - /// The relative length of the corner lines for all corners (0 to 1). - public static void DrawCornersRelative(this Rect rect, LineDrawingInfo lineInfo, float cornerLengthFactor) - => DrawCornersRelative(rect, lineInfo, cornerLengthFactor, cornerLengthFactor, cornerLengthFactor, cornerLengthFactor); - - private static void DrawRectLinesPercentageHelper(Vector2 p1, Vector2 p2, Vector2 p3, Vector2 p4, float perimeterToDraw, float size1, float size2, float lineThickness, ColorRgba color, LineCapType capType = LineCapType.CappedExtended, int capPoints = 2) + /// The rectangle whose outline is drawn. + /// The fraction of the perimeter to draw. + /// The starting edge index for the outline progress. + /// The rotation angle in degrees. + /// The world-space point used as the rotation pivot. + /// The outline thickness. + /// The outline color. + /// + /// The rectangle is converted to a rotated before drawing the partial outline. + /// + public static void DrawOutlineBar(this Rect rect, float f, int startIndex, float angleDeg, Vector2 pivot, float lineThickness, ColorRgba color) { - // Draw first segment - var curP = p1; - var nextP = p2; - if (perimeterToDraw < size1) - { - float p = perimeterToDraw / size1; - nextP = curP.Lerp(nextP, p); - SegmentDrawing.DrawSegment(curP, nextP, lineThickness, color, capType, capPoints); - return; - } - - SegmentDrawing.DrawSegment(curP, nextP, lineThickness, color, capType, capPoints); - perimeterToDraw -= size1; - - // Draw second segment - curP = nextP; - nextP = p3; - if (perimeterToDraw < size2) - { - float p = perimeterToDraw / size2; - nextP = curP.Lerp(nextP, p); - SegmentDrawing.DrawSegment(curP, nextP, lineThickness, color, capType, capPoints); - return; - } - - SegmentDrawing.DrawSegment(curP, nextP, lineThickness, color, capType, capPoints); - perimeterToDraw -= size2; - - // Draw third segment - curP = nextP; - nextP = p4; - if (perimeterToDraw < size1) - { - float p = perimeterToDraw / size1; - nextP = curP.Lerp(nextP, p); - SegmentDrawing.DrawSegment(curP, nextP, lineThickness, color, capType, capPoints); - return; - } - - SegmentDrawing.DrawSegment(curP, nextP, lineThickness, color, capType, capPoints); - perimeterToDraw -= size1; - - // Draw fourth segment - curP = nextP; - nextP = p1; - if (perimeterToDraw < size2) - { - float p = perimeterToDraw / size2; - nextP = curP.Lerp(nextP, p); - } - SegmentDrawing.DrawSegment(curP, nextP, lineThickness, color, capType, capPoints); + var q = new Quad(rect, angleDeg, pivot); + q.DrawLinesPercentage(f, startIndex, lineThickness, color); + } + + /// + /// Draws a filled bar inside a rectangle, representing progress with customizable margins and colors. + /// + /// The rectangle to draw the bar in. + /// The progress value (0 to 1) indicating how much of the bar to fill. + /// The color of the filled bar. + /// The background color of the rectangle. + /// The left margin left * (1 - f) to determine the fill behavior (default 0). + /// The right margin right * (1 - f) to determine the fill behavior (default 1). + /// The top margin top * (1 - f) to determine the fill behavior (default 0). + /// The bottom margin bottom * (1 - f) to determine the fill behavior (default 0). + /// + /// The bar is drawn inside the rectangle, with the filled area shrinking as the progress value increases. + /// The default margin values represent a bar that fills from the left to the right. + /// + /// + /// + /// left 1, right 0, top 0, bottom 0 -> bar fills from right to left. + /// left 0, right 0, top 1, bottom 0 -> bar fills from bottom to top. + /// left 0, right 0, top 0, bottom 1 -> bar fills from top to bottom. + /// left 0.5, right 0.5, top 0, bottom 0 -> bar fills from center to left and right edges. + /// + /// + public static void DrawBar(this Rect rect, float f, ColorRgba barColorRgba, ColorRgba bgColorRgba, float left = 0f, float right = 1f, float top = 0f, float bottom = 0f) + { + f = 1.0f - f; + Rect.Margins progressMargins = new(f * top, f * right, f * bottom, f * left); + var progressRect = rect.ApplyMargins(progressMargins); + rect.Draw(bgColorRgba); + progressRect.Draw(barColorRgba); } + /// + /// Draws a rotated filled bar inside a rectangle using an anchor-based pivot. + /// + /// The rectangle that defines the bar bounds. + /// The progress value that determines the filled portion. + /// The rotation angle in degrees. + /// The anchor point used as the rotation pivot. + /// The fill color of the progress bar. + /// The background color drawn behind the bar. + /// The normalized left margin used to shape the fill direction. + /// The normalized right margin used to shape the fill direction. + /// The normalized top margin used to shape the fill direction. + /// The normalized bottom margin used to shape the fill direction. + /// + /// Margins are applied before the bar is converted to rotated quads for drawing. + /// + public static void DrawBar(this Rect rect, float f, float angleDeg, AnchorPoint pivot, ColorRgba barColorRgba, ColorRgba bgColorRgba, float left = 0f, float right = 1f, float top = 0f, float bottom = 0f) + { + f = 1.0f - f; + Rect.Margins progressMargins = new(f * top, f * right, f * bottom, f * left); + var progressRect = rect.ApplyMargins(progressMargins); + var quad = new Quad(rect, angleDeg, pivot); + quad.Draw(bgColorRgba); + var progressQuad = new Quad(progressRect, angleDeg, pivot); + progressQuad.Draw(barColorRgba); + } + + /// + /// Draws a rotated filled bar inside a rectangle using a world-space pivot. + /// + /// The rectangle that defines the bar bounds. + /// The progress value that determines the filled portion. + /// The rotation angle in degrees. + /// The world-space point used as the rotation pivot. + /// The fill color of the progress bar. + /// The background color drawn behind the bar. + /// The normalized left margin used to shape the fill direction. + /// The normalized right margin used to shape the fill direction. + /// The normalized top margin used to shape the fill direction. + /// The normalized bottom margin used to shape the fill direction. + /// + /// Margins are applied before the bar is converted to rotated quads for drawing. + /// + public static void DrawBar(this Rect rect, float f, float angleDeg, Vector2 pivot, ColorRgba barColorRgba, ColorRgba bgColorRgba, float left = 0f, float right = 1f, float top = 0f, float bottom = 0f) + { + f = 1.0f - f; + Rect.Margins progressMargins = new(f * top, f * right, f * bottom, f * left); + var progressRect = rect.ApplyMargins(progressMargins); + var quad = new Quad(rect, angleDeg, pivot); + quad.Draw(bgColorRgba); + var progressQuad = new Quad(progressRect, angleDeg, pivot); + progressQuad.Draw(barColorRgba); + } + #endregion } - + + \ No newline at end of file diff --git a/ShapeEngine/Geometry/RectDef/RectMath.cs b/ShapeEngine/Geometry/RectDef/RectMath.cs index fb69605c..5857b519 100644 --- a/ShapeEngine/Geometry/RectDef/RectMath.cs +++ b/ShapeEngine/Geometry/RectDef/RectMath.cs @@ -11,29 +11,13 @@ public readonly partial struct Rect { #region Transform - /// - /// Scales the size of the rectangle by the specified horizontal and vertical amounts, keeping the center fixed. - /// - /// The amount to scale horizontally (positive to expand, negative to shrink). - /// The amount to scale vertically (positive to expand, negative to shrink). - /// A new rectangle with the scaled size. - public Rect ScaleSize(float horizontalAmount, float verticalAmount) - { - return new - ( - X - horizontalAmount, - Y - verticalAmount, - Width + horizontalAmount * 2f, - Height + verticalAmount * 2f - ); - } - /// /// Scales the size of the rectangle by a uniform factor, using the specified anchor point for alignment. /// /// The uniform scale factor. /// The anchor point for alignment. /// A new rectangle with the scaled size. + /// Use a negative scale value to mirror the rect. public Rect ScaleSize(float scale, AnchorPoint alignment) => new(GetPoint(alignment), Size * scale, alignment); /// @@ -42,6 +26,7 @@ public Rect ScaleSize(float horizontalAmount, float verticalAmount) /// The scale factor for width and height. /// The anchor point for alignment. /// A new rectangle with the scaled size. + /// Use negative scale values to mirror the rect. public Rect ScaleSize(Vector2 scale, AnchorPoint alignment) => new(GetPoint(alignment), Size * scale, alignment); /// @@ -49,6 +34,7 @@ public Rect ScaleSize(float horizontalAmount, float verticalAmount) /// /// The new size for the rectangle. /// A new rectangle with the specified size. + /// Use negative size values to mirror the rect. public Rect SetSize(Size newSize) => new(TopLeft, newSize); /// @@ -67,6 +53,23 @@ public Rect ScaleSize(float horizontalAmount, float verticalAmount) /// A new rectangle with the specified size and alignment. public Rect SetSize(float newSize, AnchorPoint alignment) => new(GetPoint(alignment), new Size(newSize), alignment); + /// + /// Changes the size of the rectangle by the specified horizontal and vertical amounts, keeping it centered. + /// + /// The amount to add to the width. + /// The amount to add to the height. + /// A new rectangle with the adjusted size. + public Rect ChangeSize(float horizontalAmount, float verticalAmount) + { + return new + ( + X - horizontalAmount * 0.5f, + Y - verticalAmount * 0.5f, + Width + horizontalAmount, + Height + verticalAmount + ); + } + /// /// Changes the size of the rectangle by a uniform amount, using the specified anchor point for alignment. /// @@ -156,6 +159,34 @@ public Rect SetTransform(Transform2D transform, AnchorPoint alignment) }; return points; } + + /// + /// Writes this rectangle's original corners and their projected counterparts into . + /// + /// The destination collection that will be cleared and populated with the original and projected corner points. + /// The vector used to offset the projected corner points. + /// true if is non-zero and was populated; otherwise, false. + /// + /// Points are written in this order: A, B, C, D, A + , B + , C + , D + . + /// + public bool GetProjectedShapePoints(Points result, Vector2 v) + { + if (v.LengthSquared() <= 0f) return false; + + result.Clear(); + result.EnsureCapacity(8); + + result.Add(A); + result.Add(B); + result.Add(C); + result.Add(D); + result.Add(A + v); + result.Add(B + v); + result.Add(C + v); + result.Add(D + v); + + return true; + } /// /// Projects the shape of the rectangle in the direction of the given vector, returning a convex hull. @@ -174,7 +205,37 @@ public Rect SetTransform(Transform2D transform, AnchorPoint alignment) C + v, D + v }; - return Polygon.FindConvexHull(points); + + var result = new Polygon(8); + points.FindConvexHull(result); + return result; + } + + /// + /// Projects this rectangle along the given vector and writes the convex hull of the combined corner set into . + /// + /// The destination polygon that receives the convex hull of the original and projected rectangle corners. + /// The vector used to offset the projected corner points. + /// true if is non-zero and was populated; otherwise, false. + /// + /// This method constructs a temporary set containing the four rectangle corners and those same corners translated by , then computes the convex hull into . + /// + public bool ProjectShape(Polygon result, Vector2 v) + { + if (v.LengthSquared() <= 0f) return false; + + var points = new Points + { + A, B, C, D, + A + v, + B + v, + C + v, + D + v + }; + + points.FindConvexHull(result); + + return true; } /// @@ -436,5 +497,14 @@ public Rect Clamp(Vector2 min, Vector2 max) return Clamp(new Rect(min, max)); } + /// + /// Gets the length of the diagonal connecting corner A and corner C. + /// + public float GetDiagonalLengt() => (A - C).Length(); + + /// + /// Gets the squared length of the diagonal connecting corner A and corner C. + /// + public float GetDiagonalLengthSquare() => (A - C).LengthSquared(); #endregion } \ No newline at end of file diff --git a/ShapeEngine/Geometry/SegmentDef/SegmentDrawing.cs b/ShapeEngine/Geometry/SegmentDef/SegmentDrawing.cs index 2d3885dc..72c11c99 100644 --- a/ShapeEngine/Geometry/SegmentDef/SegmentDrawing.cs +++ b/ShapeEngine/Geometry/SegmentDef/SegmentDrawing.cs @@ -1,6 +1,8 @@ +using System.Drawing; using System.Numerics; using Raylib_cs; using ShapeEngine.Color; +using ShapeEngine.Core.Structs; using ShapeEngine.Geometry.CircleDef; using ShapeEngine.Geometry.CollisionSystem; using ShapeEngine.Geometry.PolygonDef; @@ -36,6 +38,259 @@ public static class SegmentDrawing /// private static readonly float MinSegmentDrawLengthSquared = MinSegmentDrawLength * MinSegmentDrawLength; + #region Draw Segment + /// + /// Draws a segment from to a point along the direction to , scaled by . + /// + /// The starting point of the segment. + /// The ending point of the segment. + /// The thickness of the segment. + /// The color of the segment. + /// The factor by which to scale the segment's length (0 = no length, 1 = full length). + /// The type of line cap to use at the ends of the segment. + /// The number of points used to draw the cap (for rounded or custom caps). + public static void DrawSegment(Vector2 start, Vector2 end, float thickness, ColorRgba color, float sideLengthFactor, LineCapType capType = LineCapType.None, int capPoints = 0) + { + var dir = end - start; + var newEnd = start + dir * sideLengthFactor; + DrawSegment(start, newEnd, thickness, color, capType, capPoints); + } + + /// + /// Draws a segment from to with the specified thickness, color, and cap style. + /// + /// The starting point of the segment. + /// The ending point of the segment. + /// The thickness of the segment. + /// The color of the segment. + /// The type of line cap to use at the ends of the segment. + /// The number of points used to draw the cap (for rounded or custom caps). + /// + /// If is less than , it will be clamped. + /// + public static void DrawSegment(Vector2 start, Vector2 end, float thickness, ColorRgba color, LineCapType capType = LineCapType.None, int capPoints = 0) + { + if (thickness < LineDrawingInfo.LineMinThickness) thickness = LineDrawingInfo.LineMinThickness; + var w = end - start; + float ls = w.X * w.X + w.Y * w.Y; // w.LengthSquared(); + if (ls <= MinSegmentDrawLengthSquared) return; + var l = MathF.Sqrt(ls); + var dir = w / l; + var pR = new Vector2(-dir.Y, dir.X);//perpendicular right + var pL = new Vector2(dir.Y, -dir.X);//perpendicular left + + if (capType == LineCapType.Extended) //expand outwards + { + start -= dir * thickness; + end += dir * thickness; + } + else if (capType == LineCapType.Capped)//shrink inwards so that the line with cap is the same length + { + var offset = MathF.Min(l * 0.5f, thickness); + start += dir * offset; + end -= dir * offset; + } + + var tl = start + pL * thickness; + var bl = start + pR * thickness; + var br = end + pR * thickness; + var tr = end + pL * thickness; + + Raylib.DrawTriangle(tl, bl, br, color.ToRayColor()); + Raylib.DrawTriangle(tl, br, tr, color.ToRayColor()); + + if (capType is LineCapType.None or LineCapType.Extended) return; + if (capPoints <= 0) return; + + //Draw Cap + if (capPoints == 1) + { + var capStart = start - dir * thickness; + var capEnd = end + dir * thickness; + + Raylib.DrawTriangle(tl, capStart, bl, color.ToRayColor()); + Raylib.DrawTriangle(tr, br, capEnd, color.ToRayColor()); + } + else + { + var curStart = tl; + var curEnd = br; + float angleStep = (180f / (capPoints + 1)) * ShapeMath.DEGTORAD; + + for (var i = 1; i <= capPoints; i++) + { + var pStart = start + pL.Rotate(- angleStep * i) * thickness; + Raylib.DrawTriangle(pStart, start, curStart, color.ToRayColor()); + curStart = pStart; + + var pEnd = end + pR.Rotate(- angleStep * i) * thickness; + Raylib.DrawTriangle(pEnd, end, curEnd, color.ToRayColor()); + curEnd = pEnd; + } + Raylib.DrawTriangle(curStart, bl, start, color.ToRayColor()); + Raylib.DrawTriangle(curEnd, tr, end, color.ToRayColor()); + + } + } + + /// + /// Draws a segment using float coordinates for start and end points. + /// + /// The X coordinate of the start point. + /// The Y coordinate of the start point. + /// The X coordinate of the end point. + /// The Y coordinate of the end point. + /// The thickness of the segment. + /// The color of the segment. + /// The type of line cap to use at the ends of the segment. + /// The number of points used to draw the cap (for rounded or custom caps). + public static void DrawSegment(float startX, float startY, float endX, float endY, float thickness, + ColorRgba color, LineCapType capType = LineCapType.None, int capPoints = 0) + => DrawSegment(new(startX, startY), new(endX, endY), thickness, color, capType, capPoints); + + /// + /// Draws a segment using the provided . + /// + /// The starting point of the segment. + /// The ending point of the segment. + /// The line drawing information (thickness, color, cap type, cap points). + public static void DrawSegment(Vector2 start, Vector2 end, LineDrawingInfo info) => DrawSegment(start, end, info.Thickness, info.Color, info.CapType, info.CapPoints); + /// + /// Draws a segment with scaling applied from a specified origin along the segment. + /// + /// The starting point of the segment. + /// The ending point of the segment. + /// The line drawing information (thickness, color, cap type, cap points). + /// The factor by which to scale the segment's length (0 = no length, 1 = full length). + /// The point along the segment (0 = start, 1 = end) to scale from. + public static void DrawSegment(Vector2 start, Vector2 end, LineDrawingInfo info, float scaleFactor, float scaleOrigin = 0.5f) + { + var p = start.Lerp(end, scaleOrigin); + var s = start - p; + var e = end - p; + + var newStart = p + s * scaleFactor; + var newEnd = p + e * scaleFactor; + DrawSegment(newStart, newEnd, info); + } + + /// + /// Draws a segment using float coordinates for start and end points and the provided . + /// + /// The X coordinate of the start point. + /// The Y coordinate of the start point. + /// The X coordinate of the end point. + /// The Y coordinate of the end point. + /// The line drawing information (thickness, color, cap type, cap points). + public static void DrawSegment(float startX, float startY, float endX, float endY, LineDrawingInfo info) + => DrawSegment(new(startX, startY), new(endX, endY), info.Thickness, info.Color, info.CapType, info.CapPoints); + + /// + /// Draws a segment from to with the specified thickness, color, and different cap styles for start and end. + /// + /// The starting point of the segment. + /// The ending point of the segment. + /// The thickness of the segment. + /// The color of the segment. + /// The type of line cap to use at the start of the segment. + /// The number of points used to draw the start cap (for rounded or custom caps). + /// The type of line cap to use at the end of the segment. + /// The number of points used to draw the end cap (for rounded or custom caps). + /// + /// If is less than , it will be clamped. + /// + public static void DrawSegmentSeparateCaps(Vector2 start, Vector2 end, float thickness, ColorRgba color, LineCapType startCapType = LineCapType.None, int startCapPoints = 0, LineCapType endCapType = LineCapType.None, int endCapPoints = 0) + { + if (thickness < LineDrawingInfo.LineMinThickness) thickness = LineDrawingInfo.LineMinThickness; + var w = end - start; + float ls = w.X * w.X + w.Y * w.Y; // w.LengthSquared(); + if (ls <= MinSegmentDrawLengthSquared) return; + + var dir = w / MathF.Sqrt(ls); + var pR = new Vector2(-dir.Y, dir.X);//perpendicular right + var pL = new Vector2(dir.Y, -dir.X);//perpendicular left + + if (startCapType == LineCapType.Extended) //expand outwards + { + start -= dir * thickness; + } + else if (startCapType == LineCapType.Capped) //shrink inwards so that the line with cap is the same length + { + start += dir * thickness; + } + + if (endCapType == LineCapType.Extended) //expand outwards + { + end += dir * thickness; + } + else if (endCapType == LineCapType.Capped) //shrink inwards so that the line with cap is the same length + { + end -= dir * thickness; + } + + var tl = start + pL * thickness; + var bl = start + pR * thickness; + var br = end + pR * thickness; + var tr = end + pL * thickness; + + Raylib.DrawTriangle(tl, bl, br, color.ToRayColor()); + Raylib.DrawTriangle(tl, br, tr, color.ToRayColor()); + + if ((startCapType is LineCapType.None or LineCapType.Extended && endCapType is LineCapType.None or LineCapType.Extended) || + (startCapPoints <= 0 && endCapPoints <= 0)) return; + + //Draw Start Cap + if (startCapType is LineCapType.Capped or LineCapType.CappedExtended && startCapPoints > 0) + { + if (startCapPoints == 1) + { + var capStart = start - dir * thickness; + Raylib.DrawTriangle(tl, capStart, bl, color.ToRayColor()); + } + else + { + var curStart = tl; + float angleStep = (180f / (startCapPoints + 1)) * ShapeMath.DEGTORAD; + + for (var i = 1; i <= startCapPoints; i++) + { + var pStart = start + pL.Rotate(- angleStep * i) * thickness; + Raylib.DrawTriangle(pStart, start, curStart, color.ToRayColor()); + curStart = pStart; + } + Raylib.DrawTriangle(curStart, bl, start, color.ToRayColor()); + + } + } + + + //Draw End Cap + if (endCapType is LineCapType.Capped or LineCapType.CappedExtended && endCapPoints > 0) + { + if (endCapPoints == 1) + { + var capEnd = end + dir * thickness; + Raylib.DrawTriangle(tr, br, capEnd, color.ToRayColor()); + } + else + { + var curEnd = br; + float angleStep = (180f / (endCapPoints + 1)) * ShapeMath.DEGTORAD; + + for (var i = 1; i <= endCapPoints; i++) + { + var pEnd = end + pR.Rotate(- angleStep * i) * thickness; + Raylib.DrawTriangle(pEnd, end, curEnd, color.ToRayColor()); + curEnd = pEnd; + } + Raylib.DrawTriangle(curEnd, tr, end, color.ToRayColor()); + } + } + + } + + #endregion + #region Draw Masked /// /// Helper that draws the appropriate subsegments when a segment intersects a closed mask. @@ -410,100 +665,99 @@ public static void DrawMasked(this Segment segment, T mask, LineDrawingInfo l } #endregion + #region Draw + /// - /// Draws a segment from to a point along the direction to , scaled by . + /// Draws the specified with the given thickness, color, and cap style. /// - /// The starting point of the segment. - /// The ending point of the segment. + /// The segment to draw. /// The thickness of the segment. /// The color of the segment. - /// The factor by which to scale the segment's length (0 = no length, 1 = full length). /// The type of line cap to use at the ends of the segment. /// The number of points used to draw the cap (for rounded or custom caps). - public static void DrawSegment(Vector2 start, Vector2 end, float thickness, ColorRgba color, float sideLengthFactor, LineCapType capType = LineCapType.None, int capPoints = 0) + public static void Draw(this Segment segment, float thickness, ColorRgba color, LineCapType capType = LineCapType.None, int capPoints = 0 ) + => DrawSegment(segment.Start, segment.End, thickness, color, capType, capPoints); + + /// + /// Draws the specified using separate cap styles for the start and end. + /// + /// The segment to draw. + /// The thickness of the segment. Values below are clamped. + /// Color used to draw the segment. + /// Cap type to apply at the segment start. + /// Number of points for the start cap (rounded/custom). If <= 0 a simple cap is used. + /// Cap type to apply at the segment end. + /// Number of points for the end cap (rounded/custom). If <= 0 a simple cap is used. + public static void DrawSeparateCaps(this Segment segment, float thickness, ColorRgba color, LineCapType startCapType = LineCapType.None, int startCapPoints = 0, LineCapType endCapType = LineCapType.None, int endCapPoints = 0) + => DrawSegmentSeparateCaps(segment.Start, segment.End, thickness, color, startCapType, startCapPoints, endCapType, endCapPoints); + + /// + /// Draws the specified using the provided . + /// + /// The segment to draw. + /// The line drawing information (thickness, color, cap type, cap points). + public static void Draw(this Segment segment, LineDrawingInfo lineInfo) + => DrawSegment(segment.Start, segment.End, lineInfo); + /// + /// Draws the specified with rotation and origin, using the provided . + /// + /// The segment to draw. + /// The point to rotate the segment around (0 = start, 1 = end). + /// The rotation angle in radians. + /// The line drawing information (thickness, color, cap type, cap points). + public static void Draw(this Segment segment, float originF, float angleRad, LineDrawingInfo lineInfo) { - var dir = end - start; - var newEnd = start + dir * sideLengthFactor; - DrawSegment(start, newEnd, thickness, color, capType, capPoints); + if (angleRad != 0f) + { + segment.ChangeRotation(angleRad, originF).Draw(lineInfo); + return; + + } + + DrawSegment(segment.Start, segment.End, lineInfo); } + + #endregion + #region Draw Segments /// - /// Draws a segment from to with the specified thickness, color, and cap style. + /// Draws all segments in the specified collection using the provided . /// - /// The starting point of the segment. - /// The ending point of the segment. - /// The thickness of the segment. - /// The color of the segment. - /// The type of line cap to use at the ends of the segment. - /// The number of points used to draw the cap (for rounded or custom caps). - /// - /// If is less than , it will be clamped. - /// - public static void DrawSegment(Vector2 start, Vector2 end, float thickness, ColorRgba color, LineCapType capType = LineCapType.None, int capPoints = 0) + /// The collection of segments to draw. + /// The line drawing information (thickness, color, cap type, cap points). + public static void Draw(this Segments segments, LineDrawingInfo lineInfo) { - if (thickness < LineDrawingInfo.LineMinThickness) thickness = LineDrawingInfo.LineMinThickness; - var w = end - start; - float ls = w.X * w.X + w.Y * w.Y; // w.LengthSquared(); - if (ls <= MinSegmentDrawLengthSquared) return; - - var dir = w / MathF.Sqrt(ls); - var pR = new Vector2(-dir.Y, dir.X);//perpendicular right - var pL = new Vector2(dir.Y, -dir.X);//perpendicular left - - if (capType == LineCapType.Extended) //expand outwards - { - start -= dir * thickness; - end += dir * thickness; - } - else if (capType == LineCapType.Capped)//shrink inwards so that the line with cap is the same length + if (segments.Count <= 0) return; + foreach (var seg in segments) { - start += dir * thickness; - end -= dir * thickness; + seg.Draw(lineInfo); } - - var tl = start + pL * thickness; - var bl = start + pR * thickness; - var br = end + pR * thickness; - var tr = end + pL * thickness; - - Raylib.DrawTriangle(tl, bl, br, color.ToRayColor()); - Raylib.DrawTriangle(tl, br, tr, color.ToRayColor()); + } - if (capType is LineCapType.None or LineCapType.Extended) return; - if (capPoints <= 0) return; - - //Draw Cap - if (capPoints == 1) - { - var capStart = start - dir * thickness; - var capEnd = end + dir * thickness; - - Raylib.DrawTriangle(tl, capStart, bl, color.ToRayColor()); - Raylib.DrawTriangle(tr, br, capEnd, color.ToRayColor()); - } - else + /// + /// Draws all segments in the specified collection, cycling through the provided colors. + /// + /// The collection of segments to draw. + /// The thickness of each segment. + /// A list of colors to use for the segments. + /// Colors are cycled if there are more segments than colors. + /// The type of line cap to use at the ends of the segments. + /// The number of points used to draw the cap (for rounded or custom caps). + public static void Draw(this Segments segments, float thickness, List colors, LineCapType capType = LineCapType.None, int capPoints = 0) + { + if (segments.Count <= 0 || colors.Count <= 0) return; + // LineDrawingInfo info = new(thickness, ColorRgba.White, capType, capPoints); + for (var i = 0; i < segments.Count; i++) { - var curStart = tl; - var curEnd = br; - float angleStep = (180f / (capPoints + 1)) * ShapeMath.DEGTORAD; - - for (var i = 1; i <= capPoints; i++) - { - var pStart = start + pL.Rotate(- angleStep * i) * thickness; - Raylib.DrawTriangle(pStart, start, curStart, color.ToRayColor()); - curStart = pStart; - - var pEnd = end + pR.Rotate(- angleStep * i) * thickness; - Raylib.DrawTriangle(pEnd, end, curEnd, color.ToRayColor()); - curEnd = pEnd; - } - Raylib.DrawTriangle(curStart, bl, start, color.ToRayColor()); - Raylib.DrawTriangle(curEnd, tr, end, color.ToRayColor()); - + var c = colors[i % colors.Count]; + segments[i].Draw(thickness, c, capType, capPoints); } } - /// + #endregion + + #region Draw Percentage + /// /// Draws part of a line from start to end depending on f. /// /// The start point. @@ -533,30 +787,7 @@ public static void DrawSegmentPercentage(Vector2 start, Vector2 end, float f, fl } - - /// - /// Draws a segment using float coordinates for start and end points. - /// - /// The X coordinate of the start point. - /// The Y coordinate of the start point. - /// The X coordinate of the end point. - /// The Y coordinate of the end point. - /// The thickness of the segment. - /// The color of the segment. - /// The type of line cap to use at the ends of the segment. - /// The number of points used to draw the cap (for rounded or custom caps). - public static void DrawSegment(float startX, float startY, float endX, float endY, float thickness, - ColorRgba color, LineCapType capType = LineCapType.None, int capPoints = 0) - => DrawSegment(new(startX, startY), new(endX, endY), thickness, color, capType, capPoints); - - /// - /// Draws a segment using the provided . - /// - /// The starting point of the segment. - /// The ending point of the segment. - /// The line drawing information (thickness, color, cap type, cap points). - public static void DrawSegment(Vector2 start, Vector2 end, LineDrawingInfo info) => DrawSegment(start, end, info.Thickness, info.Color, info.CapType, info.CapPoints); - + /// /// Draws a portion of a segment using the provided and percentage . /// @@ -569,48 +800,7 @@ public static void DrawSegment(float startX, float startY, float endX, float end /// public static void DrawSegmentPercentage(Vector2 start, Vector2 end, float f, LineDrawingInfo info) => DrawSegmentPercentage(start, end, f, info.Thickness, info.Color, info.CapType, info.CapPoints); - - /// - /// Draws a segment with scaling applied from a specified origin along the segment. - /// - /// The starting point of the segment. - /// The ending point of the segment. - /// The line drawing information (thickness, color, cap type, cap points). - /// The factor by which to scale the segment's length (0 = no length, 1 = full length). - /// The point along the segment (0 = start, 1 = end) to scale from. - public static void DrawSegment(Vector2 start, Vector2 end, LineDrawingInfo info, float scaleFactor, float scaleOrigin = 0.5f) - { - var p = start.Lerp(end, scaleOrigin); - var s = start - p; - var e = end - p; - - var newStart = p + s * scaleFactor; - var newEnd = p + e * scaleFactor; - DrawSegment(newStart, newEnd, info); - } - - /// - /// Draws a segment using float coordinates for start and end points and the provided . - /// - /// The X coordinate of the start point. - /// The Y coordinate of the start point. - /// The X coordinate of the end point. - /// The Y coordinate of the end point. - /// The line drawing information (thickness, color, cap type, cap points). - public static void DrawSegment(float startX, float startY, float endX, float endY, LineDrawingInfo info) - => DrawSegment(new(startX, startY), new(endX, endY), info.Thickness, info.Color, info.CapType, info.CapPoints); - - /// - /// Draws the specified with the given thickness, color, and cap style. - /// - /// The segment to draw. - /// The thickness of the segment. - /// The color of the segment. - /// The type of line cap to use at the ends of the segment. - /// The number of points used to draw the cap (for rounded or custom caps). - public static void Draw(this Segment segment, float thickness, ColorRgba color, LineCapType capType = LineCapType.None, int capPoints = 0 ) - => DrawSegment(segment.Start, segment.End, thickness, color, capType, capPoints); - + /// /// Draws a portion of the specified based on the percentage . /// @@ -625,15 +815,7 @@ public static void Draw(this Segment segment, float thickness, ColorRgba color, /// public static void DrawPercentage(this Segment segment, float f, float thickness, ColorRgba color, LineCapType capType = LineCapType.None, int capPoints = 0 ) => DrawSegmentPercentage(segment.Start, segment.End, f, thickness, color, capType, capPoints); - - /// - /// Draws the specified using the provided . - /// - /// The segment to draw. - /// The line drawing information (thickness, color, cap type, cap points). - public static void Draw(this Segment segment, LineDrawingInfo lineInfo) - => DrawSegment(segment.Start, segment.End, lineInfo); - + /// /// Draws a portion of the specified using the provided and percentage . /// @@ -646,72 +828,10 @@ public static void Draw(this Segment segment, LineDrawingInfo lineInfo) public static void DrawPercentage(this Segment segment, float f, LineDrawingInfo lineInfo) => DrawSegmentPercentage(segment.Start, segment.End, f, lineInfo); - /// - /// Draws the specified with rotation and origin, using the provided . - /// - /// The segment to draw. - /// The point to rotate the segment around (0 = start, 1 = end). - /// The rotation angle in radians. - /// The line drawing information (thickness, color, cap type, cap points). - public static void Draw(this Segment segment, float originF, float angleRad, LineDrawingInfo lineInfo) - { - if (angleRad != 0f) - { - segment.ChangeRotation(angleRad, originF).Draw(lineInfo); - return; - - } - - DrawSegment(segment.Start, segment.End, lineInfo); - } - - /// - /// Draws all segments in the specified collection using the provided . - /// - /// The collection of segments to draw. - /// The line drawing information (thickness, color, cap type, cap points). - public static void Draw(this Segments segments, LineDrawingInfo lineInfo) - { - if (segments.Count <= 0) return; - foreach (var seg in segments) - { - seg.Draw(lineInfo); - } - } - - /// - /// Draws all segments in the specified collection, cycling through the provided colors. - /// - /// The collection of segments to draw. - /// The thickness of each segment. - /// A list of colors to use for the segments. - /// Colors are cycled if there are more segments than colors. - /// The type of line cap to use at the ends of the segments. - /// The number of points used to draw the cap (for rounded or custom caps). - public static void Draw(this Segments segments, float thickness, List colors, LineCapType capType = LineCapType.None, int capPoints = 0) - { - if (segments.Count <= 0 || colors.Count <= 0) return; - // LineDrawingInfo info = new(thickness, ColorRgba.White, capType, capPoints); - for (var i = 0; i < segments.Count; i++) - { - var c = colors[i % colors.Count]; - segments[i].Draw(thickness, c, capType, capPoints); - } - } - - /// - /// Draws circles at the start and end vertices of the specified . - /// - /// The segment whose vertices to draw. - /// The radius of the vertex circles. - /// The color of the vertex circles. - /// The number of segments to use for drawing the circles (default is 16). - public static void DrawVertices(this Segment segment, float vertexRadius, ColorRgba color, int vertexSegments = 16) - { - segment.Start.Draw( vertexRadius, color, vertexSegments); - segment.End.Draw(vertexRadius, color, vertexSegments); - } - + #endregion + + #region Draw Glow + /// /// Draws a glowing segment from to by interpolating width and color. /// @@ -795,7 +915,10 @@ public static void DrawGlow(this Segments segments, float width, float endWidth, } } - /// + #endregion + + #region Draw Scaled + /// /// Draws a segment scaled towards a specified origin along the segment. /// /// The segment to draw. @@ -848,4 +971,222 @@ public static void DrawScaled(this Segment s, float originF, float angleRad, Lin } } + #endregion + + #region Draw Vertices + /// + /// Draws circles at the start and end vertices of the specified . + /// + /// The segment whose vertices to draw. + /// The radius of the vertex circles. + /// The color of the vertex circles. + /// + /// The smoothness value (0-1). This controls the visual quality of the circle by inversely interpolating the current . + /// A value of 0 uses the maximum side length (fewer sides, less smooth), while 1 uses the minimum side length (more sides, smoother). + /// The resulting side length determines the number of polygon sides used to approximate the circle. + /// + public static void DrawVertices(this Segment segment, float vertexRadius, ColorRgba color, float smoothness = 0.25f) + { + segment.Start.Draw(vertexRadius, color, smoothness); + segment.End.Draw(vertexRadius, color, smoothness); + } + + #endregion + + #region Draw Cap + + /// + /// Draws a round cap at the specified center with a given direction, radius, and number of cap points. + /// + /// The center of the round cap. + /// The direction of the cap. + /// The radius of the cap. + /// The number of points to use for drawing the cap. + /// The color of the cap. + public static void DrawRoundCap(Vector2 center, Vector2 dir, float radius, int capPoints, ColorRgba color) + { + if(capPoints <= 0) return; + dir = -dir; + var pR = new Vector2(-dir.Y, dir.X);//perpendicular right + var pL = new Vector2(dir.Y, -dir.X);//perpendicular left + + var capStartLeft = center + pL * radius; + var capStartRight = center + pR * radius; + + var curStart = capStartLeft; + float angleStep = (180f / (capPoints + 1)) * ShapeMath.DEGTORAD; + + for (var i = 1; i <= capPoints; i++) + { + var pStart = center + pL.Rotate(- angleStep * i) * radius; + Raylib.DrawTriangle(pStart, center, curStart, color.ToRayColor()); + curStart = pStart; + } + Raylib.DrawTriangle(curStart, capStartRight, center, color.ToRayColor()); + } + + /// + /// Draws a round cap at the specified left and right points with a given number of cap points and color. + /// + /// The left point of the cap. + /// The right point of the cap. + /// The number of points to use for drawing the cap. + /// The color of the cap. + public static void DrawRoundCap(Vector2 left, Vector2 right, int capPoints, ColorRgba color) + { + if(capPoints <= 0) return; + var w = left - right; + var ls = w.LengthSquared(); + if (ls <= 0f) return; + var l = MathF.Sqrt(ls); + var radius = l * 0.5f; + var dir = w / l; + var center = right + dir * radius; + var curStart = left; + float angleStep = (180f / (capPoints + 1)) * ShapeMath.DEGTORAD; + + for (var i = 1; i <= capPoints; i++) + { + var pStart = center + dir.Rotate(- angleStep * i) * radius; + Raylib.DrawTriangle(pStart, center, curStart, color.ToRayColor()); + curStart = pStart; + } + Raylib.DrawTriangle(curStart, right, center, color.ToRayColor()); + } + #endregion + + #region Gapped + /// + /// Draws a line segment with gaps, creating a dashed or segmented effect. + /// + /// The starting point of the line segment. + /// The ending point of the line segment. + /// + /// The length of the line. + /// If zero or negative, the method calculates it automatically. + /// Providing a known length avoids redundant calculations and improves performance, especially for static segments. + /// Parameters describing how to draw the line (e.g., color, thickness). + /// Parameters describing the gap configuration (number of gaps, gap percentage, etc.). + /// + /// The length of the line if positive; otherwise, -1. + /// If the shape does not change, the valid length can be reused in subsequent frames to avoid recalculating. + /// + /// + /// - If is 0 or is 0, the line is drawn solid. + /// - If is 1 or greater, no line is drawn. + /// + public static float DrawGappedSegment(Vector2 start, Vector2 end, float length, LineDrawingInfo lineInfo, GappedOutlineDrawingInfo gapDrawingInfo) + { + if (gapDrawingInfo.Gaps <= 0 || gapDrawingInfo.GapPerimeterPercentage <= 0f) + { + SegmentDrawing.DrawSegment(start, end, lineInfo); + return length > 0f ? length : -1f; + } + + if (gapDrawingInfo.GapPerimeterPercentage >= 1f) return length > 0f ? length : -1f; + + var linePercentage = 1f - gapDrawingInfo.GapPerimeterPercentage; + var lines = gapDrawingInfo.Gaps; + var gapPercentageRange = gapDrawingInfo.GapPerimeterPercentage / gapDrawingInfo.Gaps; + var linePercentageRange = linePercentage / lines; + + var w = end - start; + if (length <= 0) length = w.Length(); + if (length <= 0) return -1f; + var dir = w / length; + + var lineLength = length * linePercentageRange; + var gapLength = length * gapPercentageRange; + + var curDistance = gapDrawingInfo.StartOffset < 1f ? gapDrawingInfo.StartOffset * length : 0f; + var curStart = start + dir * curDistance; + Vector2 curEnd; + var remainingLineLength = 0f; + if (curDistance + lineLength >= length) + { + curEnd = end; + var tempLength = (curEnd - curStart).Length(); + curDistance = 0; + remainingLineLength = lineLength - tempLength; + } + else + { + curEnd = curStart + dir * lineLength; + curDistance += lineLength; + } + + int drawnLines = 0; + while (drawnLines <= lines) + { + SegmentDrawing.DrawSegment(curStart, curEnd, lineInfo); + + if (remainingLineLength > 0f) + { + curStart = start; + curEnd = curStart + dir * remainingLineLength; + curDistance = remainingLineLength; + remainingLineLength = 0f; + drawnLines++; + } + else + { + if (curDistance + gapLength >= length) //gap overshoots end + { + var tempLength = (end - curEnd).Length(); + var remaining = gapLength - tempLength; + curDistance = remaining; + + curStart = start + dir * curDistance; + } + else //advance gap length to find new start + { + curStart = curEnd + dir * gapLength; + curDistance += gapLength; + } + + if (curDistance + lineLength >= length) //line overshoots end + { + curEnd = end; + var tempLength = (curEnd - curStart).Length(); + curDistance = 0; + remainingLineLength = lineLength - tempLength; + } + else //advance line length to find new end + { + curEnd = curStart + dir * lineLength; + curDistance += lineLength; + drawnLines++; + } + } + + } + + return length; + } + + /// + /// Draws a segment with gaps, creating a dashed or segmented effect. + /// + /// The segment to draw. + /// + /// The length of the segment. + /// If zero or negative, the method calculates it automatically. + /// Providing a known length avoids redundant calculations and improves performance, especially for static segments. + /// + /// Parameters describing how to draw the segment. + /// Parameters describing the gap configuration. + /// + /// Returns the segment length if positive; otherwise, returns -1. + /// If the shape does not change, the valid length can be reused in subsequent frames to avoid recalculating. + /// + /// + /// This is a convenience wrapper for . + /// + public static float DrawGapped(this Segment s, float length, LineDrawingInfo lineInfo, GappedOutlineDrawingInfo gapDrawingInfo) + { + return DrawGappedSegment(s.Start, s.End, length, lineInfo, gapDrawingInfo); + } + + #endregion + } \ No newline at end of file diff --git a/ShapeEngine/Geometry/SegmentDef/SegmentMath.cs b/ShapeEngine/Geometry/SegmentDef/SegmentMath.cs index c9e58875..a035b3a1 100644 --- a/ShapeEngine/Geometry/SegmentDef/SegmentMath.cs +++ b/ShapeEngine/Geometry/SegmentDef/SegmentMath.cs @@ -66,6 +66,30 @@ public Segment Truncate() }; return points; } + + /// + /// Writes the segment endpoints and their projected counterparts into . + /// + /// The destination collection that will be cleared and populated with the original and projected points. + /// The vector along which to project the segment. + /// true if is non-zero and was populated; otherwise, false. + /// + /// Points are written in this order: , , Start + v, End + v. + /// + public bool GetProjectedShapePoints(Points result, Vector2 v) + { + if (v.LengthSquared() <= 0f) return false; + + result.Clear(); + result.EnsureCapacity(4); + + result.Add(Start); + result.Add(End); + result.Add(Start + v); + result.Add(End + v); + + return true; + } /// /// Returns the convex hull polygon formed by projecting the segment along a given vector. @@ -85,9 +109,51 @@ public Segment Truncate() Start + v, End + v, }; - return Polygon.FindConvexHull(points); + var result = new Polygon(4); + points.FindConvexHull(result); + return result; + } + + /// + /// Projects the segment along the given vector and writes the convex hull of the combined point set into . + /// + /// The destination polygon that receives the convex hull of the original and projected segment endpoints. + /// The vector along which to project the segment. + /// true if is non-zero and was populated; otherwise, false. + /// + /// This method forms a temporary four-point set consisting of the segment endpoints and those same endpoints translated by , then computes the convex hull into . + /// + public bool ProjectShape(Polygon result, Vector2 v) + { + if (v.LengthSquared() <= 0f) return false; + var points = new Points + { + Start, + End, + Start + v, + End + v, + }; + + points.FindConvexHull(result); + return true; } + /// + /// Scales the segment from a specific origin point along the segment. + /// + /// The factor by which to scale the segment. + /// The origin point as a fraction (0=Start, 1=End) to scale from. + /// A new scaled by the given factor relative to the origin. + public Segment ScaleSegment(float scaleFactor, float scaleOrigin) + { + var p = Start.Lerp(End, scaleOrigin); + var s = Start - p; + var e = End - p; + + var newStart = p + s * scaleFactor; + var newEnd = p + e * scaleFactor; + return new Segment(newStart, newEnd); + } #endregion #region Transform diff --git a/ShapeEngine/Geometry/SegmentsDef/Segments.cs b/ShapeEngine/Geometry/SegmentsDef/Segments.cs index 9409e7a7..8c2ff825 100644 --- a/ShapeEngine/Geometry/SegmentsDef/Segments.cs +++ b/ShapeEngine/Geometry/SegmentsDef/Segments.cs @@ -1,5 +1,4 @@ using System.Numerics; -using ShapeEngine.Core; using ShapeEngine.Geometry.PointsDef; using ShapeEngine.Geometry.SegmentDef; using ShapeEngine.Random; @@ -13,6 +12,15 @@ namespace ShapeEngine.Geometry.SegmentsDef; /// public partial class Segments : ShapeList { + #region Helper + + private static HashSet hashSetVector2Buffer = new(); + private static HashSet hashSetSegmentBuffer = new(); + private static List segmentsBuffer = new(); + private static List> weightedSegmentBuffer = new(); + + #endregion + #region Constructors /// /// Creates an empty list of segments. @@ -47,16 +55,16 @@ public bool Equals(Segments? other) } return true; } + /// /// Gets the hash code for the list of segments. /// /// The hash code for the list of segments. public override int GetHashCode() => Game.GetHashCode(this); - + #endregion #region Public - /// /// Gets the segment at the specified index. /// @@ -64,44 +72,52 @@ public bool Equals(Segments? other) /// The segment at the specified index, with the index wrapped if out of bounds. public Segment GetSegment(int index) { - // if (index < 0) return new Segment(); if (Count <= 0) return new Segment(); var i =ShapeMath.WrapI(index, 0, Count - 1); - // var i = ((index % Count) + Count) % Count; - // var i = index % Count; return this[i]; } /// - /// Gets a list of unique points from all the segments in the list. + /// Collects all unique segment endpoints in this collection and writes them into . /// - /// A list of unique points. - public Points GetUniquePoints() + /// The destination collection that will be cleared and populated with the unique points. + /// + /// This method does not modify the current collection. Point uniqueness is determined by the equality comparer used by the internal . + /// + public void GetUniquePoints(Points result) { - var uniqueVertices = new HashSet(); + hashSetVector2Buffer.Clear(); for (int i = 0; i < Count; i++) { var seg = this[i]; - uniqueVertices.Add(seg.Start); - uniqueVertices.Add(seg.End); + hashSetVector2Buffer.Add(seg.Start); + hashSetVector2Buffer.Add(seg.End); } - return new(uniqueVertices); + result.Clear(); + result.EnsureCapacity(hashSetVector2Buffer.Count); + result.AddRange(hashSetVector2Buffer); } + /// - /// Gets a list of unique segments from the list. + /// Collects all unique segments in this collection and writes them into . /// - /// A new list of segments containing only the unique segments from the original list. - public Segments GetUniqueSegments() + /// The destination collection that will be cleared and populated with the unique segments. + /// + /// This method does not modify the current collection. Segment uniqueness is determined by the equality comparer used by the internal . + /// + public void GetUniqueSegments(Segments result) { - var uniqueSegments = new HashSet(); + hashSetSegmentBuffer.Clear(); for (int i = 0; i < Count; i++) { var seg = this[i]; - uniqueSegments.Add(seg); + hashSetSegmentBuffer.Add(seg); } - return new(uniqueSegments); + result.Clear(); + result.EnsureCapacity(hashSetSegmentBuffer.Count); + result.AddRange(hashSetSegmentBuffer); } /// @@ -110,39 +126,71 @@ public Segments GetUniqueSegments() /// A random segment from the list. The longer the segment, the higher the chance of being picked. public Segment GetRandomSegment() { - var items = new WeightedItem[Count]; + weightedSegmentBuffer.Clear(); + weightedSegmentBuffer.EnsureCapacity(Count); for (var i = 0; i < Count; i++) { var seg = this[i]; - items[i] = new(seg, (int)seg.LengthSquared); + weightedSegmentBuffer.Add(new(seg, (int)seg.LengthSquared)); } - return Rng.Instance.PickRandomItem(items); + return Rng.Instance.PickRandomItem(weightedSegmentBuffer); } + /// /// Gets a random point on a random segment from the list. /// /// A random point on a random segment. public Vector2 GetRandomPoint() => GetRandomSegment().GetRandomPoint(); + /// - /// Gets a list of random points on random segments from the list. + /// Writes random points sampled from randomly selected segments into . /// /// The amount of random points to generate. - /// A list of random points. - public Points GetRandomPoints(int amount) + /// The destination collection that will be cleared and populated with the generated points. + /// + /// Segment selection is weighted by each segment's squared length, so longer segments are more likely to be chosen. + /// + public void GetRandomPoints(int amount, Points result) { - var items = new WeightedItem[Count]; + weightedSegmentBuffer.Clear(); + weightedSegmentBuffer.EnsureCapacity(Count); + for (var i = 0; i < Count; i++) { var seg = this[i]; - items[i] = new(seg, (int)seg.LengthSquared); + weightedSegmentBuffer.Add(new(seg, (int)seg.LengthSquared)); } - var pickedSegments = Rng.Instance.PickRandomItems(amount, items); - var randomPoints = new Points(); - foreach (var seg in pickedSegments) + + Rng.Instance.PickRandomItems(segmentsBuffer, amount, weightedSegmentBuffer); + result.Clear(); + result.EnsureCapacity(segmentsBuffer.Count); + + foreach (var seg in segmentsBuffer) { - randomPoints.Add(seg.GetRandomPoint()); + result.Add(seg.GetRandomPoint()); } - return randomPoints; + } + + /// + /// Writes randomly selected segments from this collection into . + /// + /// The number of segments to select. + /// The destination collection that will receive the selected segments. + /// + /// Segment selection is weighted by each segment's squared length, so longer segments are more likely to be chosen. + /// + public void GetRandomSegments(int amount, Segments result) + { + weightedSegmentBuffer.Clear(); + weightedSegmentBuffer.EnsureCapacity(Count); + + for (var i = 0; i < Count; i++) + { + var seg = this[i]; + weightedSegmentBuffer.Add(new(seg, (int)seg.LengthSquared)); + } + + Rng.Instance.PickRandomItems(result, amount, weightedSegmentBuffer); } /// @@ -169,6 +217,7 @@ public bool ContainsSegment(Segment seg) foreach (var segment in this) { if (segment.Equals(seg)) return true; } return false; } + /// /// Checks if a similar segment is in the list. /// @@ -179,5 +228,20 @@ public bool ContainsSegmentSimilar(Segment seg) foreach (var segment in this) { if (segment.IsSimilar(seg)) return true; } return false; } + + /// + /// Writes the direction vector of each segment into . + /// + /// The destination list that receives one direction vector per segment. + /// true to write normalized direction vectors; false to write each segment's full displacement vector. + public void GetSegmentDirections(List result, bool normalized = false) + { + result.Clear(); + result.EnsureCapacity(Count); + foreach (var seg in this) + { + result.Add(normalized ? seg.Dir : seg.Displacement); + } + } #endregion } \ No newline at end of file diff --git a/ShapeEngine/Geometry/SegmentsDef/SegmentsIntersectShape.cs b/ShapeEngine/Geometry/SegmentsDef/SegmentsIntersectShape.cs index 9bb74206..d2c25dc6 100644 --- a/ShapeEngine/Geometry/SegmentsDef/SegmentsIntersectShape.cs +++ b/ShapeEngine/Geometry/SegmentsDef/SegmentsIntersectShape.cs @@ -35,6 +35,7 @@ public partial class Segments } return intersections; } + /// /// Computes all intersection points between this segments and a collider shape. /// @@ -487,6 +488,7 @@ public int IntersectShape(Segments shape, ref IntersectionPoints points, bool re return count; } + /// /// Computes all intersection points between this segments and a triangle. /// @@ -521,6 +523,7 @@ public int IntersectShape(Triangle shape, ref IntersectionPoints points, bool re } return count; } + /// /// Computes all intersection points between this segments and a rectangle. /// @@ -555,6 +558,7 @@ public int IntersectShape(Rect shape, ref IntersectionPoints points, bool return } return count; } + /// /// Computes all intersection points between this segments and a quadrilateral. /// @@ -589,6 +593,7 @@ public int IntersectShape(Quad shape, ref IntersectionPoints points, bool return } return count; } + /// /// Computes all intersection points between this segments and a polygon. /// @@ -617,6 +622,7 @@ public int IntersectShape(Polygon shape, ref IntersectionPoints points, bool ret } return count; } + /// /// Computes all intersection points between this segments and a polyline. /// diff --git a/ShapeEngine/Geometry/SegmentsDef/SegmentsMath.cs b/ShapeEngine/Geometry/SegmentsDef/SegmentsMath.cs index 11f9e829..37004548 100644 --- a/ShapeEngine/Geometry/SegmentsDef/SegmentsMath.cs +++ b/ShapeEngine/Geometry/SegmentsDef/SegmentsMath.cs @@ -134,173 +134,205 @@ public void SetTransform(Transform2D transform, float originF = 0.5f) } } + + /// - /// Creates a new list of segments with the rotation of all segments changed by a given amount. + /// Writes copies of all segments into , with their rotation changed by the specified amount. /// + /// The destination collection that will be cleared and populated with the rotated segments. /// The amount of rotation to add in radians. /// The origin of the rotation. 0.5f is the center of the segment. - /// A new list of segments with the rotation changed. - public Segments ChangeRotationCopy(float rad, float originF = 0.5f) + /// + /// This method does not modify the current collection. Each transformed segment is written into . + /// + public void ChangeRotationCopy(Segments result, float rad, float originF = 0.5f) { - var newSegments = new Segments(Count); + result.Clear(); + result.EnsureCapacity(Count); + for (var i = 0; i < Count; i++) { - newSegments.Add(this[i].ChangeRotation(rad, originF)); + result.Add(this[i].ChangeRotation(rad, originF)); } - - return newSegments; } /// - /// Creates a new list of segments with the rotation of all segments set to a given value. + /// Writes copies of all segments into , with their rotation set to the specified value. /// + /// The destination collection that will be cleared and populated with the rotated segments. /// The new rotation in radians. /// The origin of the rotation. 0.5f is the center of the segment. - /// A new list of segments with the rotation set. - public Segments SetRotationCopy(float rad, float originF = 0.5f) + /// + /// This method does not modify the current collection. Each transformed segment is written into . + /// + public void SetRotationCopy(Segments result, float rad, float originF = 0.5f) { - var newSegments = new Segments(Count); + result.Clear(); + result.EnsureCapacity(Count); + for (var i = 0; i < Count; i++) { - newSegments.Add(this[i].SetRotation(rad, originF)); + result.Add(this[i].SetRotation(rad, originF)); } - - return newSegments; } /// - /// Creates a new list of segments with the length of all segments scaled by a given amount. + /// Writes copies of all segments into , with their lengths uniformly scaled by the specified factor. /// + /// The destination collection that will be cleared and populated with the scaled segments. /// The amount to scale the length of the segments by. /// The origin of the scaling. 0.5f is the center of the segment. - /// A new list of segments with the length scaled. - public Segments ScaleLengthCopy(float scale, float originF = 0.5f) + /// + /// This method does not modify the current collection. Each transformed segment is written into . + /// + public void ScaleLengthCopy(Segments result, float scale, float originF = 0.5f) { - var newSegments = new Segments(Count); + result.Clear(); + result.EnsureCapacity(Count); + for (var i = 0; i < Count; i++) { - newSegments.Add(this[i].ScaleLength(scale, originF)); + result.Add(this[i].ScaleLength(scale, originF)); } - - return newSegments; } /// - /// Creates a new list of segments with the length of all segments scaled by a given size. + /// Writes copies of all segments into , with their lengths scaled by the specified . /// + /// The destination collection that will be cleared and populated with the scaled segments. /// The size to scale the length of the segments by. /// The origin of the scaling. 0.5f is the center of the segment. - /// A new list of segments with the length scaled. - public Segments ScaleLengthCopy(Size scale, float originF = 0.5f) + /// + /// This method does not modify the current collection. Each transformed segment is written into . + /// + public void ScaleLengthCopy(Segments result, Size scale, float originF = 0.5f) { - var newSegments = new Segments(Count); + result.Clear(); + result.EnsureCapacity(Count); + for (var i = 0; i < Count; i++) { - newSegments.Add(this[i].ScaleLength(scale, originF)); + result.Add(this[i].ScaleLength(scale, originF)); } - - return newSegments; } /// - /// Creates a new list of segments with the length of all segments changed by a given amount. + /// Writes copies of all segments into , with their lengths changed by the specified amount. /// + /// The destination collection that will be cleared and populated with the resized segments. /// The amount to change the length of the segments by. /// The origin of the change. 0.5f is the center of the segment. - /// A new list of segments with the length changed. - public Segments ChangeLengthCopy(float amount, float originF = 0.5f) + /// + /// This method does not modify the current collection. Each transformed segment is written into . + /// + public void ChangeLengthCopy(Segments result, float amount, float originF = 0.5f) { - var newSegments = new Segments(Count); + result.Clear(); + result.EnsureCapacity(Count); + for (var i = 0; i < Count; i++) { - newSegments.Add(this[i].ChangeLength(amount, originF)); + result.Add(this[i].ChangeLength(amount, originF)); } - - return newSegments; } /// - /// Creates a new list of segments with the length of all segments set to a given value. + /// Writes copies of all segments into , with their lengths set to the specified value. /// + /// The destination collection that will be cleared and populated with the resized segments. /// The new length of the segments. /// The origin of the change. 0.5f is the center of the segment. - /// A new list of segments with the length set. - public Segments SetLengthCopy(float length, float originF = 0.5f) + /// + /// This method does not modify the current collection. Each transformed segment is written into . + /// + public void SetLengthCopy(Segments result, float length, float originF = 0.5f) { - var newSegments = new Segments(Count); + result.Clear(); + result.EnsureCapacity(Count); + for (var i = 0; i < Count; i++) { - newSegments.Add(this[i].SetLength(length, originF)); + result.Add(this[i].SetLength(length, originF)); } - - return newSegments; } /// - /// Creates a new list of segments with the position of all segments changed by a given offset. + /// Writes copies of all segments into , translated by the specified offset. /// + /// The destination collection that will be cleared and populated with the translated segments. /// The offset to apply to the position of the segments. - /// A new list of segments with the position changed. - public Segments ChangePositionCopy(Vector2 offset) + /// + /// This method does not modify the current collection. Each transformed segment is written into . + /// + public void ChangePositionCopy(Segments result, Vector2 offset) { - var newSegments = new Segments(Count); + result.Clear(); + result.EnsureCapacity(Count); + for (var i = 0; i < Count; i++) { - newSegments.Add(this[i].ChangePosition(offset)); + result.Add(this[i].ChangePosition(offset)); } - - return newSegments; } /// - /// Creates a new list of segments with the position of all segments set to a given value. + /// Writes copies of all segments into , with their positions set to the specified value. /// + /// The destination collection that will be cleared and populated with the translated segments. /// The new position of the segments. /// The origin of the change. 0.5f is the center of the segment. - /// A new list of segments with the position set. - public Segments SetPositionCopy(Vector2 position, float originF = 0.5f) + /// + /// This method does not modify the current collection. Each transformed segment is written into . + /// + public void SetPositionCopy(Segments result, Vector2 position, float originF = 0.5f) { - var newSegments = new Segments(Count); + result.Clear(); + result.EnsureCapacity(Count); + for (var i = 0; i < Count; i++) { - newSegments.Add(this[i].SetPosition(position, originF)); + result.Add(this[i].SetPosition(position, originF)); } - - return newSegments; } /// - /// Creates a new list of segments with a transform applied to all segments. + /// Writes copies of all segments into , with the specified offset transform applied. /// + /// The destination collection that will be cleared and populated with the transformed segments. /// The transform to apply. /// The origin of the transform. 0.5f is the center of the segment. - /// A new list of segments with the transform applied. - public Segments ApplyOffsetCopy(Transform2D offset, float originF = 0.5f) + /// + /// This method does not modify the current collection. Each transformed segment is written into . + /// + public void ApplyOffsetCopy(Segments result, Transform2D offset, float originF = 0.5f) { - var newSegments = new Segments(Count); + result.Clear(); + result.EnsureCapacity(Count); + for (var i = 0; i < Count; i++) { - newSegments.Add(this[i].ApplyOffset(offset, originF)); + result.Add(this[i].ApplyOffset(offset, originF)); } - - return newSegments; } /// - /// Creates a new list of segments with the transform of all segments set to a given value. + /// Writes copies of all segments into , with their transforms set to the specified value. /// + /// The destination collection that will be cleared and populated with the transformed segments. /// The new transform of the segments. /// The origin of the transform. 0.5f is the center of the segment. - /// A new list of segments with the transform set. - public Segments SetTransformCopy(Transform2D transform, float originF = 0.5f) + /// + /// This method does not modify the current collection. Each transformed segment is written into . + /// + public void SetTransformCopy(Segments result, Transform2D transform, float originF = 0.5f) { - var newSegments = new Segments(Count); + result.Clear(); + result.EnsureCapacity(Count); + for (var i = 0; i < Count; i++) { - newSegments.Add(this[i].SetTransform(transform, originF)); + result.Add(this[i].SetTransform(transform, originF)); } - - return newSegments; } } \ No newline at end of file diff --git a/ShapeEngine/Geometry/ShapeDrawing.cs b/ShapeEngine/Geometry/ShapeDrawing.cs deleted file mode 100644 index bfc04802..00000000 --- a/ShapeEngine/Geometry/ShapeDrawing.cs +++ /dev/null @@ -1,637 +0,0 @@ -using System.Numerics; -using ShapeEngine.Color; -using ShapeEngine.Core.Structs; -using ShapeEngine.Geometry.CircleDef; -using ShapeEngine.Geometry.SegmentDef; -using ShapeEngine.StaticLib; - -namespace ShapeEngine.Geometry; - -/// -/// Provides extension methods for drawing shapes and outlines using lists of points. -/// -/// -/// This static class contains various helper methods for rendering polygons, outlines, and vertices with customizable styles and transformations. -/// -public static class ShapeDrawing -{ - /// - /// Draws the outline of a polygon with a color gradient between the start and end colors. - /// - /// The list of points defining the polygon. - /// The thickness of the outline. - /// The color at the start of the outline. - /// The color at the end of the outline. - /// The type of line cap to use at the ends of each segment. - /// The number of points used for the cap. - /// - /// The outline is drawn by interpolating the color between the start and end colors for each segment. - /// - public static void DrawOutline(this List points, float lineThickness, ColorRgba startColorRgba, ColorRgba endColorRgba, LineCapType capType = LineCapType.CappedExtended, int capPoints = 2) - { - if (points.Count < 3) return; - - int redStep = (endColorRgba.R - startColorRgba.R) / points.Count; - int greenStep = (endColorRgba.G - startColorRgba.G) / points.Count; - int blueStep = (endColorRgba.B - startColorRgba.B) / points.Count; - int alphaStep = (endColorRgba.A - startColorRgba.A) / points.Count; - for (var i = 0; i < points.Count; i++) - { - var start = points[i]; - var end = points[(i + 1) % points.Count]; - ColorRgba finalColorRgba = new - ( - startColorRgba.R + redStep * i, - startColorRgba.G + greenStep * i, - startColorRgba.B + blueStep * i, - startColorRgba.A + alphaStep * i - ); - SegmentDrawing.DrawSegment(start, end, lineThickness, finalColorRgba, capType, capPoints); - } - } - - /// - /// Draws the outline of a polygon with a uniform color. - /// - /// The list of points defining the polygon. - /// The thickness of the outline. - /// The color of the outline. - /// The type of line cap to use at the ends of each segment. - /// The number of points used for the cap. - public static void DrawOutline(this List shapePoints, float lineThickness, ColorRgba color, LineCapType capType = LineCapType.CappedExtended, int capPoints = 2) - { - if (shapePoints.Count < 3) return; - - for (var i = 0; i < shapePoints.Count; i++) - { - var start = shapePoints[i]; - var end = shapePoints[(i + 1) % shapePoints.Count]; - SegmentDrawing.DrawSegment(start, end, lineThickness, color, capType, capPoints); - } - } - - /// - /// Draws the outline of a polygon with a uniform color and scales each side by a specified factor. - /// - /// The list of points defining the polygon. - /// The thickness of the outline. - /// The color of the outline. - /// The factor by which to scale the length of each side. - /// The type of line cap to use at the ends of each segment. - /// The number of points used for the cap. - public static void DrawOutline(this List shapePoints, float lineThickness, ColorRgba color, float sideLengthFactor, LineCapType capType = LineCapType.CappedExtended, int capPoints = 2) - { - if (shapePoints.Count < 3) return; - - for (var i = 0; i < shapePoints.Count; i++) - { - var start = shapePoints[i]; - var end = shapePoints[(i + 1) % shapePoints.Count]; - SegmentDrawing.DrawSegment(start, end, lineThickness, color, sideLengthFactor, capType, capPoints); - } - } - - /// - /// Draws the outline of a polygon using the specified . - /// - /// The list of points defining the polygon. - /// The line drawing information (thickness, color, cap type, etc.). - public static void DrawOutline(this List shapePoints, LineDrawingInfo lineInfo) - { - DrawOutline(shapePoints, lineInfo.Thickness, lineInfo.Color, lineInfo.CapType, lineInfo.CapPoints); - } - - /// - /// Draws the outline of a polygon transformed by position, size, and rotation. - /// - /// The list of relative points defining the polygon. - /// The position of the polygon's center. - /// The scale factor for the polygon. - /// The rotation in degrees. - /// The thickness of the outline. - /// The color of the outline. - /// The type of line cap to use at the ends of each segment. - /// The number of points used for the cap. - /// - /// Each point is transformed by the specified position, size, and rotation before drawing. - /// - public static void DrawOutline(this List relativePoints, Vector2 pos, float size, float rotDeg, float lineThickness, ColorRgba color, LineCapType capType = LineCapType.CappedExtended, int capPoints = 2) - { - if (relativePoints.Count < 3) return; - - for (var i = 0; i < relativePoints.Count; i++) - { - var start = pos + (relativePoints[i] * size).Rotate(rotDeg * ShapeMath.DEGTORAD); - var end = pos + (relativePoints[(i + 1) % relativePoints.Count] * size).Rotate(rotDeg * ShapeMath.DEGTORAD); - SegmentDrawing.DrawSegment(start, end, lineThickness, color, capType, capPoints); - } - } - - /// - /// Draws the outline of a polygon using a for transformation. - /// - /// The list of relative points defining the polygon. - /// The transformation to apply (position, scale, rotation). - /// The thickness of the outline. - /// The color of the outline. - /// The type of line cap to use at the ends of each segment. - /// The number of points used for the cap. - public static void DrawOutline(this List relativePoints, Transform2D transform, float lineThickness, ColorRgba color, LineCapType capType = LineCapType.CappedExtended, int capPoints = 2) - { - DrawOutline(relativePoints, transform.Position, transform.ScaledSize.Length, transform.RotationDeg, lineThickness, color, capType, capPoints); - } - - /// - /// Draws the outline of a polygon using a and transformation parameters. - /// - /// The list of relative points defining the polygon. - /// The position of the polygon's center. - /// The scale factor for the polygon. - /// The rotation in degrees. - /// The line drawing information (thickness, color, cap type, etc.). - public static void DrawOutline(this List relativePoints, Vector2 pos, float size, float rotDeg, LineDrawingInfo lineInfo) - => DrawOutline(relativePoints, pos, size, rotDeg, lineInfo.Thickness, lineInfo.Color, lineInfo.CapType, lineInfo.CapPoints); - - /// - /// Draws the outline of a polygon using a and a . - /// - /// The list of relative points defining the polygon. - /// The transformation to apply (position, scale, rotation). - /// The line drawing information (thickness, color, cap type, etc.). - public static void DrawOutline(this List relativePoints, Transform2D transform, LineDrawingInfo lineInfo) - => DrawOutline(relativePoints, transform.Position, transform.ScaledSize.Length, transform.RotationDeg, lineInfo.Thickness, lineInfo.Color, lineInfo.CapType, lineInfo.CapPoints); - - /// - /// Draws a specified amount of the polygon's perimeter as an outline. - /// - /// The list of points defining the polygon. - /// - /// The length of the perimeter to draw. - /// If negative, the outline is drawn in the clockwise direction. - /// - /// The index of the corner at which to start drawing. - /// The thickness of the outline. - /// The color of the outline. - /// The type of line cap to use at the ends of each segment. - /// The number of points used for the cap. - /// - /// Useful for animating outlines or drawing partial polygons. - /// - public static void DrawOutlinePerimeter(this List shapePoints, float perimeterToDraw, int startIndex, float lineThickness, ColorRgba color, LineCapType capType = LineCapType.CappedExtended, int capPoints = 2) - { - if (shapePoints.Count < 3 || perimeterToDraw == 0) return; - - int currentIndex = ShapeMath.Clamp(startIndex, 0, shapePoints.Count - 1); - - bool reverse = perimeterToDraw < 0; - if (reverse) perimeterToDraw *= -1; - - for (var i = 0; i < shapePoints.Count; i++) - { - var start = shapePoints[currentIndex]; - if (reverse) currentIndex = ShapeMath.WrapIndex(shapePoints.Count, currentIndex - 1); // (currentIndex - 1) % shapePoints.Count; - else currentIndex = (currentIndex + 1) % shapePoints.Count; - var end = shapePoints[currentIndex]; - var l = (end - start).Length(); - if (l <= perimeterToDraw) - { - perimeterToDraw -= l; - SegmentDrawing.DrawSegment(start, end, lineThickness, color, capType, capPoints); - } - else - { - float f = perimeterToDraw / l; - end = start.Lerp(end, f); - SegmentDrawing.DrawSegment(start, end, lineThickness, color, capType, capPoints); - return; - } - - } - - - } - - /// - /// Draws a specified percentage of the polygon's outline. - /// - /// The list of points defining the polygon. - /// - /// The percentage of the outline to draw, with the following behavior: - /// - /// If negative, the outline is drawn in the clockwise direction. - /// The integer part determines the starting corner index. - /// The fractional part determines the percentage of the outline to draw. - /// Example: 0.35 starts at corner 0 and draws 35% in CCW; -2.7 starts at corner 2 and draws 70% in CW. - /// - /// - /// The thickness of the outline. - /// The color of the outline. - /// The type of line cap to use at the ends of each segment. - /// The number of points used for the cap. - /// - /// Useful for progress indicators or animated outlines. - /// - public static void DrawOutlinePercentage(this List shapePoints, float f, float lineThickness, ColorRgba color, LineCapType capType = LineCapType.CappedExtended, int capPoints = 2) - { - if (shapePoints.Count < 3 || f == 0f) return; - - bool negative = false; - if (f < 0) - { - negative = true; - f *= -1; - } - int startIndex = (int)f; - float percentage = f - startIndex; - if (percentage <= 0) - { - return; - } - if (percentage >= 1) - { - DrawOutline(shapePoints, lineThickness, color, capType, capPoints); - return; - } - - float perimeter = 0f; - for (var i = 0; i < shapePoints.Count; i++) - { - var start = shapePoints[i]; - var end = shapePoints[(i + 1) % shapePoints.Count]; - var l = (end - start).Length(); - perimeter += l; - } - - DrawOutlinePerimeter(shapePoints, perimeter * f * (negative ? -1 : 1), startIndex, lineThickness, color, capType, capPoints); - } - - /// - /// Draws the polygon as a series of lines, scaling each side towards its origin by a specified factor. - /// - /// The list of points defining the polygon. - /// The line drawing information (thickness, color, cap type, etc.). - /// - /// The scale factor for each side: - /// - /// 0: no polygon is drawn - /// 1: the normal polygon is drawn - /// 0.5: each side is half as long - /// - /// - /// - /// The point along the line to scale from, in both directions. - /// Default is 0.5 (midpoint). - /// - /// - /// Allows for creative polygon effects such as shrinking or expanding sides. - /// - public static void DrawLinesScaled(this List shapePoints, LineDrawingInfo lineInfo, float sideScaleFactor, float sideScaleOrigin = 0.5f) - { - if (shapePoints.Count < 3) return; - if (sideScaleFactor <= 0) return; - - if (sideScaleFactor >= 1) - { - shapePoints.DrawOutline(lineInfo); - return; - } - for (var i = 0; i < shapePoints.Count; i++) - { - var start = shapePoints[i]; - var end = shapePoints[(i + 1) % shapePoints.Count]; - SegmentDrawing.DrawSegment(start, end, lineInfo, sideScaleFactor, sideScaleOrigin); - } - - } - - /// - /// Draws the polygon as a series of lines, scaling each side towards its origin by a specified factor, with transformation. - /// - /// The list of relative points defining the polygon. - /// The position of the polygon's center. - /// The scale factor for the polygon. - /// The rotation in degrees. - /// The line drawing information (thickness, color, cap type, etc.). - /// - /// The scale factor for each side: - /// - /// 0: no polygon is drawn - /// 1: the normal polygon is drawn - /// 0.5: each side is half as long - /// - /// - /// - /// The point along the line to scale from, in both directions. - /// Default is 0.5 (midpoint). - /// - public static void DrawLinesScaled(this List relativePoints, Vector2 pos, float size, float rotDeg, LineDrawingInfo lineInfo, float sideScaleFactor, float sideScaleOrigin = 0.5f) - { - if (relativePoints.Count < 3) return; - if (sideScaleFactor <= 0) return; - - if (sideScaleFactor >= 1) - { - relativePoints.DrawOutline(pos, size, rotDeg, lineInfo); - return; - } - - for (var i = 0; i < relativePoints.Count; i++) - { - var start = pos + (relativePoints[i] * size).Rotate(rotDeg * ShapeMath.DEGTORAD); - var end = pos + (relativePoints[(i + 1) % relativePoints.Count] * size).Rotate(rotDeg * ShapeMath.DEGTORAD); - SegmentDrawing.DrawSegment(start, end, lineInfo, sideScaleFactor, sideScaleOrigin); - } - - } - - /// - /// Draws the polygon as a series of lines, scaling each side towards its origin by a specified factor, using a . - /// - /// The list of relative points defining the polygon. - /// The transformation to apply (position, scale, rotation). - /// The line drawing information (thickness, color, cap type, etc.). - /// - /// The scale factor for each side: - /// - /// 0: no polygon is drawn - /// 1: the normal polygon is drawn - /// 0.5: each side is half as long - /// - /// - /// - /// The point along the line to scale from, in both directions. - /// Default is 0.5 (midpoint). - /// - public static void DrawLinesScaled(this List relativePoints, Transform2D transform, LineDrawingInfo lineInfo, float sideScaleFactor, float sideScaleOrigin = 0.5f) - { - DrawLinesScaled(relativePoints, transform.Position, transform.ScaledSize.Length, transform.RotationDeg, lineInfo, sideScaleFactor, sideScaleOrigin); - - } - - /// - /// Draws cornered outlines for a polygon, using absolute corner lengths. - /// - /// The list of points defining the polygon. - /// The thickness of the outline. - /// The color of the outline. - /// A list of lengths for each corner. - /// The type of line cap to use at the ends of each segment. - /// The number of points used for the cap. - /// - /// Each corner is drawn with the specified length, allowing for custom corner effects. - /// A corner is drawn from the previous point [i-1] to the current point [i] to the next point [i+1]. - /// - public static void DrawOutlineCornered(this List points, float lineThickness, ColorRgba color, List cornerLengths, LineCapType capType = LineCapType.CappedExtended, int capPoints = 2) - { - for (var i = 0; i < points.Count; i++) - { - float cornerLength = cornerLengths[i%cornerLengths.Count]; - var prev = points[(i - 1) % points.Count]; - var cur = points[i]; - var next = points[(i + 1) % points.Count]; - SegmentDrawing.DrawSegment(cur, cur + next.Normalize() * cornerLength, lineThickness, color, capType, capPoints); - SegmentDrawing.DrawSegment(cur, cur + prev.Normalize() * cornerLength, lineThickness, color, capType, capPoints); - } - } - - /// - /// Draws cornered outlines for a polygon, using relative corner factors. - /// - /// The list of points defining the polygon. - /// The thickness of the outline. - /// The color of the outline. - /// - /// A list of factors (0-1) for each corner, representing the relative length. - /// - /// Previous to current is interpolated with the specified corner factor. - /// Current to next is interpolated with the specified corner factor. - /// - /// - /// The type of line cap to use at the ends of each segment. - /// The number of points used for the cap. - /// - /// Each corner is drawn with a length relative to the side, allowing for proportional corner effects. - /// A corner is drawn from the previous point [i-1] to the current point [i] to the next point [i+1]. - /// - public static void DrawOutlineCorneredRelative(this List points, float lineThickness, ColorRgba color, List cornerFactors, LineCapType capType = LineCapType.CappedExtended, int capPoints = 2) - { - for (var i = 0; i < points.Count; i++) - { - float cornerF = cornerFactors[i%cornerFactors.Count]; - var prev = points[(i - 1) % points.Count]; - var cur = points[i]; - var next = points[(i + 1) % points.Count]; - SegmentDrawing.DrawSegment(cur, cur.Lerp(next, cornerF), lineThickness, color, capType, capPoints); - SegmentDrawing.DrawSegment(cur, cur.Lerp(prev, cornerF), lineThickness, color, capType, capPoints); - } - } - - /// - /// Draws cornered outlines for a polygon using absolute corner lengths and . - /// - /// The list of points defining the polygon. - /// A list of lengths for each corner. - /// The line drawing information (thickness, color, cap type, etc.). - /// A corner is drawn from the previous point [i-1] to the current point [i] to the next point [i+1] - public static void DrawOutlineCornered(this List points, List cornerLengths, LineDrawingInfo lineInfo) - { - for (var i = 0; i < points.Count; i++) - { - float cornerLength = cornerLengths[i%cornerLengths.Count]; - var prev = points[(i - 1) % points.Count]; - var cur = points[i]; - var next = points[(i + 1) % points.Count]; - SegmentDrawing.DrawSegment(cur, cur + next.Normalize() * cornerLength, lineInfo); - SegmentDrawing.DrawSegment(cur, cur + prev.Normalize() * cornerLength, lineInfo); - } - } - - /// - /// Draws cornered outlines for a polygon using relative corner factors and . - /// - /// The list of points defining the polygon. - /// - /// A list of factors (0-1) for each corner, representing the relative length. - /// - /// Previous to current is interpolated with the specified corner factor. - /// Current to next is interpolated with the specified corner factor. - /// - /// - /// The line drawing information (thickness, color, cap type, etc.). - /// A corner is drawn from the previous point [i-1] to the current point [i] to the next point [i+1] - public static void DrawOutlineCorneredRelative(this List points, List cornerFactors, LineDrawingInfo lineInfo) - { - for (var i = 0; i < points.Count; i++) - { - float cornerF = cornerFactors[i%cornerFactors.Count]; - var prev = points[(i - 1) % points.Count]; - var cur = points[i]; - var next = points[(i + 1) % points.Count]; - SegmentDrawing.DrawSegment(cur, cur.Lerp(next, cornerF), lineInfo); - SegmentDrawing.DrawSegment(cur, cur.Lerp(prev, cornerF), lineInfo); - } - } - - /// - /// Draws cornered outlines for a polygon, using a uniform corner length. - /// - /// The list of points defining the polygon. - /// The thickness of the outline. - /// The color of the outline. - /// The length for each corner. - /// The type of line cap to use at the ends of each segment. - /// The number of points used for the cap. - /// A corner is drawn from the previous point [i-1] to the current point [i] to the next point [i+1] - public static void DrawOutlineCornered(this List points, float lineThickness, ColorRgba color, float cornerLength, LineCapType capType = LineCapType.CappedExtended, int capPoints = 2) - { - for (var i = 0; i < points.Count; i++) - { - var prev = points[(i-1)%points.Count]; - var cur = points[i]; - var next = points[(i+1)%points.Count]; - SegmentDrawing.DrawSegment(cur, cur + next.Normalize() * cornerLength, lineThickness, color, capType, capPoints); - SegmentDrawing.DrawSegment(cur, cur + prev.Normalize() * cornerLength, lineThickness, color, capType, capPoints); - } - } - - /// - /// Draws cornered outlines for a polygon, using a uniform relative corner factor. - /// - /// The list of points defining the polygon. - /// The thickness of the outline. - /// The color of the outline. - /// - /// A uniform factor (0-1) for each corner, representing the relative length. - /// - /// Previous to current is interpolated with . - /// Current to next is interpolated with . - /// - /// - /// The type of line cap to use at the ends of each segment. - /// The number of points used for the cap. - /// A corner is drawn from the previous point [i-1] to the current point [i] to the next point [i+1] - public static void DrawOutlineCorneredRelative(this List points, float lineThickness, ColorRgba color, float cornerF, LineCapType capType = LineCapType.CappedExtended, int capPoints = 2) - { - for (var i = 0; i < points.Count; i++) - { - var prev = points[(i - 1) % points.Count]; - var cur = points[i]; - var next = points[(i + 1) % points.Count]; - SegmentDrawing.DrawSegment(cur, cur.Lerp(next, cornerF), lineThickness, color, capType, capPoints); - SegmentDrawing.DrawSegment(cur, cur.Lerp(prev, cornerF), lineThickness, color, capType, capPoints); - } - } - - /// - /// Draws cornered outlines for a polygon, using a uniform corner length and . - /// - /// The list of points defining the polygon. - /// The length for each corner. - /// The line drawing information (thickness, color, cap type, etc.). - /// A corner is drawn from the previous point [i-1] to the current point [i] to the next point [i+1] - public static void DrawOutlineCornered(this List points, float cornerLength, LineDrawingInfo lineInfo) - { - for (var i = 0; i < points.Count; i++) - { - var prev = points[(i-1)%points.Count]; - var cur = points[i]; - var next = points[(i+1)%points.Count]; - SegmentDrawing.DrawSegment(cur, cur + next.Normalize() * cornerLength, lineInfo); - SegmentDrawing.DrawSegment(cur, cur + prev.Normalize() * cornerLength, lineInfo); - } - } - - /// - /// Draws cornered outlines for a polygon, using a uniform relative corner factor and . - /// - /// The list of points defining the polygon. - /// - /// A uniform factor (0-1) for each corner, representing the relative length. - /// - /// Previous to current is interpolated with . - /// Current to next is interpolated with . - /// - /// - /// The line drawing information (thickness, color, cap type, etc.). - /// A corner is drawn from the previous point [i-1] to the current point [i] to the next point [i+1] - public static void DrawOutlineCorneredRelative(this List points, float cornerF, LineDrawingInfo lineInfo) - { - for (var i = 0; i < points.Count; i++) - { - var prev = points[(i - 1) % points.Count]; - var cur = points[i]; - var next = points[(i + 1) % points.Count]; - SegmentDrawing.DrawSegment(cur, cur.Lerp(next, cornerF), lineInfo); - SegmentDrawing.DrawSegment(cur, cur.Lerp(prev, cornerF), lineInfo); - } - } - - /// - /// Draws cornered outlines for a polygon using and a uniform corner length. - /// - /// The list of points defining the polygon. - /// The line drawing information (thickness, color, cap type, etc.). - /// The length for each corner. - /// A corner is drawn from the previous point [i-1] to the current point [i] to the next point [i+1] - public static void DrawOutlineCornered(this List points, LineDrawingInfo lineInfo, float cornerLength) - => DrawOutlineCornered(points, lineInfo.Thickness,lineInfo.Color, cornerLength, lineInfo.CapType, lineInfo.CapPoints); - - /// - /// Draws cornered outlines for a polygon using and a list of corner lengths. - /// - /// The list of points defining the polygon. - /// The line drawing information (thickness, color, cap type, etc.). - /// A list of lengths for each corner. - public static void DrawOutlineCornered(this List points, LineDrawingInfo lineInfo, List cornerLengths) - => DrawOutlineCornered(points, lineInfo.Thickness,lineInfo.Color, cornerLengths, lineInfo.CapType, lineInfo.CapPoints); - - /// - /// Draws cornered outlines for a polygon using and a uniform relative corner factor. - /// - /// The list of points defining the polygon. - /// The line drawing information (thickness, color, cap type, etc.). - /// - /// A uniform factor (0-1) for each corner, representing the relative length. - /// - /// Previous to current is interpolated with . - /// Current to next is interpolated with . - /// - /// - /// A corner is drawn from the previous point [i-1] to the current point [i] to the next point [i+1] - public static void DrawOutlineCorneredRelative(this List points, LineDrawingInfo lineInfo, float cornerF) - => DrawOutlineCornered(points, lineInfo.Thickness,lineInfo.Color, cornerF, lineInfo.CapType, lineInfo.CapPoints); - - /// - /// Draws cornered outlines for a polygon using and a list of relative corner factors. - /// - /// The list of points defining the polygon. - /// The line drawing information (thickness, color, cap type, etc.). - /// - /// A list of factors (0-1) for each corner, representing the relative length. - /// - /// Previous to current is interpolated with the specified corner factor. - /// Current to next is interpolated with the specified corner factor. - /// - /// - /// A corner is drawn from the previous point [i-1] to the current point [i] to the next point [i+1] - public static void DrawOutlineCorneredRelative(this List points, LineDrawingInfo lineInfo, List cornerFactors) - => DrawOutlineCornered(points, lineInfo.Thickness,lineInfo.Color, cornerFactors, lineInfo.CapType, lineInfo.CapPoints); - - /// - /// Draws circles at each vertex of the polygon. - /// - /// The list of points (vertices) to draw. - /// The radius of each vertex circle. - /// The color of the vertex circles. - /// The number of segments to use for each circle. - /// - /// Useful for visualizing vertices or debugging polygon shapes. - /// - public static void DrawVertices(this List points, float vertexRadius, ColorRgba color, int circleSegments) - { - foreach (var p in points) - { - CircleDrawing.DrawCircle(p, vertexRadius, color, circleSegments); - } - } -} \ No newline at end of file diff --git a/ShapeEngine/Geometry/ShapeList.cs b/ShapeEngine/Geometry/ShapeList.cs index a63bcb2d..3e5569a3 100644 --- a/ShapeEngine/Geometry/ShapeList.cs +++ b/ShapeEngine/Geometry/ShapeList.cs @@ -7,9 +7,6 @@ namespace ShapeEngine.Geometry; /// A strongly-typed list for shapes, providing utility methods for copying, random access, and index validation. /// /// The type of elements in the list. -/// -/// ShapeList does not perform deep copies in its method. -/// public class ShapeList : List { /// @@ -25,6 +22,7 @@ public ShapeList(int capacity) : base(capacity) { } + /// /// Adds a range of items to the list. /// @@ -44,6 +42,21 @@ public virtual ShapeList Copy() newList.AddRange(this); return newList; } + + /// + /// Copies the contents of this list into . + /// + /// The destination list that will be cleared and populated with the current elements. + /// + /// This method performs a shallow copy of the elements and does not modify the current list. + /// + public virtual void Copy(ShapeList result) + { + result.Clear(); + result.EnsureCapacity(Count); + result.AddRange(this); + } + /// /// Determines whether the specified index is valid for this list. /// @@ -53,6 +66,7 @@ public bool IsIndexValid(int index) { return index >= 0 && index < Count; } + /// /// Returns a hash code for the list based on its elements. /// @@ -71,21 +85,42 @@ public override int GetHashCode() /// Gets a random item from the list, or null if the list is empty. /// /// A random item, or null if the list is empty. - public T? GetRandomItem() => Rng.Instance.RandCollection(this); + public T? GetRandomItem() + { + return Rng.Instance.RandCollection(this); + } /// /// Gets a list of random items from the list. /// /// The number of random items to retrieve. /// A list of random items. - public List GetRandomItems(int amount) => Rng.Instance.RandCollection(this, amount); + public List GetRandomItems(int amount) + { + return Rng.Instance.RandCollection(this, amount); + } + + /// + /// Selects random items from the list and writes them into . + /// + /// The destination list that will receive the selected items. + /// The number of random items to retrieve. + /// The number of items written to . + public int GetRandomItems(List result, int amount) + { + return Rng.Instance.RandCollection(this, result, amount); + } + /// /// Gets the item at the specified index, wrapping the index if necessary. /// /// The index of the item to retrieve. /// The item at the wrapped index, or the default value if the list is empty. - public T? GetItem(int index) => Count <= 0 ? default(T) : this[ShapeMath.WrapIndex(Count, index)]; - + public T? GetItem(int index) + { + return Count <= 0 ? default(T) : this[ShapeMath.WrapIndex(Count, index)]; + } + /// /// Retrieves and removes a random item from the list. /// diff --git a/ShapeEngine/Geometry/TriangleDef/Triangle.cs b/ShapeEngine/Geometry/TriangleDef/Triangle.cs index 974e9eda..e1a8b53f 100644 --- a/ShapeEngine/Geometry/TriangleDef/Triangle.cs +++ b/ShapeEngine/Geometry/TriangleDef/Triangle.cs @@ -22,6 +22,7 @@ namespace ShapeEngine.Geometry.TriangleDef; /// public readonly partial struct Triangle : IEquatable, IShapeTypeProvider, IClosedShapeTypeProvider { + #region Members /// /// The first vertex of the triangle (point A). @@ -127,6 +128,12 @@ public Triangle(Vector2 p, Segment s) } } #endregion + + #region Helper + + private static Points triangulationPointList = new(); + + #endregion #region Shapes @@ -184,6 +191,13 @@ public Circle GetCircumCircle() /// A Points collection containing the three vertices of the triangle. public Points ToPoints() => new() {A, B, C}; + /// + /// Converts the triangle to a polyline representation. + /// + /// A polyline containing the three vertices of the triangle. + /// The resulting polyline represents the triangle's perimeter as a series of connected line segments. + public Polyline ToPolyline() => new() { A, B, C }; + /// /// Converts the triangle to a polygon representation. /// @@ -195,20 +209,49 @@ public Circle GetCircumCircle() /// Converts this triangle to a polygon by adding its vertices to the provided polygon. /// /// A reference to a that will be cleared and filled with the triangle's vertices. - public void ToPolygon(ref Polygon result) + public void ToPolygon(Polygon result) + { + result.Clear(); + result.EnsureCapacity(3); + + result.Add(A); + result.Add(B); + result.Add(C); + } + + /// + /// Converts this triangle to a collection by writing its three vertices into . + /// + /// The destination collection that will be cleared and populated with the triangle vertices. + /// + /// Points are written in vertex order: , , . + /// + public void ToPoints(Points result) { - if(result.Count > 0) result.Clear(); + result.Clear(); + result.EnsureCapacity(3); + result.Add(A); result.Add(B); result.Add(C); } /// - /// Converts the triangle to a polyline representation. + /// Converts this triangle to a by writing its three vertices into . /// - /// A polyline containing the three vertices of the triangle. - /// The resulting polyline represents the triangle's perimeter as a series of connected line segments. - public Polyline ToPolyline() => new() { A, B, C }; + /// The destination polyline that will be cleared and populated with the triangle vertices. + /// + /// Points are written in vertex order: , , . The first point is not repeated at the end. + /// + public void ToPolyline(Polyline result) + { + result.Clear(); + result.EnsureCapacity(3); + + result.Add(A); + result.Add(B); + result.Add(C); + } /// /// Gets all three edges of the triangle as a collection of line segments. @@ -217,6 +260,22 @@ public void ToPolygon(ref Polygon result) /// The segments are ordered as: A-B, B-C, C-A, maintaining the counter-clockwise orientation. public Segments GetEdges() => new() { SegmentAToB, SegmentBToC, SegmentCToA }; + /// + /// Gets all three edges of the triangle and writes them into . + /// + /// The destination collection that will be cleared and populated with the triangle edges. + /// + /// Segments are written in this order: A-B, B-C, C-A. + /// + public void GetEdges(Segments result) + { + result.Clear(); + result.EnsureCapacity(3); + result.Add(SegmentAToB); + result.Add(SegmentBToC); + result.Add(SegmentCToA); + } + /// /// Constructs an adjacent triangle sharing an edge with this triangle, using the specified point as the third vertex. /// @@ -236,78 +295,92 @@ public Triangle ConstructAdjacentTriangle(Vector2 p) var closest = GetClosestSegment(p, out float _); return new Triangle(p, closest.segment); } - + /// - /// Triangulates this triangle using its centroid as an interior point. + /// Triangulates this triangle using its centroid as the interior vertex and writes the result into . /// - /// A triangulation containing three sub-triangles formed by connecting the centroid to each vertex. - /// This creates a simple triangulation by connecting the triangle's centroid to each of its vertices. - public Triangulation Triangulate() => this.Triangulate(GetCentroid()); - + /// The destination triangulation that will be cleared and populated with the generated sub-triangles. + /// + /// This creates three triangles by connecting the centroid to each edge of the triangle. + /// + public void Triangulate(Triangulation result) => this.Triangulate(result, GetCentroid()); + /// - /// Triangulates this triangle by adding random interior points and performing Delaunay triangulation. + /// Triangulates this triangle by adding random interior points and performing point-cloud triangulation into . /// + /// The destination triangulation that will be cleared and populated with the generated triangles. /// The number of random interior points to add before triangulation. - /// A Delaunay triangulation of the triangle with the specified number of interior points. /// - /// If pointCount is negative, returns a triangulation containing only this triangle. - /// Random points are generated using barycentric coordinates to ensure they lie within the triangle. + /// If is negative, receives only this triangle. + /// Otherwise, the triangle vertices and the generated interior points are passed to TriangulatePointCloud. + /// Random points are generated using barycentric coordinates so they lie inside the triangle. /// - public Triangulation Triangulate(int pointCount) + public void Triangulate(Triangulation result, int pointCount) { - if (pointCount < 0) return new() { new(A, B, C) }; - - Points points = new() { A, B, C }; - + result.Clear(); + if (pointCount < 0) + { + result.Add(new(A, B, C)); + return; + } + triangulationPointList.Clear(); + triangulationPointList.Add(A); + triangulationPointList.Add(B); + triangulationPointList.Add(C); + for (int i = 0; i < pointCount; i++) { float f1 = Rng.Instance.RandF(); float f2 = Rng.Instance.RandF(); Vector2 randPoint = GetPoint(f1, f2); - points.Add(randPoint); + triangulationPointList.Add(randPoint); } - return Polygon.TriangulateDelaunay(points); + triangulationPointList.TriangulatePointCloud(result); } /// - /// Triangulates this triangle to achieve a target minimum area per sub-triangle. + /// Triangulates this triangle based on a target minimum area and writes the result into . /// + /// The destination triangulation that will be cleared and populated with the generated triangles. /// The minimum area that each resulting triangle should have. - /// A triangulation where each sub-triangle has approximately the specified minimum area. /// - /// If minArea is less than or equal to zero, returns a triangulation containing only this triangle. - /// The method calculates the number of points needed based on the ratio of triangle area to minimum area. + /// If is less than or equal to zero, receives only this triangle. + /// Otherwise, the method estimates how many interior random points to generate from the ratio of triangle area to , then delegates to . /// - public Triangulation Triangulate(float minArea) + public void Triangulate(Triangulation result, float minArea) { - if (minArea <= 0) return new() { new(A,B,C) }; + result.Clear(); + if (minArea <= 0) + { + result.Add(new(A, B, C)); + return; + } float triArea = GetArea(); float pieceCount = triArea / minArea; int points = (int)MathF.Floor((pieceCount - 1f) * 0.5f); - return Triangulate(points); + Triangulate(result, points); } /// - /// Triangulates this triangle using the specified point as an interior vertex. + /// Triangulates this triangle using the specified point as an interior vertex and writes the result into . /// + /// The destination triangulation that will be cleared and populated with the generated sub-triangles. /// The interior point to use for triangulation. - /// A triangulation containing three sub-triangles formed by connecting the point to each edge. /// /// The resulting triangulation contains three triangles: A-B-P, B-C-P, and C-A-P. /// This method does not verify that the point is actually inside the triangle. /// - public Triangulation Triangulate(Vector2 p) + public void Triangulate(Triangulation result, Vector2 p) { - return new() - { - new(A, B, p), - new(B, C, p), - new(C, A, p) - }; + result.Clear(); + result.Add(new(A, B, p)); + result.Add(new(B, C, p)); + result.Add(new(C, A, p)); } + /// /// Creates a smaller triangle inside this triangle by interpolating along each edge. /// @@ -380,6 +453,24 @@ public Points GetRandomPointsInside(int amount) return points; } + /// + /// Writes a specified number of random points inside this triangle into . + /// + /// The destination collection that will be cleared and populated with the generated points. + /// The number of random interior points to generate. + /// + /// Each point is generated independently using . + /// + public void GetRandomPointsInside(Points result, int amount) + { + result.Clear(); + result.EnsureCapacity(amount); + for (int i = 0; i < amount; i++) + { + result.Add(GetRandomPointInside()); + } + } + /// /// Selects one of the triangle's vertices at random. /// @@ -396,22 +487,48 @@ public Vector2 GetRandomVertex() /// Selects one of the triangle's edges at random. /// /// One of the three edges selected randomly with equal probability. - public Segment GetRandomEdge() => GetEdges().GetRandomSegment(); - + public Segment GetRandomEdge() + { + return GetEdges().GetRandomSegment(); + } + /// /// Generates a random point on the triangle's perimeter. /// /// A point positioned randomly on one of the triangle's edges. /// The point can be anywhere along any of the three edges with uniform probability distribution. - public Vector2 GetRandomPointOnEdge() => GetRandomEdge().GetRandomPoint(); - + public Vector2 GetRandomPointOnEdge() + { + return GetRandomEdge().GetRandomPoint(); + } + /// /// Generates multiple random points on the triangle's perimeter. /// /// The number of random points to generate on the edges. - /// A collection of points positioned randomly along the triangle's perimeter. - public Points GetRandomPointsOnEdge(int amount) => GetEdges().GetRandomPoints(amount); + /// A collection of points positioned randomly along the triangle's perimeter with uniform probability distribution. + public Points GetRandomPointsOnEdge(int amount) + { + var edges = GetEdges(); + var points = new Points(); + edges.GetRandomPoints(amount, points); + return points; + } + /// + /// Writes a specified number of random points on the triangle perimeter into . + /// + /// The destination collection that receives the generated edge points. + /// The number of random perimeter points to generate. + /// + /// Edge selection and sampling are delegated to using the triangle's three edges. + /// + public void GetRandomPointsOnEdge(Points result, int amount) + { + var edges = GetEdges(); + edges.GetRandomPoints(amount, result); + } + /// /// Gets a specific edge of the triangle by index. /// @@ -424,7 +541,8 @@ public Vector2 GetRandomVertex() public Segment GetSegment(int index) { if (index < 0) return new Segment(); - var i = index % 3; + // var i = index % 3; + var i = ShapeMath.WrapIndex(3, index); if(i == 0) return SegmentAToB; if(i == 1) return SegmentBToC; return SegmentCToA; @@ -508,8 +626,16 @@ public override readonly int GetHashCode() return HashCode.Combine(A, B, C); } + /// + /// Gets the closed-shape type represented by this instance. + /// + /// . public ClosedShapeType GetClosedShapeType() => ClosedShapeType.Triangle; + /// + /// Gets the general shape type represented by this instance. + /// + /// . public ShapeType GetShapeType() => ShapeType.Triangle; /// @@ -836,6 +962,24 @@ public Points GetInterpolatedEdgePoints(float t) return new Points(3){a1, b1, c1}; } + /// + /// Writes one interpolated point per triangle edge into . + /// + /// The destination collection that will be cleared and populated with the interpolated edge points. + /// The interpolation factor applied along each edge. + /// + /// Points are written for edges A-B, B-C, and C-A in that order. + /// + public void GetInterpolatedEdgePoints(Points result, float t) + { + result.Clear(); + result.EnsureCapacity(3); + + result.Add(A.Lerp(B, t)); + result.Add(B.Lerp(C, t)); + result.Add(C.Lerp(A, t)); + } + /// /// Gets interpolated points by performing multiple steps of edge interpolation, creating a fractal-like effect. /// @@ -850,6 +994,28 @@ public Points GetInterpolatedEdgePoints(float t) public Points GetInterpolatedEdgePoints(float t, int steps) { if(steps <= 1) return GetInterpolatedEdgePoints(t); + + var result = new Points(3); + GetInterpolatedEdgePoints(result, t, steps); + return result; + } + + /// + /// Performs repeated edge interpolation and writes the final three points into . + /// + /// The destination collection that receives the final interpolated points. + /// The interpolation factor applied at each step. + /// The number of interpolation iterations to perform. Values less than or equal to one fall back to a single interpolation pass. + /// + /// Each iteration interpolates between the points produced by the previous iteration while preserving the triangle edge order. + /// + public void GetInterpolatedEdgePoints(Points result, float t, int steps) + { + if (steps <= 1) + { + GetInterpolatedEdgePoints(result, t); + return; + } var a1 = A.Lerp(B, t); var b1 = B.Lerp(C, t); @@ -869,8 +1035,12 @@ public Points GetInterpolatedEdgePoints(float t, int steps) remainingSteps--; } - return new Points(3){a1, b1, c1}; + result.Clear(); + result.EnsureCapacity(3); + + result.Add(a1); + result.Add(b1); + result.Add(c1); } - #endregion } diff --git a/ShapeEngine/Geometry/TriangleDef/TriangleDrawing.cs b/ShapeEngine/Geometry/TriangleDef/TriangleDrawing.cs index 25687c5c..6c380224 100644 --- a/ShapeEngine/Geometry/TriangleDef/TriangleDrawing.cs +++ b/ShapeEngine/Geometry/TriangleDef/TriangleDrawing.cs @@ -1,13 +1,14 @@ using System.Numerics; using Raylib_cs; using ShapeEngine.Color; +using ShapeEngine.Core.Structs; using ShapeEngine.Geometry.CircleDef; using ShapeEngine.Geometry.PolygonDef; using ShapeEngine.Geometry.QuadDef; using ShapeEngine.Geometry.RectDef; using ShapeEngine.Geometry.SegmentDef; -using ShapeEngine.Geometry.TriangulationDef; using ShapeEngine.StaticLib; +using Ray = ShapeEngine.Geometry.RayDef.Ray; namespace ShapeEngine.Geometry.TriangleDef; @@ -15,11 +16,721 @@ namespace ShapeEngine.Geometry.TriangleDef; /// Provides static methods for drawing triangles and collections of triangles with various styles and options. /// /// -/// This class contains extension methods for drawing and objects, +/// This class contains extension methods for drawing objects, /// as well as static methods for drawing triangles using raw vertex data. Supports filled, outlined, partial, and scaled outlines. /// public static class TriangleDrawing { + #region Draw + + /// + /// Draws a filled triangle using the specified vertices and color. + /// + /// The first vertex of the triangle. + /// The second vertex of the triangle. + /// The third vertex of the triangle. + /// The color to fill the triangle with. + public static void DrawTriangle(Vector2 a, Vector2 b, Vector2 c, ColorRgba color) + { + Raylib.DrawTriangle(a, b, c, color.ToRayColor()); + } + + /// + /// Draws a filled triangle using the specified and color. + /// + /// The triangle to draw. + /// The color to fill the triangle with. + public static void Draw(this Triangle t, ColorRgba color) + { + Raylib.DrawTriangle(t.A, t.B, t.C, color.ToRayColor()); + } + + #endregion + + #region Draw Scaled + + /// + /// Draws a triangle with scaled sides based on a specific draw type. + /// + /// The triangle to draw. + /// The color of the drawn shape. + /// The scale factor of the sides (0 to 1). If >= 1, the full triangle is drawn. If <= 0, nothing is drawn. + /// The origin point for scaling the sides (0 = start, 1 = end, 0.5 = center). + /// + /// The style of drawing: + /// + /// 0: [Filled] Drawn as 4 filled triangles, effectivly cutting of corners. + /// 1: [Sides] Each side is connected to the triangle's centroid. + /// 2: [Sides Inverse] The start of 1 side is connected to the end of the next side and is connected to the triangle's centroid. + /// + /// + public static void DrawScaled(this Triangle t, ColorRgba color, float sideScaleFactor, float sideScaleOrigin, int drawType) + { + if (sideScaleFactor <= 0) return; + if (sideScaleFactor >= 1) + { + t.Draw(color); + return; + } + + var s1 = new Segment(t.A, t.B).ScaleSegment(sideScaleFactor, sideScaleOrigin); + var s2 = new Segment(t.B, t.C).ScaleSegment(sideScaleFactor, sideScaleOrigin); + var s3 = new Segment(t.C, t.A).ScaleSegment(sideScaleFactor, sideScaleOrigin); + + var rayColor = color.ToRayColor(); + if (drawType == 0) + { + Raylib.DrawTriangle(s1.Start, s1.End, s2.Start, rayColor); + Raylib.DrawTriangle(s1.Start, s2.Start, s3.End, rayColor); + Raylib.DrawTriangle(s3.End, s2.Start, s2.End, rayColor); + Raylib.DrawTriangle(s3.End, s2.End, s3.Start, rayColor); + } + else if (drawType == 1) + { + var center = t.GetCentroid(); + Raylib.DrawTriangle(s1.Start, s1.End, center, rayColor); + Raylib.DrawTriangle(s2.Start, s2.End, center, rayColor); + Raylib.DrawTriangle(s3.Start, s3.End, center, rayColor); + } + else + { + var center = t.GetCentroid(); + Raylib.DrawTriangle(s3.End, s1.Start, center, rayColor); + Raylib.DrawTriangle(s1.End, s2.Start, center, rayColor); + Raylib.DrawTriangle(s2.End, s3.Start, center, rayColor); + } + } + + #endregion + + #region Draw Lines + + /// + /// Draws the outline of the triangle with specified line thickness and color. + /// + /// The triangle to draw. + /// The thickness of the outline lines. + /// The color of the outline. + /// The limit for miter joins to prevent sharp spikes. Defaults to 4f. + /// If true, uses beveled joins when the miter limit is exceeded; otherwise, cuts off the miter point. + public static void DrawLines(this Triangle t, float lineThickness, ColorRgba color, float miterLimit = 4f, bool beveled = true) + { + DrawLinesHelper(t, lineThickness, color, miterLimit, beveled); + } + + /// + /// Draws the outline of the triangle using . + /// + /// The triangle to draw. + /// Contains line style information such as thickness and color. + /// The limit for miter joins to prevent sharp spikes. Defaults to 4f. + /// If true, uses beveled joins when the miter limit is exceeded; otherwise, cuts off the miter point. + public static void DrawLines(this Triangle t, LineDrawingInfo lineInfo, float miterLimit = 4f, bool beveled = true) + { + DrawLinesHelper(t, lineInfo.Thickness, lineInfo.Color, miterLimit, beveled); + } + + #endregion + + #region Draw Lines Percentage + + /// + /// Draws a partial outline of the triangle based on a percentage coverage. + /// + /// The triangle to draw. + /// The percentage of the outline to draw (0 to 1). Negative values reverse the drawing direction. + /// The index of the corner to start drawing from (0, 1, or 2). + /// Contains line style information such as thickness and color. + /// The limit for miter joins to prevent sharp spikes. Defaults to 4f. + /// If true, uses beveled joins when the miter limit is exceeded; otherwise, cuts off the miter point. + public static void DrawLinesPercentage(this Triangle t, float f, int startCorner, LineDrawingInfo lineInfo, float miterLimit = 4f, bool beveled = true) + { + t.DrawLinesPercentage(f, startCorner, lineInfo.Thickness, lineInfo.Color, miterLimit, beveled); + } + + /// + /// Draws a partial outline of the triangle based on a percentage coverage. + /// + /// The triangle to draw. + /// The percentage of the outline to draw (0 to 1). Negative values reverse the drawing direction. + /// The index of the corner to start drawing from (0, 1, or 2). + /// The thickness of the outline lines. + /// The color of the outline. + /// The limit for miter joins to prevent sharp spikes. Defaults to 4f. + /// If true, uses beveled joins when the miter limit is exceeded; otherwise, cuts off the miter point. + public static void DrawLinesPercentage(this Triangle t, float f, int startCorner, float lineThickness, ColorRgba color, float miterLimit = 4f, bool beveled = true) + { + bool cw = false; + if (f < 0) + { + cw = true; + f *= -1; + } + + f = ShapeMath.Clamp(0f, 1f, f); + + if (f <= 0) return; + if (f >= 1) + { + t.DrawLines(lineThickness, color, miterLimit, beveled); + return; + } + + startCorner = ShapeMath.WrapI(startCorner, 0, 3); + + Triangle triangle; + + if (startCorner == 0) + { + triangle = cw ? new(t.A, t.C, t.B) : new(t.A, t.B, t.C); + } + else if (startCorner == 1) + { + triangle = cw ? new(t.C, t.B, t.A) : new(t.B, t.C, t.A); + } + else + { + triangle = cw ? new(t.B, t.A, t.C) : new(t.C, t.A, t.B); + } + + DrawLinesPercentageHelper(triangle, f, !cw, lineThickness, color, miterLimit, beveled); + } + + #endregion + + #region Draw Lines Scaled + + /// + /// Draws a triangle outline where each side can be scaled towards the origin of the side. + /// + /// The triangle to draw. + /// The line drawing information (thickness, color, cap type, etc.). + /// The scale factor for each side (0 = No Side, 1 = Full Side). + /// The point along each side to scale from in both directions (0 = Start, 1 = End). + /// + /// Allows for dynamic scaling of triangle sides, useful for effects or partial outlines. + /// + public static void DrawLinesScaled(this Triangle t, LineDrawingInfo lineInfo, float sideScaleFactor, float sideScaleOrigin = 0.5f) + { + if (sideScaleFactor <= 0) return; + if (sideScaleFactor >= 1) + { + t.DrawLines(lineInfo); + return; + } + + SegmentDrawing.DrawSegment(t.A, t.B, lineInfo, sideScaleFactor, sideScaleOrigin); + SegmentDrawing.DrawSegment(t.B, t.C, lineInfo, sideScaleFactor, sideScaleOrigin); + SegmentDrawing.DrawSegment(t.C, t.A, lineInfo, sideScaleFactor, sideScaleOrigin); + } + + #endregion + + #region Draw Corners + + /// + /// Draws the corners of the triangle with a specific length along the edges. + /// + /// The triangle to draw the corners for. + /// The thickness of the corner lines. + /// The color of the corners. + /// The absolute length of the corner segments along the edges from each vertex. + /// The limit for miter joins to prevent sharp spikes. Defaults to 4f. + /// If true, uses beveled joins when the miter limit is exceeded; otherwise, cuts off the miter point. + public static void DrawCorners(this Triangle triangle, float lineThickness, ColorRgba color, float cornerLength, float miterLimit = 4f, bool beveled = true) + { + if(triangle.IsCollinear()) return; + + if(cornerLength <= 0f) return; + + var edgeAB = triangle.B - triangle.A; + var edgeBC = triangle.C - triangle.B; + var edgeCA = triangle.A - triangle.C; + + var lengthAB = edgeAB.Length(); + if (lengthAB <= 0f) return; + + var lengthBC = edgeBC.Length(); + if (lengthBC <= 0f) return; + + var lengthCA = edgeCA.Length(); + if (lengthCA <= 0f) return; + + var minLength = MathF.Min(lengthAB, MathF.Min(lengthBC, lengthCA)); + if (minLength <= cornerLength) + { + triangle.DrawLines(lineThickness, color, miterLimit, beveled); + return; + } + var cornerLengthFactor = ShapeMath.Clamp(0f, 1f, cornerLength / minLength); + + float totalMiterLengthLimit = lineThickness * 0.5f * MathF.Max(2f, miterLimit); + + float perimeter = lengthAB + lengthBC + lengthCA; + Vector2 incenter = (triangle.A * lengthBC + triangle.B * lengthCA + triangle.C * lengthAB) / perimeter; + + var innerMaxLengthA = (incenter - triangle.A).Length(); + if(innerMaxLengthA <= 0f) return; + + var innerMaxLengthB = (incenter - triangle.B).Length(); + if(innerMaxLengthB <= 0f) return; + + var innerMaxLengthC = (incenter - triangle.C).Length(); + if(innerMaxLengthC <= 0f) return; + + var edgeABDir = edgeAB / lengthAB; + var edgeBCDir = edgeBC / lengthBC; + var edgeCADir = edgeCA / lengthCA; + + var normalAB = edgeABDir.GetPerpendicularRight(); + var normalBC = edgeBCDir.GetPerpendicularRight(); + var normalCA = edgeCADir.GetPerpendicularRight(); + + var miterDirA = (normalCA + normalAB).Normalize(); + var miterDirB = (normalAB + normalBC).Normalize(); + var miterDirC = (normalBC + normalCA).Normalize(); + + float miterAngleRadA = MathF.Abs(miterDirA.AngleRad(normalAB)); + float miterLengthA = lineThickness / MathF.Cos(miterAngleRadA); + + float miterAngleRadB = MathF.Abs(miterDirB.AngleRad(normalBC)); + float miterLengthB = lineThickness / MathF.Cos(miterAngleRadB); + + float miterAngleRadC = MathF.Abs(miterDirC.AngleRad(normalCA)); + float miterLengthC = lineThickness / MathF.Cos(miterAngleRadC); + + Vector2 aOuterPrev, aOuterNext; + Vector2 bOuterPrev, bOuterNext; + Vector2 cOuterPrev, cOuterNext; + + Vector2 aInner = triangle.A - miterDirA * MathF.Min(miterLengthA, innerMaxLengthA); + Vector2 bInner = triangle.B - miterDirB * MathF.Min(miterLengthB, innerMaxLengthB); + Vector2 cInner = triangle.C - miterDirC * MathF.Min(miterLengthC, innerMaxLengthC); + + var rayColor = color.ToRayColor(); + + if (miterLimit < 2f || miterLengthA < totalMiterLengthLimit) + { + aOuterPrev = triangle.A + miterDirA * miterLengthA; + aOuterNext = aOuterPrev; + } + else + { + if (beveled) + { + aOuterPrev = triangle.A + normalCA * lineThickness; + aOuterNext = triangle.A + normalAB * lineThickness; + } + else + { + var p = triangle.A + miterDirA * totalMiterLengthLimit; + var dir = (p - triangle.A).Normalize(); + var pr = dir.GetPerpendicularRight(); + var offsetC = triangle.C + normalCA * lineThickness; + + var ip = Ray.IntersectRayRay(p, pr, offsetC, edgeCADir); + if (ip.Valid) + { + aOuterPrev = ip.Point; + aOuterNext = p - pr * (p - ip.Point).Length(); + } + else + { + aOuterPrev = triangle.A + normalCA * lineThickness; + aOuterNext = triangle.A + normalAB * lineThickness; + } + } + Raylib.DrawTriangle(aOuterPrev, aOuterNext, aInner, rayColor); + } + + if (miterLimit < 2f || miterLengthB < totalMiterLengthLimit) + { + bOuterPrev = triangle.B + miterDirB * miterLengthB; + bOuterNext = bOuterPrev; + } + else + { + if (beveled) + { + bOuterPrev = triangle.B + normalAB * lineThickness; + bOuterNext = triangle.B + normalBC * lineThickness; + } + else + { + var p = triangle.B + miterDirB * totalMiterLengthLimit; + var dir = (p - triangle.B).Normalize(); + var pr = dir.GetPerpendicularRight(); + var offsetA = triangle.A + normalAB * lineThickness; + + var ip = Ray.IntersectRayRay(p, pr, offsetA, edgeABDir); + if (ip.Valid) + { + bOuterPrev = ip.Point; + bOuterNext = p - pr * (p - ip.Point).Length(); + } + else + { + bOuterPrev = triangle.B + normalAB * lineThickness; + bOuterNext = triangle.B + normalBC * lineThickness; + } + } + Raylib.DrawTriangle(bOuterPrev, bOuterNext, bInner, rayColor); + } + + if (miterLimit < 2f || miterLengthC < totalMiterLengthLimit) + { + cOuterPrev = triangle.C + miterDirC * miterLengthC; + cOuterNext = cOuterPrev; + } + else + { + if (beveled) + { + cOuterPrev = triangle.C + normalBC * lineThickness; + cOuterNext = triangle.C + normalCA * lineThickness; + } + else + { + var p = triangle.C + miterDirC * totalMiterLengthLimit; + var dir = (p - triangle.C).Normalize(); + var pr = dir.GetPerpendicularRight(); + var offsetB = triangle.B + normalBC * lineThickness; + + var ip = Ray.IntersectRayRay(p, pr, offsetB, edgeBCDir); + if (ip.Valid) + { + cOuterPrev = ip.Point; + cOuterNext = p - pr * (p - ip.Point).Length(); + } + else + { + cOuterPrev = triangle.C + normalBC * lineThickness; + cOuterNext = triangle.C + normalCA * lineThickness; + } + } + Raylib.DrawTriangle(cOuterPrev, cOuterNext, cInner, rayColor); + } + + Vector2 sideACenter = triangle.A + edgeABDir * lengthAB * 0.5f; + Vector2 sideBCenter = triangle.B + edgeBCDir * lengthBC * 0.5f; + Vector2 sideCCenter = triangle.C + edgeCADir * lengthCA * 0.5f; + + Vector2 sideACenterOutside = sideACenter + normalAB * lineThickness; + Vector2 sideACenterInside = sideACenter - normalAB * lineThickness; + + Vector2 sideBCenterOutside = sideBCenter + normalBC * lineThickness; + Vector2 sideBCenterInside = sideBCenter - normalBC * lineThickness; + + Vector2 sideCCenterOutside = sideCCenter + normalCA * lineThickness; + Vector2 sideCCenterInside = sideCCenter - normalCA * lineThickness; + + var aInnerNextEnd = aInner.Lerp(sideACenterInside, cornerLengthFactor); + var aInnerPrevEnd = aInner.Lerp(sideCCenterInside, cornerLengthFactor); + var aOuterNextEnd = aOuterNext.Lerp(sideACenterOutside, cornerLengthFactor); + var aOuterPrevEnd = aOuterPrev.Lerp(sideCCenterOutside, cornerLengthFactor); + + var bInnerNextEnd = bInner.Lerp(sideBCenterInside, cornerLengthFactor); + var bInnerPrevEnd = bInner.Lerp(sideACenterInside, cornerLengthFactor); + var bOuterNextEnd = bOuterNext.Lerp(sideBCenterOutside, cornerLengthFactor); + var bOuterPrevEnd = bOuterPrev.Lerp(sideACenterOutside, cornerLengthFactor); + + var cInnerNextEnd = cInner.Lerp(sideCCenterInside, cornerLengthFactor); + var cInnerPrevEnd = cInner.Lerp(sideBCenterInside, cornerLengthFactor); + var cOuterNextEnd = cOuterNext.Lerp(sideCCenterOutside, cornerLengthFactor); + var cOuterPrevEnd = cOuterPrev.Lerp(sideBCenterOutside, cornerLengthFactor); + // var aInnerNextEnd = aInner.LerpByLength(sideACenterInside, cornerLength); + // var aInnerPrevEnd = aInner.LerpByLength(sideCCenterInside, cornerLength); + // var aOuterNextEnd = aOuterNext.LerpByLength(sideACenterOutside, cornerLength); + // var aOuterPrevEnd = aOuterPrev.LerpByLength(sideCCenterOutside, cornerLength); + // + // var bInnerNextEnd = bInner.LerpByLength(sideBCenterInside, cornerLength); + // var bInnerPrevEnd = bInner.LerpByLength(sideACenterInside, cornerLength); + // var bOuterNextEnd = bOuterNext.LerpByLength(sideBCenterOutside, cornerLength); + // var bOuterPrevEnd = bOuterPrev.LerpByLength(sideACenterOutside, cornerLength); + // + // var cInnerNextEnd = cInner.LerpByLength(sideCCenterInside, cornerLength); + // var cInnerPrevEnd = cInner.LerpByLength(sideBCenterInside, cornerLength); + // var cOuterNextEnd = cOuterNext.LerpByLength(sideCCenterOutside, cornerLength); + // var cOuterPrevEnd = cOuterPrev.LerpByLength(sideBCenterOutside, cornerLength); + + Raylib.DrawTriangle(aOuterPrev, aInner, aOuterPrevEnd, rayColor); + Raylib.DrawTriangle(aInner, aInnerPrevEnd, aOuterPrevEnd, rayColor); + + Raylib.DrawTriangle(aOuterNext, aOuterNextEnd, aInner, rayColor); + Raylib.DrawTriangle(aOuterNextEnd, aInnerNextEnd, aInner, rayColor); + + Raylib.DrawTriangle(bOuterPrev, bInner, bOuterPrevEnd, rayColor); + Raylib.DrawTriangle(bInner, bInnerPrevEnd, bOuterPrevEnd, rayColor); + + Raylib.DrawTriangle(bOuterNext, bOuterNextEnd, bInner, rayColor); + Raylib.DrawTriangle(bOuterNextEnd, bInnerNextEnd, bInner, rayColor); + + Raylib.DrawTriangle(cOuterPrev, cInner, cOuterPrevEnd, rayColor); + Raylib.DrawTriangle(cInner, cInnerPrevEnd, cOuterPrevEnd, rayColor); + + Raylib.DrawTriangle(cOuterNext, cOuterNextEnd, cInner, rayColor); + Raylib.DrawTriangle(cOuterNextEnd, cInnerNextEnd, cInner, rayColor); + } + + #endregion + + #region Draw Corners Relative + + /// + /// Draws the corners of the triangle where the length of the corner segments is relative to the shortest edge. + /// + /// The triangle to draw the corners for. + /// The thickness of the corner lines. + /// The color of the corners. + /// The length factor of the corner segments relative to the shortest edge (0 to 0.5 recommended). + /// The limit for miter joins to prevent sharp spikes. Defaults to 4f. + /// If true, uses beveled joins when the miter limit is exceeded; otherwise, cuts off the miter point. + public static void DrawCornersRelative(this Triangle triangle, float lineThickness, ColorRgba color, float cornerLengthFactor, float miterLimit = 4f, bool beveled = true) + { + if(triangle.IsCollinear()) return; + + if(cornerLengthFactor <= 0f) return; + + if(cornerLengthFactor >= 1f) + { + triangle.DrawLines(lineThickness, color, miterLimit, beveled); + return; + } + + float totalMiterLengthLimit = lineThickness * 0.5f * MathF.Max(2f, miterLimit); + + var edgeAB = triangle.B - triangle.A; + var edgeBC = triangle.C - triangle.B; + var edgeCA = triangle.A - triangle.C; + + var lengthAB = edgeAB.Length(); + if (lengthAB <= 0f) return; + + var lengthBC = edgeBC.Length(); + if (lengthBC <= 0f) return; + + var lengthCA = edgeCA.Length(); + if (lengthCA <= 0f) return; + + float perimeter = lengthAB + lengthBC + lengthCA; + Vector2 incenter = (triangle.A * lengthBC + triangle.B * lengthCA + triangle.C * lengthAB) / perimeter; + + var innerMaxLengthA = (incenter - triangle.A).Length(); + if(innerMaxLengthA <= 0f) return; + + var innerMaxLengthB = (incenter - triangle.B).Length(); + if(innerMaxLengthB <= 0f) return; + + var innerMaxLengthC = (incenter - triangle.C).Length(); + if(innerMaxLengthC <= 0f) return; + + var edgeABDir = edgeAB / lengthAB; + var edgeBCDir = edgeBC / lengthBC; + var edgeCADir = edgeCA / lengthCA; + + var normalAB = edgeABDir.GetPerpendicularRight(); + var normalBC = edgeBCDir.GetPerpendicularRight(); + var normalCA = edgeCADir.GetPerpendicularRight(); + + var miterDirA = (normalCA + normalAB).Normalize(); + var miterDirB = (normalAB + normalBC).Normalize(); + var miterDirC = (normalBC + normalCA).Normalize(); + + float miterAngleRadA = MathF.Abs(miterDirA.AngleRad(normalAB)); + float miterLengthA = lineThickness / MathF.Cos(miterAngleRadA); + + float miterAngleRadB = MathF.Abs(miterDirB.AngleRad(normalBC)); + float miterLengthB = lineThickness / MathF.Cos(miterAngleRadB); + + float miterAngleRadC = MathF.Abs(miterDirC.AngleRad(normalCA)); + float miterLengthC = lineThickness / MathF.Cos(miterAngleRadC); + + Vector2 aOuterPrev, aOuterNext; + Vector2 bOuterPrev, bOuterNext; + Vector2 cOuterPrev, cOuterNext; + + Vector2 aInner = triangle.A - miterDirA * MathF.Min(miterLengthA, innerMaxLengthA); + Vector2 bInner = triangle.B - miterDirB * MathF.Min(miterLengthB, innerMaxLengthB); + Vector2 cInner = triangle.C - miterDirC * MathF.Min(miterLengthC, innerMaxLengthC); + + var rayColor = color.ToRayColor(); + + if (miterLimit < 2f || miterLengthA < totalMiterLengthLimit) + { + aOuterPrev = triangle.A + miterDirA * miterLengthA; + aOuterNext = aOuterPrev; + } + else + { + if (beveled) + { + aOuterPrev = triangle.A + normalCA * lineThickness; + aOuterNext = triangle.A + normalAB * lineThickness; + } + else + { + var p = triangle.A + miterDirA * totalMiterLengthLimit; + var dir = (p - triangle.A).Normalize(); + var pr = dir.GetPerpendicularRight(); + var offsetC = triangle.C + normalCA * lineThickness; + + var ip = Ray.IntersectRayRay(p, pr, offsetC, edgeCADir); + if (ip.Valid) + { + aOuterPrev = ip.Point; + aOuterNext = p - pr * (p - ip.Point).Length(); + } + else + { + aOuterPrev = triangle.A + normalCA * lineThickness; + aOuterNext = triangle.A + normalAB * lineThickness; + } + } + Raylib.DrawTriangle(aOuterPrev, aOuterNext, aInner, rayColor); + } + + if (miterLimit < 2f || miterLengthB < totalMiterLengthLimit) + { + bOuterPrev = triangle.B + miterDirB * miterLengthB; + bOuterNext = bOuterPrev; + } + else + { + if (beveled) + { + bOuterPrev = triangle.B + normalAB * lineThickness; + bOuterNext = triangle.B + normalBC * lineThickness; + } + else + { + var p = triangle.B + miterDirB * totalMiterLengthLimit; + var dir = (p - triangle.B).Normalize(); + var pr = dir.GetPerpendicularRight(); + var offsetA = triangle.A + normalAB * lineThickness; + + var ip = Ray.IntersectRayRay(p, pr, offsetA, edgeABDir); + if (ip.Valid) + { + bOuterPrev = ip.Point; + bOuterNext = p - pr * (p - ip.Point).Length(); + } + else + { + bOuterPrev = triangle.B + normalAB * lineThickness; + bOuterNext = triangle.B + normalBC * lineThickness; + } + } + Raylib.DrawTriangle(bOuterPrev, bOuterNext, bInner, rayColor); + } + + if (miterLimit < 2f || miterLengthC < totalMiterLengthLimit) + { + cOuterPrev = triangle.C + miterDirC * miterLengthC; + cOuterNext = cOuterPrev; + } + else + { + if (beveled) + { + cOuterPrev = triangle.C + normalBC * lineThickness; + cOuterNext = triangle.C + normalCA * lineThickness; + } + else + { + var p = triangle.C + miterDirC * totalMiterLengthLimit; + var dir = (p - triangle.C).Normalize(); + var pr = dir.GetPerpendicularRight(); + var offsetB = triangle.B + normalBC * lineThickness; + + var ip = Ray.IntersectRayRay(p, pr, offsetB, edgeBCDir); + if (ip.Valid) + { + cOuterPrev = ip.Point; + cOuterNext = p - pr * (p - ip.Point).Length(); + } + else + { + cOuterPrev = triangle.C + normalBC * lineThickness; + cOuterNext = triangle.C + normalCA * lineThickness; + } + } + Raylib.DrawTriangle(cOuterPrev, cOuterNext, cInner, rayColor); + } + + Vector2 sideACenter = triangle.A + edgeABDir * lengthAB * 0.5f; + Vector2 sideBCenter = triangle.B + edgeBCDir * lengthBC * 0.5f; + Vector2 sideCCenter = triangle.C + edgeCADir * lengthCA * 0.5f; + + Vector2 sideACenterOutside = sideACenter + normalAB * lineThickness; + Vector2 sideACenterInside = sideACenter - normalAB * lineThickness; + + Vector2 sideBCenterOutside = sideBCenter + normalBC * lineThickness; + Vector2 sideBCenterInside = sideBCenter - normalBC * lineThickness; + + Vector2 sideCCenterOutside = sideCCenter + normalCA * lineThickness; + Vector2 sideCCenterInside = sideCCenter - normalCA * lineThickness; + + var aInnerNextEnd = aInner.Lerp(sideACenterInside, cornerLengthFactor); + var aInnerPrevEnd = aInner.Lerp(sideCCenterInside, cornerLengthFactor); + var aOuterNextEnd = aOuterNext.Lerp(sideACenterOutside, cornerLengthFactor); + var aOuterPrevEnd = aOuterPrev.Lerp(sideCCenterOutside, cornerLengthFactor); + + var bInnerNextEnd = bInner.Lerp(sideBCenterInside, cornerLengthFactor); + var bInnerPrevEnd = bInner.Lerp(sideACenterInside, cornerLengthFactor); + var bOuterNextEnd = bOuterNext.Lerp(sideBCenterOutside, cornerLengthFactor); + var bOuterPrevEnd = bOuterPrev.Lerp(sideACenterOutside, cornerLengthFactor); + + var cInnerNextEnd = cInner.Lerp(sideCCenterInside, cornerLengthFactor); + var cInnerPrevEnd = cInner.Lerp(sideBCenterInside, cornerLengthFactor); + var cOuterNextEnd = cOuterNext.Lerp(sideCCenterOutside, cornerLengthFactor); + var cOuterPrevEnd = cOuterPrev.Lerp(sideBCenterOutside, cornerLengthFactor); + + Raylib.DrawTriangle(aOuterPrev, aInner, aOuterPrevEnd, rayColor); + Raylib.DrawTriangle(aInner, aInnerPrevEnd, aOuterPrevEnd, rayColor); + + Raylib.DrawTriangle(aOuterNext, aOuterNextEnd, aInner, rayColor); + Raylib.DrawTriangle(aOuterNextEnd, aInnerNextEnd, aInner, rayColor); + + Raylib.DrawTriangle(bOuterPrev, bInner, bOuterPrevEnd, rayColor); + Raylib.DrawTriangle(bInner, bInnerPrevEnd, bOuterPrevEnd, rayColor); + + Raylib.DrawTriangle(bOuterNext, bOuterNextEnd, bInner, rayColor); + Raylib.DrawTriangle(bOuterNextEnd, bInnerNextEnd, bInner, rayColor); + + Raylib.DrawTriangle(cOuterPrev, cInner, cOuterPrevEnd, rayColor); + Raylib.DrawTriangle(cInner, cInnerPrevEnd, cOuterPrevEnd, rayColor); + + Raylib.DrawTriangle(cOuterNext, cOuterNextEnd, cInner, rayColor); + Raylib.DrawTriangle(cOuterNextEnd, cInnerNextEnd, cInner, rayColor); + } + + #endregion + + #region Draw Vertices + + /// + /// Draws circles at each vertex of the triangle. + /// + /// The triangle whose vertices to draw. + /// The radius of each vertex circle. + /// The color of the vertex circles. + /// + /// The smoothness value (0-1). This controls the visual quality of the circle by inversely interpolating the current . + /// A value of 0 uses the maximum side length (fewer sides, less smooth), while 1 uses the minimum side length (more sides, smoother). + /// The resulting side length determines the number of polygon sides used to approximate the circle. + /// + public static void DrawVertices(this Triangle t, float vertexRadius, ColorRgba color, float smoothness) + { + var circle = new Circle(t.A, vertexRadius); + circle.Draw(color, smoothness); + circle = circle.SetPosition(t.B); + circle.Draw(color, smoothness); + circle = circle.SetPosition(t.C); + circle.Draw(color, smoothness); + } + + #endregion + #region Draw Masked /// @@ -37,6 +748,7 @@ public static void DrawLinesMasked(this Triangle triangle, Triangle mask, LineDr triangle.SegmentBToC.DrawMasked(mask, lineInfo, reversedMask); triangle.SegmentCToA.DrawMasked(mask, lineInfo, reversedMask); } + /// /// Draws the triangle's three segments using the provided , /// clipped by a circular mask. @@ -52,6 +764,7 @@ public static void DrawLinesMasked(this Triangle triangle, Circle mask, LineDraw triangle.SegmentBToC.DrawMasked(mask, lineInfo, reversedMask); triangle.SegmentCToA.DrawMasked(mask, lineInfo, reversedMask); } + /// /// Draws the triangle's three segments using the provided , /// clipped by a rectangular mask. @@ -83,6 +796,7 @@ public static void DrawLinesMasked(this Triangle triangle, Quad mask, LineDrawin triangle.SegmentBToC.DrawMasked(mask, lineInfo, reversedMask); triangle.SegmentCToA.DrawMasked(mask, lineInfo, reversedMask); } + /// /// Draws the triangle's three segments using the provided , /// clipped by a polygonal mask. @@ -98,6 +812,7 @@ public static void DrawLinesMasked(this Triangle triangle, Polygon mask, LineDra triangle.SegmentBToC.DrawMasked(mask, lineInfo, reversedMask); triangle.SegmentCToA.DrawMasked(mask, lineInfo, reversedMask); } + /// /// Draws the triangle's three segments using the provided , /// clipped by a closed-shape mask of the generic type . @@ -117,466 +832,588 @@ public static void DrawLinesMasked(this Triangle triangle, T mask, LineDrawin triangle.SegmentBToC.DrawMasked(mask, lineInfo, reversedMask); triangle.SegmentCToA.DrawMasked(mask, lineInfo, reversedMask); } + #endregion + #region Gapped /// - /// Draws a filled triangle using the specified vertices and color. + /// Draws a gapped outline for a triangle, creating a dashed or segmented effect along the triangle's perimeter. /// - /// The first vertex of the triangle. - /// The second vertex of the triangle. - /// The third vertex of the triangle. - /// The color to fill the triangle with. - public static void DrawTriangle(Vector2 a, Vector2 b, Vector2 c, ColorRgba color) => Raylib.DrawTriangle(a, b, c, color.ToRayColor()); - - /// - /// Draws the outline of a triangle with specified line thickness and style. - /// - /// The first vertex of the triangle. - /// The second vertex of the triangle. - /// The third vertex of the triangle. - /// The thickness of the outline. - /// The color of the outline. - /// The style of the line caps. - /// The number of points used for the cap style. - public static void DrawTriangleLines(Vector2 a, Vector2 b, Vector2 c, float lineThickness, ColorRgba color, LineCapType capType = LineCapType.CappedExtended, int capPoints = 2) - { - SegmentDrawing.DrawSegment(a, b, lineThickness, color, capType, capPoints); - SegmentDrawing.DrawSegment(b, c, lineThickness, color, capType, capPoints); - SegmentDrawing.DrawSegment(c, a, lineThickness, color, capType, capPoints); - } - - /// - /// Draws the outline of a triangle with each side scaled by a factor. - /// - /// The first vertex of the triangle. - /// The second vertex of the triangle. - /// The third vertex of the triangle. - /// The thickness of the outline. - /// The color of the outline. - /// The factor by which to scale each side (0 = no triangle, 1 = full side length). - /// The style of the line caps. - /// The number of points used for the cap style. + /// The triangle to draw. + /// + /// The total length of the triangle's perimeter. + /// If zero or negative, the method calculates it automatically. + /// Providing a known length avoids redundant calculations and improves performance, especially for static segments. + /// + /// Parameters describing how to draw the outline. + /// Parameters describing the gap configuration. + /// + /// The perimeter of the triangle if positive; otherwise, -1. + /// If the shape does not change, the valid length can be reused in subsequent frames to avoid recalculating. + /// /// - /// Each side is drawn from its starting vertex towards the next, scaled by . + /// - If is 0 or is 0, the outline is drawn solid. + /// - If is 1 or greater, no outline is drawn. /// - public static void DrawTriangleLines(Vector2 a, Vector2 b, Vector2 c, float lineThickness, ColorRgba color, float sideLengthFactor, LineCapType capType = LineCapType.CappedExtended, int capPoints = 2) + public static float DrawGappedOutline(this Triangle triangle, float perimeter, LineDrawingInfo lineInfo, GappedOutlineDrawingInfo gapDrawingInfo) { - var side1 = b - a; - var end1 = a + side1 * sideLengthFactor; + if (gapDrawingInfo.Gaps <= 0 || gapDrawingInfo.GapPerimeterPercentage <= 0f) + { + triangle.DrawLines(lineInfo); + return perimeter > 0f ? perimeter : -1f; + } - var side2 = c - b; - var end2 = b + side2 * sideLengthFactor; + if (gapDrawingInfo.GapPerimeterPercentage >= 1f) return perimeter > 0f ? perimeter : -1f; - var side3 = a - c; - var end3 = c + side3 * sideLengthFactor; + var nonGapPercentage = 1f - gapDrawingInfo.GapPerimeterPercentage; - SegmentDrawing.DrawSegment(a, end1, lineThickness, color, capType, capPoints); - SegmentDrawing.DrawSegment(b, end2, lineThickness, color, capType, capPoints); - SegmentDrawing.DrawSegment(c, end3, lineThickness, color, capType, capPoints); - } + var gapPercentageRange = gapDrawingInfo.GapPerimeterPercentage / gapDrawingInfo.Gaps; + var nonGapPercentageRange = nonGapPercentage / gapDrawingInfo.Gaps; - /// - /// Draws the outline of a triangle using a object for style. - /// - /// The first vertex of the triangle. - /// The second vertex of the triangle. - /// The third vertex of the triangle. - /// The line drawing information (thickness, color, cap type, etc.). - public static void DrawTriangleLines(Vector2 a, Vector2 b, Vector2 c, LineDrawingInfo lineInfo) - { - SegmentDrawing.DrawSegment(a, b, lineInfo); - SegmentDrawing.DrawSegment(b, c, lineInfo); - SegmentDrawing.DrawSegment(c, a, lineInfo); - } + + var shapePoints = new[] {triangle.A, triangle.B, triangle.C}; + int sides = shapePoints.Length; - /// - /// Draws a filled triangle using the specified and color. - /// - /// The triangle to draw. - /// The color to fill the triangle with. - public static void Draw(this Triangle t, ColorRgba color) => Raylib.DrawTriangle(t.A, t.B, t.C, color.ToRayColor()); + if (perimeter <= 0f) + { + perimeter = 0f; + for (int i = 0; i < sides; i++) + { + var curP = shapePoints[i]; + var nextP = shapePoints[(i + 1) % sides]; + perimeter += (nextP - curP).Length(); + } + } + - /// - /// Draws the outline of a triangle with specified line thickness and style. - /// - /// The triangle to draw. - /// The thickness of the outline. - /// The color of the outline. - /// The style of the line caps. - /// The number of points used for the cap style. - public static void DrawLines(this Triangle t, float lineThickness, ColorRgba color, LineCapType capType = LineCapType.CappedExtended, int capPoints = 2) - { - DrawTriangleLines(t.A, t.B, t.C, lineThickness, color, capType, capPoints); - } + var startDistance = perimeter * gapDrawingInfo.StartOffset; + var curDistance = 0f; + var nextDistance = startDistance; + + var curIndex = 0; + var curPoint = shapePoints[0]; + var nextPoint= shapePoints[1]; + var curW = nextPoint - curPoint; + var curDis = curW.Length(); + + var points = new List(3); - /// - /// Draws the outline of a triangle with each side scaled by a factor. - /// - /// The triangle to draw. - /// The thickness of the outline. - /// The color of the outline. - /// The factor by which to scale each side (0 = no triangle, 1 = full side length). - /// The style of the line caps. - /// The number of points used for the cap style. - /// - /// Each side is drawn from its starting vertex towards the next, scaled by . - /// - public static void DrawLines(this Triangle t, float lineThickness, ColorRgba color, float sideLengthFactor, LineCapType capType = LineCapType.CappedExtended, int capPoints = 2) - { - DrawTriangleLines(t.A, t.B, t.C, lineThickness, color, sideLengthFactor, capType, capPoints); - } + int whileCounter = gapDrawingInfo.Gaps; + + while (whileCounter > 0) + { + if (curDistance + curDis >= nextDistance) + { + var p = curPoint + (curW / curDis) * (nextDistance - curDistance); + + + if (points.Count == 0) + { + nextDistance += nonGapPercentageRange * perimeter; + points.Add(p); - /// - /// Draws the outline of a triangle using a object for style. - /// - /// The triangle to draw. - /// The line drawing information (thickness, color, cap type, etc.). - public static void DrawLines(this Triangle t, LineDrawingInfo lineInfo) => DrawTriangleLines(t.A, t.B, t.C, lineInfo); + } + else + { + nextDistance += gapPercentageRange * perimeter; + points.Add(p); + if (points.Count == 2) + { + SegmentDrawing.DrawSegment(points[0], points[1], lineInfo); + } + else + { + for (var i = 0; i < points.Count - 1; i++) + { + var p1 = points[i]; + var p2 = points[(i + 1) % points.Count]; + SegmentDrawing.DrawSegment(p1, p2, lineInfo); + } + } + + points.Clear(); + whileCounter--; + } - /// - /// Draws the outline of a triangle using a object, with rotation applied. - /// - /// The triangle to draw. - /// The line drawing information (thickness, color, cap type, etc.). - /// The rotation in degrees to apply to the triangle. - /// The origin point to rotate around (absolute coordinates). - public static void DrawLines(this Triangle t, LineDrawingInfo lineInfo, float rotDeg, Vector2 rotOrigin) - { - t = t.ChangeRotation(rotDeg * ShapeMath.DEGTORAD, rotOrigin); - DrawTriangleLines(t.A, t.B, t.C, lineInfo); + } + else + { + + if(points.Count > 0) points.Add(nextPoint); + + curDistance += curDis; + curIndex = (curIndex + 1) % sides; + curPoint = shapePoints[curIndex]; + nextPoint = shapePoints[(curIndex + 1) % sides]; + curW = nextPoint - curPoint; + curDis = curW.Length(); + } + + } + + return perimeter; } + + #endregion + + #region Helper - /// - /// Draws circles at each vertex of the triangle. - /// - /// The triangle whose vertices to draw. - /// The radius of each vertex circle. - /// The color of the vertex circles. - /// The number of segments to use for each circle (default is 8). - public static void DrawVertices(this Triangle t, float vertexRadius, ColorRgba color, int circleSegments = 8) + private static void DrawLinesHelper(Triangle triangle, float lineThickness, ColorRgba color, float miterLimit = 4f, bool beveled = true) { - CircleDrawing.DrawCircle(t.A, vertexRadius, color, circleSegments); - CircleDrawing.DrawCircle(t.B, vertexRadius, color, circleSegments); - CircleDrawing.DrawCircle(t.C, vertexRadius, color, circleSegments); - } + if (triangle.IsCollinear()) return; + + float totalMiterLengthLimit = lineThickness * 0.5f * MathF.Max(2f, miterLimit); + + var edgeAB = triangle.B - triangle.A; + var edgeBC = triangle.C - triangle.B; + var edgeCA = triangle.A - triangle.C; + + var lengthAB = edgeAB.Length(); + if (lengthAB <= 0f) return; + + var lengthBC = edgeBC.Length(); + if (lengthBC <= 0f) return; + + var lengthCA = edgeCA.Length(); + if (lengthCA <= 0f) return; - /// - /// Draws a collection of triangles filled with the specified color. - /// - /// The collection of triangles to draw. - /// The color to fill each triangle with. - public static void Draw(this Triangulation triangles, ColorRgba color) { foreach (var t in triangles) t.Draw(color); } + float perimeter = lengthAB + lengthBC + lengthCA; + Vector2 incenter = (triangle.A * lengthBC + triangle.B * lengthCA + triangle.C * lengthAB) / perimeter; + + var innerMaxLengthA = (incenter - triangle.A).Length(); + if(innerMaxLengthA <= 0f) return; + + var innerMaxLengthB = (incenter - triangle.B).Length(); + if(innerMaxLengthB <= 0f) return; + + var innerMaxLengthC = (incenter - triangle.C).Length(); + if(innerMaxLengthC <= 0f) return; + + var edgeABDir = edgeAB / lengthAB; + var edgeBCDir = edgeBC / lengthBC; + var edgeCADir = edgeCA / lengthCA; + + var normalAB = edgeABDir.GetPerpendicularRight(); + var normalBC = edgeBCDir.GetPerpendicularRight(); + var normalCA = edgeCADir.GetPerpendicularRight(); + + var miterDirA = (normalCA + normalAB).Normalize(); + var miterDirB = (normalAB + normalBC).Normalize(); + var miterDirC = (normalBC + normalCA).Normalize(); + + float miterAngleRadA = MathF.Abs(miterDirA.AngleRad(normalAB)); + float miterLengthA = lineThickness / MathF.Cos(miterAngleRadA); + + float miterAngleRadB = MathF.Abs(miterDirB.AngleRad(normalBC)); + float miterLengthB = lineThickness / MathF.Cos(miterAngleRadB); + + float miterAngleRadC = MathF.Abs(miterDirC.AngleRad(normalCA)); + float miterLengthC = lineThickness / MathF.Cos(miterAngleRadC); - /// - /// Draws the outlines of a collection of triangles with specified line thickness and style. - /// - /// The collection of triangles to draw. - /// The thickness of the outlines. - /// The color of the outlines. - /// The style of the line caps. - /// The number of points used for the cap style. - public static void DrawLines(this Triangulation triangles, float lineThickness, ColorRgba color, LineCapType capType = LineCapType.CappedExtended, int capPoints = 2) - { - foreach (var t in triangles) t.DrawLines(lineThickness, color, capType, capPoints); - } + Vector2 aOuterPrev, aOuterNext; + Vector2 bOuterPrev, bOuterNext; + Vector2 cOuterPrev, cOuterNext; + + Vector2 aInner = triangle.A - miterDirA * MathF.Min(miterLengthA, innerMaxLengthA); + Vector2 bInner = triangle.B - miterDirB * MathF.Min(miterLengthB, innerMaxLengthB); + Vector2 cInner = triangle.C - miterDirC * MathF.Min(miterLengthC, innerMaxLengthC); + + var rayColor = color.ToRayColor(); + + if (miterLimit < 2f || miterLengthA < totalMiterLengthLimit) + { + aOuterPrev = triangle.A + miterDirA * miterLengthA; + aOuterNext = aOuterPrev; + + } + else + { + if (beveled) + { + aOuterPrev = triangle.A + normalCA * lineThickness; + aOuterNext = triangle.A + normalAB * lineThickness; + } + else + { + var p = triangle.A + miterDirA * totalMiterLengthLimit; + var dir = (p - triangle.A).Normalize(); + var pr = dir.GetPerpendicularRight(); + var offsetC = triangle.C + normalCA * lineThickness; - /// - /// Draws the outlines of a collection of triangles using a object for style. - /// - /// The collection of triangles to draw. - /// The line drawing information (thickness, color, cap type, etc.). - public static void DrawLines(this Triangulation triangles, LineDrawingInfo lineInfo) - { - foreach (var t in triangles) t.DrawLines(lineInfo); - } + var ip = Ray.IntersectRayRay(p, pr, offsetC, edgeCADir); + if (ip.Valid) + { + aOuterPrev = ip.Point; + aOuterNext = p - pr * (p - ip.Point).Length(); + } + else + { + aOuterPrev = triangle.A + normalCA * lineThickness; + aOuterNext = triangle.A + normalAB * lineThickness; + } + } + Raylib.DrawTriangle(aOuterPrev, aOuterNext, aInner, rayColor); + } - /// - /// Draws a certain percentage of a triangle's outline. - /// - /// The first vertex of the triangle. - /// The second vertex of the triangle. - /// The third vertex of the triangle. - /// - /// Specifies which portion of the triangle's outline to draw, as well as the starting corner and direction. - /// - /// The integer part (0-2) selects the starting corner: - /// - /// Counter-Clockwise -> 0 = a, 1 = b, 2 = c - /// Clockwise -> 0 = a, 1 = c, 2 = b - /// - /// - /// The fractional part (0.0-1.0) determines the percentage of the outline to draw, relative to the full perimeter. - /// A negative value reverses the drawing direction (clockwise instead of counter-clockwise). - /// - /// Examples: - /// - /// 0.35 - Start at a, draw 35% of the outline counter-clockwise. - /// -2.7 - Start at b, draw 70% of the outline clockwise. - /// - /// - /// The thickness of the outline. - /// The color of the outline. - /// The style of the line caps. - /// The number of points used for the cap style. - /// - /// Useful for animating or highlighting portions of a triangle's outline. - /// - public static void DrawTriangleLinesPercentage(Vector2 a, Vector2 b, Vector2 c, float f, float lineThickness, ColorRgba color, LineCapType capType = LineCapType.CappedExtended, int capPoints = 2) - { - if (f == 0) return; + if (miterLimit < 2f || miterLengthB < totalMiterLengthLimit) + { + bOuterPrev = triangle.B + miterDirB * miterLengthB; + bOuterNext = bOuterPrev; + } + else + { + if (beveled) + { + bOuterPrev = triangle.B + normalAB * lineThickness; + bOuterNext = triangle.B + normalBC * lineThickness; + } + else + { + var p = triangle.B + miterDirB * totalMiterLengthLimit; + var dir = (p - triangle.B).Normalize(); + var pr = dir.GetPerpendicularRight(); + var offsetA = triangle.A + normalAB * lineThickness; - bool negative = false; - if (f < 0) + var ip = Ray.IntersectRayRay(p, pr, offsetA, edgeABDir); + if (ip.Valid) + { + bOuterPrev = ip.Point; + bOuterNext = p - pr * (p - ip.Point).Length(); + } + else + { + bOuterPrev = triangle.B + normalAB * lineThickness; + bOuterNext = triangle.B + normalBC * lineThickness; + } + } + Raylib.DrawTriangle(bOuterPrev, bOuterNext, bInner, rayColor); + } + + if (miterLimit < 2f || miterLengthC < totalMiterLengthLimit) { - negative = true; - f *= -1; + cOuterPrev = triangle.C + miterDirC * miterLengthC; + cOuterNext = cOuterPrev; } + else + { + if (beveled) + { + cOuterPrev = triangle.C + normalBC * lineThickness; + cOuterNext = triangle.C + normalCA * lineThickness; + } + else + { + var p = triangle.C + miterDirC * totalMiterLengthLimit; + var dir = (p - triangle.C).Normalize(); + var pr = dir.GetPerpendicularRight(); + var offsetB = triangle.B + normalBC * lineThickness; - int startCorner = (int)f; - float percentage = f - startCorner; - if (percentage <= 0) return; + var ip = Ray.IntersectRayRay(p, pr, offsetB, edgeBCDir); + if (ip.Valid) + { + cOuterPrev = ip.Point; + cOuterNext = p - pr * (p - ip.Point).Length(); + } + else + { + cOuterPrev = triangle.C + normalBC * lineThickness; + cOuterNext = triangle.C + normalCA * lineThickness; + } + } + Raylib.DrawTriangle(cOuterPrev, cOuterNext, cInner, rayColor); + } + + Raylib.DrawTriangle(aOuterNext, bOuterPrev, aInner, rayColor); + Raylib.DrawTriangle(aInner, bOuterPrev, bInner, rayColor); + + Raylib.DrawTriangle(bOuterNext, cOuterPrev, cInner, rayColor); + Raylib.DrawTriangle(bInner, bOuterNext, cInner, rayColor); + + Raylib.DrawTriangle(cInner, cOuterNext, aOuterPrev, rayColor); + Raylib.DrawTriangle(cInner, aOuterPrev, aInner, rayColor); - startCorner = ShapeMath.Clamp(startCorner, 0, 2); + } + + private static void DrawLinesPercentageHelper(Triangle triangle, float f, bool ccw, float lineThickness, ColorRgba color, float miterLimit = 4f, bool beveled = true) + { + if (triangle.IsCollinear()) return; + + var edgeAB = triangle.B - triangle.A; + var edgeBC = triangle.C - triangle.B; + var edgeCA = triangle.A - triangle.C; + + var lengthAB = edgeAB.Length(); + if (lengthAB <= 0f) return; + + var lengthBC = edgeBC.Length(); + if (lengthBC <= 0f) return; + + var lengthCA = edgeCA.Length(); + if (lengthCA <= 0f) return; - if (startCorner == 0) + float perimeter = lengthAB + lengthBC + lengthCA; + Vector2 incenter = (triangle.A * lengthBC + triangle.B * lengthCA + triangle.C * lengthAB) / perimeter; + + var innerMaxLengthA = (incenter - triangle.A).Length(); + if(innerMaxLengthA <= 0f) return; + + var innerMaxLengthB = (incenter - triangle.B).Length(); + if(innerMaxLengthB <= 0f) return; + + var innerMaxLengthC = (incenter - triangle.C).Length(); + if(innerMaxLengthC <= 0f) return; + + var edgeABDir = edgeAB / lengthAB; + var edgeBCDir = edgeBC / lengthBC; + var edgeCADir = edgeCA / lengthCA; + + var normalAB = ccw ? edgeABDir.GetPerpendicularRight() : edgeABDir.GetPerpendicularLeft(); + var normalBC = ccw ? edgeBCDir.GetPerpendicularRight() : edgeBCDir.GetPerpendicularLeft(); + var normalCA = ccw ? edgeCADir.GetPerpendicularRight() : edgeCADir.GetPerpendicularLeft(); + + var miterDirA = (normalCA + normalAB).Normalize(); + var miterDirB = (normalAB + normalBC).Normalize(); + var miterDirC = (normalBC + normalCA).Normalize(); + + float miterAngleRadA = MathF.Abs(miterDirA.AngleRad(normalAB)); + float miterLengthA = lineThickness / MathF.Cos(miterAngleRadA); + + float miterAngleRadB = MathF.Abs(miterDirB.AngleRad(normalBC)); + float miterLengthB = lineThickness / MathF.Cos(miterAngleRadB); + + float miterAngleRadC = MathF.Abs(miterDirC.AngleRad(normalCA)); + float miterLengthC = lineThickness / MathF.Cos(miterAngleRadC); + + Vector2 aOuterPrev, aOuterNext; + Vector2 bOuterPrev, bOuterNext; + Vector2 cOuterPrev, cOuterNext; + + Vector2 aInner = triangle.A - miterDirA * MathF.Min(miterLengthA, innerMaxLengthA); + Vector2 bInner = triangle.B - miterDirB * MathF.Min(miterLengthB, innerMaxLengthB); + Vector2 cInner = triangle.C - miterDirC * MathF.Min(miterLengthC, innerMaxLengthC); + + float totalMiterLengthLimit = lineThickness * 0.5f * MathF.Max(2f, miterLimit); + + if (miterLimit < 2f || miterLengthA < totalMiterLengthLimit) { - if (negative) //CW + aOuterPrev = triangle.A + miterDirA * miterLengthA; + aOuterNext = aOuterPrev; + + } + else + { + if (beveled) { - DrawTriangleLinesPercentageHelper(a, c, b, percentage, lineThickness, color, capType, capPoints); + aOuterPrev = triangle.A + normalCA * lineThickness; + aOuterNext = triangle.A + normalAB * lineThickness; } - else //CCW + else { - DrawTriangleLinesPercentageHelper(a, b, c, percentage, lineThickness, color, capType, capPoints); + var p = triangle.A + miterDirA * totalMiterLengthLimit; + var dir = (p - triangle.A).Normalize(); + var pr = dir.GetPerpendicularRight(); + var offsetC = triangle.C + normalCA * lineThickness; + + var ip = Ray.IntersectRayRay(p, pr, offsetC, edgeCADir); + if (ip.Valid) + { + aOuterPrev = ip.Point; + aOuterNext = p - pr * (p - ip.Point).Length(); + } + else + { + aOuterPrev = triangle.A + normalCA * lineThickness; + aOuterNext = triangle.A + normalAB * lineThickness; + } } } - else if (startCorner == 1) + + if (miterLimit < 2f || miterLengthB < totalMiterLengthLimit) + { + bOuterPrev = triangle.B + miterDirB * miterLengthB; + bOuterNext = bOuterPrev; + } + else { - if (negative) //CW + if (beveled) { - DrawTriangleLinesPercentageHelper(c, b, a, percentage, lineThickness, color, capType, capPoints); + bOuterPrev = triangle.B + normalAB * lineThickness; + bOuterNext = triangle.B + normalBC * lineThickness; } - else //CCW + else { - DrawTriangleLinesPercentageHelper(b, c, a, percentage, lineThickness, color, capType, capPoints); + var p = triangle.B + miterDirB * totalMiterLengthLimit; + var dir = (p - triangle.B).Normalize(); + var pr = dir.GetPerpendicularRight(); + var offsetA = triangle.A + normalAB * lineThickness; + + var ip = Ray.IntersectRayRay(p, pr, offsetA, edgeABDir); + if (ip.Valid) + { + bOuterPrev = ip.Point; + bOuterNext = p - pr * (p - ip.Point).Length(); + } + else + { + bOuterPrev = triangle.B + normalAB * lineThickness; + bOuterNext = triangle.B + normalBC * lineThickness; + } } } - else if (startCorner == 2) + + if (miterLimit < 2f || miterLengthC < totalMiterLengthLimit) { - if (negative) //CW + cOuterPrev = triangle.C + miterDirC * miterLengthC; + cOuterNext = cOuterPrev; + } + else + { + if (beveled) { - DrawTriangleLinesPercentageHelper(b, a, c, percentage, lineThickness, color, capType, capPoints); + cOuterPrev = triangle.C + normalBC * lineThickness; + cOuterNext = triangle.C + normalCA * lineThickness; } - else //CCW + else { - DrawTriangleLinesPercentageHelper(c, a, b, percentage, lineThickness, color, capType, capPoints); + var p = triangle.C + miterDirC * totalMiterLengthLimit; + var dir = (p - triangle.C).Normalize(); + var pr = dir.GetPerpendicularRight(); + var offsetB = triangle.B + normalBC * lineThickness; + + var ip = Ray.IntersectRayRay(p, pr, offsetB, edgeBCDir); + if (ip.Valid) + { + cOuterPrev = ip.Point; + cOuterNext = p - pr * (p - ip.Point).Length(); + } + else + { + cOuterPrev = triangle.C + normalBC * lineThickness; + cOuterNext = triangle.C + normalCA * lineThickness; + } } } - } - - /// - /// Draws a certain percentage of a triangle's outline using a object. - /// - /// The first vertex of the triangle. - /// The second vertex of the triangle. - /// The third vertex of the triangle. - /// - /// Specifies which portion of the triangle's outline to draw, as well as the starting corner and direction. - /// - /// The integer part (0-2) selects the starting corner: - /// - /// Counter-Clockwise -> 0 = a, 1 = b, 2 = c - /// Clockwise -> 0 = a, 1 = c, 2 = b - /// - /// - /// The fractional part (0.0-1.0) determines the percentage of the outline to draw, relative to the full perimeter. - /// A negative value reverses the drawing direction (clockwise instead of counter-clockwise). - /// - /// Examples: - /// - /// 0.35 - Start at a, draw 35% of the outline counter-clockwise. - /// -2.7 - Start at b, draw 70% of the outline clockwise. - /// - /// - /// The line drawing information (thickness, color, cap type, etc.). - /// - /// Useful for animating or highlighting portions of a triangle's outline. - /// - public static void DrawTriangleLinesPercentage(Vector2 a, Vector2 b, Vector2 c, float f, LineDrawingInfo lineInfo) - { - DrawTriangleLinesPercentage(a, b, c, f, lineInfo.Thickness, lineInfo.Color, lineInfo.CapType, lineInfo.CapPoints); - } - /// - /// Draws a certain percentage of a triangle's outline. - /// - /// The triangle to draw. - /// - /// Specifies which portion of the triangle's outline to draw, as well as the starting corner and direction. - /// - /// The integer part (0-2) selects the starting corner: - /// - /// Counter-Clockwise -> 0 = a, 1 = b, 2 = c - /// Clockwise -> 0 = a, 1 = c, 2 = b - /// - /// - /// The fractional part (0.0-1.0) determines the percentage of the outline to draw, relative to the full perimeter. - /// A negative value reverses the drawing direction (clockwise instead of counter-clockwise). - /// - /// Examples: - /// - /// 0.35 - Start at a, draw 35% of the outline counter-clockwise. - /// -2.7 - Start at b, draw 70% of the outline clockwise. - /// - /// - /// The thickness of the outline. - /// The color of the outline. - /// The style of the line caps. - /// The number of points used for the cap style. - /// - /// Useful for animating or highlighting portions of a triangle's outline. - /// - public static void DrawLinesPercentage(this Triangle t, float f, float lineThickness, ColorRgba color, LineCapType capType = LineCapType.CappedExtended, int capPoints = 2) - { - DrawTriangleLinesPercentage(t.A, t.B, t.C, f, lineThickness, color, capType, capPoints); - } + var rayColor = color.ToRayColor(); + + // Calculate corner lengths (outer distance) to allow smooth interpolation + float lenCornerA = (aOuterNext - aOuterPrev).Length(); + float lenCornerB = (bOuterNext - bOuterPrev).Length(); + float lenCornerC = (cOuterNext - cOuterPrev).Length(); - /// - /// Draws a certain percentage of a triangle's outline using a object. - /// - /// The triangle to draw. - /// - /// Specifies which portion of the triangle's outline to draw, as well as the starting corner and direction. - /// - /// The integer part (0-2) selects the starting corner: - /// - /// Counter-Clockwise -> 0 = a, 1 = b, 2 = c - /// Clockwise -> 0 = a, 1 = c, 2 = b - /// - /// - /// The fractional part (0.0-1.0) determines the percentage of the outline to draw, relative to the full perimeter. - /// A negative value reverses the drawing direction (clockwise instead of counter-clockwise). - /// - /// Examples: - /// - /// 0.35 - Start at a, draw 35% of the outline counter-clockwise. - /// -2.7 - Start at b, draw 70% of the outline clockwise. - /// - /// - /// The line drawing information (thickness, color, cap type, etc.). - /// - /// Useful for animating or highlighting portions of a triangle's outline. - /// - public static void DrawLinesPercentage(this Triangle t, float f, LineDrawingInfo lineInfo) - { - DrawTriangleLinesPercentage(t.A, t.B, t.C, f, lineInfo); - } + // Effective perimeter now includes the visual corners + float effectivePerimeter = lengthAB + lengthBC + lengthCA + lenCornerA + lenCornerB + lenCornerC; + float remainingLength = effectivePerimeter * f; - /// - /// Draws a certain percentage of a triangle's outline using a object, with rotation applied. - /// - /// The triangle to draw. - /// - /// Specifies which portion of the triangle's outline to draw, as well as the starting corner and direction. - /// - /// The integer part (0-2) selects the starting corner: - /// - /// Counter-Clockwise -> 0 = a, 1 = b, 2 = c - /// Clockwise -> 0 = a, 1 = c, 2 = b - /// - /// - /// The fractional part (0.0-1.0) determines the percentage of the outline to draw, relative to the full perimeter. - /// A negative value reverses the drawing direction (clockwise instead of counter-clockwise). - /// - /// Examples: - /// - /// 0.35 - Start at a, draw 35% of the outline counter-clockwise. - /// -2.7 - Start at b, draw 70% of the outline clockwise. - /// - /// - /// The line drawing information (thickness, color, cap type, etc.). - /// The rotation in degrees to apply to the triangle. - /// The origin point to rotate around (absolute coordinates). - /// - /// Useful for animating or highlighting portions of a triangle's outline. - /// - public static void DrawLinesPercentage(this Triangle t, float f, LineDrawingInfo lineInfo, float rotDeg, Vector2 rotOrigin) - { - t = t.ChangeRotation(rotDeg * ShapeMath.DEGTORAD, rotOrigin); - DrawTriangleLinesPercentage(t.A, t.B, t.C, f, lineInfo); - } + // 1. Edge AB + if (remainingLength > 0) + { + float drawLen = MathF.Min(remainingLength, lengthAB); + float t = drawLen / lengthAB; + + Vector2 currentOuterEnd = Vector2.Lerp(aOuterNext, bOuterPrev, t); + Vector2 currentInnerEnd = Vector2.Lerp(aInner, bInner, t); + + DrawQuad(aInner, currentInnerEnd, aOuterNext, currentOuterEnd); + + remainingLength -= lengthAB; + } - /// - /// Draws a triangle outline where each side can be scaled towards the origin of the side. - /// - /// The triangle to draw. - /// The line drawing information (thickness, color, cap type, etc.). - /// The rotation in degrees to apply to the triangle. - /// The origin point to rotate around (absolute coordinates). - /// The scale factor for each side (0 = No Side, 1 = Full Side). - /// The point along each side to scale from in both directions (0 = Start, 1 = End). - /// - /// Allows for dynamic scaling of triangle sides, useful for effects or partial outlines. - /// - public static void DrawLinesScaled(this Triangle t, LineDrawingInfo lineInfo, float rotDeg, Vector2 rotOrigin, float sideScaleFactor, float sideScaleOrigin = 0.5f) - { - if (sideScaleFactor <= 0) return; - if (sideScaleFactor >= 1) + // 2. Corner B + if (remainingLength > 0) { - t.DrawLines(lineInfo, rotDeg, rotOrigin); - return; + float drawLen = MathF.Min(remainingLength, lenCornerB); + if(lenCornerB > 0) + { + DrawCornerPartial(bInner, bOuterPrev, bOuterNext, drawLen, lenCornerB); + } + remainingLength -= lenCornerB; } - if(rotDeg != 0) t = t.ChangeRotation(rotDeg * ShapeMath.DEGTORAD, rotOrigin); + // 3. Edge BC + if (remainingLength > 0) + { + float drawLen = MathF.Min(remainingLength, lengthBC); + float t = drawLen / lengthBC; - SegmentDrawing.DrawSegment(t.A, t.B, lineInfo, sideScaleFactor, sideScaleOrigin); - SegmentDrawing.DrawSegment(t.B, t.C, lineInfo, sideScaleFactor, sideScaleOrigin); - SegmentDrawing.DrawSegment(t.C, t.A, lineInfo, sideScaleFactor, sideScaleOrigin); - } + Vector2 currentOuterEnd = Vector2.Lerp(bOuterNext, cOuterPrev, t); + Vector2 currentInnerEnd = Vector2.Lerp(bInner, cInner, t); - private static void DrawTriangleLinesPercentageHelper(Vector2 p1, Vector2 p2, Vector2 p3, float percentage, float lineThickness, ColorRgba color, LineCapType capType = LineCapType.CappedExtended, int capPoints = 2) - { - var l1 = (p2 - p1).Length(); - var l2 = (p3 - p2).Length(); - var l3 = (p1 - p3).Length(); - var perimeterToDraw = (l1 + l2 + l3) * percentage; - - // Draw first segment - var curP = p1; - var nextP = p2; - if (perimeterToDraw < l1) - { - float p = perimeterToDraw / l1; - nextP = curP.Lerp(nextP, p); - SegmentDrawing.DrawSegment(curP, nextP, lineThickness, color); - return; + DrawQuad(bInner, currentInnerEnd, bOuterNext, currentOuterEnd); + + remainingLength -= lengthBC; } - SegmentDrawing.DrawSegment(curP, nextP, lineThickness, color, capType, capPoints); - perimeterToDraw -= l1; - - // Draw second segment - curP = nextP; - nextP = p3; - if (perimeterToDraw < l2) + // 4. Corner C + if (remainingLength > 0) { - float p = perimeterToDraw / l2; - nextP = curP.Lerp(nextP, p); - SegmentDrawing.DrawSegment(curP, nextP, lineThickness, color, capType, capPoints); - return; + float drawLen = MathF.Min(remainingLength, lenCornerC); + if(lenCornerC > 0) + { + DrawCornerPartial(cInner, cOuterPrev, cOuterNext, drawLen, lenCornerC); + } + remainingLength -= lenCornerC; } - SegmentDrawing.DrawSegment(curP, nextP, lineThickness, color, capType, capPoints); - perimeterToDraw -= l2; + // 5. Edge CA + if (remainingLength > 0) + { + float drawLen = MathF.Min(remainingLength, lengthCA); + float t = drawLen / lengthCA; + + Vector2 currentOuterEnd = Vector2.Lerp(cOuterNext, aOuterPrev, t); + Vector2 currentInnerEnd = Vector2.Lerp(cInner, aInner, t); - // Draw third segment - curP = nextP; - nextP = p1; - if (perimeterToDraw < l3) + DrawQuad(cInner, currentInnerEnd, cOuterNext, currentOuterEnd); + + remainingLength -= lengthCA; + } + + // 6. Corner A (Closing Loop) + if (remainingLength > 0) { - float p = perimeterToDraw / l3; - nextP = curP.Lerp(nextP, p); + float drawLen = MathF.Min(remainingLength, lenCornerA); + // Only draw closing corner if we completely finished the last edge + if(lenCornerA > 0) + { + DrawCornerPartial(aInner, aOuterPrev, aOuterNext, drawLen, lenCornerA); + } + } + + // Helper to draw a quad segment + void DrawQuad(Vector2 startInner, Vector2 endInner, Vector2 startOuter, Vector2 endOuter) + { + if (ccw) + { + Raylib.DrawTriangle(startOuter, endOuter, startInner, rayColor); + Raylib.DrawTriangle(startInner, endOuter, endInner, rayColor); + } + else + { + Raylib.DrawTriangle(startOuter, startInner, endOuter, rayColor); + Raylib.DrawTriangle(startInner, endInner, endOuter, rayColor); + } + } - SegmentDrawing.DrawSegment(curP, nextP, lineThickness, color, capType, capPoints); + // Helper to draw a corner partial + void DrawCornerPartial(Vector2 pivotInner, Vector2 startOuter, Vector2 endOuter, float currentLen, float maxLen) + { + float t = currentLen / maxLen; + Vector2 currentEndOuter = Vector2.Lerp(startOuter, endOuter, t); + + if (ccw) + { + Raylib.DrawTriangle(pivotInner, startOuter, currentEndOuter, rayColor); + } + else + { + Raylib.DrawTriangle(pivotInner, currentEndOuter, startOuter, rayColor); + } + } + } -} \ No newline at end of file + + #endregion +} + diff --git a/ShapeEngine/Geometry/TriangleDef/TriangleMath.cs b/ShapeEngine/Geometry/TriangleDef/TriangleMath.cs index f6a3ea1f..5653841d 100644 --- a/ShapeEngine/Geometry/TriangleDef/TriangleMath.cs +++ b/ShapeEngine/Geometry/TriangleDef/TriangleMath.cs @@ -357,6 +357,46 @@ public Triangle ApplyTransform(Transform2D transform) #region Math + /// + /// Gets the length of the shortest side of the triangle. + /// + /// The length of the shortest side. + public float GetMinSideLength() + { + var sideALengthSquared = SideA.LengthSquared(); + var sideBLengthSquared = SideB.LengthSquared(); + var sideCLengthSquared = SideC.LengthSquared(); + + if(sideALengthSquared < sideBLengthSquared && sideALengthSquared < sideCLengthSquared) return MathF.Sqrt(sideALengthSquared); + if(sideBLengthSquared < sideALengthSquared && sideBLengthSquared < sideCLengthSquared) return MathF.Sqrt(sideBLengthSquared); + return MathF.Sqrt(sideCLengthSquared); + } + + /// + /// Gets the length of the longest side of the triangle. + /// + /// The length of the longest side. + public float GetMaxSideLength() + { + var sideALengthSquared = SideA.LengthSquared(); + var sideBLengthSquared = SideB.LengthSquared(); + var sideCLengthSquared = SideC.LengthSquared(); + + if(sideALengthSquared > sideBLengthSquared && sideALengthSquared > sideCLengthSquared) return MathF.Sqrt(sideALengthSquared); + if(sideBLengthSquared > sideALengthSquared && sideBLengthSquared > sideCLengthSquared) return MathF.Sqrt(sideBLengthSquared); + return MathF.Sqrt(sideCLengthSquared); + } + + /// + /// Checks if the triangle's vertices are collinear (lie on a straight line). + /// + /// Tolerance for float precision when determining collinearity. + /// True if the area is less than or equal to epsilon, indicating collinearity; otherwise, false. + public bool IsCollinear(float epsilon = 1e-6f) + { + return GetArea() <= epsilon; + } + /// /// Determines whether the triangle is geometrically valid. /// @@ -380,6 +420,22 @@ public bool IsValid() /// public Vector2 GetCentroid() => (A + B + C) / 3; + /// + /// Calculates and returns the incenter of the triangle. + /// + /// The incenter point, which is the intersection of the angle bisectors of the triangle. + public Vector2 GetIncenter() + { + var lenAB = SideA.Length(); + var lenBC = SideB.Length(); + var lenCA = SideC.Length(); + var perimeter = lenAB + lenBC + lenCA; + // Calculate Incenter (intersection of angle bisectors) + // Incenter = (a*A + b*B + c*C) / perimeter + Vector2 incenter = (A * lenBC + B * lenCA + C * lenAB) / perimeter; + return incenter; + } + /// /// Gets the points that form the projected shape when this triangle is extruded along a vector. /// @@ -403,7 +459,34 @@ public bool IsValid() }; return points; } + + /// + /// Writes this triangle's vertices and their projected counterparts into . + /// + /// The destination collection that will be cleared and populated with the original and projected triangle points. + /// The vector along which to project the triangle. + /// true if is non-zero and was populated; otherwise, false. + /// + /// Points are written in this order: , , , A + v, B + v, C + v. + /// + public bool GetProjectedShapePoints(Points result, Vector2 v) + { + if (v.LengthSquared() <= 0f) return false; + + result.Clear(); + result.EnsureCapacity(6); + + result.Add(A); + result.Add(B); + result.Add(C); + result.Add(A + v); + result.Add(B + v); + result.Add(C + v); + + return true; + } + /// /// Projects the triangle along a vector to create a convex hull polygon. /// @@ -425,7 +508,34 @@ public bool IsValid() B + v, C + v }; - return Polygon.FindConvexHull(points); + var result = new Polygon(6); + points.FindConvexHull(result); + return result; + } + + /// + /// Projects this triangle along the given vector and writes the convex hull of the combined point set into . + /// + /// The destination polygon that receives the convex hull of the original and projected triangle vertices. + /// The vector along which to project the triangle. + /// true if is non-zero and was populated; otherwise, false. + /// + /// This method constructs a temporary six-point set containing the triangle vertices and those same vertices translated by , then computes the convex hull into . + /// + public bool ProjectShape(Polygon result, Vector2 v) + { + if (v.LengthSquared() <= 0f) return false; + var points = new Points + { + A, + B, + C, + A + v, + B + v, + C + v + }; + points.FindConvexHull(result); + return true; } /// @@ -475,6 +585,12 @@ public Triangle Truncate() /// The perimeter is useful for calculations involving the triangle's boundary length. public float GetPerimeter() => SideA.Length() + SideB.Length() + SideC.Length(); + /// + /// Calculates the semi-perimeter (half of the triangle's perimeter). + /// + /// The semi-perimeter as a float: (sideA + sideB + sideC) / 2. + public float GetSemiPerimeter() => GetPerimeter() * 0.5f; + /// /// Calculates the squared perimeter of the triangle. /// @@ -495,6 +611,55 @@ public Triangle Truncate() /// public float GetArea() => MathF.Abs((A.X - C.X) * (B.Y - C.Y) - (A.Y - C.Y) * (B.X - C.X)) / 2f; + /// + /// Calculates the triangle area using Heron's formula (based on side lengths). + /// + /// + /// The area of the triangle in square units. Returns 0 for degenerate triangles (invalid or zero-area). + /// + /// + /// Computes side lengths from the triangle's edges and applies: + /// area = sqrt(s * (s - a) * (s - b) * (s - c)), + /// where s is the semi-perimeter. For numerical stability in many cases, prefer GetArea(). + /// + public float GetAreaHeron() + { + float a = SideA.Length(); + float b = SideB.Length(); + float c = SideC.Length(); + // Calculate semi-perimeter + float s = (a + b + c) * 0.5f; + return MathF.Sqrt(s * (s - a) * (s - b) * (s - c)); + } + + /// + /// Calculates the inradius (radius of the inscribed circle) of the triangle. + /// + /// + /// The inradius as a positive float; returns 0 if the triangle is degenerate or has zero area. + /// + /// + /// Computes side lengths, uses Heron's formula to obtain the area, and applies the + /// inradius formula: r = area / s, where s is the semi-perimeter. + /// + public float GetInradius() + { + float a = SideA.Length(); + float b = SideB.Length(); + float c = SideC.Length(); + + // Calculate semi-perimeter + float s = (a + b + c) * 0.5f; + + // Calculate area using Heron's formula + float area = MathF.Sqrt(s * (s - a) * (s - b) * (s - c)); + + if (area <= 0f) return 0f; + + // Inradius formula: r = area / s + return area / s; + } + /// /// Determines whether the triangle is narrow (has very small angles) based on cross product analysis. /// @@ -533,6 +698,106 @@ public bool IsNarrow(float narrowValue = 0.2f) cross = nextToCur.Cross(prevToCur); return MathF.Abs(cross) < narrowValue; } + #endregion + + #region Isoscele Triangle Math (Static) + public static float GetIsoscelesSideLength(float baseLength, Vector2 dir1, Vector2 dir2) + { + float dot = Vector2.Dot(dir1, dir2); + // Identity: 2 * sin(theta/2) = sqrt(2 * (1 - cos(theta))) + float denom = MathF.Sqrt(2f * (1f - dot)); + + // Avoid division by zero + if (denom < 0.0001f) return 0; + + return baseLength / denom; + } + + public static float GetIsoscelesBaseLength(float sideLength, Vector2 dir1, Vector2 dir2) + { + float dot = Vector2.Dot(dir1, dir2); + // Identity: 2 * sin(theta/2) = sqrt(2 * (1 - cos(theta))) + float denom = MathF.Sqrt(2f * (1f - dot)); + + return sideLength * denom; + } + #endregion + + #region Right Triangle Math (Static) + + /// + /// Calculates the length of the hypotenuse given two legs using the Pythagorean theorem. + /// + public static float RightTriangleGetHypotenuse(float legA, float legB) => MathF.Sqrt(legA * legA + legB * legB); + + /// + /// Calculates the length of a leg given the hypotenuse and the other leg. + /// + /// The length of the leg, or 0 if the hypotenuse is shorter than the provided leg. + public static float RightTriangleGetLeg(float hypotenuse, float otherLeg) + { + var val = hypotenuse * hypotenuse - otherLeg * otherLeg; + return val <= 0f ? 0f : MathF.Sqrt(val); + } + + /// + /// Calculates the opposite side length given an angle (radians) and the hypotenuse (Sin). + /// + public static float RightTriangleGetOpposite(float angleRad, float hypotenuse) => MathF.Sin(angleRad) * hypotenuse; + + /// + /// Calculates the opposite side length given an angle (radians) and the adjacent side (Tan). + /// + public static float RightTriangleGetOppositeFromAdjacent(float angleRad, float adjacent) => MathF.Tan(angleRad) * adjacent; + + /// + /// Calculates the adjacent side length given an angle (radians) and the hypotenuse (Cos). + /// + public static float RightTriangleGetAdjacent(float angleRad, float hypotenuse) => MathF.Cos(angleRad) * hypotenuse; + + /// + /// Calculates the adjacent side length given an angle (radians) and the opposite side (Cot). + /// + public static float RightTriangleGetAdjacentFromOpposite(float angleRad, float opposite) => opposite / MathF.Tan(angleRad); + + /// + /// Calculates the hypotenuse length given an angle (radians) and the opposite side (Csc). + /// + public static float RightTriangleGetHypotenuseFromOpposite(float angleRad, float opposite) => opposite / MathF.Sin(angleRad); + + /// + /// Calculates the hypotenuse length given an angle (radians) and the adjacent side (Sec). + /// + public static float RightTriangleGetHypotenuseFromAdjacent(float angleRad, float adjacent) => adjacent / MathF.Cos(angleRad); + + /// + /// Calculates the angle (radians) given the opposite and adjacent sides (ArcTan). + /// + public static float RightTriangleGetAngle(float opposite, float adjacent) => MathF.Atan(opposite / adjacent); + + /// + /// Calculates the angle (radians) given the opposite side and the hypotenuse (ArcSin). + /// + public static float RightTriangleGetAngleFromOpposite(float opposite, float hypotenuse) + { + if (hypotenuse <= 0f) return 0f; + var val = opposite / hypotenuse; + if (val < -1f) val = -1f; + else if (val > 1f) val = 1f; + return MathF.Asin(val); + } + + /// + /// Calculates the angle (radians) given the adjacent side and the hypotenuse (ArcCos). + /// + public static float RightTriangleGetAngleFromAdjacent(float adjacent, float hypotenuse) + { + if (hypotenuse <= 0f) return 0f; + var val = adjacent / hypotenuse; + if (val < -1f) val = -1f; + else if (val > 1f) val = 1f; + return MathF.Acos(val); + } #endregion -} +} \ No newline at end of file diff --git a/ShapeEngine/Geometry/TriangulationDef/Triangulation.cs b/ShapeEngine/Geometry/TriangulationDef/Triangulation.cs index ddc54796..964b7346 100644 --- a/ShapeEngine/Geometry/TriangulationDef/Triangulation.cs +++ b/ShapeEngine/Geometry/TriangulationDef/Triangulation.cs @@ -1,10 +1,11 @@ using System.Numerics; -using ShapeEngine.Core; using ShapeEngine.Geometry.PointsDef; using ShapeEngine.Geometry.SegmentDef; using ShapeEngine.Geometry.SegmentsDef; using ShapeEngine.Geometry.TriangleDef; using ShapeEngine.Random; +using ShapeEngine.ShapeClipper; +using ShapeEngine.StaticLib; using Game = ShapeEngine.Core.GameDef.Game; namespace ShapeEngine.Geometry.TriangulationDef; @@ -20,17 +21,27 @@ namespace ShapeEngine.Geometry.TriangulationDef; /// public partial class Triangulation : ShapeList { + #region Helper + + private static Triangulation queueBuffer = new(); + private static Triangulation subdivBuffer = new(); + private static HashSet uniquePointsBuffer = new(); + private static HashSet uniqueSegmentsBuffer = new(); + private static HashSet uniqueTrianglesBuffer = new(); + #endregion + #region Constructors /// /// Initializes a new instance of the class. /// public Triangulation() { } + /// /// Initializes a new instance of the class with the specified capacity. /// /// The number of triangles the collection can initially store. public Triangulation(int capacity) : base(capacity) { } - //public Triangulation(IShape shape) { AddRange(shape.Triangulate()); } + /// /// Initializes a new instance of the class with the specified triangles. /// @@ -64,70 +75,89 @@ public bool Equals(Triangulation? other) #endregion #region Public - /// - /// Gets all unique points from all triangles in the triangulation. + /// Collects all unique triangle vertices in this triangulation and writes them into . /// - /// A collection containing all unique vertices. - public Points GetUniquePoints() + /// The destination collection that will be cleared and populated with the unique vertices. + /// + /// This method does not modify the current triangulation. Vertex uniqueness is determined by the equality comparer used by the internal . + /// + public void GetUniquePoints(Points result) { - var uniqueVertices = new HashSet(); + uniquePointsBuffer.Clear(); + uniquePointsBuffer.EnsureCapacity(Count * 3); for (var i = 0; i < Count; i++) { var tri = this[i]; - uniqueVertices.Add(tri.A); - uniqueVertices.Add(tri.B); - uniqueVertices.Add(tri.C); + uniquePointsBuffer.Add(tri.A); + uniquePointsBuffer.Add(tri.B); + uniquePointsBuffer.Add(tri.C); } - - return new(uniqueVertices); + + result.Clear(); + result.EnsureCapacity(uniquePointsBuffer.Count); + result.AddRange(uniquePointsBuffer); } + /// - /// Gets all unique segments from all triangles in the triangulation. + /// Collects all unique triangle edges in this triangulation and writes them into . /// - /// A collection containing all unique segments. - public Segments GetUniqueSegments() + /// The destination collection that will be cleared and populated with the unique segments. + /// + /// This method does not modify the current triangulation. Segment uniqueness is determined by the equality comparer used by the internal . + /// + public void GetUniqueSegments(Segments result) { - var unique = new HashSet(); + uniqueSegmentsBuffer.Clear(); + uniqueSegmentsBuffer.EnsureCapacity(Count * 3); for (var i = 0; i < Count; i++) { var tri = this[i]; - unique.Add(tri.SegmentAToB); - unique.Add(tri.SegmentBToC); - unique.Add(tri.SegmentCToA); + uniqueSegmentsBuffer.Add(tri.SegmentAToB); + uniqueSegmentsBuffer.Add(tri.SegmentBToC); + uniqueSegmentsBuffer.Add(tri.SegmentCToA); } - return new(unique); + result.Clear(); + result.EnsureCapacity(uniqueSegmentsBuffer.Count); + result.AddRange(uniqueSegmentsBuffer); } + /// - /// Gets all unique triangles in the triangulation. + /// Collects all unique triangles in this triangulation and writes them into . /// - /// A containing all unique triangles. - public Triangulation GetUniqueTriangles() + /// The destination triangulation that will be cleared and populated with the unique triangles. + /// + /// This method does not modify the current triangulation. Triangle uniqueness is determined by the equality comparer used by the internal . + /// + public void GetUniqueTriangles(Triangulation result) { - var uniqueTriangles = new HashSet(); + uniqueTrianglesBuffer.Clear(); + uniqueTrianglesBuffer.EnsureCapacity(Count); for (var i = 0; i < Count; i++) { - var tri = this[i]; - uniqueTriangles.Add(tri); + uniqueTrianglesBuffer.Add(this[i]); } - return new(uniqueTriangles); + result.Clear(); + result.EnsureCapacity(uniqueTrianglesBuffer.Count); + result.AddRange(uniqueTrianglesBuffer); } + /// - /// Gets all triangles that contain the specified point. + /// Finds all triangles in this triangulation that contain the specified point and writes them into . /// + /// The destination triangulation that will be cleared and populated with the containing triangles. /// The point to test for containment. - /// A containing all triangles that contain the point. - public Triangulation GetContainingTriangles(Vector2 p) + public void GetContainingTriangles(Triangulation result, Vector2 p) { - Triangulation result = new(); + result.Clear(); + result.EnsureCapacity(Count); for (var i = 0; i < Count; i++) { var tri = this[i]; if (tri.ContainsPoint(p)) result.Add(tri); } - return result; } /// @@ -139,7 +169,8 @@ public Triangulation GetContainingTriangles(Vector2 p) /// Indices are wrapped using modulo operation. public Segment GetSegment(int triangleIndex, int segmentIndex) { - var i = triangleIndex % Count; + var i = ShapeMath.WrapIndex(Count, triangleIndex); + // var i = triangleIndex % Count; return this[i].GetSegment(segmentIndex); } @@ -166,83 +197,104 @@ public int Remove(float areaThreshold) #endregion #region Triangulation + /// - /// Creates a new triangulation containing triangles from the current triangulation that meet the specified area threshold. + /// Filters this triangulation by triangle area and writes triangles whose area is greater than or equal to into . /// - /// The minimum area a triangle must have to be included in the new triangulation. - /// A new containing triangles with an area greater than or equal to the threshold. - public Triangulation Get(float areaThreshold) + /// The destination triangulation that will be cleared and populated with the filtered triangles. + /// The minimum area a triangle must have to be copied into . + /// + /// If is less than or equal to 0, the method returns immediately without modifying . + /// + public void Get(Triangulation result, float areaThreshold) { - Triangulation newTriangulation = new(); - if (areaThreshold <= 0f) return newTriangulation; + if (areaThreshold <= 0f) return; + result.Clear(); for (int i = Count - 1; i >= 0; i--) { var t = this[i]; if (t.GetArea() >= areaThreshold) { - newTriangulation.Add(t); + result.Add(t); } } - - return newTriangulation; } - + /// - /// Subdivide the triangulation until all triangles are smaller than min area. + /// Recursively subdivides triangles in this triangulation until each resulting triangle has an area smaller than , then writes the final triangles into . /// - /// A triangle will always be subdivided if the area is bigger than min area. - /// - public Triangulation Subdivide(float minArea) + /// The destination triangulation that will be cleared and populated with the subdivided triangles. + /// The area threshold below which a triangle is kept instead of subdivided further. + /// + /// This method clears before processing. Triangles with area greater than or equal to are subdivided by calling Triangle.Triangulate. + /// + public void Subdivide(Triangulation result, float minArea) { - Triangulation final = []; - - Triangulation queue = []; - queue.AddRange(this); - while (queue.Count > 0) + result.Clear(); + queueBuffer.Clear(); + queueBuffer.AddRange(this); + while (queueBuffer.Count > 0) { - int endIndex = queue.Count - 1; - var tri = queue[endIndex]; + int endIndex = queueBuffer.Count - 1; + var tri = queueBuffer[endIndex]; float triArea = tri.GetArea(); - if (triArea < minArea) final.Add(tri); - else queue.AddRange(tri.Triangulate(minArea)); - queue.RemoveAt(endIndex); + if (triArea < minArea) + { + result.Add(tri); + } + else + { + subdivBuffer.Clear(); + tri.Triangulate(subdivBuffer, minArea); + queueBuffer.AddRange(subdivBuffer); + } + queueBuffer.RemoveAt(endIndex); } - return final; } - + /// - /// Subdivide the triangles further based on the parameters. + /// Recursively subdivides triangles in this triangulation using area limits, optional random retention, and narrow-triangle rejection, writing accepted triangles into . /// - /// Triangles with an area smaller than min area will never be subdivided. - /// Triangles with an area bigger than maxArea will always be subdivided. - /// The chance to keep a triangle and not subdivide it. - /// Triangles that are considered narrow will not be subdivided. - /// - public Triangulation Subdivide(float minArea, float maxArea, float keepChance = 0.5f, float narrowValue = 0.2f) + /// The destination triangulation that receives the accepted triangles. + /// Triangles with area smaller than this value are kept without further subdivision. + /// Triangles with area greater than this value are always subdivided. + /// The probability of keeping a triangle whose area lies between and . Values outside the range [0, 1] cause a probability to be derived from the triangle area. + /// Triangles considered narrow by this threshold are kept without further subdivision. + /// + /// If the triangulation is empty, the method returns immediately without modifying . Unlike the other Subdivide overload, this method does not clear before appending accepted triangles. + /// + public void Subdivide(Triangulation result, float minArea, float maxArea, float keepChance = 0.5f, float narrowValue = 0.2f) { - if (this.Count <= 0) return this; - - Triangulation final = new(); - Triangulation queue = new(); - - queue.AddRange(this.Count == 1 ? this[0].Triangulate(minArea) : this); - - - while (queue.Count > 0) + if (this.Count <= 0) return; + queueBuffer.Clear(); + if (Count == 1) { - int endIndex = queue.Count - 1; - var tri = queue[endIndex]; + subdivBuffer.Clear(); + this[0].Triangulate(subdivBuffer, minArea); + queueBuffer.AddRange(subdivBuffer); + } + else + { + queueBuffer.AddRange(this); + } + + while (queueBuffer.Count > 0) + { + int endIndex = queueBuffer.Count - 1; + var tri = queueBuffer[endIndex]; var triArea = tri.GetArea(); if (triArea < minArea || tri.IsNarrow(narrowValue)) //too small or narrow { - final.Add(tri); + result.Add(tri); } else if (triArea > maxArea) //always subdivide because too big { - queue.AddRange(tri.Triangulate(minArea)); + subdivBuffer.Clear(); + tri.Triangulate(subdivBuffer, minArea); + queueBuffer.AddRange(subdivBuffer); } else //subdivde or keep { @@ -252,12 +304,37 @@ public Triangulation Subdivide(float minArea, float maxArea, float keepChance = chance = (triArea - minArea) / (maxArea - minArea); } - if (Rng.Instance.Chance(chance)) final.Add(tri); - else queue.AddRange(tri.Triangulate(minArea)); + if (Rng.Instance.Chance(chance)) + { + result.Add(tri); + } + else + { + subdivBuffer.Clear(); + tri.Triangulate(subdivBuffer, minArea); + queueBuffer.AddRange(subdivBuffer); + } } - queue.RemoveAt(endIndex); + queueBuffer.RemoveAt(endIndex); } - return final; } #endregion + + /// + /// Converts this triangulation into a by writing each triangle's vertices into . + /// + /// The destination mesh that will be cleared and populated with triangle vertex data. + /// + /// Each triangle contributes its vertices in A-B-C order to dst.Triangles. + /// + public void ToTriMesh(TriMesh dst) + { + dst.Clear(); + foreach (var t in this) + { + dst.Triangles.Add(t.A); + dst.Triangles.Add(t.B); + dst.Triangles.Add(t.C); + } + } } \ No newline at end of file diff --git a/ShapeEngine/Geometry/TriangulationDef/TriangulationContainsPoint.cs b/ShapeEngine/Geometry/TriangulationDef/TriangulationContainsPoint.cs index f3f2d4b5..ae9b4776 100644 --- a/ShapeEngine/Geometry/TriangulationDef/TriangulationContainsPoint.cs +++ b/ShapeEngine/Geometry/TriangulationDef/TriangulationContainsPoint.cs @@ -74,6 +74,7 @@ public bool ContainsCollisionObject(CollisionObject collisionObject, out int tri } return false; } + /// /// Checks if a collider is contained within any triangle of this triangulation. /// @@ -98,6 +99,7 @@ public bool ContainsCollider(Collider collider, out int triangleIndex) } return false; } + /// /// Checks if a segment is contained within any triangle of this triangulation. /// @@ -169,6 +171,7 @@ public bool ContainsShape(Rect rect, out int triangleIndex) return false; } + /// /// Checks if a triangle is contained within any triangle of this triangulation. /// @@ -192,6 +195,7 @@ public bool ContainsShape(Triangle triangle, out int triangleIndex) return false; } + /// /// Checks if a quad is contained within any triangle of this triangulation. /// @@ -239,6 +243,7 @@ public bool ContainsShape(Polyline polyline, out int triangleIndex) return false; } + /// /// Checks if a polygon is contained within any triangle of this triangulation. /// diff --git a/ShapeEngine/Geometry/TriangulationDef/TriangulationDrawing.cs b/ShapeEngine/Geometry/TriangulationDef/TriangulationDrawing.cs new file mode 100644 index 00000000..bfb71f8a --- /dev/null +++ b/ShapeEngine/Geometry/TriangulationDef/TriangulationDrawing.cs @@ -0,0 +1,147 @@ +using ShapeEngine.Color; +using ShapeEngine.Geometry.TriangleDef; + +namespace ShapeEngine.Geometry.TriangulationDef; + +/// +/// Collection of extension drawing helpers for . +/// Contains methods to render filled triangles, outlines (with configurable thickness or style), +/// rounded corners and glow-like multi-pass drawing. All members are static extension methods +/// intended to be called on instances. +/// +public static class TriangulationDrawing +{ + #region Draw + + /// + /// Draws a collection of triangles filled with the specified color. + /// + /// The collection of triangles to draw. + /// The color to fill each triangle with. + public static void Draw(this Triangulation triangles, ColorRgba color) + { + foreach (var t in triangles) t.Draw(color); + } + + #endregion + + #region Draw Scaled + + /// + /// Draws each triangle in the triangulation with scaled sides based on a specific draw type. + /// + /// The collection of triangles to draw. + /// The color of the drawn shapes. + /// The scale factor of the sides (0 to 1). If >= 1, the full triangle is drawn. If <= 0, nothing is drawn. + /// The origin point for scaling the sides (0 = start, 1 = end, 0.5 = center). + /// + /// The style of drawing applied to each triangle: + /// + /// 0: [Filled] Drawn as 4 filled triangles, effectivly cutting of corners. + /// 1: [Sides] Each side is connected to the triangle's centroid. + /// 2: [Sides Inverse] The start of 1 side is connected to the end of the next side and is connected to the triangle's centroid. + /// + /// + public static void DrawScaled(this Triangulation triangles, ColorRgba color, float sideScaleFactor, float sideScaleOrigin, int drawType) + { + foreach (var t in triangles) t.DrawScaled(color, sideScaleFactor, sideScaleOrigin, drawType); + } + #endregion + + #region Draw Lines + + /// + /// Draws the outline of each triangle in the triangulation with specified line thickness and color. + /// + /// The collection of triangles to draw. + /// The thickness of the outline lines. + /// The color of the outline. + /// The limit for miter joins to prevent sharp spikes. Defaults to 4f. + /// If true, uses beveled joins when the miter limit is exceeded; otherwise, cuts off the miter point. + public static void DrawLines(this Triangulation triangles, float lineThickness, ColorRgba color, float miterLimit = 4f, bool beveled = true) + { + foreach (var t in triangles) t.DrawLines(lineThickness, color, miterLimit, beveled); + } + + /// + /// Draws the outline of each triangle in the triangulation using . + /// + /// The collection of triangles to draw. + /// Contains line style information such as thickness and color. + /// The limit for miter joins to prevent sharp spikes. Defaults to 4f. + /// If true, uses beveled joins when the miter limit is exceeded; otherwise, cuts off the miter point. + public static void DrawLines(this Triangulation triangles, LineDrawingInfo lineInfo, float miterLimit = 4f, bool beveled = true) + { + foreach (var t in triangles) t.DrawLines(lineInfo, miterLimit, beveled); + } + + #endregion + + #region Draw Lines Scaled + + /// + /// Draws the outlines of each triangle in the triangulation where each side can be scaled towards the origin of the side. + /// + /// The collection of triangles to draw. + /// The line drawing information (thickness, color, cap type, etc.). + /// The scale factor for each side (0 = No Side, 1 = Full Side). + /// The point along each side to scale from in both directions (0 = Start, 1 = End). + /// + /// Allows for dynamic scaling of triangle sides, useful for effects or partial outlines. + /// + public static void DrawLinesScaled(this Triangulation triangles, LineDrawingInfo lineInfo, float sideScaleFactor, float sideScaleOrigin = 0.5f) + { + foreach (var t in triangles) t.DrawLinesScaled(lineInfo, sideScaleFactor, sideScaleOrigin); + } + + #endregion + + #region Draw Vertices + + /// + /// Draws circles at each vertex of every triangle in the triangulation. + /// + /// The collection of triangles whose vertices to draw. + /// The radius of each vertex circle. + /// The color of the vertex circles. + /// + /// The smoothness value (0-1). This controls the visual quality of the circle by inversely interpolating the current . + /// A value of 0 uses the maximum side length (fewer sides, less smooth), while 1 uses the minimum side length (more sides, smoother). + /// + public static void DrawVertices(this Triangulation triangles, float vertexRadius, ColorRgba color, float smoothness) + { + foreach (var t in triangles) t.DrawVertices(vertexRadius, color, smoothness); + } + #endregion + + #region Draw Glow + + /// + /// Draws the triangulation using a glow-like effect by repeatedly drawing the triangles + /// with interpolated colors between and . + /// + /// The triangulation to draw (extension method target). + /// The starting color for the glow (first pass). + /// The ending color for the glow (last pass). + /// Number of passes used to interpolate between colors. Must be >= 1; 1 draws a single pass using . + public static void DrawGlow(this Triangulation triangulation, ColorRgba color, ColorRgba endColorRgba, int steps) + { + if (triangulation.Count < 3 || steps <= 0) return; + + if (steps == 1) + { + triangulation.Draw(color); + return; + } + + for (var s = 0; s < steps; s++) + { + float f = s / (float)(steps - 1); + var currentColor = color.Lerp(endColorRgba, f); + triangulation.Draw(currentColor); + } + } + + #endregion + +} \ No newline at end of file diff --git a/ShapeEngine/Geometry/TriangulationDef/TriangulationIntersectShape.cs b/ShapeEngine/Geometry/TriangulationDef/TriangulationIntersectShape.cs index 2fe09d65..e8ed8918 100644 --- a/ShapeEngine/Geometry/TriangulationDef/TriangulationIntersectShape.cs +++ b/ShapeEngine/Geometry/TriangulationDef/TriangulationIntersectShape.cs @@ -15,306 +15,298 @@ namespace ShapeEngine.Geometry.TriangulationDef; public partial class Triangulation { + /// - /// Computes intersection points between the triangles and all colliders in the specified . + /// Computes intersections between this triangulation and all colliders in the specified , writing the results into . /// + /// The destination dictionary that will be cleared and populated with one entry per intersecting collider. /// The collision object containing colliders to test for intersection. - /// - /// A mapping each collider to its intersection points, - /// or null if no colliders are present or no intersections are found. - /// - public Dictionary?>? Intersect(CollisionObject collisionObject) + /// + /// If has no colliders, the method returns immediately without modifying . Only colliders that produce at least one valid triangle intersection are added. + /// + public void Intersect(Dictionary> result, CollisionObject collisionObject) { - if (!collisionObject.HasColliders) return null; + if (!collisionObject.HasColliders) return; + result.Clear(); - Dictionary?>? intersections = null; + var buffer = new Dictionary(); foreach (var collider in collisionObject.Colliders) { - var result = Intersect(collider); - if(result == null) continue; - intersections ??= new(); - intersections.Add(collider, result); + Intersect(buffer, collider); + if(buffer.Count <= 0) continue; + result.Add(collider, new Dictionary(buffer)); } - return intersections; } + /// - /// Checks for intersections between the triangles in this triangulation and the specified collider. + /// Computes intersections between the triangles in this triangulation and the specified , writing them into . /// + /// The destination dictionary that will be cleared and populated with one entry per intersecting triangle index. /// The collider to check for intersections. - /// A dictionary mapping the index of each intersecting triangle to the resulting . Returns null if no intersections are found. - /// Only triangles with valid intersections are included in the result. - public Dictionary? Intersect(Collider collider) + /// If is not enabled, the method returns immediately without modifying . Only triangles with valid intersections are included. + public void Intersect(Dictionary result, Collider collider) { - if (!collider.Enabled) return null; - Dictionary? result = null; + if (!collider.Enabled) return; + + result.Clear(); for (int i = 0; i < Count; i++) { var tri = this[i]; var intersection = tri.Intersect(collider); if (intersection != null && intersection.Valid) { - result ??= new(); result.Add(i, intersection); } } - - return result; } /// - /// Checks for intersections between the triangles in this triangulation and the specified line. + /// Computes intersections between the triangles in this triangulation and the specified line, writing them into . /// + /// The destination dictionary that will be cleared and populated with one entry per intersecting triangle index. /// The line to check for intersections. - /// A dictionary mapping the index of each intersecting triangle to the resulting . Returns null if no intersections are found. /// Only triangles with valid intersections are included in the result. - public Dictionary? IntersectShape(Line shape) + public void IntersectShape(Dictionary result, Line shape) { - Dictionary? result = null; + result.Clear(); for (int i = 0; i < Count; i++) { var tri = this[i]; var intersection = tri.IntersectShape(shape); if (intersection != null && intersection.Valid) { - result ??= new(); result.Add(i, intersection); } } - - return result; } /// - /// Checks for intersections between the triangles in this triangulation and the specified ray. + /// Computes intersections between the triangles in this triangulation and the specified ray, writing them into . /// + /// The destination dictionary that will be cleared and populated with one entry per intersecting triangle index. /// The ray to check for intersections. - /// A dictionary mapping the index of each intersecting triangle to the resulting . Returns null if no intersections are found. /// Only triangles with valid intersections are included in the result. - public Dictionary? IntersectShape(Ray shape) + public void IntersectShape(Dictionary result, Ray shape) { - Dictionary? result = null; + result.Clear(); for (int i = 0; i < Count; i++) { var tri = this[i]; var intersection = tri.IntersectShape(shape); if (intersection != null && intersection.Valid) { - result ??= new(); result.Add(i, intersection); } } - - return result; } /// - /// Checks for intersections between the triangles in this triangulation and the specified segment. + /// Computes intersections between the triangles in this triangulation and the specified segment, writing them into . /// + /// The destination dictionary that will be cleared and populated with one entry per intersecting triangle index. /// The segment to check for intersections. - /// A dictionary mapping the index of each intersecting triangle to the resulting . Returns null if no intersections are found. /// Only triangles with valid intersections are included in the result. - public Dictionary? IntersectShape(Segment shape) + public void IntersectShape(Dictionary result, Segment shape) { - Dictionary? result = null; + result.Clear(); for (int i = 0; i < Count; i++) { var tri = this[i]; var intersection = tri.IntersectShape(shape); if (intersection != null && intersection.Valid) { - result ??= new(); result.Add(i, intersection); } } - - return result; } /// - /// Checks for intersections between the triangles in this triangulation and the specified circle. + /// Computes intersections between the triangles in this triangulation and the specified circle, writing them into . /// + /// The destination dictionary that will be cleared and populated with one entry per intersecting triangle index. /// The circle to check for intersections. - /// A dictionary mapping the index of each intersecting triangle to the resulting . Returns null if no intersections are found. /// Only triangles with valid intersections are included in the result. - public Dictionary? IntersectShape(Circle shape) + public void IntersectShape(Dictionary result, Circle shape) { - Dictionary? result = null; + result.Clear(); for (int i = 0; i < Count; i++) { var tri = this[i]; var intersection = tri.IntersectShape(shape); if (intersection != null && intersection.Valid) { - result ??= new(); result.Add(i, intersection); } } - - return result; } /// - /// Checks for intersections between the triangles in this triangulation and the specified triangle. + /// Computes intersections between the triangles in this triangulation and the specified triangle, writing them into . /// + /// The destination dictionary that will be cleared and populated with one entry per intersecting triangle index. /// The triangle to check for intersections. - /// A dictionary mapping the index of each intersecting triangle to the resulting . Returns null if no intersections are found. /// Only triangles with valid intersections are included in the result. - public Dictionary? IntersectShape(Triangle shape) + public void IntersectShape(Dictionary result, Triangle shape) { - Dictionary? result = null; + result.Clear(); for (int i = 0; i < Count; i++) { var tri = this[i]; var intersection = tri.IntersectShape(shape); if (intersection != null && intersection.Valid) { - result ??= new(); result.Add(i, intersection); } } - - return result; } /// - /// Checks for intersections between the triangles in this triangulation and the specified rectangle. + /// Computes intersections between the triangles in this triangulation and the specified rectangle, writing them into . /// + /// The destination dictionary that will be cleared and populated with one entry per intersecting triangle index. /// The rectangle to check for intersections. - /// A dictionary mapping the index of each intersecting triangle to the resulting . Returns null if no intersections are found. /// Only triangles with valid intersections are included in the result. - public Dictionary? IntersectShape(Rect shape) + public void IntersectShape(Dictionary result, Rect shape) { - Dictionary? result = null; + result.Clear(); for (int i = 0; i < Count; i++) { var tri = this[i]; var intersection = tri.IntersectShape(shape); if (intersection != null && intersection.Valid) { - result ??= new(); result.Add(i, intersection); } } - - return result; } /// - /// Checks for intersections between the triangles in this triangulation and the specified quad. + /// Computes intersections between the triangles in this triangulation and the specified quad, writing them into . /// + /// The destination dictionary that will be cleared and populated with one entry per intersecting triangle index. /// The quad to check for intersections. - /// A dictionary mapping the index of each intersecting triangle to the resulting . Returns null if no intersections are found. /// Only triangles with valid intersections are included in the result. - public Dictionary? IntersectShape(Quad shape) + public void IntersectShape(Dictionary result, Quad shape) { - Dictionary? result = null; + result.Clear(); for (int i = 0; i < Count; i++) { var tri = this[i]; var intersection = tri.IntersectShape(shape); if (intersection != null && intersection.Valid) { - result ??= new(); result.Add(i, intersection); } } - - return result; } /// - /// Checks for intersections between the triangles in this triangulation and the specified polygon. + /// Computes intersections between the triangles in this triangulation and the specified polygon, writing them into . /// + /// The destination dictionary that will be cleared and populated with one entry per intersecting triangle index. /// The polygon to check for intersections. - /// A dictionary mapping the index of each intersecting triangle to the resulting . Returns null if no intersections are found. - /// Only triangles with valid intersections are included in the result. - public Dictionary? IntersectShape(Polygon shape) + /// If contains fewer than three points, the method returns immediately without modifying . Otherwise, only triangles with valid intersections are included. + public void IntersectShape(Dictionary result, Polygon shape) { - if (shape.Count < 3) return null; - Dictionary? result = null; + if (shape.Count < 3) return; + + result.Clear(); + for (int i = 0; i < Count; i++) { var tri = this[i]; var intersection = tri.IntersectShape(shape); if (intersection != null && intersection.Valid) { - result ??= new(); result.Add(i, intersection); } } - return result; } /// - /// Checks for intersections between the triangles in this triangulation and the specified polyline. + /// Computes intersections between the triangles in this triangulation and the specified polyline, writing them into . /// + /// The destination dictionary that will be cleared and populated with one entry per intersecting triangle index. /// The polyline to check for intersections. - /// A dictionary mapping the index of each intersecting triangle to the resulting . Returns null if no intersections are found. - /// Only triangles with valid intersections are included in the result. - public Dictionary? IntersectShape(Polyline shape) + /// If contains fewer than two points, the method returns immediately without modifying . Otherwise, only triangles with valid intersections are included. + public void IntersectShape(Dictionary result, Polyline shape) { - if (shape.Count < 2) return null; - Dictionary? result = null; + if (shape.Count < 2) return; + + result.Clear(); + for (int i = 0; i < Count; i++) { var tri = this[i]; var intersection = tri.IntersectShape(shape); if (intersection != null && intersection.Valid) { - result ??= new(); result.Add(i, intersection); } } - - return result; } /// - /// Checks for intersections between the triangles in this triangulation and the specified segments. + /// Computes intersections between the triangles in this triangulation and the specified segments collection, writing them into . /// + /// The destination dictionary that will be cleared and populated with one entry per intersecting triangle index. /// The segments to check for intersections. - /// A dictionary mapping the index of each intersecting triangle to the resulting . Returns null if no intersections are found. /// Only triangles with valid intersections are included in the result. - public Dictionary? IntersectShape(Segments shape) + public void IntersectShape(Dictionary result, Segments shape) { - Dictionary? result = null; + result.Clear(); + for (int i = 0; i < Count; i++) { var tri = this[i]; var intersection = tri.IntersectShape(shape); if (intersection != null && intersection.Valid) { - result ??= new(); result.Add(i, intersection); } } - - return result; } /// - /// Checks for intersections between the triangles in this triangulation and the specified shape implementing . + /// Computes intersections between the triangles in this triangulation and the specified , writing them into . /// + /// The destination dictionary that will be populated by the shape-specific intersection routine. /// The shape to check for intersections. - /// - /// A dictionary mapping the index of each intersecting triangle to the resulting . - /// Returns null if no intersections are found or the shape type is not supported. - /// - public Dictionary? IntersectShape(IShape shape) + /// + /// This method dispatches to the corresponding IntersectShape overload based on 's runtime shape type. If the shape type is not handled by the switch, is left unchanged by this method. + /// + public void IntersectShape(Dictionary result, IShape shape) { - return shape.GetShapeType() switch + switch (shape.GetShapeType()) { - ShapeType.Circle => IntersectShape(shape.GetCircleShape()), - ShapeType.Segment => IntersectShape(shape.GetSegmentShape()), - ShapeType.Ray => IntersectShape(shape.GetRayShape()), - ShapeType.Line => IntersectShape(shape.GetLineShape()), - ShapeType.Triangle => IntersectShape(shape.GetTriangleShape()), - ShapeType.Rect => IntersectShape(shape.GetRectShape()), - ShapeType.Quad => IntersectShape(shape.GetQuadShape()), - ShapeType.Poly => IntersectShape(shape.GetPolygonShape()), - ShapeType.PolyLine => IntersectShape(shape.GetPolylineShape()), - _ => null - }; + case ShapeType.Circle: + IntersectShape(result, shape.GetCircleShape()); + break; + case ShapeType.Segment: + IntersectShape(result, shape.GetSegmentShape()); + break; + case ShapeType.Ray: + IntersectShape(result, shape.GetRayShape()); + break; + case ShapeType.Line: + IntersectShape(result, shape.GetLineShape()); + break; + case ShapeType.Triangle: + IntersectShape(result, shape.GetTriangleShape()); + break; + case ShapeType.Rect: + IntersectShape(result, shape.GetRectShape()); + break; + case ShapeType.Quad: + IntersectShape(result, shape.GetQuadShape()); + break; + case ShapeType.Poly: + IntersectShape(result, shape.GetPolygonShape()); + break; + case ShapeType.PolyLine: + IntersectShape(result, shape.GetPolylineShape()); + break; + } } } \ No newline at end of file diff --git a/ShapeEngine/Geometry/TriangulationDef/TriangulationMath.cs b/ShapeEngine/Geometry/TriangulationDef/TriangulationMath.cs index 5da83923..11214ea6 100644 --- a/ShapeEngine/Geometry/TriangulationDef/TriangulationMath.cs +++ b/ShapeEngine/Geometry/TriangulationDef/TriangulationMath.cs @@ -244,285 +244,286 @@ public void SetTransform(Transform2D transform, Vector2 origin) } } + /// - /// Rotates all triangles in the triangulation by the specified radians around their origin and returns a new triangulation. + /// Writes copies of all triangles into , rotated by the specified angle. /// + /// The destination triangulation that will be cleared and populated with the rotated triangles. /// The angle in radians to rotate each triangle. - /// A new with all triangles rotated. - /// Does not modify the original triangulation. - public Triangulation ChangeRotationCopy(float rad) + /// This method does not modify the current triangulation. Each transformed triangle is written into . + public void ChangeRotationCopy(Triangulation result, float rad) { - var newTriangulation = new Triangulation(Count); + result.Clear(); + result.EnsureCapacity(Count); + for (var i = 0; i < Count; i++) { - newTriangulation.Add(this[i].ChangeRotation(rad)); + result.Add(this[i].ChangeRotation(rad)); } - - return newTriangulation; } /// - /// Rotates all triangles in the triangulation by the specified radians around a given origin and returns a new triangulation. + /// Writes copies of all triangles into , rotated by the specified angle around . /// + /// The destination triangulation that will be cleared and populated with the rotated triangles. /// The angle in radians to rotate each triangle. /// The origin point to rotate around. - /// A new with all triangles rotated around the origin. - /// Does not modify the original triangulation. - public Triangulation ChangeRotationCopy(float rad, Vector2 origin) + /// This method does not modify the current triangulation. Each transformed triangle is written into . + public void ChangeRotationCopy(Triangulation result, float rad, Vector2 origin) { - var newTriangulation = new Triangulation(Count); + result.Clear(); + result.EnsureCapacity(Count); + for (var i = 0; i < Count; i++) { - newTriangulation.Add(this[i].ChangeRotation(rad, origin)); + result.Add(this[i].ChangeRotation(rad, origin)); } - - return newTriangulation; } /// - /// Sets the rotation of all triangles in the triangulation to the specified radians and returns a new triangulation. + /// Writes copies of all triangles into , with their rotation set to the specified angle. /// + /// The destination triangulation that will be cleared and populated with the rotated triangles. /// The angle in radians to set for each triangle. - /// A new with all triangles set to the specified rotation. - /// Does not modify the original triangulation. - public Triangulation SetRotationCopy(float rad) + /// This method does not modify the current triangulation. Each transformed triangle is written into . + public void SetRotationCopy(Triangulation result, float rad) { - var newTriangulation = new Triangulation(Count); + result.Clear(); + result.EnsureCapacity(Count); + for (var i = 0; i < Count; i++) { - newTriangulation.Add(this[i].SetRotation(rad)); + result.Add(this[i].SetRotation(rad)); } - - return newTriangulation; } /// - /// Sets the rotation of all triangles in the triangulation to the specified radians around a given origin and returns a new triangulation. + /// Writes copies of all triangles into , with their rotation set to the specified angle around . /// + /// The destination triangulation that will be cleared and populated with the rotated triangles. /// The angle in radians to set for each triangle. /// The origin point to rotate around. - /// A new with all triangles set to the specified rotation around the origin. - /// Does not modify the original triangulation. - public Triangulation SetRotationCopy(float rad, Vector2 origin) + /// This method does not modify the current triangulation. Each transformed triangle is written into . + public void SetRotationCopy(Triangulation result, float rad, Vector2 origin) { - var newTriangulation = new Triangulation(Count); + result.Clear(); + result.EnsureCapacity(Count); + for (var i = 0; i < Count; i++) { - newTriangulation.Add(this[i].SetRotation(rad, origin)); + result.Add(this[i].SetRotation(rad, origin)); } - - return newTriangulation; } /// - /// Scales all triangles in the triangulation by the specified uniform scale factor and returns a new triangulation. + /// Writes copies of all triangles into , uniformly scaled by the specified factor. /// + /// The destination triangulation that will be cleared and populated with the scaled triangles. /// The uniform scale factor to apply to each triangle. - /// A new with all triangles scaled. - /// Does not modify the original triangulation. - public Triangulation ScaleSizeCopy(float scale) + /// This method does not modify the current triangulation. Each transformed triangle is written into . + public void ScaleSizeCopy(Triangulation result, float scale) { - var newTriangulation = new Triangulation(Count); + result.Clear(); + result.EnsureCapacity(Count); + for (var i = 0; i < Count; i++) { - newTriangulation.Add(this[i].ScaleSize(scale)); + result.Add(this[i].ScaleSize(scale)); } - - return newTriangulation; } /// - /// Scales all triangles in the triangulation by the specified size scale and returns a new triangulation. + /// Writes copies of all triangles into , scaled by the specified . /// + /// The destination triangulation that will be cleared and populated with the scaled triangles. /// The size scale to apply to each triangle. - /// A new with all triangles scaled. - /// Does not modify the original triangulation. - public Triangulation ScaleSizeCopy(Size scale) + /// This method does not modify the current triangulation. Each transformed triangle is written into . + public void ScaleSizeCopy(Triangulation result, Size scale) { - var newTriangulation = new Triangulation(Count); + result.Clear(); + result.EnsureCapacity(Count); + for (var i = 0; i < Count; i++) { - newTriangulation.Add(this[i].ScaleSize(scale)); + result.Add(this[i].ScaleSize(scale)); } - - return newTriangulation; } /// - /// Scales all triangles in the triangulation by the specified uniform scale factor around a given origin and returns a new triangulation. + /// Writes copies of all triangles into , uniformly scaled by the specified factor around . /// + /// The destination triangulation that will be cleared and populated with the scaled triangles. /// The uniform scale factor to apply to each triangle. /// The origin point to scale around. - /// A new with all triangles scaled around the origin. - /// Does not modify the original triangulation. - public Triangulation ScaleSizeCopy(float scale, Vector2 origin) + /// This method does not modify the current triangulation. Each transformed triangle is written into . + public void ScaleSizeCopy(Triangulation result, float scale, Vector2 origin) { - var newTriangulation = new Triangulation(Count); + result.Clear(); + result.EnsureCapacity(Count); + for (var i = 0; i < Count; i++) { - newTriangulation.Add(this[i].ScaleSize(scale, origin)); + result.Add(this[i].ScaleSize(scale, origin)); } - - return newTriangulation; } /// - /// Scales all triangles in the triangulation by the specified size scale around a given origin and returns a new triangulation. + /// Writes copies of all triangles into , scaled by the specified around . /// + /// The destination triangulation that will be cleared and populated with the scaled triangles. /// The size scale to apply to each triangle. /// The origin point to scale around. - /// A new with all triangles scaled around the origin. - /// Does not modify the original triangulation. - public Triangulation ScaleSizeCopy(Size scale, Vector2 origin) + /// This method does not modify the current triangulation. Each transformed triangle is written into . + public void ScaleSizeCopy(Triangulation result, Size scale, Vector2 origin) { - var newTriangulation = new Triangulation(Count); + result.Clear(); + result.EnsureCapacity(Count); + for (var i = 0; i < Count; i++) { - newTriangulation.Add(this[i].ScaleSize(scale, origin)); + result.Add(this[i].ScaleSize(scale, origin)); } - - return newTriangulation; } /// - /// Changes the size of all triangles in the triangulation by the specified amount and returns a new triangulation. + /// Writes copies of all triangles into , with their size changed by the specified amount. /// + /// The destination triangulation that will be cleared and populated with the resized triangles. /// The amount to change the size of each triangle. - /// A new with all triangles changed in size. - /// Does not modify the original triangulation. - public Triangulation ChangeSizeCopy(float amount) + /// This method does not modify the current triangulation. Each transformed triangle is written into . + public void ChangeSizeCopy(Triangulation result, float amount) { - var newTriangulation = new Triangulation(Count); + result.Clear(); + result.EnsureCapacity(Count); + for (var i = 0; i < Count; i++) { - newTriangulation.Add(this[i].ChangeSize(amount)); + result.Add(this[i].ChangeSize(amount)); } - - return newTriangulation; } /// - /// Changes the size of all triangles in the triangulation by the specified amount around a given origin and returns a new triangulation. + /// Writes copies of all triangles into , with their size changed by the specified amount around . /// + /// The destination triangulation that will be cleared and populated with the resized triangles. /// The amount to change the size of each triangle. /// The origin point to scale around. - /// A new with all triangles changed in size around the origin. - /// Does not modify the original triangulation. - public Triangulation ChangeSizeCopy(float amount, Vector2 origin) + /// This method does not modify the current triangulation. Each transformed triangle is written into . + public void ChangeSizeCopy(Triangulation result, float amount, Vector2 origin) { - var newTriangulation = new Triangulation(Count); + result.Clear(); + result.EnsureCapacity(Count); + for (var i = 0; i < Count; i++) { - newTriangulation.Add(this[i].ChangeSize(amount, origin)); + result.Add(this[i].ChangeSize(amount, origin)); } - - return newTriangulation; } /// - /// Sets the size of all triangles in the triangulation to the specified value and returns a new triangulation. + /// Writes copies of all triangles into , with their size set to the specified value. /// + /// The destination triangulation that will be cleared and populated with the resized triangles. /// The size to set for each triangle. - /// A new with all triangles set to the specified size. - /// Does not modify the original triangulation. - public Triangulation SetSizeCopy(float size) + /// This method does not modify the current triangulation. Each transformed triangle is written into . + public void SetSizeCopy(Triangulation result, float size) { - var newTriangulation = new Triangulation(Count); + result.Clear(); + result.EnsureCapacity(Count); + for (var i = 0; i < Count; i++) { - newTriangulation.Add(this[i].SetSize(size)); + result.Add(this[i].SetSize(size)); } - - return newTriangulation; } /// - /// Sets the size of all triangles in the triangulation to the specified value around a given origin and returns a new triangulation. + /// Writes copies of all triangles into , with their size set to the specified value around . /// + /// The destination triangulation that will be cleared and populated with the resized triangles. /// The size to set for each triangle. /// The origin point to scale around. - /// A new with all triangles set to the specified size around the origin. - /// Does not modify the original triangulation. - public Triangulation SetSizeCopy(float size, Vector2 origin) + /// This method does not modify the current triangulation. Each transformed triangle is written into . + public void SetSizeCopy(Triangulation result, float size, Vector2 origin) { - var newTriangulation = new Triangulation(Count); + result.Clear(); + result.EnsureCapacity(Count); + for (var i = 0; i < Count; i++) { - newTriangulation.Add(this[i].SetSize(size, origin)); + result.Add(this[i].SetSize(size, origin)); } - - return newTriangulation; } /// - /// Changes the position of all triangles in the triangulation by the specified offset and returns a new triangulation. + /// Writes copies of all triangles into , translated by the specified offset. /// + /// The destination triangulation that will be cleared and populated with the translated triangles. /// The offset to apply to each triangle. - /// A new with all triangles moved by the offset. - /// Does not modify the original triangulation. - public Triangulation ChangePositionCopy(Vector2 offset) + /// This method does not modify the current triangulation. Each transformed triangle is written into . + public void ChangePositionCopy(Triangulation result, Vector2 offset) { - var newTriangulation = new Triangulation(Count); + result.Clear(); + result.EnsureCapacity(Count); + for (var i = 0; i < Count; i++) { - newTriangulation.Add(this[i].ChangePosition(offset)); + result.Add(this[i].ChangePosition(offset)); } - - return newTriangulation; } /// - /// Sets the position of all triangles in the triangulation to the specified position around a given origin and returns a new triangulation. + /// Writes copies of all triangles into , with their position set to the specified value around . /// + /// The destination triangulation that will be cleared and populated with the translated triangles. /// The position to set for each triangle. /// The origin point to use for positioning. - /// A new with all triangles set to the specified position around the origin. - /// Does not modify the original triangulation. - public Triangulation SetPositionCopy(Vector2 position, Vector2 origin) + /// This method does not modify the current triangulation. Each transformed triangle is written into . + public void SetPositionCopy(Triangulation result, Vector2 position, Vector2 origin) { - var newTriangulation = new Triangulation(Count); + result.Clear(); + result.EnsureCapacity(Count); + for (var i = 0; i < Count; i++) { - newTriangulation.Add(this[i].SetPosition(position, origin)); + result.Add(this[i].SetPosition(position, origin)); } - - return newTriangulation; } /// - /// Applies the specified transform offset to all triangles in the triangulation around a given origin and returns a new triangulation. + /// Writes copies of all triangles into , with the specified offset transform applied around . /// + /// The destination triangulation that will be cleared and populated with the transformed triangles. /// The transform offset to apply to each triangle. /// The origin point to use for the transformation. - /// A new with all triangles transformed by the offset around the origin. - /// Does not modify the original triangulation. - public Triangulation ApplyOffsetCopy(Transform2D offset, Vector2 origin) + /// This method does not modify the current triangulation. Each transformed triangle is written into . + public void ApplyOffsetCopy(Triangulation result, Transform2D offset, Vector2 origin) { - var newTriangulation = new Triangulation(Count); + result.Clear(); + result.EnsureCapacity(Count); + for (var i = 0; i < Count; i++) { - newTriangulation.Add(this[i].ApplyOffset(offset, origin)); + result.Add(this[i].ApplyOffset(offset, origin)); } - - return newTriangulation; } /// - /// Sets the transform of all triangles in the triangulation to the specified transform around a given origin and returns a new triangulation. + /// Writes copies of all triangles into , with their transform set to the specified value around . /// + /// The destination triangulation that will be cleared and populated with the transformed triangles. /// The transform to set for each triangle. /// The origin point to use for the transformation. - /// A new with all triangles set to the specified transform around the origin. - /// Does not modify the original triangulation. - public Triangulation SetTransformCopy(Transform2D transform, Vector2 origin) + /// This method does not modify the current triangulation. Each transformed triangle is written into . + public void SetTransformCopy(Triangulation result, Transform2D transform, Vector2 origin) { - var newTriangulation = new Triangulation(Count); + result.Clear(); + result.EnsureCapacity(Count); + for (var i = 0; i < Count; i++) { - newTriangulation.Add(this[i].SetTransform(transform, origin)); + result.Add(this[i].SetTransform(transform, origin)); } - - return newTriangulation; } #endregion diff --git a/ShapeEngine/Geometry/TriangulationDef/TriangulationOverlap.cs b/ShapeEngine/Geometry/TriangulationDef/TriangulationOverlap.cs index f2a6e78b..b6b869f4 100644 --- a/ShapeEngine/Geometry/TriangulationDef/TriangulationOverlap.cs +++ b/ShapeEngine/Geometry/TriangulationDef/TriangulationOverlap.cs @@ -29,6 +29,7 @@ public bool Overlap(CollisionObject collision) return false; } + /// /// Determines if any triangle in this collection overlaps with the specified collider. /// diff --git a/ShapeEngine/Geometry/UIDrawing.cs b/ShapeEngine/Geometry/UIDrawing.cs deleted file mode 100644 index defc598b..00000000 --- a/ShapeEngine/Geometry/UIDrawing.cs +++ /dev/null @@ -1,228 +0,0 @@ -using System.Numerics; -using ShapeEngine.Color; -using ShapeEngine.Geometry.CircleDef; -using ShapeEngine.Geometry.RectDef; -using ShapeEngine.Geometry.SegmentDef; -using ShapeEngine.StaticLib; - -namespace ShapeEngine.Geometry; - -/// -/// Provides extension methods for drawing UI shapes such as bars and outlines for rectangles and circles. -/// -/// -/// These methods are intended for rendering progress bars, outlines, and similar UI elements with customizable appearance. -/// -public static class UIDrawing -{ - /// - /// Draws an outline bar along the border of a rectangle, filling the outline based on the specified progress value. - /// - /// The rectangle to draw the outline on. - /// The thickness of the outline. - /// The progress value (0 to 1) indicating how much of the outline to draw. - /// The color of the outline. - /// - /// The outline is drawn in four segments (top, right, bottom, left), and the progress value determines how many segments are filled. - /// - public static void DrawOutlineBar(this Rect rect, float thickness, float f, ColorRgba color) - { - var thicknessOffsetX = new Vector2(thickness, 0f); - var thicknessOffsetY = new Vector2(0f, thickness); - - var tl = new Vector2(rect.X, rect.Y); - var br = tl + new Vector2(rect.Width, rect.Height); - var tr = tl + new Vector2(rect.Width, 0); - var bl = tl + new Vector2(0, rect.Height); - - var lines = (int)MathF.Ceiling(4 * ShapeMath.Clamp(f, 0f, 1f)); - float fMin = 0.25f * (lines - 1); - float fMax = fMin + 0.25f; - float newF = ShapeMath.RemapFloat(f, fMin, fMax, 0f, 1f); - for (var i = 0; i < lines; i++) - { - Vector2 end; - Vector2 start; - if (i == 0) - { - start = tl - thicknessOffsetX / 2; - end = tr - thicknessOffsetX / 2; - } - else if (i == 1) - { - start = tr - thicknessOffsetY / 2; - end = br - thicknessOffsetY / 2; - } - else if (i == 2) - { - start = br + thicknessOffsetX / 2; - end = bl + thicknessOffsetX / 2; - } - else - { - start = bl + thicknessOffsetY / 2; - end = tl + thicknessOffsetY / 2; - } - - //last line - if (i == lines - 1) end = ShapeVec.Lerp(start, end, newF); - SegmentDrawing.DrawSegment(start, end, thickness, color); - // DrawLineEx(start, end, thickness, color.ToRayColor()); - } - } - - /// - /// Draws an outline bar along the border of a rotated rectangle, filling the outline based on the specified progress value. - /// - /// The rectangle to draw the outline on. - /// The pivot point for rotation. - /// The rotation angle in degrees. - /// The thickness of the outline. - /// The progress value (0 to 1) indicating how much of the outline to draw. - /// The color of the outline. - /// - /// The outline is drawn in four segments (top, right, bottom, left), and the progress value determines how many segments are filled. - /// The rectangle is rotated around the specified pivot point. - /// - public static void DrawOutlineBar(this Rect rect, Vector2 pivot, float angleDeg, float thickness, float f, ColorRgba color) - { - var rr = rect.RotateCorners(pivot, angleDeg); - //Vector2 thicknessOffsetX = new Vector2(thickness, 0f); - //Vector2 thicknessOffsetY = new Vector2(0f, thickness); - - var leftExtension = new Vector2(-thickness / 2, 0f).Rotate(angleDeg * ShapeMath.DEGTORAD); - var rightExtension = new Vector2(thickness / 2, 0f).Rotate(angleDeg * ShapeMath.DEGTORAD); - - var tl = rr.tl; - var br = rr.br; - var tr = rr.tr; - var bl = rr.bl; - - int lines = (int)MathF.Ceiling(4 * ShapeMath.Clamp(f, 0f, 1f)); - float fMin = 0.25f * (lines - 1); - float fMax = fMin + 0.25f; - float newF = ShapeMath.RemapFloat(f, fMin, fMax, 0f, 1f); - for (int i = 0; i < lines; i++) - { - Vector2 end; - Vector2 start; - if (i == 0) - { - start = tl + leftExtension; - end = tr + rightExtension; - } - else if (i == 1) - { - start = tr; - end = br; - } - else if (i == 2) - { - start = br + rightExtension; - end = bl + leftExtension; - } - else - { - start = bl; - end = tl; - } - - //last line - if (i == lines - 1) end = ShapeVec.Lerp(start, end, newF); - SegmentDrawing.DrawSegment(start, end, thickness, color); - // Raylib.DrawLineEx(start, end, thickness, color.ToRayColor()); - } - } - - /// - /// Draws an outline bar along the circumference of a circle, filling the outline based on the specified progress value. - /// - /// The circle to draw the outline on. - /// The thickness of the outline. - /// The progress value (0 to 1) indicating how much of the outline to draw (as a fraction of the circle). - /// The color of the outline. - /// - /// The outline is drawn as a sector of the circle, starting from 0 degrees. - /// - public static void DrawOutlineBar(this Circle c, float thickness, float f, ColorRgba color) => CircleDrawing.DrawCircleSectorLines(c.Center, c.Radius, 0, 360 * f, thickness, color, false); - - /// - /// Draws an outline bar along the circumference of a circle, starting at a specified angle offset, and filling based on the progress value. - /// - /// The circle to draw the outline on. - /// The starting angle offset in degrees. - /// The thickness of the outline. - /// The progress value (0 to 1) indicating how much of the outline to draw (as a fraction of the circle). - /// The color of the outline. - /// - /// The outline is drawn as a sector of the circle, starting from the specified angle offset. - /// - public static void DrawOutlineBar(this Circle c, float startOffsetDeg, float thickness, float f, ColorRgba color) => CircleDrawing.DrawCircleSectorLines(c.Center, c.Radius, 0, 360 * f, startOffsetDeg, thickness, color, false); - - /// - /// Draws a filled bar inside a rectangle, representing progress with customizable margins and colors. - /// - /// The rectangle to draw the bar in. - /// The progress value (0 to 1) indicating how much of the bar to fill. - /// The color of the filled bar. - /// The background color of the rectangle. - /// The left margin left * (1 - f) to determine the fill behavior (default 0). - /// The right margin right * (1 - f) to determine the fill behavior (default 1). - /// The top margin top * (1 - f) to determine the fill behavior (default 0). - /// The bottom margin bottom * (1 - f) to determine the fill behavior (default 0). - /// - /// The bar is drawn inside the rectangle, with the filled area shrinking as the progress value increases. - /// The default margin values represent a bar that fills from the left to the right. - /// - /// - /// - /// left 1, right 0, top 0, bottom 0 -> bar fills from right to left. - /// left 0, right 0, top 1, bottom 0 -> bar fills from bottom to top. - /// left 0, right 0, top 0, bottom 1 -> bar fills from top to bottom. - /// left 0.5, right 0.5, top 0, bottom 0 -> bar fills from center to left and right edges. - /// - /// - public static void DrawBar(this Rect rect, float f, ColorRgba barColorRgba, ColorRgba bgColorRgba, float left = 0f, float right = 1f, float top = 0f, float bottom = 0f) - { - f = 1.0f - f; - Rect.Margins progressMargins = new(f * top, f * right, f * bottom, f * left); - var progressRect = rect.ApplyMargins(progressMargins); // progressMargins.Apply(rect); - rect.Draw(bgColorRgba); - progressRect.Draw(barColorRgba); - } - - /// - /// Draws a filled bar inside a rotated rectangle, representing progress with customizable margins and colors. - /// - /// The rectangle to draw the bar in. - /// The pivot point for rotation. - /// The rotation angle in degrees. - /// The progress value (0 to 1) indicating how much of the bar to fill. - /// The color of the filled bar. - /// The background color of the rectangle. - /// The left margin left * (1 - f) to determine the fill behavior (default 0). - /// The right margin right * (1 - f) to determine the fill behavior (default 1). - /// The top margin top * (1 - f) to determine the fill behavior (default 0). - /// The bottom margin bottom * (1 - f) to determine the fill behavior (default 0). - /// - /// The bar is drawn inside the rectangle, with the filled area shrinking as the progress value increases. - /// The default margin values represent a bar that fills from the left to the right. - /// The rectangle is rotated around the specified pivot point. - /// - /// - /// - /// left 1, right 0, top 0, bottom 0 -> bar fills from right to left. - /// left 0, right 0, top 1, bottom 0 -> bar fills from bottom to top. - /// left 0, right 0, top 0, bottom 1 -> bar fills from top to bottom. - /// left 0.5, right 0.5, top 0, bottom 0 -> bar fills from center to left and right edges. - /// - /// - public static void DrawBar(this Rect rect, Vector2 pivot, float angleDeg, float f, ColorRgba barColorRgba, ColorRgba bgColorRgba, float left = 0f, float right = 1f, float top = 0f, float bottom = 0f) - { - f = 1.0f - f; - Rect.Margins progressMargins = new(f * top, f * right, f * bottom, f * left); - var progressRect = rect.ApplyMargins(progressMargins); // progressMargins.Apply(rect); - rect.Draw(pivot, angleDeg, bgColorRgba); - progressRect.Draw(pivot, angleDeg, barColorRgba); - } -} \ No newline at end of file diff --git a/ShapeEngine/Random/Rng.cs b/ShapeEngine/Random/Rng.cs index 7c989812..58bc9b8d 100644 --- a/ShapeEngine/Random/Rng.cs +++ b/ShapeEngine/Random/Rng.cs @@ -11,6 +11,8 @@ namespace ShapeEngine.Random; /// public class Rng { + #region Fields + /// /// Singleton instance of the class. /// @@ -26,6 +28,10 @@ public class Rng /// public int Seed { get; private set; } + #endregion + + #region Constructors + /// /// Initializes a new instance of the class with a time-based seed. /// @@ -53,7 +59,11 @@ public Rng(int seed) Rand = new System.Random(seed); Seed = seed; } + + #endregion + #region Seed + /// /// Sets the random number generator to use the specified seed. /// @@ -64,6 +74,8 @@ public void SetSeed(int seed) Seed = seed; } + #endregion + #region Weighted /// /// Picks a random item from the given weighted items. @@ -90,6 +102,7 @@ public void SetSeed(int seed) return default(T); } + /// /// Picks a random item from the given items and weights. /// @@ -115,6 +128,7 @@ public void SetSeed(int seed) return default(T); } + /// /// Picks a random string from the given string-weight pairs. /// @@ -139,6 +153,85 @@ public string PickRandomItem(params (string id, int weight)[] items) return ""; } + + /// + /// Picks a random item from the given weighted items. + /// + /// The type of the items. + /// A read-only list of weighted items. + /// A randomly selected item, or default if no item is selected. + public T? PickRandomItem(IReadOnlyList> items) + { + int totalWeight = 0; + foreach (var item in items) + { + totalWeight += item.weight; + } + + int ticket = RandI(0, totalWeight); + + int curWeight = 0; + foreach (var item in items) + { + curWeight += item.weight; + if (ticket <= curWeight) return item.item; + } + + return default(T); + } + + /// + /// Picks a random item from the given items and weights. + /// + /// The type of the items. + /// A read-only list of tuples containing items and their weights. + /// A randomly selected item, or default if no item is selected. + public T? PickRandomItem(IReadOnlyList<(T item, int weight)> items) + { + int totalWeight = 0; + foreach (var item in items) + { + totalWeight += item.weight; + } + + int ticket = RandI(0, totalWeight); + + int curWeight = 0; + foreach (var item in items) + { + curWeight += item.weight; + if (ticket <= curWeight) return item.item; + } + + return default(T); + } + + /// + /// Picks a random string from the given string-weight pairs. + /// + /// A read-only list of tuples containing string IDs and their weights. + /// A randomly selected string, or empty string if none. + public string PickRandomItem(IReadOnlyList<(string id, int weight)> items) + { + int totalWeight = 0; + foreach (var item in items) + { + totalWeight += item.weight; + } + + int ticket = RandI(0, totalWeight); + + int curWeight = 0; + foreach (var item in items) + { + curWeight += item.weight; + if (ticket <= curWeight) return item.id; + } + + return ""; + } + + /// /// Picks multiple random items from the given weighted items. /// @@ -149,6 +242,52 @@ public string PickRandomItem(params (string id, int weight)[] items) public List PickRandomItems(int amount, params WeightedItem[] items) { List chosen = new(); + PickRandomItems(chosen, amount, items); + return chosen; + } + + /// + /// Picks multiple random items from the given items and weights. + /// + /// The type of the items. + /// The number of items to pick. + /// An array of tuples containing items and their weights. + /// A list of randomly selected items. + public List PickRandomItems(int amount, params (T item, int weight)[] items) + { + List chosen = new(); + PickRandomItems(chosen, amount, items); + return chosen; + } + + /// + /// Picks multiple random strings from the given string-weight pairs. + /// + /// The number of strings to pick. + /// An array of tuples containing string IDs and their weights. + /// A list of randomly selected strings. + public List PickRandomItems(int amount, params (string id, int weight)[] items) + { + List chosen = new(); + PickRandomItems(chosen, amount, items); + return chosen; + } + + /// + /// Picks multiple random items from the given weighted items and writes them into . + /// + /// The type of the items. + /// The destination list that will be cleared and populated with the selected items. + /// The number of items to pick. + /// An array of weighted items. + /// + /// This method samples with replacement. The same item may be selected multiple times. + /// + public void PickRandomItems(List result, int amount, params WeightedItem[] items) + { + result.Clear(); + result.EnsureCapacity(amount); + int totalWeight = 0; foreach (var item in items) { @@ -165,30 +304,34 @@ public List PickRandomItems(int amount, params WeightedItem[] items) curWeight += item.weight; if (ticket <= curWeight) { - chosen.Add(item.item); + result.Add(item.item); break; } } } - return chosen; } + /// - /// Picks multiple random items from the given items and weights. + /// Picks multiple random items from the given items and weights and writes them into . /// /// The type of the items. + /// The destination list that will be cleared and populated with the selected items. /// The number of items to pick. /// An array of tuples containing items and their weights. - /// A list of randomly selected items. - public List PickRandomItems(int amount, params (T item, int weight)[] items) + /// + /// This method samples with replacement. The same item may be selected multiple times. + /// + public void PickRandomItems(List result, int amount, params (T item, int weight)[] items) { - List chosen = new(); + result.Clear(); + result.EnsureCapacity(amount); + int totalWeight = 0; foreach (var item in items) { totalWeight += item.weight; } - - + for (int i = 0; i < amount; i++) { int ticket = RandI(0, totalWeight); @@ -199,23 +342,27 @@ public List PickRandomItems(int amount, params (T item, int weight)[] item curWeight += item.weight; if (ticket <= curWeight) { - chosen.Add(item.item); + result.Add(item.item); break; } } } - - return chosen; } + /// - /// Picks multiple random strings from the given string-weight pairs. + /// Picks multiple random strings from the given string-weight pairs and writes them into . /// + /// The destination list that will be cleared and populated with the selected strings. /// The number of strings to pick. /// An array of tuples containing string IDs and their weights. - /// A list of randomly selected strings. - public List PickRandomItems(int amount, params (string id, int weight)[] items) + /// + /// This method samples with replacement. The same string may be selected multiple times. + /// + public void PickRandomItems(List result, int amount, params (string id, int weight)[] items) { - List chosen = new(); + result.Clear(); + result.EnsureCapacity(amount); + int totalWeight = 0; foreach (var item in items) { @@ -233,13 +380,124 @@ public List PickRandomItems(int amount, params (string id, int weight)[] curWeight += item.weight; if (ticket <= curWeight) { - chosen.Add(item.id); + result.Add(item.id); break; } } } + } + + /// + /// Picks multiple random items from the given weighted items and writes them into . + /// + /// The type of the items. + /// The destination list that will be cleared and populated with the selected items. + /// The number of items to pick. + /// A read-only list of weighted items. + /// + /// This method samples with replacement. The same item may be selected multiple times. + /// + public void PickRandomItems(List result, int amount, IReadOnlyList> items) + { + result.Clear(); + result.EnsureCapacity(amount); + + int totalWeight = 0; + foreach (var item in items) + { + totalWeight += item.weight; + } - return chosen; + for (int i = 0; i < amount; i++) + { + int ticket = RandI(0, totalWeight); + + int curWeight = 0; + foreach (var item in items) + { + curWeight += item.weight; + if (ticket <= curWeight) + { + result.Add(item.item); + break; + } + } + } + } + + /// + /// Picks multiple random items from the given items and weights and writes them into . + /// + /// The type of the items. + /// The destination list that will be cleared and populated with the selected items. + /// The number of items to pick. + /// A read-only list of tuples containing items and their weights. + /// + /// This method samples with replacement. The same item may be selected multiple times. + /// + public void PickRandomItems(List result, int amount, IReadOnlyList<(T item, int weight)> items) + { + result.Clear(); + result.EnsureCapacity(amount); + + int totalWeight = 0; + foreach (var item in items) + { + totalWeight += item.weight; + } + + for (int i = 0; i < amount; i++) + { + int ticket = RandI(0, totalWeight); + + int curWeight = 0; + foreach (var item in items) + { + curWeight += item.weight; + if (ticket <= curWeight) + { + result.Add(item.item); + break; + } + } + } + } + + /// + /// Picks multiple random strings from the given string-weight pairs and writes them into . + /// + /// The destination list that will be cleared and populated with the selected strings. + /// The number of strings to pick. + /// A read-only list of tuples containing string IDs and their weights. + /// + /// This method samples with replacement. The same string may be selected multiple times. + /// + public void PickRandomItems(List result, int amount, IReadOnlyList<(string id, int weight)> items) + { + result.Clear(); + result.EnsureCapacity(amount); + + int totalWeight = 0; + foreach (var item in items) + { + totalWeight += item.weight; + } + + for (int i = 0; i < amount; i++) + { + int ticket = RandI(0, totalWeight); + + int curWeight = 0; + foreach (var item in items) + { + curWeight += item.weight; + if (ticket <= curWeight) + { + result.Add(item.id); + break; + } + } + } } #endregion @@ -316,15 +574,17 @@ public float RandF(float min, float max) /// Returns a non-negative random integer. /// public int RandI() { return Rand.Next(); } + /// - /// Returns a random integer between 0 and max (or max and 0 if max is negative). + /// Returns a random integer between 0 and max (exclusive) when max is positive + /// or max (exclusive) and 0 if max is negative. /// /// The maximum value. public int RandI(int max) { if (max < 0) { - return RandI(max, 0); + return RandI(max + 1, 1); } else if (max > 0) { @@ -332,6 +592,7 @@ public int RandI(int max) } else return 0; } + /// /// Returns a random integer between min and max. /// @@ -405,88 +666,102 @@ public Vector2 RandVec2() /// Returns a color with a random red channel. /// /// The base color. - public ColorRgba RandColorRed(ColorRgba colorRgba) => colorRgba.SetRed((byte)RandI(0, 255)); + public ColorRgba RandColorRed(ColorRgba colorRgba) => colorRgba.SetRed((byte)RandI(0, 256)); + /// - /// Returns a color with a random red channel up to the specified max. + /// Returns a color with a random red channel up to the specified max (exclusive). /// /// The base color. /// The maximum red value. public ColorRgba RandColorRed(ColorRgba colorRgba, int max) => colorRgba.SetRed((byte)RandI(0, max)); + /// - /// Returns a color with a random red channel between min and max. + /// Returns a color with a random red channel between min (inclusive) and max (exclusive). /// /// The base color. /// The minimum red value. /// The maximum red value. public ColorRgba RandColorRed(ColorRgba colorRgba, int min, int max) => colorRgba.SetRed((byte)RandI(min, max)); + /// /// Returns a color with a random green channel. /// /// The base color. - public ColorRgba RandColorGreen(ColorRgba colorRgba) => colorRgba.SetGreen((byte)RandI(0, 255)); + public ColorRgba RandColorGreen(ColorRgba colorRgba) => colorRgba.SetGreen((byte)RandI(0, 256)); + /// - /// Returns a color with a random green channel up to the specified max. + /// Returns a color with a random green channel up to the specified max (exclusive). /// /// The base color. /// The maximum green value. public ColorRgba RandColorGreen(ColorRgba colorRgba, int max) => colorRgba.SetGreen((byte)RandI(0, max)); + /// - /// Returns a color with a random green channel between min and max. + /// Returns a color with a random green channel between min (inclusive) and max (exclusive). /// /// The base color. /// The minimum green value. /// The maximum green value. public ColorRgba RandColorGreen(ColorRgba colorRgba, int min, int max) => colorRgba.SetGreen((byte)RandI(min, max)); + /// /// Returns a color with a random blue channel. /// /// The base color. - public ColorRgba RandColorBlue(ColorRgba colorRgba) => colorRgba.SetBlue((byte)RandI(0, 255)); + public ColorRgba RandColorBlue(ColorRgba colorRgba) => colorRgba.SetBlue((byte)RandI(0, 256)); + /// - /// Returns a color with a random blue channel up to the specified max. + /// Returns a color with a random blue channel up to the specified max (exclusive). /// /// The base color. /// The maximum blue value. public ColorRgba RandColorBlue(ColorRgba colorRgba, int max) => colorRgba.SetBlue((byte)RandI(0, max)); + /// - /// Returns a color with a random blue channel between min and max. + /// Returns a color with a random blue channel between min (inclusive) and max (exclusive). /// /// The base color. /// The minimum blue value. /// The maximum blue value. public ColorRgba RandColorBlue(ColorRgba colorRgba, int min, int max) => colorRgba.SetBlue((byte)RandI(min, max)); + /// /// Returns a color with a random alpha channel. /// /// The base color. - public ColorRgba RandColorAlpha(ColorRgba colorRgba) => colorRgba.SetAlpha((byte)RandI(0, 255)); + public ColorRgba RandColorAlpha(ColorRgba colorRgba) => colorRgba.SetAlpha((byte)RandI(0, 256)); + /// - /// Returns a color with a random alpha channel up to the specified max. + /// Returns a color with a random alpha channel up to the specified max (exclusive). /// /// The base color. /// The maximum alpha value. public ColorRgba RandColorAlpha(ColorRgba colorRgba, int max) => colorRgba.SetAlpha((byte)RandI(0, max)); + /// - /// Returns a color with a random alpha channel between min and max. + /// Returns a color with a random alpha channel between min (inclusive) and max (exclusive). /// /// The base color. /// The minimum alpha value. /// The maximum alpha value. public ColorRgba RandColorAlpha(ColorRgba colorRgba, int min, int max) => colorRgba.SetAlpha((byte)RandI(min, max)); + /// /// Returns a random color with all channels between 0 and 255. /// - public ColorRgba RandColor() => RandColor(0, 255); + public ColorRgba RandColor() => RandColor(0, 256); + /// /// Returns a random color with all channels between 0 and 255 and the specified alpha. /// /// The alpha value. - public ColorRgba RandColor(int alpha) => RandColor(0, 255, alpha); + public ColorRgba RandColor(int alpha) => RandColor(0, 256, alpha); + /// /// Returns a random color with all channels between min and max, and optionally a specified alpha. /// - /// The minimum value for each channel. - /// The maximum value for each channel. + /// The minimum value for each channel (inclusive). + /// The maximum value for each channel (exclusive). /// The alpha value, or -1 for random alpha. public ColorRgba RandColor(int min, int max, int alpha = -1) { @@ -589,6 +864,14 @@ public Rect RandRect(Vector2 origin, float posMin, float posMax, float sizeMin, return new(origin + pos, size, alignment); } + /// + /// Returns a random rectangle contained within the specified , using a random size in the provided range and the specified alignment. + /// + /// The area within which the rectangle position is generated. + /// The minimum random size value for both width and height. + /// The maximum random size value for both width and height. + /// The anchor point used when constructing the rectangle. + /// A random rectangle positioned within . public Rect RandRect(Rect area, float minSize, float maxSize, AnchorPoint alignment) { var size = RandSize(minSize, maxSize); @@ -616,6 +899,7 @@ public Rect RandRect(Rect area, float minSize, float maxSize, AnchorPoint alignm if (pop) list.RemoveAt(index); return t; } + /// /// Returns a list of random elements from the source list, optionally removing them. /// @@ -626,19 +910,11 @@ public Rect RandRect(Rect area, float minSize, float maxSize, AnchorPoint alignm /// A list of randomly selected elements. public List RandCollection(List source, int amount, bool pop = false) { - if (source.Count <= 0 || amount <= 0) return []; - if (pop) amount = Math.Min(amount, source.Count); var list = new List(); - for (var i = 0; i < amount; i++) - { - int index = RandI(0, source.Count); - var element = source[index]; - list.Add(element); - if (pop) source.RemoveAt(index); - } + RandCollection(source, list, amount, pop); return list; - } + /// /// Returns a random element from the array. /// @@ -650,5 +926,43 @@ public List RandCollection(List source, int amount, bool pop = false) if (array.Length <= 0) return default; return array[RandI(0, array.Length)]; } + + + /// + /// Picks random elements from and writes them into . + /// + /// The type of the elements. + /// The source list to pick from. + /// The destination list that will receive the selected elements. + /// The number of elements to pick. + /// true to remove selected elements from as they are chosen; otherwise, false. + /// The number of elements written to . + /// + /// If is empty or is less than or equal to zero, + /// the method returns 0 without modifying . + /// When is false, sampling is performed with replacement. + /// When is true, the requested amount is clamped to the source count + /// and sampling is performed without replacement. + /// + public int RandCollection(List source, List result, int amount, bool pop = false) + { + if (source.Count <= 0 || amount <= 0) return 0; + + if (pop) amount = Math.Min(amount, source.Count); + + result.Clear(); + result.EnsureCapacity(amount); + + for (var i = 0; i < amount; i++) + { + int index = RandI(0, source.Count); + var element = source[index]; + result.Add(element); + if (pop) source.RemoveAt(index); + } + + return result.Count; + } + #endregion } \ No newline at end of file diff --git a/ShapeEngine/Screen/CameraFollowerMulti.cs b/ShapeEngine/Screen/CameraFollowerMulti.cs index 81fa31bf..1956b9c9 100644 --- a/ShapeEngine/Screen/CameraFollowerMulti.cs +++ b/ShapeEngine/Screen/CameraFollowerMulti.cs @@ -68,7 +68,7 @@ public void Reset() public void DrawDebugRect() { prevCameraRect.DrawLines(6f, new(System.Drawing.Color.IndianRed)); - prevCameraRect.Center.Draw(4f, new(System.Drawing.Color.LimeGreen)); + prevCameraRect.Center.Draw(4f, new(System.Drawing.Color.LimeGreen), 0.25f); } /// diff --git a/ShapeEngine/Screen/CustomScreenTextureHandler.cs b/ShapeEngine/Screen/CustomScreenTextureHandler.cs index 1549129c..e17a6d24 100644 --- a/ShapeEngine/Screen/CustomScreenTextureHandler.cs +++ b/ShapeEngine/Screen/CustomScreenTextureHandler.cs @@ -72,5 +72,5 @@ public abstract class CustomScreenTextureHandler /// /// A tuple containing the background and a indicating if the background should be cleared. /// - public virtual (ColorRgba color, bool clear) GetBackgroundClearColor() => (ColorRgba.Clear, true); + public virtual (ColorRgba color, bool clear) GetBackgroundClearColor() => (ColorRgba.Transparent, true); } \ No newline at end of file diff --git a/ShapeEngine/Screen/ScreenTexture.cs b/ShapeEngine/Screen/ScreenTexture.cs index 17fd9e9d..c563be81 100644 --- a/ShapeEngine/Screen/ScreenTexture.cs +++ b/ShapeEngine/Screen/ScreenTexture.cs @@ -74,7 +74,7 @@ public sealed class ScreenTexture /// /// The background color used when clearing the render texture. /// - public ColorRgba BackgroundColor = ColorRgba.Clear; + public ColorRgba BackgroundColor = ColorRgba.Transparent; /// /// Information about the game area and mouse position in world coordinates. /// diff --git a/ShapeEngine/ShapeClipper/ClipperImmediate2D.cs b/ShapeEngine/ShapeClipper/ClipperImmediate2D.cs new file mode 100644 index 00000000..baac0d35 --- /dev/null +++ b/ShapeEngine/ShapeClipper/ClipperImmediate2D.cs @@ -0,0 +1,1313 @@ +using System.Numerics; +using Clipper2Lib; +using ShapeEngine.Color; +using ShapeEngine.Geometry.PolygonDef; +using ShapeEngine.Geometry.PolylineDef; +using ShapeEngine.Geometry.RectDef; +using ShapeEngine.Geometry.TriangulationDef; +using ShapeEngine.StaticLib; + + + +//TODO: +// - Go through Polygon/Polyline/Points and clean up (no duplicates, clear naming, result parameter instead of return value, no extra static functions that could be instance functions, etc.) +// - ClipperImmediate2D needs Draw functions for filled polygons and all types of outlines and all types of polyline drawing (so internal buffers can be used) +// - Keep Cache System? Does it make sense? Can it be simplified? How is cache kept clean, what happens to cached triangulations of polygons that have changed? +// - PolygonDrawing / PolylineDrawing overhaul with ClipperImmediate2d +// - Quad/Rect Chamfered Corners using ClipperImmeadiate2D +// - Conversion for Polyline? + + +//TODO: Remove all functions that use Polygons, Polygon, Polyline, or Polylines parameter and replace it with IReadOnlyList or IReadOnlyList +namespace ShapeEngine.ShapeClipper; + +//TODO: Rename +public static class ClipperImmediate2D +{ + #region Public Settings + /// Max cached triangulations (simple cap; cache clears when exceeded). + public static int MaxTriangulationCacheEntries = 512; + + public static int DecimalPlaces + { + get => scale.DecimalPlaces; + set + { + scale = new(value); + OffsetEngine.Scale = scale; + } + } + + public static double Scale => scale.Scale; + public static double InvScale => scale.InvScale; + #endregion + + #region Private Settings + private static ShapeClipperScale scale = new(4); + #endregion + + #region Reused Clipper Engines + public static ShapeClipperOffset OffsetEngine { get; private set; } = new(); + public static ShapeClipper64 ClipEngine { get; private set; } = new(); + #endregion + + #region Reused Buffers + + private static readonly Path64 path64Buffer = new(); + private static readonly Path64 path64Buffer2 = new(); + private static readonly Paths64 paths64Buffer = new(); + private static readonly Paths64 _tmpOuter = new(); + private static readonly Paths64 _tmpInner = new(); + private static readonly Paths64 _tmpRing = new(); + private static readonly Paths64 _tmpStroke = new(); + private static readonly TriMesh _triMeshBuffer = new(); + private static readonly Paths64PooledBuffer paths64ConversionBuffer = new(); + #endregion + + #region Cache + private static int _nextTriId = 1; + private static readonly Dictionary _keyToId = new(256); + private static readonly Dictionary _idToMesh = new(256); + private static readonly Stack _meshPool = new(); + #endregion + + #region Drawing + //Info: Caching should be used for static polygons / polylines only! Otherwise create triangulation and then move, scale, rotate triangulation and then draw triangulation each frame + public static void DrawPolygonOutline(IReadOnlyList polygonCCW, float thickness, ColorRgba color, float miterLimit = 2f, bool beveled = false, bool useDelaunay = false, bool cached = false) + { + if (polygonCCW.Count < 3 || thickness <= 0f) return; + + if (cached) + { + int id = CachePolygonOutlineTriangulation(polygonCCW, thickness, miterLimit, beveled, useDelaunay); + DrawCachedTriangulation(id, color); + } + else + { + var mesh = RentMesh(); + try + { + CreatePolygonOutlineTriangulation(polygonCCW, thickness, miterLimit, beveled, useDelaunay, mesh); + mesh.Draw(color); + } + finally { ReturnMesh(mesh); } + } + } + + public static void DrawPolyline(IReadOnlyList polyline, float thickness, ColorRgba color, float miterLimit = 2f, bool beveled = false, ShapeClipperEndType endType = ShapeClipperEndType.Butt, bool useDelaunay = false, bool cached = false) + { + if (polyline.Count < 2 || thickness <= 0f) return; + + if (cached) + { + int id = CachePolylineTriangulation(polyline, thickness, miterLimit, beveled, endType, useDelaunay); + DrawCachedTriangulation(id, color); + } + else + { + var mesh = RentMesh(); + try + { + CreatePolylineTriangulation(polyline, thickness, miterLimit, beveled, endType, useDelaunay, mesh); + mesh.Draw(color); + } + finally { ReturnMesh(mesh); } + } + } + + public static void DrawCachedTriangulation(int triangulationId, ColorRgba color) + { + if (triangulationId == 0) return; + if (!_idToMesh.TryGetValue(triangulationId, out var mesh)) return; + mesh.Draw(color); + } + #endregion + + #region Create Outline Triangulation + public static void CreatePolygonOutlineTriangulation(IReadOnlyList polygonCCW, float thickness, float miterLimit, bool beveled, bool useDelaunay, TriMesh result) + { + if (result == null) throw new ArgumentNullException(nameof(result)); + result.Clear(); + + if (polygonCCW.Count < 3 || thickness <= 0f) return; + + OffsetEngine.OffsetPolygon(polygonCCW, +thickness, miterLimit, beveled, _tmpOuter); + OffsetEngine.OffsetPolygon(polygonCCW, -thickness, miterLimit, beveled, _tmpInner); + if (_tmpOuter.Count == 0) return; + + _tmpRing.Clear(); + ClipEngine.Execute(_tmpOuter, _tmpInner, ShapeClipperClipType.Difference, _tmpRing); + + if (_tmpRing.Count == 0) return; + + result.TriangulatePaths64ToMesh(_tmpRing, useDelaunay); + } + + public static void CreatePolygonOutlineTriangulation(IReadOnlyList polygonCCW, float thickness, float miterLimit, bool beveled, bool useDelaunay, Triangulation result) + { + _triMeshBuffer.Clear(); + CreatePolygonOutlineTriangulation(polygonCCW, thickness, miterLimit, beveled, useDelaunay, _triMeshBuffer); + _triMeshBuffer.ToTriangulation(result); + } + + public static void CreatePolylineTriangulation(IReadOnlyList polyline, float thickness, float miterLimit, bool beveled, ShapeClipperEndType endType, bool useDelaunay, TriMesh result) + { + if (result == null) throw new ArgumentNullException(nameof(result)); + result.Clear(); + + if (polyline.Count < 2 || thickness <= 0f) return; + + OffsetEngine.OffsetPolyline(polyline, thickness, miterLimit, beveled, endType, _tmpStroke); + if (_tmpStroke.Count == 0) return; + + result.TriangulatePaths64ToMesh(_tmpStroke, useDelaunay); + } + + public static void CreatePolylineTriangulation(IReadOnlyList polyline, float thickness, float miterLimit, bool beveled, ShapeClipperEndType endType, bool useDelaunay, Triangulation result) + { + _triMeshBuffer.Clear(); + CreatePolylineTriangulation(polyline, thickness, miterLimit, beveled, endType, useDelaunay, _triMeshBuffer); + _triMeshBuffer.ToTriangulation(result); + } + + #endregion + + #region Triangulation + + public static void CreatePolygonTriangulation(Paths64 polygonWithHoles, bool useDelaunay, TriMesh result) + { + if (result == null) throw new ArgumentNullException(nameof(result)); + result.Clear(); + + if (polygonWithHoles.Count == 0) return; + + result.TriangulatePaths64ToMesh(polygonWithHoles, useDelaunay); + } + + public static void CreatePolygonTriangulation(Paths64 polygonWithHoles, bool useDelaunay, Triangulation result) + { + _triMeshBuffer.Clear(); + CreatePolygonTriangulation(polygonWithHoles, useDelaunay, _triMeshBuffer); + _triMeshBuffer.ToTriangulation(result); + } + + public static void CreatePolygonTriangulation(IReadOnlyList> polygonWithHoles, bool useDelaunay, TriMesh result) + { + if (result == null) throw new ArgumentNullException(nameof(result)); + result.Clear(); + + if (polygonWithHoles.Count == 0) return; + paths64ConversionBuffer.PrepareBuffer(polygonWithHoles.Count); + polygonWithHoles.ToPaths64(paths64ConversionBuffer.Buffer); + + result.TriangulatePaths64ToMesh(paths64ConversionBuffer.Buffer, useDelaunay); + } + + public static void CreatePolygonTriangulation(IReadOnlyList> polygonWithHoles, bool useDelaunay, Triangulation result) + { + _triMeshBuffer.Clear(); + CreatePolygonTriangulation(polygonWithHoles, useDelaunay, _triMeshBuffer); + _triMeshBuffer.ToTriangulation(result); + } + + public static void CreatePolygonTriangulation(IReadOnlyList polygon, bool useDelaunay, TriMesh result) + { + if (result == null) throw new ArgumentNullException(nameof(result)); + result.Clear(); + + paths64ConversionBuffer.PrepareBuffer(1); + polygon.ToPaths64(paths64ConversionBuffer.Buffer); + + result.TriangulatePaths64ToMesh(paths64ConversionBuffer.Buffer, useDelaunay); + } + + public static void CreatePolygonTriangulation(IReadOnlyList polygon, bool useDelaunay, Triangulation result) + { + _triMeshBuffer.Clear(); + CreatePolygonTriangulation(polygon, useDelaunay, _triMeshBuffer); + _triMeshBuffer.ToTriangulation(result); + } + + #endregion + + #region Cached Triangulation + + public static int CachePolygonOutlineTriangulation(IReadOnlyList polygonCCW, float thickness, float miterLimit, bool beveled, bool useDelaunay) + { + if (polygonCCW.Count < 3 || thickness <= 0f) return 0; + + var key = TriKey.FromPolygonOutline(polygonCCW, thickness, miterLimit, beveled, useDelaunay, DecimalPlaces); + if (_keyToId.TryGetValue(key, out int id)) return id; + + EnsureCacheSpace(); + + id = _nextTriId++; + var mesh = RentMesh(); + CreatePolygonOutlineTriangulation(polygonCCW, thickness, miterLimit, beveled, useDelaunay, mesh); + + _keyToId[key] = id; + _idToMesh[id] = mesh; + return id; + } + + public static int CachePolylineTriangulation(IReadOnlyList polyline, float thickness, float miterLimit, bool beveled, ShapeClipperEndType endType, bool useDelaunay) + { + if (polyline.Count < 2 || thickness <= 0f) return 0; + + var key = TriKey.FromPolyline(polyline, thickness, miterLimit, beveled, endType, useDelaunay, DecimalPlaces); + if (_keyToId.TryGetValue(key, out int id)) return id; + + EnsureCacheSpace(); + + id = _nextTriId++; + var mesh = RentMesh(); + CreatePolylineTriangulation(polyline, thickness, miterLimit, beveled, endType, useDelaunay, mesh); + + _keyToId[key] = id; + _idToMesh[id] = mesh; + return id; + } + + public static int CachePolygonTriangulation(Paths64 polygonWithHoles, bool useDelaunay) + { + if (polygonWithHoles.Count == 0) return 0; + + var key = TriKey.FromPaths64(polygonWithHoles, useDelaunay, DecimalPlaces); + if (_keyToId.TryGetValue(key, out int id)) return id; + + EnsureCacheSpace(); + + id = _nextTriId++; + var mesh = RentMesh(); + CreatePolygonTriangulation(polygonWithHoles, useDelaunay, mesh); + + _keyToId[key] = id; + _idToMesh[id] = mesh; + return id; + } + + public static int CachePolygonTriangulation(IReadOnlyList> polygonWithHoles, bool useDelaunay) + { + if (polygonWithHoles.Count == 0) return 0; + paths64ConversionBuffer.PrepareBuffer(polygonWithHoles.Count); + polygonWithHoles.ToPaths64(paths64ConversionBuffer.Buffer); + var key = TriKey.FromPaths64(paths64ConversionBuffer.Buffer, useDelaunay, DecimalPlaces); + if (_keyToId.TryGetValue(key, out int id)) return id; + + EnsureCacheSpace(); + + id = _nextTriId++; + var mesh = RentMesh(); + CreatePolygonTriangulation(paths64ConversionBuffer.Buffer, useDelaunay, mesh); + + _keyToId[key] = id; + _idToMesh[id] = mesh; + return id; + } + + public static bool GetCachedTriangulation(int triangulationId, out TriMesh mesh) + { + return _idToMesh.TryGetValue(triangulationId, out mesh!); + } + + public static void ClearTriangulationCache() + { + _keyToId.Clear(); + + foreach (var kv in _idToMesh) + { + kv.Value.Clear(); + _meshPool.Push(kv.Value); + } + _idToMesh.Clear(); + } + #endregion + + #region Inflate + + public static void InflatePolyline(this IReadOnlyList polyline, Polygons result, float delta, float miterLimit, bool beveled, ShapeClipperEndType endType = ShapeClipperEndType.Butt) + { + if (delta < 0f) delta *= -1f; + OffsetEngine.OffsetPolyline(polyline, delta, miterLimit, beveled, endType, paths64Buffer); + paths64Buffer.ToPolygons(result, true); + } + public static void InflatePolygon(this IReadOnlyList polygon, Polygons result, float delta, float miterLimit, bool beveled) + { + OffsetEngine.OffsetPolygon(polygon, delta, miterLimit, beveled, paths64Buffer); + paths64Buffer.ToPolygons(result, true); + } + #endregion + + #region Rect Clipping + public static Paths64 ClipRect(this Rect rect, Paths64 poly) + { + return Clipper.RectClip(rect.ToRect64(), poly); + } + public static Paths64 ClipRectLines(this Rect rect, Paths64 poly) + { + return Clipper.RectClipLines(rect.ToRect64(), poly); + } + public static Paths64 ClipRect(this Rect rect, List poly) + { + paths64ConversionBuffer.PrepareBuffer(poly.Count); + poly.ToPaths64(paths64ConversionBuffer.Buffer); + return Clipper.RectClip(rect.ToRect64(), paths64ConversionBuffer.Buffer); + } + public static Paths64 ClipRectLines(this Rect rect, List poly) + { + paths64ConversionBuffer.PrepareBuffer(poly.Count); + poly.ToPaths64(paths64ConversionBuffer.Buffer); + return Clipper.RectClipLines(rect.ToRect64(), paths64ConversionBuffer.Buffer); + } + public static Paths64 ClipRect(this Rect rect, List> polyWithHoles) + { + paths64ConversionBuffer.PrepareBuffer(polyWithHoles.Count); + polyWithHoles.ToPaths64(paths64ConversionBuffer.Buffer); + return Clipper.RectClip(rect.ToRect64(), paths64ConversionBuffer.Buffer); + } + public static Paths64 ClipRectLines(this Rect rect, List> polyWithHoles) + { + paths64ConversionBuffer.PrepareBuffer(polyWithHoles.Count); + polyWithHoles.ToPaths64(paths64ConversionBuffer.Buffer); + return Clipper.RectClipLines(rect.ToRect64(), paths64ConversionBuffer.Buffer); + } + #endregion + + #region Create Shapes + + public static Path64 CreateEllipse(Vector2 center, double radiusX, double radiusY = 0f, int steps = 0) + { + return Clipper.Ellipse(center.ToPoint64(), radiusX, radiusY, steps); + } + public static void CreateEllipse(Vector2 center, double radiusX, double radiusY, int steps, Paths64 result) + { + result.Clear(); + result.Add(Clipper.Ellipse(center.ToPoint64(), radiusX, radiusY, steps)); + } + public static void CreateEllipse(Vector2 center, double radiusX, double radiusY, int steps, List result) + { + var ellipse = Clipper.Ellipse(center.ToPoint64(), radiusX, radiusY, steps); + ellipse.ToVector2List(result); + } + public static void CreateEllipse(Vector2 center, double radiusX, double radiusY, int steps, List> result) + { + var ellipse = Clipper.Ellipse(center.ToPoint64(), radiusX, radiusY, steps); + ellipse.ToVector2Lists(result); + } + #endregion + + #region Trim Collinear + public static Path64 TrimCollinear(this Path64 polygon, bool isOpen = false) + { + return Clipper.TrimCollinear(polygon, isOpen); + } + public static Path64 TrimCollinear(this IReadOnlyList polygon, bool isOpen = false) + { + polygon.ToPath64(path64Buffer); + return Clipper.TrimCollinear(path64Buffer, isOpen); + } + public static void TrimCollinear(this IReadOnlyList polygon, bool isOpen, List result) + { + polygon.ToPath64(path64Buffer); + var trimmedPath = Clipper.TrimCollinear(path64Buffer, isOpen); + trimmedPath.ToVector2List(result); + } + public static void TrimCollinear(this IReadOnlyList> polygonWithHoles, bool isOpen, Paths64 result) + { + foreach (var p in polygonWithHoles) + { + p.ToPath64(path64Buffer); + var trimmedPath = Clipper.TrimCollinear(path64Buffer, isOpen); + result.Add(trimmedPath); + } + } + public static void TrimCollinear(this IReadOnlyList> polygonWithHoles, bool isOpen, List> result) + { + paths64Buffer.Clear(); + for (int i = 0; i < polygonWithHoles.Count; i++) + { + polygonWithHoles[i].ToPath64(path64Buffer); + var trimmedPath = Clipper.TrimCollinear(path64Buffer, isOpen); + paths64Buffer.Add(trimmedPath); + } + paths64Buffer.ToVector2Lists(result); + } + #endregion + + #region Strip Duplicates + public static Path64 StripDuplicates(this Path64 polygon, bool isClosedPath = false) + { + return Clipper.StripDuplicates(polygon, isClosedPath); + } + public static Path64 StripDuplicates(this IReadOnlyList polygon, bool isClosedPath = false) + { + polygon.ToPath64(path64Buffer); + return Clipper.StripDuplicates(path64Buffer, isClosedPath); + } + public static void StripDuplicates(this IReadOnlyList polygon, bool isClosedPath, List result) + { + polygon.ToPath64(path64Buffer); + var trimmedPath = Clipper.StripDuplicates(path64Buffer, isClosedPath); + trimmedPath.ToVector2List(result); + } + public static void StripDuplicates(this IReadOnlyList> polygonWithHoles, bool isClosedPath, Paths64 result) + { + foreach (var p in polygonWithHoles) + { + p.ToPath64(path64Buffer); + var trimmedPath = Clipper.StripDuplicates(path64Buffer, isClosedPath); + result.Add(trimmedPath); + } + } + public static void StripDuplicates(this IReadOnlyList> polygonWithHoles, bool isClosedPath, List> result) + { + paths64Buffer.Clear(); + for (int i = 0; i < polygonWithHoles.Count; i++) + { + polygonWithHoles[i].ToPath64(path64Buffer); + var trimmedPath = Clipper.StripDuplicates(path64Buffer, isClosedPath); + paths64Buffer.Add(trimmedPath); + } + paths64Buffer.ToVector2Lists(result); + } + #endregion + + #region Point In Polygon + + public static PointInPolygonResult PointInPolygon(this Path64 polygon, Vector2 p) + { + return Clipper.PointInPolygon(p.ToPoint64(), polygon); + } + public static PointInPolygonResult PointInPolygon(this IReadOnlyList polygon, Vector2 p) + { + polygon.ToPath64(path64Buffer); + return Clipper.PointInPolygon(p.ToPoint64(), path64Buffer); + } + public static PointInPolygonResult PointInPolygons(this IReadOnlyList> polygonWithHoles, Vector2 p) + { + if (polygonWithHoles.Count <= 0) return PointInPolygonResult.IsOutside; + if(polygonWithHoles.Count == 1) return PointInPolygon(polygonWithHoles[0], p); + var result = PointInPolygon(polygonWithHoles[0], p); + + //point is outside of the main polygon + if (result == PointInPolygonResult.IsOutside) return result; + + var point = p.ToPoint64(); + for (int i = 1; i < polygonWithHoles.Count; i++) + { + var hole = polygonWithHoles[i]; + hole.ToPath64(path64Buffer); + var holeResult = Clipper.PointInPolygon(point, path64Buffer); + + //point is inside the polygon but also inside a hole -> therefore it is considered outside + if (holeResult == PointInPolygonResult.IsInside) return PointInPolygonResult.IsOutside; + } + + //point is inside polygon but not inside any hole + return PointInPolygonResult.IsInside; + } + #endregion + + #region Minkowski + public static Paths64 MinkowskiDiff(this Path64 polygon, Path64 pattern, bool isClosed = false) + { + return Minkowski.Diff(pattern, polygon, isClosed); + } + public static Paths64 MinkowskiDiff(this IReadOnlyList polygon, IReadOnlyList pattern, bool isClosed = false) + { + polygon.ToPath64(path64Buffer); + pattern.ToPath64(path64Buffer2); + + return Minkowski.Diff(path64Buffer2, path64Buffer, isClosed); + } + public static void MinkowskiDiff(this IReadOnlyList polygon, IReadOnlyList pattern, bool isClosed, List> result) + { + polygon.ToPath64(path64Buffer); + pattern.ToPath64(path64Buffer2); + + var diff = Minkowski.Diff(path64Buffer2, path64Buffer, isClosed); + diff.ToVector2Lists(result); + } + public static Paths64 MinkowskiSum(this Path64 polygon, Path64 pattern, bool isClosed = false) + { + return Minkowski.Sum(pattern, polygon, isClosed); + } + public static Paths64 MinkowskiSum(this IReadOnlyList polygon, IReadOnlyList pattern, bool isClosed = false) + { + polygon.ToPath64(path64Buffer); + pattern.ToPath64(path64Buffer2); + + return Minkowski.Sum(path64Buffer2, path64Buffer, isClosed); + } + public static void MinkowskiSum(this IReadOnlyList polygon, IReadOnlyList pattern, bool isClosed, List> result) + { + polygon.ToPath64(path64Buffer); + pattern.ToPath64(path64Buffer2); + + var diff = Minkowski.Sum(path64Buffer2, path64Buffer, isClosed); + diff.ToVector2Lists(result); + } + #endregion + + #region Simplify + public static Path64 Simplify(this Path64 polygon, float epsilon, bool isClosedPath = false) + { + return Clipper.SimplifyPath(polygon, epsilon, isClosedPath); + } + public static Paths64 Simplify(this Paths64 polygonWithHoles, float epsilon, bool isClosedPath = false) + { + return Clipper.SimplifyPaths(polygonWithHoles, epsilon, isClosedPath); + } + + public static Path64 Simplify(this IReadOnlyList polygon, float epsilon, bool isClosedPath = false) + { + polygon.ToPath64(path64Buffer); + return Clipper.SimplifyPath(path64Buffer, epsilon, isClosedPath); + } + public static Paths64 Simplify(this IReadOnlyList> polygonWithHoles, float epsilon, bool isClosedPath = false) + { + paths64ConversionBuffer.PrepareBuffer(polygonWithHoles.Count); + polygonWithHoles.ToPaths64(paths64ConversionBuffer.Buffer); + return Clipper.SimplifyPaths(paths64ConversionBuffer.Buffer, epsilon, isClosedPath); + } + + public static void Simplify(this IReadOnlyList polygon, float epsilon, bool isClosedPath, List result) + { + polygon.ToPath64(path64Buffer); + var solution = Clipper.SimplifyPath(path64Buffer, epsilon, isClosedPath); + solution.ToVector2List(result); + } + public static void Simplify(this IReadOnlyList> polygonWithHoles, float epsilon, bool isClosedPath, List> result) + { + paths64ConversionBuffer.PrepareBuffer(polygonWithHoles.Count); + polygonWithHoles.ToPaths64(paths64ConversionBuffer.Buffer); + var solution = Clipper.SimplifyPaths(paths64ConversionBuffer.Buffer, epsilon, isClosedPath); + solution.ToVector2Lists(result); + } + #endregion + + #region Simplify Ramer-Douglas-Peucker + + public static Path64 SimplifyRamerDouglasPeucker(this Path64 polygon, float epsilon) + { + return Clipper.RamerDouglasPeucker(polygon, epsilon); + } + public static Paths64 SimplifyRamerDouglasPeucker(this Paths64 polygonWithHoles, float epsilon) + { + return Clipper.RamerDouglasPeucker(polygonWithHoles, epsilon); + } + + public static Path64 SimplifyRamerDouglasPeucker(this IReadOnlyList polygon, float epsilon) + { + polygon.ToPath64(path64Buffer); + return Clipper.RamerDouglasPeucker(path64Buffer, epsilon); + } + public static Paths64 SimplifyRamerDouglasPeucker(this IReadOnlyList> polygonWithHoles, float epsilon) + { + paths64ConversionBuffer.PrepareBuffer(polygonWithHoles.Count); + polygonWithHoles.ToPaths64(paths64ConversionBuffer.Buffer); + return Clipper.RamerDouglasPeucker(paths64ConversionBuffer.Buffer, epsilon); + } + + public static void SimplifyRamerDouglasPeucker(this IReadOnlyList polygon, float epsilon, List result) + { + polygon.ToPath64(path64Buffer); + var solution = Clipper.RamerDouglasPeucker(path64Buffer, epsilon); + solution.ToVector2List(result); + } + public static void SimplifyRamerDouglasPeucker(this IReadOnlyList> polygonWithHoles, float epsilon, List> result) + { + paths64ConversionBuffer.PrepareBuffer(polygonWithHoles.Count); + polygonWithHoles.ToPaths64(paths64ConversionBuffer.Buffer); + var solution = Clipper.RamerDouglasPeucker(paths64ConversionBuffer.Buffer, epsilon); + solution.ToVector2Lists(result); + } + + #endregion + + #region Winding Order & Area + public static double GetArea(Path64 path) + { + return Clipper.Area(path); + } + + public static bool IsPositive(Path64 path) + { + return Clipper.IsPositive(path); + } + + public static bool IsClockwise(Path64 path) + { + return !Clipper.IsPositive(path); + } + + public static bool IsCounterClockwise(Path64 path) + { + return Clipper.IsPositive(path); + } + + public static double GetArea(this IReadOnlyList polygon) + { + polygon.ToPath64(path64Buffer); + return Clipper.Area(path64Buffer); + } + + public static bool IsPositive(this IReadOnlyList polygon) + { + polygon.ToPath64(path64Buffer); + return Clipper.IsPositive(path64Buffer); + } + + public static bool IsClockwise(this IReadOnlyList polygon) + { + polygon.ToPath64(path64Buffer); + return !Clipper.IsPositive(path64Buffer); + } + + public static bool IsCounterClockwise(this IReadOnlyList polygon) + { + polygon.ToPath64(path64Buffer); + return Clipper.IsPositive(path64Buffer); + } + #endregion + + #region Holes + + public static bool IsHole(this Path64 path) + { + return !Clipper.IsPositive(path); + } + + public static bool IsHole(this IReadOnlyList polygon) + { + polygon.ToPath64(path64Buffer); + return path64Buffer.IsHole(); + } + + public static int RemoveAllHoles(this Paths64 paths) + { + return paths.RemoveAll((p) => p.IsHole()); + } + + public static int RemoveAllHoles(this List> polygonWithHoles) + { + int count = 0; + for (int i = polygonWithHoles.Count - 1; i >= 0; i--) + { + var p = polygonWithHoles[i]; + if (p.IsHole()) + { + polygonWithHoles.RemoveAt(i); + count++; + } + } + return count; + } + public static int RemoveAllHoles(this Polygons polygonWithHoles) + { + int count = 0; + for (int i = polygonWithHoles.Count - 1; i >= 0; i--) + { + var p = polygonWithHoles[i]; + if (p.IsHole()) + { + polygonWithHoles.RemoveAt(i); + count++; + } + } + return count; + } + public static int RemoveAllHoles(this Paths64 paths, Paths64 result) + { + int count = 0; + result.Clear(); + for (int i = paths.Count - 1; i >= 0; i--) + { + var p = paths[i]; + if (p.IsHole()) + { + count++; + } + else + { + result.Add(p); + } + } + return count; + } + + public static int RemoveAllHoles(this IReadOnlyList> polygonWithHoles, List> result) + { + int count = 0; + result.Clear(); + for (int i = polygonWithHoles.Count - 1; i >= 0; i--) + { + var p = polygonWithHoles[i]; + if (p.IsHole()) + { + count++; + } + else + { + result.Add(p); + } + } + return count; + } + + public static int GetAllHoles(this Paths64 paths) + { + return paths.RemoveAll((p) => !p.IsHole()); + } + + public static int GetAllHoles(this List> polygonWithHoles) + { + int count = 0; + for (int i = polygonWithHoles.Count - 1; i >= 0; i--) + { + var p = polygonWithHoles[i]; + if (!p.IsHole()) + { + polygonWithHoles.RemoveAt(i); + count++; + } + } + return count; + } + + public static int GetAllHoles(this Paths64 paths, Paths64 result) + { + int count = 0; + result.Clear(); + for (int i = paths.Count - 1; i >= 0; i--) + { + var p = paths[i]; + if (!p.IsHole()) + { + count++; + } + else + { + result.Add(p); + } + } + return count; + } + + public static int GetAllHoles(this IReadOnlyList> polygonWithHoles, List> result) + { + int count = 0; + result.Clear(); + for (int i = polygonWithHoles.Count - 1; i >= 0; i--) + { + var p = polygonWithHoles[i]; + if (!p.IsHole()) + { + count++; + } + else + { + result.Add(p); + } + } + return count; + } + + + #endregion + + #region Conversion + //Single to Single + public static void ToPath64(this IReadOnlyList src, Path64 dst) + { + dst.Clear(); + dst.EnsureCapacity(src.Count); + + for (int i = 0; i < src.Count; i++) + { + dst.Add(src[i].ToPoint64()); + } + } + public static void ToVector2List(this Path64 src, List dst) + { + dst.Clear(); + dst.EnsureCapacity(src.Count); + + for (int i = 0; i < src.Count; i++) + { + dst.Add(src[i].ToVec2()); + } + } + //Multi to Multi + public static void ToPaths64(this IReadOnlyList> src, Paths64 dst) + { + for (int i = 0; i < src.Count; i++) + { + if(dst.Count <= i) + { + var dstItem = new Path64(); + dst.Add(dstItem); + + var srcItem = src[i]; + if(srcItem.Count <= 0) continue; + + srcItem.ToPath64(dstItem); + } + else + { + var srcItem = src[i]; + if(srcItem.Count <= 0) continue; + var dstItem = dst[i]; + srcItem.ToPath64(dstItem); + } + } + if (dst.Count > src.Count) + { + for (int i = dst.Count - 1; i >= src.Count; i--) + { + dst.RemoveAt(i); + } + } + } + public static void ToVector2Lists(this Paths64 src, List> dst) + { + for (int i = 0; i < src.Count; i++) + { + if(dst.Count <= i) + { + var dstItem = new List(); + dst.Add(dstItem); + + var srcItem = src[i]; + if(srcItem.Count <= 0) continue; + + srcItem.ToVector2List(dstItem); + } + else + { + var srcItem = src[i]; + if(srcItem.Count <= 0) continue; + var dstItem = dst[i]; + srcItem.ToVector2List(dstItem); + } + } + if (dst.Count > src.Count) + { + for (int i = dst.Count - 1; i >= src.Count; i--) + { + dst.RemoveAt(i); + } + } + } + public static void ToPolygons(this Paths64 src, Polygons dst) + { + for (int i = 0; i < src.Count; i++) + { + if(dst.Count <= i) + { + var dstItem = new Polygon(); + dst.Add(dstItem); + + var srcItem = src[i]; + if(srcItem.Count <= 0) continue; + + srcItem.ToVector2List(dstItem); + } + else + { + var srcItem = src[i]; + if(srcItem.Count <= 0) continue; + var dstItem = dst[i]; + srcItem.ToVector2List(dstItem); + } + } + if (dst.Count > src.Count) + { + for (int i = dst.Count - 1; i >= src.Count; i--) + { + dst.RemoveAt(i); + } + } + } + public static void ToPolygons(this Paths64 src, Polygons dst, bool removeHoles) + { + for (int i = 0; i < src.Count; i++) + { + var srcItem = src[i]; + if(srcItem.Count <= 0) continue; + if(removeHoles && srcItem.IsHole()) continue; + if(dst.Count <= i) + { + var dstItem = new Polygon(); + dst.Add(dstItem); + srcItem.ToVector2List(dstItem); + } + else + { + var dstItem = dst[i]; + srcItem.ToVector2List(dstItem); + } + } + if (dst.Count > src.Count) + { + for (int i = dst.Count - 1; i >= src.Count; i--) + { + dst.RemoveAt(i); + } + } + } + //Single to Multi + public static void ToPaths64(this IReadOnlyList src, Paths64 dst) + { + if (dst.Count <= 0) + { + var dstItem = new Path64(); + src.ToPath64(dstItem); + dst.Add(dstItem); + } + else + { + var dstItem = dst[0]; + src.ToPath64(dstItem); + if (dst.Count > 1) + { + dst.Clear(); + dst.Add(dstItem); + } + } + } + public static void ToVector2Lists(this Path64 src, List> dst) + { + if (dst.Count <= 0) + { + var dstItem = new List(); + src.ToVector2List(dstItem); + dst.Add(dstItem); + } + else + { + var dstItem = dst[0]; + src.ToVector2List(dstItem); + if (dst.Count > 1) + { + dst.Clear(); + dst.Add(dstItem); + } + } + } + public static void ToPolygons(this Path64 src, Polygons dst) + { + if (dst.Count <= 0) + { + var dstItem = new Polygon(); + src.ToVector2List(dstItem); + dst.Add(dstItem); + } + else + { + var dstItem = dst[0]; + src.ToVector2List(dstItem); + if (dst.Count > 1) + { + dst.Clear(); + dst.Add(dstItem); + } + } + } + public static Rect64 ToRect64(this Rect r) + { + // long left = (long)Math.Round(r.X * Scale); + // long top = (long)Math.Round(r.Y * Scale); + // long right = (long)Math.Round((r.X + r.Width) * Scale); + // long bottom = (long)Math.Round((r.Y + r.Height) * Scale); + // return new Rect64(left, top, right, bottom); + + long left = (long)Math.Round(r.X * Scale); + long bottom = (long)Math.Round(-r.Y * Scale); + long right = (long)Math.Round((r.X + r.Width) * Scale); + long top = (long)Math.Round((-r.Y - r.Height) * Scale); + return new Rect64(left, top, right, bottom); + } + public static Rect ToRect(this Rect64 r) + { + // float x = (float)(r.left * InvScale); + // float y = (float)(r.top * InvScale); + // float w = (float)((r.right - r.left) * InvScale); + // float h = (float)((r.bottom - r.top) * InvScale); + // + // return new Rect(x, y, w, h); + + float x = (float)(r.left * InvScale); + float y = (float)((-r.top - r.Height) * InvScale); + float w = (float)(r.Width * InvScale); + float h = (float)(r.Height * InvScale); + + return new Rect(x, y, w, h); + } + + public static Point64 ToPoint64(this Vector2 v) + { + long x = (long)Math.Round(v.X * Scale); + long y = (long)Math.Round(-v.Y * Scale); + return new Point64(x,y); + } + + public static Vector2 ToVec2(this Point64 p) + { + return new Vector2((float)(p.X * InvScale), (float)(-p.Y * InvScale)); + } + #endregion + + #region Enum Conversion + /// + /// Converts a to the Clipper enum. + /// + /// The ShapeClipper fill rule to convert. + /// The equivalent value. + public static FillRule ToClipperFillRule(this ShapeClipperFillRule fillRule) + { + return (FillRule)fillRule; + } + + /// + /// Converts a to the Clipper enum. + /// + /// The ShapeClipper join type to convert. + /// The equivalent value. + public static JoinType ToClipperJoinType(this ShapeClipperJoinType joinType) + { + return (JoinType)joinType; + } + + /// + /// Converts a to the Clipper enum. + /// + /// The ShapeClipper end type to convert. + /// The equivalent value. + public static EndType ToClipperEndType(this ShapeClipperEndType endType) + { + return (EndType)endType; + } + + //TODO: Docs + public static ClipType ToClipperClipType(this ShapeClipperClipType clipType) + { + return (ClipType)clipType; + } + + /// + /// Converts a Clipper to the local enum. + /// + /// The Clipper fill rule to convert. + /// The equivalent value. + public static ShapeClipperFillRule ToShapeClipperFillRule(this FillRule fillRule) + { + return (ShapeClipperFillRule)fillRule; + } + + /// + /// Converts a Clipper to the local enum. + /// + /// The Clipper join type to convert. + /// The equivalent value. + public static ShapeClipperJoinType ToShapeClipperJoinType(this JoinType joinType) + { + return (ShapeClipperJoinType)joinType; + } + + /// + /// Converts a Clipper to the local enum. + /// + /// The Clipper end type to convert. + /// The equivalent value. + public static ShapeClipperEndType ToShapeClipperEndType(this EndType endType) + { + return (ShapeClipperEndType)endType; + } + + //TODO: Docs + public static ShapeClipperClipType ToShapeClipperClipType(this ClipType clipType) + { + return (ShapeClipperClipType)clipType; + } + #endregion + + #region Internal Cache + private static void EnsureCacheSpace() + { + if (_idToMesh.Count < MaxTriangulationCacheEntries) return; + ClearTriangulationCache(); + } + + private readonly struct TriKey : IEquatable + { + public readonly ulong Hash; + public readonly int DP; + public readonly byte Kind; + public readonly bool UseDelaunay; + + private TriKey(ulong hash, int dp, byte kind, bool useDelaunay) + { + Hash = hash; + DP = dp; + Kind = kind; + UseDelaunay = useDelaunay; + } + + public static TriKey FromPolygonOutline(IReadOnlyList polygon, float thickness, float miterLimit, bool beveled, bool useDelaunay, int dp) + { + ulong h = HashPoints(polygon, dp); + h = HashFloat(h, thickness, dp); + h = HashFloat(h, miterLimit, dp); + h = HashBool(h, beveled); + return new TriKey(h, dp, kind: 1, useDelaunay); + } + + public static TriKey FromPolyline(IReadOnlyList polyline, float thickness, float miterLimit, bool beveled, ShapeClipperEndType endType, bool useDelaunay, int dp) + { + ulong h = HashPoints(polyline, dp); + h = HashFloat(h, thickness, dp); + h = HashFloat(h, miterLimit, dp); + h = HashBool(h, beveled); + h = HashInt(h, (int)endType); + return new TriKey(h, dp, kind: 2, useDelaunay); + } + + public static TriKey FromPaths64(Paths64 paths, bool useDelaunay, int dp) + { + ulong h = HashPaths64(paths); + return new TriKey(h, dp, kind: 3, useDelaunay); + } + + public bool Equals(TriKey other) => + Hash == other.Hash && DP == other.DP && Kind == other.Kind && UseDelaunay == other.UseDelaunay; + + public override bool Equals(object? obj) => obj is TriKey k && Equals(k); + + public override int GetHashCode() + { + unchecked + { + int hc = (int)(Hash ^ (Hash >> 32)); + hc = (hc * 397) ^ DP; + hc = (hc * 397) ^ Kind; + hc = (hc * 397) ^ (UseDelaunay ? 1 : 0); + return hc; + } + } + + private static ulong HashPoints(IReadOnlyList pts, int dp) + { + const ulong FNV_OFFSET = 14695981039346656037UL; + const ulong FNV_PRIME = 1099511628211UL; + + ulong h = FNV_OFFSET; + unchecked + { + h ^= (ulong)pts.Count; h *= FNV_PRIME; + + double scale = ToScale(dp); + for (int i = 0; i < pts.Count; i++) + { + long qx = (long)Math.Round(pts[i].X * scale); + long qy = (long)Math.Round(pts[i].Y * scale); + h ^= (ulong)qx; h *= FNV_PRIME; + h ^= (ulong)qy; h *= FNV_PRIME; + } + } + return h; + } + + private static ulong HashPaths64(Paths64 paths) + { + const ulong FNV_OFFSET = 14695981039346656037UL; + const ulong FNV_PRIME = 1099511628211UL; + + ulong h = FNV_OFFSET; + unchecked + { + h ^= (ulong)paths.Count; h *= FNV_PRIME; + + for (int i = 0; i < paths.Count; i++) + { + var path = paths[i]; + h ^= (ulong)path.Count; h *= FNV_PRIME; + + for (int j = 0; j < path.Count; j++) + { + var p = path[j]; + h ^= (ulong)p.X; h *= FNV_PRIME; + h ^= (ulong)p.Y; h *= FNV_PRIME; + } + } + } + return h; + } + + private static ulong HashFloat(ulong h, float v, int dp) + { + const ulong FNV_PRIME = 1099511628211UL; + double scale = ToScale(dp); + long q = (long)Math.Round(v * scale); + unchecked { h ^= (ulong)q; h *= FNV_PRIME; } + return h; + } + + private static ulong HashInt(ulong h, int v) + { + const ulong FNV_PRIME = 1099511628211UL; + unchecked { h ^= (ulong)v; h *= FNV_PRIME; } + return h; + } + + private static ulong HashBool(ulong h, bool v) + { + const ulong FNV_PRIME = 1099511628211UL; + unchecked { h ^= (ulong)(v ? 1 : 0); h *= FNV_PRIME; } + return h; + } + + private static double ToScale(int dp) + { + if (dp <= 0) return 1.0; + double s = 1.0; + for (int i = 0; i < dp; i++) s *= 10.0; + return s; + } + } + #endregion + + #region Internal TriMesh Pooling + private static TriMesh RentMesh() + { + if (_meshPool.Count > 0) + { + var m = _meshPool.Pop(); + m.Clear(); + return m; + } + return new TriMesh(); + } + + private static void ReturnMesh(TriMesh mesh) + { + if (_meshPool.Count < MaxTriangulationCacheEntries) + { + mesh.Clear(); + _meshPool.Push(mesh); + } + } + + //NOTE: Moved to TriMesh + private static float Cross(in Vector2 a, in Vector2 b) => a.X * b.Y - a.Y * b.X; + + //NOTE: Moved to ClipperScale struct + private static double Pow10(int dp) + { + if (dp <= 0) return 1.0; + double s = 1.0; + for (int i = 0; i < dp; i++) s *= 10.0; + return s; + } + #endregion +} \ No newline at end of file diff --git a/ShapeEngine/ShapeClipper/Paths64PooledBuffer.cs b/ShapeEngine/ShapeClipper/Paths64PooledBuffer.cs new file mode 100644 index 00000000..e2a89c7d --- /dev/null +++ b/ShapeEngine/ShapeClipper/Paths64PooledBuffer.cs @@ -0,0 +1,54 @@ +using Clipper2Lib; + +namespace ShapeEngine.ShapeClipper; + +public sealed class Paths64PooledBuffer +{ + private Stack path64Pool; + + public Paths64 Buffer = new(); + + public Paths64PooledBuffer(int poolCapacity = 64) + { + path64Pool = new Stack(poolCapacity); + } + + public void PrepareBuffer(int targetCount) + { + if (Buffer.Count > targetCount) + { + for (int i = Buffer.Count - 1; i >= targetCount; i--) + { + var path = Buffer[i]; + Buffer.RemoveAt(i); + ReturnPath64(path); + } + } + else if (Buffer.Count < targetCount) + { + var diff = targetCount - Buffer.Count; + for (int i = 0; i < diff; i++) + { + Buffer.Add(RentPath64()); + } + } + } + public void ClearBuffer() + { + foreach (var path in Buffer) + { + ReturnPath64(path); + } + Buffer.Clear(); + } + + private Path64 RentPath64() + { + if (path64Pool.Count > 0) return path64Pool.Pop(); + return new Path64(); + } + private void ReturnPath64(Path64 path64) + { + path64Pool.Push(path64); + } +} \ No newline at end of file diff --git a/ShapeEngine/ShapeClipper/ShapeClipper64.cs b/ShapeEngine/ShapeClipper/ShapeClipper64.cs new file mode 100644 index 00000000..613c4a0c --- /dev/null +++ b/ShapeEngine/ShapeClipper/ShapeClipper64.cs @@ -0,0 +1,349 @@ +using System.Numerics; +using Clipper2Lib; +using ShapeEngine.Geometry.PolygonDef; + +namespace ShapeEngine.ShapeClipper; + +public class ShapeClipper64 +{ + private readonly Clipper64 clipEngine; + private readonly Paths64PooledBuffer paths64SubjectBuffer = new(); + private readonly Paths64PooledBuffer paths64ClipBuffer = new(); + private readonly Path64 path64SubjectBuffer = new(); + private readonly Path64 path64ClipBuffer = new(); + private readonly Paths64 paths64SolutionBuffer = new(); + + public ShapeClipperFillRule FillRule = ShapeClipperFillRule.NonZero; + + public bool PreserveCollinear + { + get => clipEngine.PreserveCollinear; + set => clipEngine.PreserveCollinear = value; + } + + public bool ReverseSolution + { + get => clipEngine.ReverseSolution; + set => clipEngine.ReverseSolution = value; + } + + public ShapeClipper64(bool preserveCollinear = true, bool reverseSolution = false) + { + clipEngine = new(); + clipEngine.PreserveCollinear = preserveCollinear; + clipEngine.ReverseSolution = reverseSolution; + } + + public void Execute(Paths64 subject, Paths64 clip, ShapeClipperClipType clipType, Paths64 solutionClosed) + { + clipEngine.Clear(); + clipEngine.AddSubject(subject); + clipEngine.AddClip(clip); + clipEngine.Execute(clipType.ToClipperClipType(), FillRule.ToClipperFillRule(), solutionClosed); + } + + public void Execute(Paths64 subject, Path64 clip, ShapeClipperClipType clipType, Paths64 solutionClosed) + { + clipEngine.Clear(); + clipEngine.AddSubject(subject); + clipEngine.AddClip(clip); + clipEngine.Execute(clipType.ToClipperClipType(), FillRule.ToClipperFillRule(), solutionClosed); + } + + public void Execute(Path64 subject, Paths64 clip, ShapeClipperClipType clipType, Paths64 solutionClosed) + { + clipEngine.Clear(); + clipEngine.AddSubject(subject); + clipEngine.AddClip(clip); + clipEngine.Execute(clipType.ToClipperClipType(), FillRule.ToClipperFillRule(), solutionClosed); + } + + public void Execute(Path64 subject, Path64 clip, ShapeClipperClipType clipType, Paths64 solutionClosed) + { + clipEngine.Clear(); + clipEngine.AddSubject(subject); + clipEngine.AddClip(clip); + clipEngine.Execute(clipType.ToClipperClipType(), FillRule.ToClipperFillRule(), solutionClosed); + } + + + public void ExecuteMany(Paths64 subject, Paths64 clips, ShapeClipperClipType clipType, Paths64 solutionClosed) + { + var clipperClipType = clipType.ToClipperClipType(); + var clipperFillRule = FillRule.ToClipperFillRule(); + bool started = false; + foreach (var c in clips) + { + clipEngine.Clear(); + clipEngine.AddSubject(started ? solutionClosed : subject); + clipEngine.AddClip(c); + clipEngine.Execute(clipperClipType, clipperFillRule, solutionClosed); + + if (!started) started = true; + } + } + + public void ExecuteMany(Path64 subject, Paths64 clips, ShapeClipperClipType clipType, Paths64 solutionClosed) + { + var clipperClipType = clipType.ToClipperClipType(); + var clipperFillRule = FillRule.ToClipperFillRule(); + bool started = false; + foreach (var c in clips) + { + clipEngine.Clear(); + if(started)clipEngine.AddSubject(solutionClosed); + else clipEngine.AddSubject(subject); + clipEngine.AddClip(c); + clipEngine.Execute(clipperClipType, clipperFillRule, solutionClosed); + + if (!started) started = true; + } + } + + public void ExecuteMany(IReadOnlyList subject, Paths64 clips, ShapeClipperClipType clipType, Paths64 solutionClosed) + { + var clipperClipType = clipType.ToClipperClipType(); + var clipperFillRule = FillRule.ToClipperFillRule(); + bool started = false; + + subject.ToPath64(path64ClipBuffer); + + foreach (var c in clips) + { + clipEngine.Clear(); + if(started)clipEngine.AddSubject(solutionClosed); + else clipEngine.AddSubject(path64ClipBuffer); + clipEngine.AddClip(c); + clipEngine.Execute(clipperClipType, clipperFillRule, solutionClosed); + + if (!started) started = true; + } + } + + + public void Execute(IReadOnlyList> subject, IReadOnlyList> clip, ShapeClipperClipType clipType, Paths64 solutionClosed) + { + clipEngine.Clear(); + paths64SubjectBuffer.PrepareBuffer(subject.Count); + paths64ClipBuffer.PrepareBuffer(clip.Count); + subject.ToPaths64(paths64SubjectBuffer.Buffer); + clip.ToPaths64(paths64ClipBuffer.Buffer); + clipEngine.AddSubject(paths64SubjectBuffer.Buffer); + clipEngine.AddClip(paths64ClipBuffer.Buffer); + clipEngine.Execute(clipType.ToClipperClipType(), FillRule.ToClipperFillRule(), solutionClosed); + } + + public void Execute(IReadOnlyList> subject, IReadOnlyList clip, ShapeClipperClipType clipType, Paths64 solutionClosed) + { + clipEngine.Clear(); + paths64SubjectBuffer.PrepareBuffer(subject.Count); + subject.ToPaths64(paths64SubjectBuffer.Buffer); + clipEngine.AddSubject(paths64SubjectBuffer.Buffer); + clip.ToPath64(path64ClipBuffer); + clipEngine.AddClip(path64ClipBuffer); + clipEngine.Execute(clipType.ToClipperClipType(), FillRule.ToClipperFillRule(), solutionClosed); + } + + public void Execute(IReadOnlyList subject, IReadOnlyList> clip, ShapeClipperClipType clipType, Paths64 solutionClosed) + { + clipEngine.Clear(); + subject.ToPath64(path64SubjectBuffer); + clipEngine.AddSubject(path64SubjectBuffer); + + paths64ClipBuffer.PrepareBuffer(clip.Count); + clip.ToPaths64(paths64ClipBuffer.Buffer); + clipEngine.AddClip(paths64ClipBuffer.Buffer); + clipEngine.Execute(clipType.ToClipperClipType(), FillRule.ToClipperFillRule(), solutionClosed); + } + + public void Execute(IReadOnlyList subject, IReadOnlyList clip, ShapeClipperClipType clipType, Paths64 solutionClosed) + { + clipEngine.Clear(); + subject.ToPath64(path64SubjectBuffer); + clipEngine.AddSubject(path64SubjectBuffer); + clip.ToPath64(path64ClipBuffer); + clipEngine.AddClip(path64ClipBuffer); + clipEngine.Execute(clipType.ToClipperClipType(), FillRule.ToClipperFillRule(), solutionClosed); + } + + + public void ExecuteMany(IReadOnlyList> subject, IReadOnlyList> clips, ShapeClipperClipType clipType, Paths64 solutionClosed) + { + var clipperClipType = clipType.ToClipperClipType(); + var clipperFillRule = FillRule.ToClipperFillRule(); + bool started = false; + + paths64SubjectBuffer.PrepareBuffer(subject.Count); + subject.ToPaths64(paths64SubjectBuffer.Buffer); + + paths64ClipBuffer.PrepareBuffer(clips.Count); + clips.ToPaths64(paths64ClipBuffer.Buffer); + + foreach (var c in paths64ClipBuffer.Buffer) + { + clipEngine.Clear(); + clipEngine.AddSubject(started ? solutionClosed : paths64SubjectBuffer.Buffer); + clipEngine.AddClip(c); + clipEngine.Execute(clipperClipType, clipperFillRule, solutionClosed); + + if (!started) started = true; + } + } + + public void ExecuteMany(IReadOnlyList subject, IReadOnlyList> clips, ShapeClipperClipType clipType, Paths64 solutionClosed) + { + var clipperClipType = clipType.ToClipperClipType(); + var clipperFillRule = FillRule.ToClipperFillRule(); + bool started = false; + + subject.ToPath64(path64ClipBuffer); + + paths64ClipBuffer.PrepareBuffer(clips.Count); + clips.ToPaths64(paths64ClipBuffer.Buffer); + + foreach (var c in paths64ClipBuffer.Buffer) + { + clipEngine.Clear(); + if(started)clipEngine.AddSubject(solutionClosed); + else clipEngine.AddSubject(path64ClipBuffer); + clipEngine.AddClip(c); + clipEngine.Execute(clipperClipType, clipperFillRule, solutionClosed); + + if (!started) started = true; + } + } + + + public void Execute(IReadOnlyList> subject, IReadOnlyList> clip, ShapeClipperClipType clipType, List> solutionClosed) + { + clipEngine.Clear(); + paths64SubjectBuffer.PrepareBuffer(subject.Count); + paths64ClipBuffer.PrepareBuffer(clip.Count); + subject.ToPaths64(paths64SubjectBuffer.Buffer); + clip.ToPaths64(paths64ClipBuffer.Buffer); + clipEngine.AddSubject(paths64SubjectBuffer.Buffer); + clipEngine.AddClip(paths64ClipBuffer.Buffer); + + paths64SolutionBuffer.Clear(); + clipEngine.Execute(clipType.ToClipperClipType(), FillRule.ToClipperFillRule(), paths64SolutionBuffer); + paths64SolutionBuffer.ToVector2Lists(solutionClosed); + } + + public void Execute(IReadOnlyList> subject, IReadOnlyList clip, ShapeClipperClipType clipType, List> solutionClosed) + { + clipEngine.Clear(); + paths64SubjectBuffer.PrepareBuffer(subject.Count); + subject.ToPaths64(paths64SubjectBuffer.Buffer); + clipEngine.AddSubject(paths64SubjectBuffer.Buffer); + clip.ToPath64(path64ClipBuffer); + clipEngine.AddClip(path64ClipBuffer); + + paths64SolutionBuffer.Clear(); + clipEngine.Execute(clipType.ToClipperClipType(), FillRule.ToClipperFillRule(), paths64SolutionBuffer); + paths64SolutionBuffer.ToVector2Lists(solutionClosed); + } + + public void Execute(IReadOnlyList subject, IReadOnlyList> clip, ShapeClipperClipType clipType, List> solutionClosed) + { + clipEngine.Clear(); + subject.ToPath64(path64SubjectBuffer); + clipEngine.AddSubject(path64SubjectBuffer); + + paths64ClipBuffer.PrepareBuffer(clip.Count); + clip.ToPaths64(paths64ClipBuffer.Buffer); + clipEngine.AddClip(paths64ClipBuffer.Buffer); + + paths64SolutionBuffer.Clear(); + clipEngine.Execute(clipType.ToClipperClipType(), FillRule.ToClipperFillRule(), paths64SolutionBuffer); + paths64SolutionBuffer.ToVector2Lists(solutionClosed); + } + + public void Execute(IReadOnlyList subject, IReadOnlyList clip, ShapeClipperClipType clipType, List> solutionClosed) + { + clipEngine.Clear(); + subject.ToPath64(path64SubjectBuffer); + clipEngine.AddSubject(path64SubjectBuffer); + clip.ToPath64(path64ClipBuffer); + clipEngine.AddClip(path64ClipBuffer); + + paths64SolutionBuffer.Clear(); + clipEngine.Execute(clipType.ToClipperClipType(), FillRule.ToClipperFillRule(), paths64SolutionBuffer); + paths64SolutionBuffer.ToVector2Lists(solutionClosed); + } + + public void Execute(IReadOnlyList subject, IReadOnlyList clip, ShapeClipperClipType clipType, Polygons solutionClosed) + { + clipEngine.Clear(); + subject.ToPath64(path64SubjectBuffer); + clipEngine.AddSubject(path64SubjectBuffer); + clip.ToPath64(path64ClipBuffer); + clipEngine.AddClip(path64ClipBuffer); + + paths64SolutionBuffer.Clear(); + clipEngine.Execute(clipType.ToClipperClipType(), FillRule.ToClipperFillRule(), paths64SolutionBuffer); + paths64SolutionBuffer.ToPolygons(solutionClosed); + } + + + public void ExecuteMany(IReadOnlyList> subject, IReadOnlyList> clips, ShapeClipperClipType clipType, List> solutionClosed) + { + var clipperClipType = clipType.ToClipperClipType(); + var clipperFillRule = FillRule.ToClipperFillRule(); + bool started = false; + + paths64SubjectBuffer.PrepareBuffer(subject.Count); + subject.ToPaths64(paths64SubjectBuffer.Buffer); + + paths64ClipBuffer.PrepareBuffer(clips.Count); + clips.ToPaths64(paths64ClipBuffer.Buffer); + + paths64SolutionBuffer.Clear(); + + foreach (var c in paths64ClipBuffer.Buffer) + { + clipEngine.Clear(); + clipEngine.AddSubject(started ? paths64SolutionBuffer : paths64SubjectBuffer.Buffer); + clipEngine.AddClip(c); + clipEngine.Execute(clipperClipType, clipperFillRule, paths64SolutionBuffer); + + if (!started) started = true; + } + paths64SolutionBuffer.ToVector2Lists(solutionClosed); + } + + public void ExecuteMany(IReadOnlyList subject, IReadOnlyList> clips, ShapeClipperClipType clipType, List> solutionClosed) + { + var clipperClipType = clipType.ToClipperClipType(); + var clipperFillRule = FillRule.ToClipperFillRule(); + bool started = false; + + subject.ToPath64(path64ClipBuffer); + + paths64ClipBuffer.PrepareBuffer(clips.Count); + clips.ToPaths64(paths64ClipBuffer.Buffer); + + paths64SolutionBuffer.Clear(); + + foreach (var c in paths64ClipBuffer.Buffer) + { + clipEngine.Clear(); + if(started)clipEngine.AddSubject(paths64SolutionBuffer); + else clipEngine.AddSubject(path64ClipBuffer); + clipEngine.AddClip(c); + clipEngine.Execute(clipperClipType, clipperFillRule, paths64SolutionBuffer); + + if (!started) started = true; + } + paths64SolutionBuffer.ToVector2Lists(solutionClosed); + } + + + public void Execute(Paths64 subject, Paths64 clip, ShapeClipperClipType clipType, Paths64 solutionClosed, Paths64 solutionOpen) + { + clipEngine.Clear(); + clipEngine.AddSubject(subject); + clipEngine.AddClip(clip); + clipEngine.Execute(clipType.ToClipperClipType(), FillRule.ToClipperFillRule(), solutionClosed, solutionOpen); + } + +} \ No newline at end of file diff --git a/ShapeEngine/ShapeClipper/ShapeClipperClipType.cs b/ShapeEngine/ShapeClipper/ShapeClipperClipType.cs new file mode 100644 index 00000000..934c9e84 --- /dev/null +++ b/ShapeEngine/ShapeClipper/ShapeClipperClipType.cs @@ -0,0 +1,10 @@ +namespace ShapeEngine.ShapeClipper; + +public enum ShapeClipperClipType +{ + NoClip, + Intersection, + Union, + Difference, + Xor, +} \ No newline at end of file diff --git a/ShapeEngine/ShapeClipper/ShapeClipperEndType.cs b/ShapeEngine/ShapeClipper/ShapeClipperEndType.cs new file mode 100644 index 00000000..b9ee18fd --- /dev/null +++ b/ShapeEngine/ShapeClipper/ShapeClipperEndType.cs @@ -0,0 +1,38 @@ +namespace ShapeEngine.ShapeClipper; + +/// +/// The ShapeClipperEndType enumerator controls how the ends of paths are handled when performing +/// offset (inflating/shrinking) operations. This enumeration is only required for +/// offset operations and is not used for polygon clipping. +/// +/// +/// With both ShapeClipperEndType.Polygon and ShapeClipperEndType.Joined, path closure will occur regardless +/// of whether or not the first and last vertices in the path match. +/// +public enum ShapeClipperEndType +{ + /// + /// The path is treated as a closed polygon; offsets consider the path closed. (Filled) + /// + Polygon, + + /// + /// The path is treated as a polyline and its ends are joined during offsetting. (Outline) + /// + Joined, + + /// + /// Path ends are squared off without any extension (flat cutoff at ends). + /// + Butt, + + /// + /// Path ends are extended by the offset amount and then squared off. + /// + Square, + + /// + /// Path ends are extended by the offset amount and rounded (arc) off. + /// + Round +} \ No newline at end of file diff --git a/ShapeEngine/ShapeClipper/ShapeClipperFillRule.cs b/ShapeEngine/ShapeClipper/ShapeClipperFillRule.cs new file mode 100644 index 00000000..6e9be5d3 --- /dev/null +++ b/ShapeEngine/ShapeClipper/ShapeClipperFillRule.cs @@ -0,0 +1,50 @@ +namespace ShapeEngine.ShapeClipper; + +/// +/// Filling rules determine which sub-regions of complex polygons are considered "inside". +/// Complex polygons are defined by one or more closed contours; only portions of these contours +/// may contribute to filled regions, so a filling rule is required to decide which sub-regions +/// are treated as inside when performing clipping operations. +/// +/// +/// Example algorithm (winding number): +/// From a point outside the polygon draw a ray through the polygon. Start with winding number 0. +/// For each contour crossed, increment the winding number if the crossing goes right-to-left +/// relative to the ray, otherwise decrement. Each sub-region gets the current winding number. +/// +/// +/// The supported fill rules are based on winding numbers derived from the orientation of each path: +/// +/// Even-Odd: toggles inside/outside each time a contour is crossed. +/// Non-Zero: considers the sum of winding contributions; non-zero means inside. +/// Positive / Negative: depend on the sign of the winding number. +/// +/// Notes: +/// +/// The most commonly used rules are Even-Odd and Non-Zero. +/// Reversing a path reverses its orientation (and the sign of winding numbers) but does not affect parity or whether a winding number is zero. +/// Filling rules are required only for clipping operations; they do not affect polygon offsetting. +/// +/// +public enum ShapeClipperFillRule +{ + /// + /// Even-Odd rule: only sub-regions with odd winding parity are filled. + /// + EvenOdd, + + /// + /// Non-Zero rule: any sub-region with a non-zero winding number is filled. + /// + NonZero, + + /// + /// Positive rule: only sub-regions with winding counts > 0 are filled. + /// + Positive, + + /// + /// Negative rule: only sub-regions with winding counts < 0 are filled. + /// + Negative +} \ No newline at end of file diff --git a/ShapeEngine/ShapeClipper/ShapeClipperJoinType.cs b/ShapeEngine/ShapeClipper/ShapeClipperJoinType.cs new file mode 100644 index 00000000..d81c3ff5 --- /dev/null +++ b/ShapeEngine/ShapeClipper/ShapeClipperJoinType.cs @@ -0,0 +1,34 @@ +namespace ShapeEngine.ShapeClipper; + +/// +/// Specifies how convex angled joins are handled when offsetting (inflating/shrinking) paths. +/// This enumeration is only required for offset operations (e.g. ClipperOffset) and is not used +/// for polygon clipping operations. +/// +public enum ShapeClipperJoinType +{ + /// + /// Edges are offset a specified distance and extended to their intersection points. + /// A miter limit is enforced to prevent very long spikes at acute angles; when the limit is + /// exceeded the join is converted to a squared/bevel form. + /// + Miter, + + /// + /// Convex joins are truncated with a squared edge. The midpoint of the squared edge is + /// exactly the offset distance from the original vertex. + /// + Square, + + /// + /// Bevel joins cut the corner with a straight edge between the offset edges. Beveling is + /// typically simpler and faster than squared joins and is common in many graphics formats. + /// + Bevel, + + /// + /// Convex joins are rounded using an arc with radius equal to the offset distance and the + /// original join vertex as the arc center. + /// + Round +} \ No newline at end of file diff --git a/ShapeEngine/ShapeClipper/ShapeClipperOffset.cs b/ShapeEngine/ShapeClipper/ShapeClipperOffset.cs new file mode 100644 index 00000000..58f1443f --- /dev/null +++ b/ShapeEngine/ShapeClipper/ShapeClipperOffset.cs @@ -0,0 +1,117 @@ +using System.Numerics; +using Clipper2Lib; + +namespace ShapeEngine.ShapeClipper; + +public class ShapeClipperOffset +{ + private ClipperOffset offsetEngine; + public ShapeClipperScale Scale; + private readonly Path64 bufferPath64 = new(256); + + public double MiterLimit + { + get => offsetEngine.MiterLimit; + set => offsetEngine.MiterLimit = value; + } + + public double ArcTolerance + { + get => offsetEngine.ArcTolerance; + set => offsetEngine.ArcTolerance = value; + } + + public bool PreseveCollinear + { + get => offsetEngine.PreserveCollinear; + set => offsetEngine.PreserveCollinear = value; + } + + public bool ReverseSolution + { + get => offsetEngine.ReverseSolution; + set => offsetEngine.ReverseSolution = value; + } + + public ShapeClipperOffset(int decimalPlaces = 4, double miterLimit = 2.0, double arcTolerance = 0.0, bool preseveCollinear = false, bool reverseSolution = false) + { + offsetEngine = new(miterLimit, arcTolerance, preseveCollinear, reverseSolution); + Scale = new(decimalPlaces); + } + + #region Offsetting + public void OffsetPolygon(IReadOnlyList polygonCCW, float offset, float miterLimit, bool beveled, Paths64 result) + { + if (result == null) throw new ArgumentNullException(nameof(result)); + result.Clear(); + + if (polygonCCW.Count < 3) return; + + if (offset == 0f) + { + var path = new Path64(); + ClipperImmediate2D.ToPath64(polygonCCW, path); + result.Add(path); + return; + } + + OffsetPolygonToPaths64(polygonCCW, offset, miterLimit, beveled, result); + } + + public void OffsetPolyline(IReadOnlyList polyline, float offsetPositive, float miterLimit, bool beveled, ShapeClipperEndType endType, Paths64 result) + { + if (result == null) throw new ArgumentNullException(nameof(result)); + result.Clear(); + + if (polyline.Count < 2 || offsetPositive <= 0f) return; + + OffsetPolylineToPaths64(polyline, offsetPositive, miterLimit, beveled, endType, result); + } + #endregion + + #region Private + + private void OffsetPolygonToPaths64(IReadOnlyList polygonCCW, float offsetWorld, float miterLimit, bool beveled, Paths64 outPaths) + { + outPaths.Clear(); + + bufferPath64.Clear(); + ClipperImmediate2D.ToPath64(polygonCCW, bufferPath64); + + JoinType jt = SelectJoinType(miterLimit, beveled); + + offsetEngine.Clear(); + if (miterLimit > 2f) offsetEngine.MiterLimit = miterLimit; + + offsetEngine.AddPath(bufferPath64, jt, EndType.Polygon); + + double delta = offsetWorld * Scale.Scale; + offsetEngine.Execute(delta, outPaths); + } + + private void OffsetPolylineToPaths64(IReadOnlyList polyline, float offsetWorldPositive, float miterLimit, bool beveled, ShapeClipperEndType endType, Paths64 outPaths) + { + outPaths.Clear(); + + bufferPath64.Clear(); + ClipperImmediate2D.ToPath64(polyline, bufferPath64); + + JoinType jt = SelectJoinType(miterLimit, beveled); + + offsetEngine.Clear(); + if (miterLimit > 2f) offsetEngine.MiterLimit = miterLimit; + + offsetEngine.AddPath(bufferPath64, jt, endType.ToClipperEndType()); + + double delta = offsetWorldPositive * Scale.Scale; + offsetEngine.Execute(delta, outPaths); + } + + private JoinType SelectJoinType(float miterLimit, bool beveled) + { + if (miterLimit > 2f) return JoinType.Miter; + return beveled ? JoinType.Bevel : JoinType.Square; + } + + #endregion +} \ No newline at end of file diff --git a/ShapeEngine/ShapeClipper/ShapeClipperScale.cs b/ShapeEngine/ShapeClipper/ShapeClipperScale.cs new file mode 100644 index 00000000..cad56e5c --- /dev/null +++ b/ShapeEngine/ShapeClipper/ShapeClipperScale.cs @@ -0,0 +1,23 @@ +namespace ShapeEngine.ShapeClipper; + +public readonly struct ShapeClipperScale +{ + public readonly int DecimalPlaces; + public readonly double Scale; + public readonly double InvScale; + + public ShapeClipperScale(int decimalPlaces = 4) + { + DecimalPlaces = Math.Clamp(decimalPlaces, 0, 8);; + Scale = Pow10(DecimalPlaces); + InvScale = 1.0 / Scale; + } + + private double Pow10(int dp) + { + if (dp <= 0) return 1.0; + double s = 1.0; + for (int i = 0; i < dp; i++) s *= 10.0; + return s; + } +} \ No newline at end of file diff --git a/ShapeEngine/ShapeClipper/TriMesh.cs b/ShapeEngine/ShapeClipper/TriMesh.cs new file mode 100644 index 00000000..39ac560c --- /dev/null +++ b/ShapeEngine/ShapeClipper/TriMesh.cs @@ -0,0 +1,89 @@ +using System.Numerics; +using Clipper2Lib; +using Raylib_cs; +using ShapeEngine.Color; +using ShapeEngine.Geometry.TriangulationDef; + +namespace ShapeEngine.ShapeClipper; + +public sealed class TriMesh +{ + /// + /// Flat list of triangles: every 3 consecutive vertices form one CCW triangle. + /// + public readonly List Triangles = new(512); + + public void Clear() => Triangles.Clear(); + + public void Draw(ColorRgba color) + { + var t = Triangles; + var rayColor = color.ToRayColor(); + for (int i = 0; i + 2 < t.Count; i += 3) + { + Vector2 a = t[i]; + Vector2 b = t[i + 1]; + Vector2 c = t[i + 2]; + + Raylib.DrawTriangle(a, b, c, rayColor); + } + } + + public bool TriangulatePaths64ToMesh(Paths64 subject, bool useDelaunay) + { + Clear(); + + TriangulateResult res = Clipper.Triangulate(subject, out Paths64 tris, useDelaunay); + if (res != TriangulateResult.success || tris.Count == 0) + { + return false; + } + + FillMesh(tris); + return true; + } + + private void FillMesh(Paths64 tris) + { + // Each Path64 in tris is a triangle with 3 points (CCW). + int triCount = tris.Count; + Triangles.Capacity = Math.Max(Triangles.Capacity, triCount * 3); + + for (int i = 0; i < triCount; i++) + { + var tri = tris[i]; + if (tri.Count < 3) continue; + + // Convert int coords back to world coords + // because y is never flipped - return triangles are in cw order and flipping 0 with 1 turns them into ccw order! + Vector2 a = ClipperImmediate2D.ToVec2(tri[1]); + Vector2 b = ClipperImmediate2D.ToVec2(tri[0]); + Vector2 c = ClipperImmediate2D.ToVec2(tri[2]); + + // enforce CCW (defensive) + if (Cross(b - a, c - a) > 0f) + { + var tmp = b; b = c; c = tmp; + } + + Triangles.Add(a); + Triangles.Add(b); + Triangles.Add(c); + } + } + + private float Cross(in Vector2 a, in Vector2 b) => a.X * b.Y - a.Y * b.X; + + public void ToTriangulation(Triangulation dst) + { + dst.Clear(); + for (int i = 0; i + 2 < Triangles.Count; i += 3) + { + Vector2 a = Triangles[i]; + Vector2 b = Triangles[i + 1]; + Vector2 c = Triangles[i + 2]; + ShapeEngine.Geometry.TriangleDef.Triangle t = new(a, b, c); + dst.Add(t); + } + } +} \ No newline at end of file diff --git a/ShapeEngine/StaticLib/PerlinNoise2D.cs b/ShapeEngine/StaticLib/PerlinNoise2D.cs index 4f4f272e..7a954aa5 100644 --- a/ShapeEngine/StaticLib/PerlinNoise2D.cs +++ b/ShapeEngine/StaticLib/PerlinNoise2D.cs @@ -7,8 +7,10 @@ namespace ShapeEngine.StaticLib; /// Produces smooth, continuous noise values (approximately in the range [-1, 1]) for given world coordinates /// scaled by a lattice cell size (gridSize). Maintains a static cache of pseudo-random gradient vectors /// and exposes an integer Seed to change the generated gradients. Changing Seed clears the gradient cache. -/// Note: the internal gradient cache is not synchronized; use external synchronization for concurrent access. /// +/// +/// The internal gradient cache is not synchronized; use external synchronization for concurrent access. +/// public static class PerlinNoise2D { private static readonly Dictionary<(int, int), Vector2> gradients = []; diff --git a/ShapeEngine/StaticLib/ShapeClipper.cs b/ShapeEngine/StaticLib/ShapeClipper.cs deleted file mode 100644 index 59cb31b7..00000000 --- a/ShapeEngine/StaticLib/ShapeClipper.cs +++ /dev/null @@ -1,744 +0,0 @@ -using System.Numerics; -using Clipper2Lib; -using ShapeEngine.Geometry.PolygonDef; -using ShapeEngine.Geometry.PolylineDef; -using ShapeEngine.Geometry.RectDef; -using ShapeEngine.Geometry.SegmentDef; -using ShapeEngine.Geometry.SegmentsDef; - -namespace ShapeEngine.StaticLib; - -/// -/// Represents a collection of objects. -/// -public class Polygons : List -{ - /// - /// Initializes a new instance of the class. - /// - public Polygons() { } - /// - /// Initializes a new instance of the class with the specified capacity. - /// - /// The number of elements that the new list can initially store. - public Polygons(int capacity) : base(capacity) { } - /// - /// Initializes a new instance of the class containing the specified polygons. - /// - /// An array of polygons to add. - public Polygons(params Polygon[] polygons) { AddRange(polygons); } - /// - /// Initializes a new instance of the class containing the specified polygons. - /// - /// An enumerable collection of polygons to add. - public Polygons(IEnumerable polygons) { AddRange(polygons); } -} - -/// -/// Represents a collection of objects. -/// -public class Polylines : List -{ - /// - /// Initializes a new instance of the class. - /// - public Polylines() { } - /// - /// Initializes a new instance of the class containing the specified polylines. - /// - /// An array of polylines to add. - public Polylines(params Polyline[] polylines) { AddRange(polylines); } - /// - /// Initializes a new instance of the class containing the specified polylines. - /// - /// An enumerable collection of polylines to add. - public Polylines(IEnumerable polylines) { AddRange(polylines); } -} - -/// -/// Provides static methods for performing geometric clipping and polygon operations using Clipper2. -/// -/// -/// This class contains extension methods for polygons, polylines, and related shapes, enabling union, intersection, difference, inflation, simplification, and conversion operations. -/// -public static class ShapeClipper -{ - /// - /// Clips a polygon to a rectangle. - /// - /// The rectangle to clip to. - /// The polygon to be clipped. - /// The decimal precision for the operation. - /// The clipped paths as . - public static PathsD ClipRect(this Rect rect, Polygon poly, int precision = 2) - { - return Clipper.RectClip(rect.ToClipperRect(), poly.ToClipperPath(), precision); - } - - /// - /// Computes the union of a polygon with multiple other polygons. - /// - /// The base polygon. - /// The collection of polygons to union with. - /// The fill rule to use. - /// The unioned paths as . - public static PathsD UnionMany(this Polygon a, Polygons other, FillRule fillRule = FillRule.NonZero) - { - return Clipper.Union(a.ToClipperPaths(), other.ToClipperPaths(), fillRule); - } - - /// - /// Computes the union of two polygons. - /// - /// The first polygon. - /// The second polygon. - /// The fill rule to use. - /// The unioned paths as . - public static PathsD Union(this Polygon a, Polygon b, FillRule fillRule = FillRule.NonZero) { return Clipper.Union(ToClipperPaths(a), ToClipperPaths(b), fillRule); } - - /// - /// Computes the intersection of two polygons. - /// - /// The subject polygon. - /// The clip polygon. - /// The fill rule to use. - /// The decimal precision for the operation. - /// The intersected paths as . - public static PathsD Intersect(this Polygon subject, Polygon clip, FillRule fillRule = FillRule.NonZero, int precision = 2) - { - return Clipper.Intersect(ToClipperPaths(subject), ToClipperPaths(clip), fillRule, precision); - } - - /// - /// Computes the intersection of a polygon with multiple subject polygons. - /// - /// The clip polygon. - /// The collection of subject polygons. - /// The fill rule to use. - /// The decimal precision for the operation. - /// The intersected paths as . - public static PathsD Intersect(this Polygon clip, Polygons subjects, FillRule fillRule = FillRule.NonZero, int precision = 2) - { - var result = new PathsD(); - foreach (var subject in subjects) - { - result.AddRange(Clipper.Intersect(subject.ToClipperPaths(), clip.ToClipperPaths(), fillRule, precision)); - } - return result; - } - - /// - /// Computes the intersection of a subject polygon with multiple clip polygons. - /// - /// The subject polygon. - /// The collection of clip polygons. - /// The fill rule to use. - /// The decimal precision for the operation. - /// The intersected paths as . - public static PathsD IntersectMany(this Polygon subject, Polygons clips, FillRule fillRule = FillRule.NonZero, int precision = 2) - { - var cur = subject.ToClipperPaths(); - foreach (var clip in clips) - { - cur = Clipper.Intersect(cur, clip.ToClipperPaths(), fillRule, precision); - } - return cur; - } - - /// - /// Computes the difference between a subject polygon and a clip polygon. - /// - /// The subject polygon. - /// The clip polygon. - /// The fill rule to use. - /// The decimal precision for the operation. - /// The resulting paths as . - public static PathsD Difference(this Polygon subject, Polygon clip, FillRule fillRule = FillRule.NonZero, int precision = 2) - { - return Clipper.Difference(ToClipperPaths(subject), ToClipperPaths(clip), fillRule, precision); - } - - /// - /// Computes the difference between multiple subject polygons and a clip polygon. - /// - /// The clip polygon. - /// The collection of subject polygons. - /// The fill rule to use. - /// The decimal precision for the operation. - /// The resulting paths as . - public static PathsD Difference(this Polygon clip, Polygons subjects, FillRule fillRule = FillRule.NonZero, int precision = 2) - { - var result = new PathsD(); - var clipPaths = clip.ToClipperPaths(); - foreach (var subject in subjects) - { - result.AddRange(Clipper.Difference(subject.ToClipperPaths(), clipPaths, fillRule, precision)); - } - return result; - } - - /// - /// Computes the difference between a subject polygon and multiple clip polygons. - /// - /// The subject polygon. - /// The collection of clip polygons. - /// The fill rule to use. - /// The decimal precision for the operation. - /// The resulting paths as . - public static PathsD DifferenceMany(this Polygon subject, Polygons clips, FillRule fillRule = FillRule.NonZero, int precision = 2) - { - var cur = subject.ToClipperPaths(); - foreach (var clip in clips) - { - cur = Clipper.Difference(cur, clip.ToClipperPaths(), fillRule, precision); - } - return cur; - } - - /// - /// Computes the difference between a subject polygon and a polyline. - /// - /// The subject polygon. - /// The polyline to subtract. - /// The fill rule to use. - /// The decimal precision for the operation. - /// The resulting paths as . - public static PathsD Difference(this Polygon subject, Polyline polyline, FillRule fillRule = FillRule.NonZero, int precision = 2) - { - return Clipper.Difference(ToClipperPaths(subject), ToClipperPaths(polyline), fillRule, precision); - } - - /// - /// Computes the difference between a subject polygon and multiple polylines. - /// - /// The subject polygon. - /// The collection of polylines to subtract. - /// The fill rule to use. - /// The decimal precision for the operation. - /// The resulting paths as . - public static PathsD DifferenceMany(this Polygon subject, Polylines polylines, FillRule fillRule = FillRule.NonZero, int precision = 2) - { - var cur = subject.ToClipperPaths(); - foreach (var clip in polylines) - { - cur = Clipper.Difference(cur, clip.ToClipperPaths(), fillRule, precision); - } - return cur; - } - - /// - /// Computes the difference between a subject polygon and a segment. - /// - /// The subject polygon. - /// The segment to subtract. - /// The fill rule to use. - /// The decimal precision for the operation. - /// The resulting paths as . - public static PathsD Difference(this Polygon subject, Segment segment, FillRule fillRule = FillRule.NonZero, int precision = 2) - { - return Clipper.Difference(ToClipperPaths(subject), ToClipperPaths(segment), fillRule, precision); - } - - /// - /// Computes the difference between a subject polygon and multiple segments. - /// - /// The subject polygon. - /// The collection of segments to subtract. - /// The fill rule to use. - /// The decimal precision for the operation. - /// The resulting paths as . - public static PathsD DifferenceMany(this Polygon subject, Segments segments, FillRule fillRule = FillRule.NonZero, int precision = 2) - { - var cur = subject.ToClipperPaths(); - foreach (var clip in segments) - { - cur = Clipper.Difference(cur, clip.ToClipperPaths(), fillRule, precision); - } - return cur; - } - - /// - /// Determines if a path is a hole (negative orientation). - /// - /// The path to check. - /// True if the path is a hole; otherwise, false. - public static bool IsHole(this PathD path) { return !Clipper.IsPositive(path); } - - /// - /// Determines if a polygon is a hole (negative orientation). - /// - /// The polygon to check. - /// True if the polygon is a hole; otherwise, false. - public static bool IsHole(this Polygon p) { return IsHole(p.ToClipperPath()); } - - /// - /// Removes all holes from the given paths in-place. - /// - /// The paths to process. - /// The modified with holes removed. - /// This method modifies the input collection. - public static PathsD RemoveAllHoles(this PathsD paths) { paths.RemoveAll((p) => { return IsHole(p); }); return paths; } - - /// - /// Removes all holes from the given polygons in-place. - /// - /// The polygons to process. - /// The modified with holes removed. - /// This method modifies the input collection. - public static Polygons RemoveAllHoles(this Polygons polygons) { polygons.RemoveAll((p) => { return IsHole(p); }); return polygons; } - - /// - /// Keeps only the holes in the given paths in-place. - /// - /// The paths to process. - /// The modified containing only holes. - /// This method modifies the input collection. - public static PathsD GetAllHoles(this PathsD paths) { paths.RemoveAll((p) => { return !IsHole(p); }); return paths; } - - /// - /// Keeps only the holes in the given polygons in-place. - /// - /// The polygons to process. - /// The modified containing only holes. - /// This method modifies the input collection. - public static Polygons GetAllHoles(this Polygons polygons) { polygons.RemoveAll((p) => { return !IsHole(p); }); return polygons; } - - /// - /// Returns a copy of the given paths with all holes removed. - /// - /// The paths to process. - /// A new with holes removed. - public static PathsD RemoveAllHolesCopy(this PathsD paths) - { - var result = new PathsD(); - foreach (var p in paths) - { - if(!IsHole(p)) result.Add(p); - } - return result; - } - - /// - /// Returns a copy of the given polygons with all holes removed. - /// - /// The polygons to process. - /// A new with holes removed. - public static Polygons RemoveAllHolesCopy(this Polygons polygons) - { - var result = new Polygons(); - foreach (var p in polygons) - { - if (!IsHole(p)) result.Add(p); - } - return result; - } - - /// - /// Returns a copy of the given paths containing only holes. - /// - /// The paths to process. - /// A new containing only holes. - public static PathsD GetAllHolesCopy(this PathsD paths) - { - var result = new PathsD(); - foreach (var p in paths) - { - if (IsHole(p)) result.Add(p); - } - return result; - } - - /// - /// Returns a copy of the given polygons containing only holes. - /// - /// The polygons to process. - /// A new containing only holes. - public static Polygons GetAllHolesCopy(this Polygons polygons) - { - var result = new Polygons(); - foreach (var p in polygons) - { - if (IsHole(p)) result.Add(p); - } - return result; - } - - /// - /// Inflates (offsets) a polyline by a specified delta. - /// - /// The polyline to inflate. - /// The offset distance. - /// The join type for corners. - /// The end type for open paths. - /// The miter limit for miter joins. - /// The decimal precision for the operation. - /// The inflated paths as . - public static PathsD Inflate(this Polyline polyline, float delta, JoinType joinType = JoinType.Square, EndType endType = EndType.Square, float miterLimit = 2f, int precision = 2) - { - return Clipper.InflatePaths(polyline.ToClipperPaths(), delta, joinType, endType, miterLimit, precision); - } - - /// - /// Inflates (offsets) a polygon by a specified delta. - /// - /// The polygon to inflate. - /// The offset distance. - /// The join type for corners. - /// The end type for closed paths. - /// The miter limit for miter joins. - /// The decimal precision for the operation. - /// The inflated paths as . - public static PathsD Inflate(this Polygon poly, float delta, JoinType joinType = JoinType.Square, EndType endType = EndType.Polygon, float miterLimit = 2f, int precision = 2) - { - return Clipper.InflatePaths(poly.ToClipperPaths(), delta, joinType, endType, miterLimit, precision); - } - - /// - /// Inflates (offsets) a collection of polygons by a specified delta. - /// - /// The polygons to inflate. - /// The offset distance. - /// The join type for corners. - /// The end type for closed paths. - /// The miter limit for miter joins. - /// The decimal precision for the operation. - /// The inflated paths as . - public static PathsD Inflate(this Polygons polygons, float delta, JoinType joinType = JoinType.Square, EndType endType = EndType.Polygon, float miterLimit = 2f, int precision = 2) - { - return Clipper.InflatePaths(polygons.ToClipperPaths(), delta, joinType, endType, miterLimit, precision); - } - - /// - /// Simplifies a polygon using the Clipper algorithm. - /// - /// The polygon to simplify. - /// The tolerance for simplification. - /// Whether the path is open or closed. - /// The simplified path as . - public static PathD Simplify(this Polygon poly, float epsilon, bool isOpen = false) { return Clipper.SimplifyPath(poly.ToClipperPath(), epsilon, isOpen); } - - /// - /// Simplifies a collection of polygons using the Clipper algorithm. - /// - /// The polygons to simplify. - /// The tolerance for simplification. - /// Whether the paths are open or closed. - /// The simplified paths as . - public static PathsD Simplify(this Polygons poly, float epsilon, bool isOpen = false) { return Clipper.SimplifyPaths(poly.ToClipperPaths(), epsilon, isOpen); } - - /// - /// Simplifies a polygon using the Ramer-Douglas-Peucker algorithm. - /// Only works on closed polygons. - /// - /// The polygon to simplify. - /// The tolerance for simplification. - /// The simplified path as . - public static PathD SimplifyRDP(this Polygon poly, float epsilon) { return Clipper.RamerDouglasPeucker(poly.ToClipperPath(), epsilon); } - - /// - /// Simplifies a collection of polygons using the Ramer-Douglas-Peucker algorithm. - /// Only works on closed polygons. - /// - /// The polygons to simplify. - /// The tolerance for simplification. - /// The simplified paths as . - public static PathsD SimplifyRDP(this Polygons poly, float epsilon) { return Clipper.SimplifyPaths(poly.ToClipperPaths(), epsilon); } - - /// - /// Determines if a point is inside a polygon using the Clipper algorithm. - /// - /// The polygon to test. - /// The point to check. - /// The result as . - public static PointInPolygonResult IsPointInsideClipper(this Polygon poly, Vector2 p) { return Clipper.PointInPolygon(p.ToClipperPoint(), poly.ToClipperPath()); } - - /// - /// Determines if a point is inside a polygon. - /// - /// The polygon to test. - /// The point to check. - /// True if the point is inside; otherwise, false. - public static bool IsPointInside(this Polygon poly, Vector2 p) { return IsPointInsideClipper(poly, p) != PointInPolygonResult.IsOutside; } - - /// - /// Removes collinear points from a polygon. - /// - /// The polygon to process. - /// The decimal precision for the operation. - /// Whether the path is open or closed. - /// The trimmed path as . - public static PathD TrimCollinear(this Polygon poly, int precision, bool isOpen = false) { return Clipper.TrimCollinear(poly.ToClipperPath(), precision, isOpen); } - - /// - /// Removes near-duplicate points from a polygon based on minimum edge length squared. - /// - /// The polygon to process. - /// The minimum squared edge length to consider as unique. - /// Whether the path is open or closed. - /// The processed path as . - public static PathD StripDuplicates(this Polygon poly, float minEdgeLengthSquared, bool isOpen = false) { return Clipper.StripNearDuplicates(poly.ToClipperPath(), minEdgeLengthSquared, isOpen); } - - /// - /// Computes the Minkowski difference between two polygons. - /// - /// The base polygon. - /// The path polygon. - /// Whether the path is closed. - /// The resulting paths as . - public static PathsD MinkowskiDiff(this Polygon poly, Polygon path, bool isClosed = false) { return Clipper.MinkowskiDiff(poly.ToClipperPath(), path.ToClipperPath(), isClosed); } - - /// - /// Computes the Minkowski sum of two polygons. - /// - /// The base polygon. - /// The path polygon. - /// Whether the path is closed. - /// The resulting paths as . - public static PathsD MinkowskiSum(this Polygon poly, Polygon path, bool isClosed = false) { return Clipper.MinkowskiSum(poly.ToClipperPath(), path.ToClipperPath(), isClosed); } - - /// - /// Computes the Minkowski difference between a polygon and a path, with the path positioned at the origin. - /// - /// The base polygon. - /// The path polygon (will be moved to origin). - /// Whether the path is closed. - /// The resulting paths as . - public static PathsD MinkowskiDiffOrigin(this Polygon poly, Polygon path, bool isClosed = false) - { - var pathCopy = path.SetPositionCopy(new(0f)); - if (pathCopy == null) return new(); - return Clipper.MinkowskiDiff(poly.ToClipperPath(), pathCopy.ToClipperPath(), isClosed); - } - - /// - /// Computes the Minkowski sum of a polygon and a path, with the path positioned at the origin. - /// - /// The base polygon. - /// The path polygon (will be moved to origin). - /// Whether the path is closed. - /// The resulting paths as . - public static PathsD MinkowskiSumOrigin(this Polygon poly, Polygon path, bool isClosed = false) - { - var pathCopy = path.SetPositionCopy(new(0f)); - if (pathCopy == null) return new(); - return Clipper.MinkowskiSum(poly.ToClipperPath(), pathCopy.ToClipperPath(), isClosed); - } - - /// - /// Creates an ellipse polygon. - /// - /// The center of the ellipse. - /// The X radius. - /// The Y radius. If zero, uses . - /// The number of steps (vertices) for the ellipse. If zero, uses default. - /// A polygon representing the ellipse. - public static Polygon CreateEllipse(Vector2 center, float radiusX, float radiusY = 0f, int steps = 0) { return Clipper.Ellipse(center.ToClipperPoint(), radiusX, radiusY, steps).ToPolygon(); } - - /// - /// Converts a to a . - /// - /// The point to convert. - /// The converted . - /// Y is flipped to match coordinate systems. - public static Vector2 ToVec2(this PointD p) { return new((float)p.x, -(float)p.y); }//flip of y necessary -> clipper up y is positve - raylib is negative - - /// - /// Converts a to a for Clipper. - /// - /// The vector to convert. - /// The converted . - /// Y is flipped to match coordinate systems. - public static PointD ToClipperPoint(this Vector2 v) { return new(v.X, -v.Y); } - - /// - /// Converts a to a for Clipper. - /// - /// The rectangle to convert. - /// The converted . - /// Y is flipped to match coordinate systems. - public static RectD ToClipperRect(this Rect r) { return new RectD(r.X, -r.Y-r.Height, r.X + r.Width, -r.Y); } - - /// - /// Converts a to a . - /// - /// The rectangle to convert. - /// The converted . - /// Y is flipped to match coordinate systems. - public static Rect ToRect(this RectD r) { return new Rect((float)r.left, (float)(-r.top-r.Height), (float)r.Width, (float)r.Height); } - - /// - /// Converts a to a . - /// - /// The path to convert. - /// The converted . - public static Polygon ToPolygon(this PathD path) - { - var poly = new Polygon(); - foreach (var point in path) - { - poly.Add(point.ToVec2()); - } - return poly; - } - - /// - /// Converts a to . - /// - /// The paths to convert. - /// If true, removes holes from the result. - /// The converted . - public static Polygons ToPolygons(this PathsD paths, bool removeHoles = false) - { - var polygons = new Polygons(); - foreach (var path in paths) - { - if (!removeHoles || !IsHole(path)) - { - polygons.Add(path.ToPolygon()); - } - } - return polygons; - } - - /// - /// Converts a to a . - /// - /// The polygon to convert. - /// The converted . - public static PathD ToClipperPath(this Polygon poly) - { - var path = new PathD(); - foreach (var vertex in poly) - { - path.Add(vertex.ToClipperPoint()); - } - return path; - } - - /// - /// Converts a to a . - /// - /// The segment to convert. - /// The converted . - public static PathD ToClipperPath(this Segment segment) - { - var path = new PathD(); - path.Add(segment.Start.ToClipperPoint()); - path.Add(segment.End.ToClipperPoint()); - return path; - } - - /// - /// Converts a to . - /// - /// The segment to convert. - /// The converted . - public static PathsD ToClipperPaths(this Segment segment){ return new PathsD() { segment.ToClipperPath() }; } - - /// - /// Converts a to . - /// - /// The polygon to convert. - /// The converted . - public static PathsD ToClipperPaths(this Polygon poly) { return new PathsD() { poly.ToClipperPath() }; } - - /// - /// Converts an array of to . - /// - /// The polygons to convert. - /// The converted . - public static PathsD ToClipperPaths(params Polygon[] polygons) { return polygons.ToClipperPaths(); } - - /// - /// Converts an enumerable of to . - /// - /// The polygons to convert. - /// The converted . - public static PathsD ToClipperPaths(this IEnumerable polygons) - { - var result = new PathsD(); - foreach(var polygon in polygons) - { - result.Add(polygon.ToClipperPath()); - } - return result; - } - - /// - /// Converts a to a . - /// - /// The path to convert. - /// The converted . - public static Polyline ToPolyline(this PathD path) - { - var polyline = new Polyline(); - foreach (var point in path) - { - polyline.Add(point.ToVec2()); - } - return polyline; - } - - /// - /// Converts a to . - /// - /// The paths to convert. - /// If true, removes holes from the result. - /// The converted . - public static Polylines ToPolylines(this PathsD paths, bool removeHoles = false) - { - var polylines = new Polylines(); - foreach (var path in paths) - { - if (!removeHoles || !IsHole(path)) - { - polylines.Add(path.ToPolyline()); - } - } - return polylines; - } - - /// - /// Converts a to a . - /// - /// The polyline to convert. - /// The converted . - public static PathD ToClipperPath(this Polyline polyline) - { - var path = new PathD(); - foreach (var vertex in polyline) - { - path.Add(vertex.ToClipperPoint()); - } - return path; - } - - /// - /// Converts a to . - /// - /// The polyline to convert. - /// The converted . - public static PathsD ToClipperPaths(this Polyline polyline) { return new PathsD() { polyline.ToClipperPath() }; } - - /// - /// Converts an array of to . - /// - /// The polylines to convert. - /// The converted . - public static PathsD ToClipperPaths(params Polyline[] polylines) { return polylines.ToClipperPaths(); } - - /// - /// Converts an enumerable of to . - /// - /// The polylines to convert. - /// The converted . - public static PathsD ToClipperPaths(this IEnumerable polylines) - { - var result = new PathsD(); - foreach (var polyline in polylines) - { - result.Add(polyline.ToClipperPath()); - } - return result; - } -} diff --git a/ShapeEngine/StaticLib/ShapeMath.cs b/ShapeEngine/StaticLib/ShapeMath.cs index 99606242..d3049969 100644 --- a/ShapeEngine/StaticLib/ShapeMath.cs +++ b/ShapeEngine/StaticLib/ShapeMath.cs @@ -39,6 +39,23 @@ public static class ShapeMath /// A small constant value used for floating-point comparisons to account for precision errors (float version). /// public const float EpsilonF = 1e-10f; + + /// + /// The radian value equivalent to 360 degrees (one full turn), equal to Tau (2π). + /// + public const float Rad360Degrees = Tau; + /// + /// The radian value equivalent to 180 degrees (half turn), equal to PI (π). + /// + public const float Rad180Degrees = PI; + /// + /// The radian value equivalent to 90 degrees (quarter turn), equal to PI * 0.5. + /// + public const float Rad90Degrees = PI * 0.5f; + /// + /// The radian value equivalent to 45 degrees (eighth turn), equal to PI * 0.25. + /// + public const float Rad45Degrees = PI * 0.25f; /// /// Number of nanoseconds in one second. /// @@ -672,8 +689,9 @@ public static int WrapIndex(int count, int index) { if (count <= 0) return 0; if (index >= count) return index % count; - else if (index < 0) return (index % count) + count; - else return index; + if (index < 0) return ((index % count) + count) % count; + + return index; } /// /// Wraps a floating-point value to a specified range [min, max). @@ -805,7 +823,22 @@ public static float AimAt(float curAngleRad, float targetAngleRad, float rotSpee else if (dir == 0) dir = 0; return dir * amount; } - + + /// + /// Computes the signed smallest angular difference from angle to in radians. + /// The returned value is the shortest rotation to reach from (positive = counter-clockwise), + /// wrapped to the range [-π, π]. + /// + /// Start angle in radians. + /// End angle in radians. + /// Signed angle difference in radians. + public static float AngleDelta(float a0, float a1) + { + float d = a1 - a0; + if (d > MathF.PI) d -= 2f * MathF.PI; + if (d < -MathF.PI) d += 2f * MathF.PI; + return d; + } #endregion #region Coordinates diff --git a/ShapeEngine/StaticLib/ShapeTween.cs b/ShapeEngine/StaticLib/ShapeTween.cs index 4cd4a9bd..6cb473a2 100644 --- a/ShapeEngine/StaticLib/ShapeTween.cs +++ b/ShapeEngine/StaticLib/ShapeTween.cs @@ -48,6 +48,7 @@ public static float Tween(float t, TweenType tweenType) case TweenType.ELASTIC_IN: return ElasticIn(t); case TweenType.ELASTIC_OUT: return ElasticOut(t); case TweenType.ELASTIC_INOUT: return ElasticInOut(t); + case TweenType.Ping_Pong: return PingPong(t); default: return t; } } @@ -467,4 +468,16 @@ public static float BounceInOut(float p) : (1f + BounceOut(2f * p - 1f)) / 2f; } + /// + /// Returns a value that oscillates between 0 and 1 as the input increases. + /// + /// The input value. Has to be positive but can increase until float.MaxValue (theoretically) + /// A ping\-ponged value in the range \[0, 1\]. + public static float PingPong(float p) + { + p %= 2f; + if (p < 0f) p += 2f; + return p <= 1f ? p : 2f - p; + } + } \ No newline at end of file diff --git a/ShapeEngine/StaticLib/ShapeVec.cs b/ShapeEngine/StaticLib/ShapeVec.cs index 868f3c23..0fd3da02 100644 --- a/ShapeEngine/StaticLib/ShapeVec.cs +++ b/ShapeEngine/StaticLib/ShapeVec.cs @@ -164,20 +164,7 @@ public static Vector2 GetOutwardFacingNormal(this Vector2 normal, Vector2 outwar if(IsNormalFacingOutward(normal, outwardDirection)) return normal; else return -normal; } - /// - /// Determines if three points are colinear (lie on a straight line). - /// - /// The first point. - /// The second point (vertex). - /// The third point. - /// True if the points are colinear, otherwise false. - public static bool IsColinear(Vector2 a, Vector2 b, Vector2 c) - { - Vector2 prevCur = a - b; - Vector2 nextCur = c - b; - - return prevCur.Cross(nextCur) == 0f; - } + /// /// Returns the area represented by the vector (X * Y). /// @@ -300,6 +287,18 @@ public static Vector2 FindArithmeticMean(IEnumerable vertices) return new Vector2(sx * invArrayLen, sy * invArrayLen); } + /// + /// Converts an angle in radians to its positive equivalent in the range [0, 2π]. + /// + /// The angle in radians to convert. + /// The equivalent positive angle in the range [0, 2π]. + /// Negative angles are wrapped to their positive equivalents by adding multiples of 2π. + public static float ToPositiveAngleRad(float angleRad) + { + angleRad %= float.Tau; // float.Tau = 2 * π + if (angleRad < 0f) angleRad += float.Tau; + return angleRad; + } //Projection /// @@ -419,6 +418,23 @@ public static Vector2 LerpDirection(this Vector2 from, Vector2 to, float t) /// The interpolated vector. public static Vector2 Lerp(this Vector2 from, Vector2 to, float t) { return Vector2.Lerp(from, to, t); } + /// + /// Linearly interpolates between two vectors by a specific length rather than a factor. + /// + /// The starting vector. + /// The target vector. + /// The distance to move from the start vector towards the target vector. + /// The interpolated vector. + public static Vector2 LerpByLength(this Vector2 from, Vector2 to, float length) + { + if (length <= 0f) return from; + var l = (to - from).Length(); + if(l <= 0f) return from; + if (length >= l) return to; + return from + (to - from) * (length / l); + + } + /// /// Exponentially decays and linearly interpolates between two vectors. /// @@ -627,7 +643,6 @@ public static Vector2 Normalize(this Vector2 v) /// The normal of the surface. /// The reflected vector. public static Vector2 Reflect(this Vector2 v, Vector2 n) { return Vector2.Reflect(v, n); } - /// /// Changes the length of the vector by the given amount. /// @@ -697,6 +712,8 @@ public static Vector2 Rotate(this Vector2 v, float angleRad) /// The angle in degrees. /// The rotated vector. public static Vector2 RotateDeg(this Vector2 v, float angleDeg) { return Rotate(v, angleDeg * ShapeMath.DEGTORAD); } + + #region Angle Calculations /// /// Calculates the angle in degrees between two vectors. /// @@ -722,7 +739,294 @@ public static Vector2 Rotate(this Vector2 v, float angleRad) /// The first vector. /// The second vector. /// The angle in radians between the two vectors. - public static float AngleRad(this Vector2 v1, Vector2 v2) { return MathF.Atan2(v2.Y, v2.X) - MathF.Atan2(v1.Y, v1.X); } + public static float AngleRad(this Vector2 v1, Vector2 v2) + { + return MathF.Atan2(v2.Y, v2.X) - MathF.Atan2(v1.Y, v1.X); + } + + /// + /// Calculates the unsigned angle in radians between two vectors. + /// This returns the acute or obtuse angle between the directions without sign, + /// i.e. the result is always in the range [0, π]. + /// + /// The first vector. + /// The second vector. + /// The unsigned angle in radians between and . + public static float AngleRadUnsigned(this Vector2 v1, Vector2 v2) + { + float cross = v1.X * v2.Y - v1.Y * v2.X; + float dot = Vector2.Dot(v1, v2); + return MathF.Atan2(MathF.Abs(cross), dot); // 0..π + } + /// + /// Calculates the unsigned angle in degrees between two vectors. + /// The result is always in the range [0, 180], representing the smallest angle between directions. + /// + /// The first vector. + /// The second vector. + /// The unsigned angle in degrees between and . + public static float AngleDegUnsigned(this Vector2 v1, Vector2 v2) + { + return AngleRadUnsigned(v1, v2) * ShapeMath.RADTODEG; + } + + /// + /// Normalizes an angle in degrees to the canonical range [-180, 180]. + /// + /// Angle in degrees (any value). + /// + /// Angle mapped into the range [-180, 180]. Note that an input of -180 degrees is mapped to 180 degrees. + /// + /// + /// Uses modulus wrapping and adjustments to ensure a stable result for large or negative inputs. + /// + public static float NormalizeAngleDeg(float angleDeg) + { + angleDeg %= 360f; + if (angleDeg <= -180f) angleDeg += 360f; + if (angleDeg > 180f) angleDeg -= 360f; + return angleDeg; + } + /// + /// Normalizes an angle in radians to the canonical range [-π, π]. + /// + /// Angle in radians (any value). + /// + /// Angle mapped into the range [-π, π]. Note that an input of -π is mapped to π. + /// + /// + /// Uses modulus with float.Tau and adjusts the result to keep it within the target interval. + /// + public static float NormalizeAngleRad(float angleRad) + { + angleRad %= float.Tau; + if (angleRad <= -float.Pi) angleRad += float.Tau; + if (angleRad > float.Pi) angleRad -= float.Tau; + return angleRad; + } + #endregion + + #region Collinear + /// + /// Determines whether a raw angle in degrees corresponds to a collinear orientation. + /// The input is normalized into the canonical \[-180, 180\] range + /// using and then checked against the provided tolerance: + /// - near 0 degrees (same direction), or + /// - near 180 / -180 degrees (opposite direction). + /// + /// Angle in degrees (any value). + /// Tolerance in degrees to consider the angle collinear (default 1e-3f). + /// True if the normalized angle is within of 0 or 180 degrees. + public static bool IsCollinearFromRawAngleDeg(float rawAngleDeg, float thresholdDeg = 1e-3f) + { + float norm = NormalizeAngleDeg(rawAngleDeg); + return MathF.Abs(norm) <= thresholdDeg || MathF.Abs(MathF.Abs(norm) - 180f) <= thresholdDeg; + } + /// + /// Determines whether a raw angle in radians corresponds to a collinear orientation. + /// The input is normalized into the canonical \[-π, π\] range + /// using and then checked against the provided tolerance: + /// - near 0 radians (same direction), or + /// - near π / -π radians (opposite direction). + /// + /// Angle in radians (any value). + /// Tolerance in radians to consider the angle collinear (default 1e-3f). + /// True if the normalized angle is within of 0 or π radians. + public static bool IsCollinearFromRawAngleRad(float rawAngleRad, float thresholdRad = 1e-3f) + { + float norm = NormalizeAngleRad(rawAngleRad); + return MathF.Abs(norm) <= thresholdRad || MathF.Abs(MathF.Abs(norm) - 180f) <= thresholdRad; + } + + /// + /// Determines whether three points (a, b, c) are colinear within a given tolerance. + /// + /// The first point. + /// The middle/vertex point. + /// The third point. + /// + /// The tolerance used to consider the cross product effectively zero. Smaller values require closer alignment. + /// + /// + /// If true, the epsilon is scaled by the geometric mean of the squared segment lengths to account for numerical precision + /// when points are far apart or segment lengths vary greatly. If false, epsilon is used directly. + /// + /// True if the points are colinear or if any segment has zero length (coincident points); otherwise false. + public static bool IsColinear(Vector2 a, Vector2 b, Vector2 c, float epsilon = 1e-6f, bool scaledEpsilon = true) + { + var prevCur = a - b; + var nextCur = c - b; + + // If either segment has zero length (coincident points) treat as colinear + float prevLenSq = prevCur.LengthSquared(); + if(prevLenSq <= 0f) return true; + float nextLenSq = nextCur.LengthSquared(); + if(nextLenSq <= 0f) return true; + + float cross = prevCur.Cross(nextCur); + + if (!scaledEpsilon) return MathF.Abs(cross) <= epsilon; + + float lenSqProduct = prevLenSq * nextLenSq; + + // Compare with a scaled epsilon to account for floating point error + return MathF.Abs(cross) <= epsilon * MathF.Sqrt(lenSqProduct); + + } + /// + /// Determines whether two direction vectors are colinear within a given tolerance. + /// + /// The first direction vector. + /// The second direction vector. + /// Tolerance used to consider the 2D cross product effectively zero. Smaller values require closer alignment. + /// + /// If true, the epsilon is scaled by the geometric mean of the squared lengths of the vectors to account for numerical precision + /// when magnitudes differ or are large; if false, epsilon is used directly. + /// + /// + /// True if the vectors are colinear or if either vector has zero length (treated as coincident); otherwise false. + /// + /// + /// Uses the 2D cross product and optional scaling to robustly handle floating point error for vectors of varying magnitude. + /// + public static bool IsColinear(Vector2 dir1, Vector2 dir2, float epsilon = 1e-6f, bool scaledEpsilon = true) + { + // If either segment has zero length (coincident points) treat as colinear + float prevLenSq = dir1.LengthSquared(); + if(prevLenSq <= 0f) return true; + float nextLenSq = dir2.LengthSquared(); + if(nextLenSq <= 0f) return true; + + float cross = dir1.Cross(dir2); + + if (!scaledEpsilon) return MathF.Abs(cross) <= epsilon; + + float lenSqProduct = prevLenSq * nextLenSq; + + // Compare with a scaled epsilon to account for floating point error + return MathF.Abs(cross) <= epsilon * MathF.Sqrt(lenSqProduct); + + } + /// + /// Determines whether three points (a, b, c) are colinear within an angle threshold in degrees. + /// + /// The first point. + /// The middle/vertex point. + /// The third point. + /// Angle threshold in degrees (e.g. 5f). + /// True if the directions differ by less than or equal to the threshold or if any segment has zero length; otherwise false. + public static bool IsColinearAngle(Vector2 a, Vector2 b, Vector2 c, float angleThresholdDeg = 0f) + { + var prevCur = b - a; + var nextCur = c - b; + + // If either segment has zero length (coincident points) treat as colinear + float prevLenSq = prevCur.LengthSquared(); + if (prevLenSq <= 0f) return true; + float nextLenSq = nextCur.LengthSquared(); + if (nextLenSq <= 0f) return true; + + float angleRad = prevCur.AngleRadUnsigned(nextCur); + float angleDeg = MathF.Abs(angleRad) * ShapeMath.RADTODEG; + + return angleDeg <= angleThresholdDeg || angleDeg >= (180f - angleThresholdDeg); + } + /// + /// Determines whether two direction vectors are colinear within a specified angular threshold (in degrees). + /// + /// The first direction vector. + /// The second direction vector. + /// + /// Angle threshold in degrees. If the absolute angle between and + /// is less than or equal to this value the vectors are considered colinear. + /// + /// + /// true if either vector has zero length (coincident) or if the absolute angle between the two vectors + /// is less than or equal to ; otherwise false. + /// + /// + /// Uses to compute the angle in radians and converts to degrees + /// using . + /// + public static bool IsColinearAngle(Vector2 dir1, Vector2 dir2, float angleThresholdDeg = 0f) + { + // If either segment has zero length (coincident points) treat as colinear + float prevLenSq = dir1.LengthSquared(); + if (prevLenSq <= 0f) return true; + float nextLenSq = dir2.LengthSquared(); + if (nextLenSq <= 0f) return true; + + float angleRad = dir1.AngleRadUnsigned(dir2); + float angleDeg = MathF.Abs(angleRad) * ShapeMath.RADTODEG; + + return angleDeg <= angleThresholdDeg || angleDeg >= (180f - angleThresholdDeg); + } + #endregion + + #region Classify Corner + /// + /// Classifies the corner formed by two direction vectors. + /// + /// The previous (incoming) direction vector. + /// The next (outgoing) direction vector. + /// Small tolerance used to treat vectors as colinear (default 1e-6). + /// + /// A tuple containing: + /// - type: an integer where 0 = colinear, 1 = ccw outwards, -1 = ccw inwards. + /// - angle: the absolute angle between the two directions in radians (range 0..π). + /// + /// + /// The classification is determined using the 2D cross product and dot product: + /// - If |cross| ≤ epsilon the vectors are considered colinear. + /// - Otherwise the sign of cross determines inward/outward classification while the angle is computed with Atan2(|cross|, dot). + /// Angle close to 0 indicates collinear in the same direction, angle close to π indicates collinear in opposite directions. + /// + public static (int type, float angle) ClassifyCorner(this Vector2 dirPrev, Vector2 dirNext, float epsilon = 1e-6f) + { + float cross = dirPrev.X * dirNext.Y - dirPrev.Y * dirNext.X; + float dot = Vector2.Dot(dirPrev, dirNext); + float angle = MathF.Atan2(MathF.Abs(cross), dot); // 0..π + + if (MathF.Abs(cross) <= epsilon) return (0, angle); /* collinear */ + + return cross < 0f ? + (1, angle)/* ccw outwards*/ : + (-1, angle); /*ccw inwards*/ + } + + /// + /// Classifies a corner based on the direction vectors of the two segments that form it. + /// + /// Normalized direction vector of the incoming segment. + /// Normalized direction vector of the outgoing segment. + /// A tuple containing the corner type (1 for convex, -1 for concave, 0 for collinear) and the cross product value. + public static (int type, float cross) ClassifyCorner2(this Vector2 dir1, Vector2 dir2) + { + // The 2D cross product (dir1.X * dir2.Y - dir1.Y * dir2.X) tells us about the turn. + // A positive value means a left turn (convex in a CCW polygon). + // A negative value means a right turn (concave in a CCW polygon). + // A value near zero means the vectors are collinear. + float cross = dir1.X * dir2.Y - dir1.Y * dir2.X; + + // The dot product tells us if the vectors are pointing in the same or opposite directions. + // A value near 1 means they are parallel and in the same direction. + // A value near -1 means they are parallel and in opposite directions. + float dot = Vector2.Dot(dir1, dir2); + + // Check for collinearity first, using a small epsilon for floating-point inaccuracies. + // If the dot product is close to 1 or -1, the lines are parallel. + if (MathF.Abs(dot) > 0.9999f) + { + return (0, cross); // Collinear + } + + // If not collinear, the sign of the cross product determines the turn direction. + if (cross > 0) return (1, cross); // Convex corner (left turn) + return (-1, cross); // Concave corner (right turn) + } + #endregion + + /// /// Calculates the distance between two vectors. /// diff --git a/ShapeEngine/Text/TextFont.cs b/ShapeEngine/Text/TextFont.cs index fb8c6ed5..db6afb9f 100644 --- a/ShapeEngine/Text/TextFont.cs +++ b/ShapeEngine/Text/TextFont.cs @@ -3,6 +3,7 @@ using ShapeEngine.Color; using ShapeEngine.Core.Structs; using ShapeEngine.Geometry.RectDef; +using ShapeEngine.StaticLib; namespace ShapeEngine.Text; @@ -184,12 +185,58 @@ public void Draw(char c, Rect rect, AnchorPoint alignment) /// The alignment of the text within the rectangle. public void Draw(string text, Rect rect, float rotDeg, AnchorPoint alignment) { - if(Math.Abs(FontSizeModifier - 1f) > 0.0001f) rect = rect.ScaleSize(FontSizeModifier, alignment); - var scaledFont = FontDimensions.ScaleDynamic(text, rect.Size); + //OLD INCORRECT SCALING VERSION + //if(Math.Abs(FontSizeModifier - 1f) > 0.0001f) rect = rect.ScaleSize(FontSizeModifier, alignment); + //var scaledFont = FontDimensions.ScaleDynamic(text, rect.Size); + //var textSize = scaledFont.GetTextSize(text); + //Rect r = new(rect.GetPoint(alignment), textSize, alignment); + //var originOffset = (alignment * textSize).ToVector2(); + //Raylib.DrawTextPro(scaledFont.Font, text, r.TopLeft + originOffset, originOffset, rotDeg, scaledFont.Size, scaledFont.Spacing, ColorRgba.ToRayColor()); + + if (Math.Abs(FontSizeModifier - 1f) > 0.0001f) + rect = rect.ScaleSize(FontSizeModifier, alignment); + + // Measure text at base size first + var baseTextSize = FontDimensions.GetTextBaseSize(text); + + float rad = rotDeg * ShapeMath.DEGTORAD; + float c = MathF.Abs(MathF.Cos(rad)); + float s = MathF.Abs(MathF.Sin(rad)); + + // Rotated axis-aligned bounds for the text at base size + float rotatedBaseWidth = baseTextSize.Width * c + baseTextSize.Height * s; + float rotatedBaseHeight = baseTextSize.Width * s + baseTextSize.Height * c; + + if (rotatedBaseWidth <= 0f || rotatedBaseHeight <= 0f) return; + + float fX = rect.Width / rotatedBaseWidth; + float fY = rect.Height / rotatedBaseHeight; + float f = MathF.Min(fX, fY); + + float scaledFontSize = FontDimensions.FontSizeRange.Clamp(FontDimensions.BaseSize * f); + f = scaledFontSize / FontDimensions.BaseSize; + + var scaledFont = new FontDimensions( + FontDimensions.Font, + scaledFontSize, + FontDimensions.Spacing * f, + FontDimensions.LineSpacing * f + ); + var textSize = scaledFont.GetTextSize(text); Rect r = new(rect.GetPoint(alignment), textSize, alignment); var originOffset = (alignment * textSize).ToVector2(); - Raylib.DrawTextPro(scaledFont.Font, text, r.TopLeft + originOffset, originOffset, rotDeg, scaledFont.Size, scaledFont.Spacing, ColorRgba.ToRayColor()); + + Raylib.DrawTextPro( + scaledFont.Font, + text, + r.TopLeft + originOffset, + originOffset, + rotDeg, + scaledFont.Size, + scaledFont.Spacing, + ColorRgba.ToRayColor() + ); } /// /// Draws a word at the specified top-left position. diff --git a/ShapeEngine/Timing/Tween.cs b/ShapeEngine/Timing/Tween.cs index 6b72d282..8ae3925d 100644 --- a/ShapeEngine/Timing/Tween.cs +++ b/ShapeEngine/Timing/Tween.cs @@ -11,7 +11,7 @@ public class Tween : ISequenceable /// /// Delegate for the tween function, called with the interpolated value. /// - /// The interpolated value (0 to 1). + /// The interpolated value. /// True if the tween should finish, otherwise false. public delegate bool TweenFunc(float f); @@ -60,11 +60,22 @@ public Tween(Tween tween) public bool Update(float dt) { if (duration <= 0f) return true; - float t = ShapeMath.Clamp(timer / duration, 0f, 1f); - float f = ShapeTween.Tween(t, tweenType); + if (tweenType == TweenType.Ping_Pong) + { + float t = timer / duration; + float f = ShapeTween.Tween(t, tweenType); - timer += dt; - return func(f) || t >= 1f; + timer += dt; + return func(f); + } + else + { + float t = ShapeMath.Clamp(timer / duration, 0f, 1f); + float f = ShapeTween.Tween(t, tweenType); + + timer += dt; + return func(f) || t >= 1f; + } } /// diff --git a/ShapeEngine/UI/ControlNode.cs b/ShapeEngine/UI/ControlNode.cs index 157d44e5..eae5d370 100644 --- a/ShapeEngine/UI/ControlNode.cs +++ b/ShapeEngine/UI/ControlNode.cs @@ -100,7 +100,13 @@ public abstract class ControlNode /// Parameters: Invoker, Value /// public event Action? OnPressedChanged; - + + /// + /// Occurs when the down state changes. + /// Parameters: Invoker, Value + /// + public event Action? OnIsDownChanged; + /// /// Occurs when the navigable state changes. /// Parameters: Invoker, Value @@ -138,32 +144,7 @@ public abstract class ControlNode private bool parentActive = true; private bool parentVisible = true; private bool selected; - private bool displayed = true; - - /// - /// Gets or sets whether this node and its children are displayed. - /// Changing this value will recursively update all children. - /// Triggers and updates navigable and visible-in-hierarchy states. - /// - public bool Displayed - { - get => displayed; - set - { - if (value == displayed) return; - prevNavigable = Navigable; - prevIsVisibleInHierarchy = IsVisibleInHierarchy; - displayed = value; - ResolveOnDisplayedChanged(); - foreach (var child in children) - { - child.Displayed = value; - } - } - } - - private bool navigationSelected; private bool prevNavigable; private bool prevIsVisibleInHierarchy; @@ -171,7 +152,7 @@ public bool Displayed #endregion #region Public Members - + /// /// The anchor point used to position this node within its parent. /// @@ -258,6 +239,27 @@ public InputFilter InputFilter #endregion #region Getters & Setters + /// + /// Gets or sets whether this node and its children are displayed. + /// Changing this value will recursively update all children. + /// Triggers and updates navigable and visible-in-hierarchy states. + /// + public bool Displayed + { + get => displayed; + set + { + if (value == displayed) return; + prevNavigable = Navigable; + prevIsVisibleInHierarchy = IsVisibleInHierarchy; + displayed = value; + ResolveOnDisplayedChanged(); + foreach (var child in children) + { + child.Displayed = value; + } + } + } /// /// Gets or sets whether this node is active. @@ -372,6 +374,11 @@ private set /// public bool Pressed { get; private set; } + /// + /// Gets whether this node is currently down. + /// + public bool IsDown { get; private set; } + /// /// Gets the parent node, or null if this is a root node. /// Can only be set internally. @@ -419,8 +426,6 @@ private set /// public IEnumerable GetChildrenEnumerable => children; - // public List GetChildren(Predicate match) => children.FindAll(match); - /// /// Gets whether this node is navigable (active, visible, and has appropriate filters). /// @@ -929,18 +934,34 @@ private void InternalUpdate(float dt, Vector2 mousePos, bool mousePosValid) if (InputFilter != InputFilter.None) { - var pressed = false; + var pressed = Pressed; if (InputFilter == InputFilter.MouseOnly) { - pressed = MouseInside && GetMousePressedState(); + if (GetMouseButtonPressedState()) + { + if (MouseInside) pressed = true; + } + else if (GetMouseButtonReleasedState()) + { + pressed = false; + } + } else if (InputFilter == InputFilter.MouseNever) { - pressed = GetPressedState(); + if (GetButtonPressedState()) pressed = true; + else if (GetButtonReleasedState()) pressed = false; } else if (InputFilter == InputFilter.All) { - pressed = (MouseInside && GetMousePressedState()) || GetPressedState(); + if (GetMouseButtonPressedState() || GetButtonPressedState()) + { + pressed = (GetMouseButtonPressedState() && MouseInside) || GetButtonPressedState(); + } + else if (GetMouseButtonReleasedState() || GetButtonReleasedState()) + { + pressed = false; + } } if (Pressed != pressed) @@ -948,6 +969,26 @@ private void InternalUpdate(float dt, Vector2 mousePos, bool mousePosValid) Pressed = pressed; ResolvePressedChanged(); } + + var down = false; + if (InputFilter == InputFilter.MouseOnly) + { + down = MouseInside && GetMouseButtonDownState(); + } + else if (InputFilter == InputFilter.MouseNever) + { + down = GetButtonDownState(); + } + else if (InputFilter == InputFilter.All) + { + down = (MouseInside && GetMouseButtonDownState()) || GetButtonDownState(); + } + + if (IsDown != down) + { + IsDown = down; + ResolveIsDownChanged(); + } } } @@ -974,15 +1015,43 @@ private void InternalDraw() /// Override this method to handle custom input logic. /// /// Returns false per default. - protected virtual bool GetPressedState() => false; + protected virtual bool GetButtonPressedState() => false; /// /// Return if the mouse button for the pressed state is down (only is called when mouse is inside). /// Override this method to handle custom input logic. /// /// Returns false per default. - protected virtual bool GetMousePressedState() => false; - + protected virtual bool GetMouseButtonPressedState() => false; + + /// + /// Returns whether the key for the released state is down. + /// Override this method to handle custom input logic for button release. + /// + /// Returns false by default. + protected virtual bool GetButtonReleasedState() => false; + + /// + /// Returns whether the mouse button for the released state is down (only called when mouse is inside). + /// Override this method to handle custom input logic for mouse button release. + /// + /// Returns false by default. + protected virtual bool GetMouseButtonReleasedState() => false; + + /// + /// Returns whether the key for the down state is currently held down. + /// Override this method to handle custom input logic for button down. + /// + /// Returns false by default. + protected virtual bool GetButtonDownState() => false; + + /// + /// Returns whether the mouse button for the down state is currently held down (only called when mouse is inside). + /// Override this method to handle custom input logic for mouse button down. + /// + /// Returns false by default. + protected virtual bool GetMouseButtonDownState() => false; + /// /// Return the direction to move to another element. /// Override this method to handle custom navigation direction. @@ -1146,6 +1215,12 @@ protected virtual void SelectedWasChanged(bool value) { } /// /// The new pressed state. protected virtual void PressedWasChanged(bool value) { } + + /// + /// Called when the IsDown state changes. + /// + /// The new IsDown state. + protected virtual void IsDownWasChanged(bool value) { } /// /// Called when the mouse filter changes. @@ -1195,19 +1270,29 @@ protected virtual void VisibleInHierarchyChanged(bool value) { } #endregion #region Private + /// + /// Resolves changes to the active-in-hierarchy state and triggers related events. + /// private void ResolveOnActiveInHierarchyChanged() { if (prevIsActiveInHierarchy == IsActiveInHierarchy) return; ActiveInHierarchyChanged(IsActiveInHierarchy); OnActiveInHierarchyChanged?.Invoke(this, IsActiveInHierarchy); } - + + /// + /// Resolves changes to the visible-in-hierarchy state and triggers related events. + /// private void ResolveOnVisibleInHierarchyChanged() { if (prevIsVisibleInHierarchy == IsVisibleInHierarchy) return; VisibleInHierarchyChanged(IsVisibleInHierarchy); OnVisibleInHierarchyChanged?.Invoke(this, IsVisibleInHierarchy); } + + /// + /// Resolves changes to the displayed state and triggers related events. + /// private void ResolveOnDisplayedChanged() { DisplayedWasChanged(displayed); @@ -1215,6 +1300,10 @@ private void ResolveOnDisplayedChanged() ResolveOnNavigableChanged(); ResolveOnVisibleInHierarchyChanged(); } + + /// + /// Resolves changes to the active state and triggers related events. + /// private void ResolveActiveChanged() { ActiveWasChanged(active); @@ -1222,6 +1311,10 @@ private void ResolveActiveChanged() ResolveOnNavigableChanged(); ResolveOnActiveInHierarchyChanged(); } + + /// + /// Resolves changes to the visible state and triggers related events. + /// private void ResolveVisibleChanged() { VisibleWasChanged(visible); @@ -1229,6 +1322,10 @@ private void ResolveVisibleChanged() ResolveOnNavigableChanged(); ResolveOnVisibleInHierarchyChanged(); } + + /// + /// Resolves changes to the parent visible state and triggers related events. + /// private void ResolveParentVisibleChanged() { ParentVisibleWasChanged(parentVisible); @@ -1236,6 +1333,10 @@ private void ResolveParentVisibleChanged() ResolveOnNavigableChanged(); ResolveOnVisibleInHierarchyChanged(); } + + /// + /// Resolves changes to the parent active state and triggers related events. + /// private void ResolveParentActiveChanged() { ParentActiveWasChanged(parentActive); @@ -1243,60 +1344,125 @@ private void ResolveParentActiveChanged() ResolveOnNavigableChanged(); ResolveOnActiveInHierarchyChanged(); } + + /// + /// Resolves changes to the parent node and triggers related events. + /// + /// The previous parent node. + /// The new parent node. private void ResolveParentChanged(ControlNode? oldParent, ControlNode? newParent) { ParentWasChanged(oldParent, newParent); OnParentChanged?.Invoke(this, oldParent, newParent); // ResolveOnNavigableChanged(Navigable); } + + /// + /// Resolves the addition of a child node and triggers related events. + /// + /// The child node that was added. private void ResolveChildAdded(ControlNode newChild) { ChildWasAdded(newChild); OnChildAdded?.Invoke(this, newChild); } + + /// + /// Resolves the removal of a child node and triggers related events. + /// + /// The child node that was removed. private void ResolveChildRemoved(ControlNode oldChild) { ChildWasRemoved(oldChild); OnChildRemoved?.Invoke(this, oldChild); } + + /// + /// Resolves the mouse entering this node and triggers related events. + /// + /// The mouse position at entry. private void ResolveMouseEntered(Vector2 mousePos) { MouseHasEntered(mousePos); OnMouseEntered?.Invoke(this, mousePos); } + + /// + /// Resolves the mouse exiting this node and triggers related events. + /// + /// The last mouse position inside the node. private void ResolveMouseExited(Vector2 mousePos) { MouseHasExited(mousePos); OnMouseExited?.Invoke(this, mousePos); } + + /// + /// Resolves changes to the selected state and triggers related events. + /// private void ResolveSelectedChanged() { // Console.WriteLine($"Selected Changed to {selected} in {this.Anchor}"); SelectedWasChanged(selected); OnSelectedChanged?.Invoke(this, selected); } + + /// + /// Resolves changes to the pressed state and triggers related events. + /// private void ResolvePressedChanged() { PressedWasChanged(Pressed); OnPressedChanged?.Invoke(this, Pressed); } + + /// + /// Resolves changes to the IsDown state and triggers related events. + /// + private void ResolveIsDownChanged() + { + IsDownWasChanged(Pressed); + OnIsDownChanged?.Invoke(this, Pressed); + } + + /// + /// Resolves changes to the mouse filter and triggers related events. + /// + /// The previous mouse filter. + /// The current mouse filter. private void ResolveOnMouseFilterChanged(MouseFilter old, MouseFilter cur) { MouseFilterWasChanged(old, cur); OnMouseFilterChanged?.Invoke(this, old, cur); } + + /// + /// Resolves changes to the selection filter and triggers related events. + /// + /// The previous selection filter. + /// The current selection filter. private void ResolveOnSelectionFilterChanged(SelectFilter old, SelectFilter cur) { SelectionFilterWasChanged(old, cur); OnSelectionFilterChanged?.Invoke(this, old, cur); ResolveOnNavigableChanged(); } + + /// + /// Resolves changes to the input filter and triggers related events. + /// + /// The previous input filter. + /// The current input filter. private void ResolveOnInputFilterChanged(InputFilter old, InputFilter cur) { InputFilterWasChanged(old, cur); OnInputFilterChanged?.Invoke(this, old, cur); ResolveOnNavigableChanged(); } + + /// + /// Resolves changes to the navigable state and triggers related events. + /// private void ResolveOnNavigableChanged() { if (prevNavigable == Navigable) return; diff --git a/ShapeEngine/UI/ControlNodeSlider.cs b/ShapeEngine/UI/ControlNodeSlider.cs index c55190ee..77c14fc3 100644 --- a/ShapeEngine/UI/ControlNodeSlider.cs +++ b/ShapeEngine/UI/ControlNodeSlider.cs @@ -17,6 +17,8 @@ public class ControlNodeSlider : ControlNode /// Occurs when the slider value changes. /// public event Action? OnValueChanged; + + // public bool AlternativeMode = true; /// /// Gets or sets whether the slider is horizontal (true) or vertical (false). @@ -37,7 +39,7 @@ public class ControlNodeSlider : ControlNode /// /// Gets the current normalized value (0-1) of the slider. /// - public float CurF { get; private set; } + public float CurF { get; protected set; } /// /// The step size (normalized) for keyboard/gamepad value changes. /// @@ -49,7 +51,7 @@ public class ControlNodeSlider : ControlNode /// /// Gets the fill rectangle representing the current value. /// - public Rect Fill { get; private set; } + public Rect Fill { get; protected set; } /// /// Initializes a new horizontal slider with default range 0-100. @@ -204,25 +206,22 @@ protected override void OnUpdate(float dt, Vector2 mousePos, bool mousePosValid) protected virtual void HandleFill(Vector2 mousePos) { // if (!Active) return; - if (MouseInside) + if (IsDown || Pressed) { - if (Pressed) + float f = Horizontal ? Rect.GetWidthFactor(mousePos.X) : Rect.GetHeightFactor(mousePos.Y); + if (MouseSnapF > 0) { - float f = Horizontal ? Rect.GetWidthFactor(mousePos.X) : Rect.GetHeightFactor(mousePos.Y); - if (MouseSnapF > 0) + var snap = (f % MouseSnapF); + if (snap > MouseSnapF / 2) //snap to next bigger value + { + f = (f - snap) + MouseSnapF; + } + else //snap to lower value { - var snap = (f % MouseSnapF); - if (snap > MouseSnapF / 2) //snap to next bigger value - { - f = (f - snap) + MouseSnapF; - } - else //snap to lower value - { - f -= snap; - } + f -= snap; } - SetCurF(f); } + SetCurF(f); } Fill = Rect.SetSize(Rect.Size * GetSizeFactor()); } @@ -239,5 +238,5 @@ private void ResolveValueChange(float prevValue, float curValue) OnValueChanged?.Invoke(prevValue, curValue); } - private Vector2 GetSizeFactor() => Horizontal ? new Vector2(CurF, 1f) : new Vector2(1f, CurF); + protected Vector2 GetSizeFactor() => Horizontal ? new Vector2(CurF, 1f) : new Vector2(1f, CurF); } \ No newline at end of file