322 lines
10 KiB
C#

using Nerd_STF.Mathematics.Geometry.Abstract;
namespace Nerd_STF.Mathematics.Geometry;
public class Ellipse : IAverage<Ellipse>, IEquatable<Ellipse>,
IGeometricScale2d<Ellipse>, IGeometricTranslate2d<Ellipse>, ILerp<Ellipse, float>,
IMedian<Ellipse>, IPresets0d<Ellipse>,
ISplittable<Ellipse, (Float2[] positions, Float2[] radii)>, ITriangulate
{
public static Ellipse Unit => new(Float2.Zero, Float2.One, true);
public float Area => Constants.Pi * Radius.x * Radius.y;
public float Perimeter => GeometryHelper.EllipsePerimeterRamanujan2(this);
public float Eccentricity =>
Mathf.Sqrt(Radius.x * Radius.x - Radius.y * Radius.y) / Radius.x;
public float H =>
((Radius.x - Radius.y) * (Radius.x - Radius.y))
/ ((Radius.x + Radius.y) * (Radius.x + Radius.y));
public Float2 Bottom
{
get => Position + Float2.Down * Radius;
set
{
Float2 pos = Position;
Float2 rad = Radius;
pos.x = value.x;
float offset = (pos + Float2.Down * rad).y - value.y;
rad.y *= offset / 2;
pos.y += offset / 2;
Position = pos;
Radius = rad;
}
}
public Float2 Left
{
get => Position + Float2.Left * Radius;
set
{
Float2 pos = Position;
Float2 rad = Radius;
pos.x = value.x;
float offset = (pos + Float2.Left * rad).y - value.y;
rad.y *= offset / 2;
pos.y += offset / 2;
Position = pos;
Radius = rad;
}
}
public Float2 Right
{
get => Position + Float2.Right * Radius;
set
{
Float2 pos = Position;
Float2 rad = Radius;
pos.x = value.x;
float offset = (pos + Float2.Right * rad).y - value.y;
rad.y *= offset / 2;
pos.y += offset / 2;
Position = pos;
Radius = rad;
}
}
public Float2 Top
{
get => Position + Float2.Up * Radius;
set
{
Float2 pos = Position;
Float2 rad = Radius;
pos.x = value.x;
float offset = (pos + Float2.Up * rad).y - value.y;
rad.y *= offset / 2;
pos.y += offset / 2;
Position = pos;
Radius = rad;
}
}
public Float2 Position
{
get => p_position;
set => p_position = value;
}
public Float2 Radius
{
get => p_radius;
set
{
float ogRadiusAspect = p_radius.y / p_radius.x,
newRadiusAspect = value.y / value.x;
if (LockAspect && ogRadiusAspect != newRadiusAspect) ThrowLockedAspect();
p_radius = Float2.Absolute(value);
}
}
private Float2 p_position;
private Float2 p_radius;
public bool LockAspect { get; init; }
public Ellipse(Float2 position, Float2 radius, bool lockAspect = false)
{
p_position = position;
p_radius = Float2.Absolute(radius);
this.LockAspect = lockAspect;
}
public Ellipse(Float2 position, float radius, bool lockAspect = false)
{
p_position = position;
p_radius = Float2.Absolute((radius, radius));
this.LockAspect = lockAspect;
}
public Ellipse(Float2 position, float radiusX, float radiusY, bool lockAspect = false)
{
p_position = position;
p_radius = Float2.Absolute((radiusX, radiusY));
this.LockAspect = lockAspect;
}
public Ellipse(float x, float y, Float2 radius, bool lockAspect = false)
{
p_position = (x, y);
p_radius = Float2.Absolute(radius);
this.LockAspect = lockAspect;
}
public Ellipse(float x, float y, float radius, bool lockAspect = false)
{
p_position = (x, y);
p_radius = Float2.Absolute((radius, radius));
this.LockAspect = lockAspect;
}
public Ellipse(float x, float y, float radiusX, float radiusY, bool lockAspect = false)
{
p_position = (x, y);
p_radius = Float2.Absolute((radiusX, radiusY));
this.LockAspect = lockAspect;
}
public Ellipse(Fill<Float2> fill, bool lockAspect = false)
: this(fill(0), fill(1), lockAspect) { }
public Ellipse(Fill<Int2> fill, bool lockAspect = false)
: this(fill(0), fill(1), lockAspect) { }
public Ellipse(Fill<float> fill, bool lockAspect = false)
: this(fill(0), fill(1), fill(2), fill(3), lockAspect) { }
public Ellipse(Fill<int> fill, bool lockAspect = false)
: this(fill(0), fill(1), fill(2), fill(3), lockAspect) { }
// TODO
public static Ellipse FromFocalPoints(Float2 left, Float2 right, float length, bool lockAspect = false)
{
if (left.y != right.y)
throw new NotAlignedException("Focal points must be aligned on the Y-axis.");
if (left.x < right.x)
throw new ArgumentException("The left focal point must be on the left.");
// TODO: E = c / a
// c = |focal - center|
// c = sqrt(a^2 - b^2) / a
Float2 center = Float2.Average(left, right);
float c = right.x - center.x;
return null!; // TODO
}
public static Ellipse FromBounds(Box2d box, bool lockAspect) =>
new(box.center, box.extents, lockAspect);
public static Ellipse FromBounds(Float2 min, Float2 max, bool lockAspect = false)
{
float a = (max.x - min.x) / 2,
b = (max.y - min.y) / 2;
Float2 center = (min + max) / 2;
return new(center, (a, b), lockAspect);
}
public static Ellipse Average(params Ellipse[] vals)
{
(Float2[] positions, Float2[] radii) = SplitArray(vals);
return new(Float2.Average(positions), Float2.Average(radii));
}
public static Ellipse Lerp(Ellipse a, Ellipse b, float t, bool clamp = true) =>
new(Float2.Lerp(a.Position, b.Position, t, clamp),
Float2.Lerp(a.Radius, b.Radius, t, clamp));
public static (Float2[] positions, Float2[] radii) SplitArray(params Ellipse[] vals)
{
Float2[] positions = new Float2[vals.Length],
radii = new Float2[vals.Length];
for (int i = 0; i < vals.Length; i++)
{
positions[i] = vals[i].Position;
radii[i] = vals[i].Radius;
}
return (positions, radii);
}
public Float2 ClosestTo(Float2 point)
{
point = (point - Position) / Radius;
point = point.Normalized;
point = (point * Radius) + Position;
return point;
}
public bool Contains(Float2 point) =>
((point.x - Position.x) / Radius.x) * ((point.x - Position.x) / Radius.x) +
((point.y - Position.y) / Radius.y) * ((point.y - Position.y) / Radius.y) <= 1;
public bool Contains<T>(T poly) where T : IPolygon<T>
{
Float3[] verts = poly.GetAllVerts();
if (verts.Length < 1) return false;
foreach (Float3 v in verts) if (!Contains((Float2)v)) return false;
return true;
}
public bool Contains(Box2d box) => Contains(box.Min) && Contains(box.Max);
public bool Contains(Line line) => Contains((Float2)line.a) && Contains((Float2)line.b);
public bool Contains(Triangle tri) =>
Contains((Float2)tri.a) && Contains((Float2)tri.b) & Contains((Float2)tri.c);
public bool Contains(IEnumerable<Float2> points)
{
foreach (Float3 p in points) if (!Contains((Float2)p)) return false;
return true;
}
public bool Contains(Fill<Float2> points, int count)
{
for (int i = 0; i < count; i++) if (!Contains(points(i))) return false;
return true;
}
public (Float2 left, Float2 right) GetFocalPoints()
{
float c = Eccentricity * Radius.x;
return (Position - (c, 0), Position + (c, 0));
}
public Float2 LerpAcrossOutline(float t, bool clamp = true)
{
if (clamp) t = Mathf.Clamp(t, 0, 1);
else t = Mathf.AbsoluteMod(t, 1);
float rot = 2 * Constants.Pi * t;
Float2 point = (Mathf.Cos(rot), Mathf.Sin(rot));
point.x *= Radius.x;
point.y *= Radius.y;
point += Position;
return point;
}
public void Scale(float factor)
{
Radius *= factor;
}
public void Scale(Float2 factor)
{
Radius *= factor;
}
public Ellipse ScaleImmutable(float factor)
{
Ellipse clone = new(Position, Radius, LockAspect);
clone.Scale(factor);
return clone;
}
public Ellipse ScaleImmutable(Float2 factor)
{
Ellipse clone = new(Position, Radius, LockAspect);
clone.Scale(factor);
return clone;
}
public void Translate(Float2 offset)
{
Position += offset;
}
public Ellipse TranslateImmutable(Float2 offset)
{
Ellipse clone = new(Position, Radius, LockAspect);
clone.Translate(offset);
return clone;
}
public override bool Equals(object? obj)
{
if (obj is null) return false;
else if (obj is Ellipse ell) return Equals(ell);
return false;
}
public bool Equals(Ellipse? other) => other is not null && Position == other.Position &&
Radius == other.Radius;
public override int GetHashCode() => base.GetHashCode();
public override string ToString() => $"{nameof(Ellipse)} {{ Position: {Position}, Radius: {Radius} }}";
// TODO: public Polygon ToPolygon()
public Triangle[] Triangulate() => Triangulate(TriangulationMode.TriangleFan, 32);
public Triangle[] Triangulate(int detail) => Triangulate(TriangulationMode.TriangleFan, detail);
public Triangle[] Triangulate(TriangulationMode mode) => Triangulate(mode, 32);
public Triangle[] Triangulate(TriangulationMode mode, int detail) => mode switch
{
TriangulationMode.TriangleFan => GeometryHelper.EllipseTriangulateFan(this, 1f / detail),
_ => throw new ArgumentException("Unknown triangulation mode \"" + mode + "\"")
};
private void ThrowLockedAspect() => throw new AspectLockedException(
"Ellipse has a locked aspect ratio which cannot be changed.", this);
public enum TriangulationMode
{
TriangleFan
}
}