From 1c664ffe6cf521d89c058637b6be7ab63c75e7bd Mon Sep 17 00:00:00 2001 From: CarnifexOptimus <156172553+CarnifexOptimus@users.noreply.github.com> Date: Fri, 3 Jan 2025 22:55:30 +0100 Subject: [PATCH] some optimisations --- BossMod/BossModule/AOEShapes.cs | 7 +- BossMod/BossModule/ArenaBounds.cs | 55 +- BossMod/BossModule/MiniArena.cs | 28 +- .../Dungeon/D02TowerOfBabil/D023Anima.cs | 2 +- .../Extreme/Ex7Zeromus/Ex7Zeromus.cs | 5 +- .../Extreme/Ex7Zeromus/VoidMeteor.cs | 54 +- BossMod/Util/Intersect.cs | 2 +- BossMod/Util/Polygon.cs | 44 +- BossMod/Util/Serialization.cs | 1 - BossMod/Util/ShapeDistance.cs | 567 ++++++++++++++++-- 10 files changed, 655 insertions(+), 110 deletions(-) diff --git a/BossMod/BossModule/AOEShapes.cs b/BossMod/BossModule/AOEShapes.cs index 0efc6b80bf..79fa1dcb6b 100644 --- a/BossMod/BossModule/AOEShapes.cs +++ b/BossMod/BossModule/AOEShapes.cs @@ -339,8 +339,7 @@ public override void Outline(MiniArena arena, WPos origin, Angle rotation, uint public override Func Distance(WPos origin, Angle rotation) { shapeDistance ??= new PolygonWithHolesDistanceFunction(origin, Polygon ?? GetCombinedPolygon(origin)); - var dis = shapeDistance.Value.Distance; - return InvertForbiddenZone ? p => -dis(p) : dis; + return InvertForbiddenZone ? shapeDistance.Value.InvertedDistance : shapeDistance.Value.Distance; } } @@ -352,7 +351,7 @@ public sealed record class AOEShapeCustomAlt(RelSimplifiedComplexPolygon Poly, A public override void Outline(MiniArena arena, WPos origin, Angle rotation, uint color = 0) => arena.AddComplexPolygon(origin, (rotation + DirectionOffset).ToDirection(), Poly, color); public override Func Distance(WPos origin, Angle rotation) { - var shapeDistance = new PolygonWithHolesDistanceFunction(origin, Poly.Transform(default, (-rotation - DirectionOffset).ToDirection())).Distance; - return InvertForbiddenZone ? p => -shapeDistance(p) : shapeDistance; + return InvertForbiddenZone ? new PolygonWithHolesDistanceFunction(origin, Poly.Transform(default, (-rotation - DirectionOffset).ToDirection())).InvertedDistance + : new PolygonWithHolesDistanceFunction(origin, Poly.Transform(default, (-rotation - DirectionOffset).ToDirection())).Distance; } } diff --git a/BossMod/BossModule/ArenaBounds.cs b/BossMod/BossModule/ArenaBounds.cs index 3b9c2112c8..8f23738444 100644 --- a/BossMod/BossModule/ArenaBounds.cs +++ b/BossMod/BossModule/ArenaBounds.cs @@ -7,9 +7,9 @@ public abstract record class ArenaBounds(float Radius, float MapResolution, floa // fields below are used for clipping & drawing borders public const float Half = 0.5f; public readonly PolygonClipper Clipper = new(); - public float MaxApproxError { get; private set; } - public RelSimplifiedComplexPolygon ShapeSimplified { get; private set; } = new(); - public List ShapeTriangulation { get; private set; } = []; + public float MaxApproxError; + public RelSimplifiedComplexPolygon ShapeSimplified = new(); + public List ShapeTriangulation = []; private readonly PolygonClipper.Operand _clipOperand = new(); public readonly Dictionary Cache = []; @@ -40,7 +40,7 @@ public float ScreenHalfSize public abstract WDir ClampToBounds(WDir offset); // functions for clipping various shapes to bounds; all shapes are expected to be defined relative to bounds center - public List ClipAndTriangulate(IEnumerable poly) => Clipper.Intersect(new(poly), _clipOperand).Triangulate(); + public List ClipAndTriangulate(WDir[] poly) => Clipper.Intersect(new PolygonClipper.Operand((ReadOnlySpan)poly), _clipOperand).Triangulate(); public List ClipAndTriangulate(RelSimplifiedComplexPolygon poly) => Clipper.Intersect(new(poly), _clipOperand).Triangulate(); public List ClipAndTriangulateCone(WDir centerOffset, float innerRadius, float outerRadius, Angle centerDirection, Angle halfAngle) @@ -50,7 +50,7 @@ public List ClipAndTriangulateCone(WDir centerOffset, float innerRa return []; var fullCircle = halfAngle.Rad >= MathF.PI; - var donut = innerRadius > 0; + var donut = innerRadius != 0; var points = (donut, fullCircle) switch { (false, false) => CurveApprox.CircleSector(outerRadius, centerDirection - halfAngle, centerDirection + halfAngle, MaxApproxError), @@ -58,17 +58,46 @@ public List ClipAndTriangulateCone(WDir centerOffset, float innerRa (true, false) => CurveApprox.DonutSector(innerRadius, outerRadius, centerDirection - halfAngle, centerDirection + halfAngle, MaxApproxError), (true, true) => CurveApprox.Donut(innerRadius, outerRadius, MaxApproxError), }; - return ClipAndTriangulate(points.Select(p => p + centerOffset)); + for (var i = 0; i < points.Length; ++i) + { + points[i] += centerOffset; + } + return ClipAndTriangulate(points); } public List ClipAndTriangulateCircle(WDir centerOffset, float radius) - => ClipAndTriangulate(CurveApprox.Circle(radius, MaxApproxError).Select(p => p + centerOffset)); + { + var points = CurveApprox.Circle(radius, MaxApproxError); + for (var i = 0; i < points.Length; ++i) + { + points[i] += centerOffset; + } + return ClipAndTriangulate(points); + } + public List ClipAndTriangulateCapsule(WDir centerOffset, WDir direction, float radius, float length) - => ClipAndTriangulate(CurveApprox.Capsule(direction, length, radius, CurveApprox.ScreenError).Select(p => p + centerOffset)); + { + var points = CurveApprox.Capsule(direction, length, radius, MaxApproxError); + for (var i = 0; i < points.Length; ++i) + { + points[i] += centerOffset; + } + return ClipAndTriangulate(points); + } + public List ClipAndTriangulateDonut(WDir centerOffset, float innerRadius, float outerRadius) - => innerRadius < outerRadius && innerRadius >= 0 - ? ClipAndTriangulate(CurveApprox.Donut(innerRadius, outerRadius, MaxApproxError).Select(p => p + centerOffset)) - : []; + { + if (innerRadius < outerRadius && innerRadius >= 0) + { + var points = CurveApprox.Donut(innerRadius, outerRadius, MaxApproxError); + for (var i = 0; i < points.Length; ++i) + { + points[i] += centerOffset; + } + return ClipAndTriangulate(points); + } + return []; + } public List ClipAndTriangulateTri(WDir oa, WDir ob, WDir oc) => ClipAndTriangulate([oa, ob, oc]); @@ -128,7 +157,7 @@ public override WDir ClampToBounds(WDir offset) private Pathfinding.Map BuildMap() { var map = new Pathfinding.Map(MapResolution, default, Radius, Radius); - map.BlockPixelsInsideConvex(p => -ShapeDistance.Circle(default, Radius)(p), -1, 0); + map.BlockPixelsInsideConvex(ShapeDistance.InvertedCircle(default, Radius), -1, 0); return map; } } @@ -149,7 +178,7 @@ private static float CalculateScaleFactor(Angle Rotation) private Pathfinding.Map BuildMap() { var map = new Pathfinding.Map(MapResolution, default, HalfWidth, HalfHeight, Rotation); - map.BlockPixelsInsideConvex(p => -ShapeDistance.Rect(default, Rotation, HalfHeight, HalfHeight, HalfWidth)(p), -1, 0); + map.BlockPixelsInsideConvex(ShapeDistance.InvertedRect(default, Rotation, HalfHeight, HalfHeight, HalfWidth), -1, 0); return map; } diff --git a/BossMod/BossModule/MiniArena.cs b/BossMod/BossModule/MiniArena.cs index 28b45147cb..000054b0ce 100644 --- a/BossMod/BossModule/MiniArena.cs +++ b/BossMod/BossModule/MiniArena.cs @@ -185,16 +185,16 @@ public void AddDonutCone(WPos center, float innerRadius, float outerRadius, Angl var sDirP = sDir + halfAngle.Rad; var sDirN = sDir - halfAngle.Rad; drawlist.PathArcTo(sCenter, innerRadius / Bounds.Radius * ScreenHalfSize, sDirP, sDirN); - drawlist.PathArcTo(sCenter, innerRadius / Bounds.Radius * ScreenHalfSize, sDirN, sDirP); + drawlist.PathArcTo(sCenter, outerRadius / Bounds.Radius * ScreenHalfSize, sDirN, sDirP); drawlist.PathStroke(color != 0 ? color : Colors.Danger, ImDrawFlags.Closed, thickness); } public void AddCapsule(WPos start, WDir direction, float radius, float length, uint color = 0, float thickness = 1) { var dirNorm = direction.Normalized(); - var halfLength = length * 0.5f; - var capsuleStart = start - dirNorm * halfLength; - var capsuleEnd = start + dirNorm * halfLength; + var halfLengthdirNorm = dirNorm * length * 0.5f; + var capsuleStart = start - halfLengthdirNorm; + var capsuleEnd = start + halfLengthdirNorm; var orthoDir = dirNorm.OrthoR(); var drawList = ImGui.GetWindowDrawList(); @@ -315,9 +315,23 @@ public void ZoneRect(WPos start, WPos end, float halfWidth, uint color) => Zone(_triCache[TriangulationCache.GetKeyHash(9, start, end, halfWidth)] ??= _bounds.ClipAndTriangulateRect(start - Center, end - Center, halfWidth), color); public void ZoneComplex(WPos origin, Angle direction, RelSimplifiedComplexPolygon poly, uint color) => Zone(_triCache[TriangulationCache.GetKeyHash(10, origin, direction, poly)] ?? Bounds.ClipAndTriangulate(poly.Transform(origin - Center, direction.ToDirection())), color); - public void ZonePoly(object key, IEnumerable contour, uint color) - => Zone(_triCache[TriangulationCache.GetKeyHash(11, key)] ??= _bounds.ClipAndTriangulate(contour.Select(p => p - Center)), color); - public void ZoneRelPoly(object key, IEnumerable relContour, uint color) + public void ZonePoly(object key, WPos[] contour, uint color) + { + var hash = TriangulationCache.GetKeyHash(11, key); + var triangulation = _triCache[hash]; + if (triangulation == null) + { + var adjustedContour = new WDir[contour.Length]; + for (var i = 0; i < contour.Length; ++i) + { + adjustedContour[i] = contour[i] - Center; + } + triangulation = _bounds.ClipAndTriangulate(adjustedContour); + _triCache[hash] = triangulation; + } + Zone(triangulation, color); + } + public void ZoneRelPoly(object key, WDir[] relContour, uint color) => Zone(_triCache[TriangulationCache.GetKeyHash(12, key)] ??= _bounds.ClipAndTriangulate(relContour), color); public void ZoneRelPoly(int key, RelSimplifiedComplexPolygon poly, uint color) => Zone(_triCache[key] ??= _bounds.ClipAndTriangulate(poly), color); diff --git a/BossMod/Modules/Endwalker/Dungeon/D02TowerOfBabil/D023Anima.cs b/BossMod/Modules/Endwalker/Dungeon/D02TowerOfBabil/D023Anima.cs index 9eefc9242d..2b0f88618a 100644 --- a/BossMod/Modules/Endwalker/Dungeon/D02TowerOfBabil/D023Anima.cs +++ b/BossMod/Modules/Endwalker/Dungeon/D02TowerOfBabil/D023Anima.cs @@ -145,7 +145,7 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme if (Actors.Contains(actor)) hints.AddForbiddenZone(ShapeDistance.Rect(Arena.Center + new WDir(19, 0), Arena.Center + new WDir(-19, 0), 20), Activation); else if (Chasers.Any(x => x.Target == actor)) - hints.AddForbiddenZone(ShapeDistance.InvertedRect(actor.Position, 90.Degrees(), 40, 40, 3)); + hints.AddForbiddenZone(ShapeDistance.InvertedRect(actor.Position, new WDir(1, 0), 40, 40, 3)); } public override void Update() diff --git a/BossMod/Modules/Endwalker/Extreme/Ex7Zeromus/Ex7Zeromus.cs b/BossMod/Modules/Endwalker/Extreme/Ex7Zeromus/Ex7Zeromus.cs index 1f350ffa94..5bd8243948 100644 --- a/BossMod/Modules/Endwalker/Extreme/Ex7Zeromus/Ex7Zeromus.cs +++ b/BossMod/Modules/Endwalker/Extreme/Ex7Zeromus/Ex7Zeromus.cs @@ -7,4 +7,7 @@ class BigCrunchPuddle(BossModule module) : Components.LocationTargetedAOEs(modul class BigCrunchSpread(BossModule module) : Components.SpreadFromCastTargets(module, ActionID.MakeSpell(AID.BigCrunchSpread), 5); [ModuleInfo(BossModuleInfo.Maturity.Verified, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 965, NameID = 12586, PlanLevel = 90)] -public class Ex7Zeromus(WorldState ws, Actor primary) : BossModule(ws, primary, new(100, 100), new ArenaBoundsSquare(20)); +public class Ex7Zeromus(WorldState ws, Actor primary) : BossModule(ws, primary, ArenaCenter, new ArenaBoundsSquare(20)) +{ + public static readonly WPos ArenaCenter = new(100, 100); +} diff --git a/BossMod/Modules/Endwalker/Extreme/Ex7Zeromus/VoidMeteor.cs b/BossMod/Modules/Endwalker/Extreme/Ex7Zeromus/VoidMeteor.cs index ce00f03193..a584b7c0c9 100644 --- a/BossMod/Modules/Endwalker/Extreme/Ex7Zeromus/VoidMeteor.cs +++ b/BossMod/Modules/Endwalker/Extreme/Ex7Zeromus/VoidMeteor.cs @@ -2,7 +2,7 @@ class MeteorImpactProximity(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.MeteorImpactProximity), new AOEShapeCircle(10)); // TODO: verify falloff -class MeteorImpactCharge(BossModule module) : BossComponent(module) +class MeteorImpactCharge(BossModule module) : Components.GenericAOEs(module) { struct PlayerState { @@ -10,18 +10,17 @@ struct PlayerState public int Order; public bool Stretched; public bool NonClipping; - public List? DangerZone; } - public int NumCasts { get; private set; } private int _numTethers; - private readonly List _meteors = []; + private readonly List _meteors = new(18); private readonly PlayerState[] _playerStates = new PlayerState[PartyState.MaxPartySize]; + private AOEInstance? _aoe; + private readonly List polygons = new(18); private const float _radius = 2; private const int _ownThickness = 2; private const int _otherThickness = 1; - private const bool _drawShadows = true; public override void AddHints(int slot, Actor actor, TextHints hints) { @@ -37,29 +36,38 @@ public override void AddHints(int slot, Actor actor, TextHints hints) hints.Add("GTFO from charges!"); } + public override IEnumerable ActiveAOEs(int slot, Actor actor) => Utils.ZeroOrOne(_aoe); + public override PlayerPriority CalcPriority(int pcSlot, Actor pc, int playerSlot, Actor player, ref uint customColor) => SourceIfActive(playerSlot) != null ? PlayerPriority.Interesting : PlayerPriority.Normal; public override void DrawArenaBackground(int pcSlot, Actor pc) { - if (_drawShadows && SourceIfActive(pcSlot) is var source && source != null) + if (SourceIfActive(pcSlot) is var source && source != null) { ref var state = ref _playerStates.AsSpan()[pcSlot]; - state.DangerZone ??= BuildShadowZone(source.Position - Module.Center); - Arena.Zone(state.DangerZone, Colors.AOE); + if (_aoe == null) + { + for (var i = 0; i < _meteors.Count; ++i) + { + polygons.Add(new PolygonCustom(BuildShadowPolygon(source.Position - Arena.Center, _meteors[i] - Arena.Center, Arena.Bounds.MaxApproxError))); + } + _aoe = new(new AOEShapeCustom(polygons), Arena.Center); + } } + base.DrawArenaBackground(pcSlot, pc); } public override void DrawArenaForeground(int pcSlot, Actor pc) { - foreach (var m in _meteors) - Arena.AddCircle(m, _radius, Colors.Object); + for (var i = 0; i < _meteors.Count; ++i) + Arena.AddCircle(_meteors[i], _radius, Colors.Object); foreach (var (slot, target) in Raid.WithSlot(true)) { if (SourceIfActive(slot) is var source && source != null) { var thickness = slot == pcSlot ? _ownThickness : _otherThickness; - if (thickness > 0) + if (thickness != 0) { var norm = (target.Position - source.Position).Normalized().OrthoL() * 2; var rot = Angle.FromDirection(target.Position - source.Position); @@ -114,27 +122,23 @@ public override void OnTethered(Actor source, ActorTetherInfo tether) _ => null }; - private static IEnumerable BuildShadowPolygon(WDir sourceOffset, WDir meteorOffset) + private static List BuildShadowPolygon(WDir sourceOffset, WDir meteorOffset, float maxerror) { + var center = Ex7Zeromus.ArenaCenter; var toMeteor = meteorOffset - sourceOffset; var dirToMeteor = Angle.FromDirection(toMeteor); var halfAngle = Angle.Asin(_radius * 2 / toMeteor.Length()); // intersection point is at dirToMeteor -+ halfAngle relative to source; relative to meteor, it is (dirToMeteor + 180) +- (90 - halfAngle) var dirFromMeteor = dirToMeteor + 180.Degrees(); var halfAngleFromMeteor = 90.Degrees() - halfAngle; - foreach (var off in CurveApprox.CircleArc(_radius * 2, dirFromMeteor + halfAngleFromMeteor, dirFromMeteor - halfAngleFromMeteor, 0.2f)) - yield return meteorOffset + off; - yield return sourceOffset + 100 * (dirToMeteor + halfAngle).ToDirection(); - yield return sourceOffset + 100 * (dirToMeteor - halfAngle).ToDirection(); - } - - private List BuildShadowZone(WDir sourceOffset) - { - PolygonClipper.Operand set = new(); - foreach (var m in _meteors) - set.AddContour(BuildShadowPolygon(sourceOffset, m - Module.Center)); - var simplified = Arena.Bounds.Clipper.Simplify(set, Clipper2Lib.FillRule.NonZero); - return Arena.Bounds.ClipAndTriangulate(simplified); + var circlearc = CurveApprox.CircleArc(_radius * 2, dirFromMeteor + halfAngleFromMeteor, dirFromMeteor - halfAngleFromMeteor, maxerror); + var count = circlearc.Length; + List vertices = new(count + 2); + for (var i = 0; i < count; ++i) + vertices.Add(meteorOffset + circlearc[i] + center); + vertices.Add(sourceOffset + 100 * (dirToMeteor + halfAngle).ToDirection() + center); + vertices.Add(sourceOffset + 100 * (dirToMeteor - halfAngle).ToDirection() + center); + return vertices; } private static bool IsClipped(WPos source, WPos target, WPos position) => position.InCircle(target, _radius) || position.InRect(source, target - source, _radius); diff --git a/BossMod/Util/Intersect.cs b/BossMod/Util/Intersect.cs index 13509b0efe..3db03c66f3 100644 --- a/BossMod/Util/Intersect.cs +++ b/BossMod/Util/Intersect.cs @@ -138,7 +138,7 @@ public static bool CircleCone(WDir circleOffset, float circleRadius, float coneR var normal = coneDir.OrthoL(); var sin = halfAngle.Sin(); var distFromAxis = circleOffset.Dot(normal); - var originInCone = (halfAngle.Rad - MathF.PI * 0.5f) switch + var originInCone = (halfAngle.Rad - Angle.HalfPi) switch { < 0 => correctSide && distFromAxis * distFromAxis <= lsq * sin * sin, > 0 => correctSide || distFromAxis * distFromAxis >= lsq * sin * sin, diff --git a/BossMod/Util/Polygon.cs b/BossMod/Util/Polygon.cs index 8d0ccf7d36..36c572eaba 100644 --- a/BossMod/Util/Polygon.cs +++ b/BossMod/Util/Polygon.cs @@ -575,10 +575,15 @@ static Edge[] GetEdges(ReadOnlySpan vertices, WPos origin) var edges = new Edge[count]; var prev = vertices[count - 1]; + var originX = origin.X; + var originZ = origin.Z; + for (var i = 0; i < count; ++i) { var curr = vertices[i]; - edges[i] = new(origin.X + prev.X, origin.Z + prev.Z, curr.X - prev.X, curr.Z - prev.Z); + var prevX = prev.X; + var prevZ = prev.Z; + edges[i] = new(originX + prevX, originZ + prevZ, curr.X - prevX, curr.Z - prevZ); prev = curr; } @@ -588,17 +593,19 @@ static Edge[] GetEdges(ReadOnlySpan vertices, WPos origin) public readonly float Distance(WPos p) { - var localPoint = new WDir(p.X - _origin.X, p.Z - _origin.Z); + var pX = p.X; + var pZ = p.Z; + var localPoint = new WDir(pX - _origin.X, pZ - _origin.Z); var isInside = _polygon.Contains(localPoint); - var minDistanceSq = float.MaxValue; - var indices = _spatialIndex.Query(p.X, p.Z); + + var indices = _spatialIndex.Query(pX, pZ); for (var i = 0; i < indices.Length; ++i) { var edge = _edges[indices[i]]; - var t = Math.Clamp(((p.X - edge.Ax) * edge.Dx + (p.Z - edge.Ay) * edge.Dy) * edge.InvLengthSq, 0, 1); - var distX = p.X - (edge.Ax + t * edge.Dx); - var distY = p.Z - (edge.Ay + t * edge.Dy); + var t = Math.Clamp(((pX - edge.Ax) * edge.Dx + (pZ - edge.Ay) * edge.Dy) * edge.InvLengthSq, 0, 1); + var distX = pX - (edge.Ax + t * edge.Dx); + var distY = pZ - (edge.Ay + t * edge.Dy); minDistanceSq = Math.Min(minDistanceSq, distX * distX + distY * distY); } @@ -606,4 +613,27 @@ public readonly float Distance(WPos p) var minDistance = MathF.Sqrt(minDistanceSq); return isInside ? -minDistance : minDistance; } + + public readonly float InvertedDistance(WPos p) + { + var pX = p.X; + var pZ = p.Z; + var localPoint = new WDir(pX - _origin.X, pZ - _origin.Z); + var isInside = _polygon.Contains(localPoint); + var minDistanceSq = float.MaxValue; + + var indices = _spatialIndex.Query(pX, pZ); + for (var i = 0; i < indices.Length; ++i) + { + var edge = _edges[indices[i]]; + var t = Math.Clamp(((pX - edge.Ax) * edge.Dx + (pZ - edge.Ay) * edge.Dy) * edge.InvLengthSq, 0, 1); + var distX = pX - (edge.Ax + t * edge.Dx); + var distY = pZ - (edge.Ay + t * edge.Dy); + + minDistanceSq = Math.Min(minDistanceSq, distX * distX + distY * distY); + } + + var minDistance = MathF.Sqrt(minDistanceSq); + return isInside ? minDistance : -minDistance; + } } diff --git a/BossMod/Util/Serialization.cs b/BossMod/Util/Serialization.cs index 01c72b2d32..7b3a4fda1b 100644 --- a/BossMod/Util/Serialization.cs +++ b/BossMod/Util/Serialization.cs @@ -1,5 +1,4 @@ using System.IO; -using System.Runtime.CompilerServices; using System.Text.Json; using System.Text.Json.Serialization; diff --git a/BossMod/Util/ShapeDistance.cs b/BossMod/Util/ShapeDistance.cs index f4d2bb8fc6..7ae15bad95 100644 --- a/BossMod/Util/ShapeDistance.cs +++ b/BossMod/Util/ShapeDistance.cs @@ -5,24 +5,76 @@ public static class ShapeDistance { - public static Func HalfPlane(WPos point, WDir normal) => p => normal.Dot(p - point); + public static Func HalfPlane(WPos point, WDir normal) + { + var normalX = normal.X; + var normalZ = normal.Z; + var pointX = point.X; + var pointZ = point.Z; + return p => normalX * (p.X - pointX) + normalZ * (p.Z - pointZ); + } + + public static Func Circle(WPos origin, float radius) + { + var radiusSq = radius * radius; + var originX = origin.X; + var originZ = origin.Z; + return radius <= 0 ? (_ => float.MaxValue) : (p => + { + var pXoriginX = p.X - originX; + var pZoriginZ = p.Z - originZ; + return pXoriginX * pXoriginX + pZoriginZ * pZoriginZ - radiusSq; + }); + } - public static Func Circle(WPos origin, float radius) => radius <= 0 ? (_ => float.MaxValue) : (p => (p - origin).Length() - radius); - public static Func InvertedCircle(WPos origin, float radius) => radius <= 0 ? (_ => float.MinValue) : (p => radius - (p - origin).Length()); + public static Func InvertedCircle(WPos origin, float radius) + { + var radiusSq = radius * radius; + var originX = origin.X; + var originZ = origin.Z; + return radius <= 0 ? (_ => float.MinValue) : (p => + { + var pXoriginX = p.X - originX; + var pZoriginZ = p.Z - originZ; + return radiusSq - (pXoriginX * pXoriginX + pZoriginZ * pZoriginZ); + }); + } public static Func Donut(WPos origin, float innerRadius, float outerRadius) { + var innerSq = innerRadius * innerRadius; + var outerSq = outerRadius * outerRadius; + var originX = origin.X; + var originZ = origin.Z; return outerRadius <= 0 || innerRadius >= outerRadius ? (_ => float.MaxValue) : innerRadius <= 0 ? Circle(origin, outerRadius) : (p => { // intersection of outer circle and inverted inner circle - var distOrigin = (p - origin).Length(); - var distOuter = distOrigin - outerRadius; - var distInner = innerRadius - distOrigin; - return Math.Max(distOuter, distInner); + var pXoriginX = p.X - originX; + var pZoriginZ = p.Z - originZ; + var distSqOrigin = pXoriginX * pXoriginX + pZoriginZ * pZoriginZ; + var distSqOuter = distSqOrigin - outerSq; + var distSqInner = innerSq - distSqOrigin; + return distSqOuter > distSqInner ? distSqOuter : distSqInner; }); } - public static Func InvertedDonut(WPos origin, float innerRadius, float outerRadius) => p => -Donut(origin, innerRadius, outerRadius)(p); + public static Func InvertedDonut(WPos origin, float innerRadius, float outerRadius) + { + var innerSq = innerRadius * innerRadius; + var outerSq = outerRadius * outerRadius; + var originX = origin.X; + var originZ = origin.Z; + return outerRadius <= 0 || innerRadius >= outerRadius ? (_ => float.MaxValue) : innerRadius <= 0 ? Circle(origin, outerRadius) : (p => + { + // intersection of outer circle and inverted inner circle + var pXoriginX = p.X - originX; + var pZoriginZ = p.Z - originZ; + var distSqOrigin = pXoriginX * pXoriginX + pZoriginZ * pZoriginZ; + var distSqOuter = distSqOrigin - outerSq; + var distSqInner = innerSq - distSqOrigin; + return distSqOuter > distSqInner ? -distSqOuter : -distSqInner; + }); + } public static Func Cone(WPos origin, float radius, Angle centerDir, Angle halfAngle) { @@ -36,44 +88,141 @@ public static Func Cone(WPos origin, float radius, Angle centerDir, float coneFactor = halfAngle.Rad > Angle.HalfPi ? -1 : 1; var nl = coneFactor * (centerDir + halfAngle).ToDirection().OrthoL(); var nr = coneFactor * (centerDir - halfAngle).ToDirection().OrthoR(); + var radiusSq = radius * radius; + var originX = origin.X; + var originZ = origin.Z; + var nlX = nl.X; + var nlZ = nl.Z; + var nrX = nr.X; + var nrZ = nr.Z; return p => { - var off = p - origin; - var distOrigin = off.Length(); - var distOuter = distOrigin - radius; - var distLeft = off.Dot(nl); - var distRight = off.Dot(nr); - return Math.Max(distOuter, coneFactor * Math.Max(distLeft, distRight)); + var pXoriginX = p.X - originX; + var pZoriginZ = p.Z - originZ; + var distSqOrigin = pXoriginX * pXoriginX + pZoriginZ * pZoriginZ; + var distSqOuter = distSqOrigin - radiusSq; + var distLeft = pXoriginX * nlX + pZoriginZ * nlZ; + var distRight = pXoriginX * nrX + pZoriginZ * nrZ; + + var maxSideDist = distLeft > distRight ? distLeft : distRight; + var conef = coneFactor * maxSideDist; + return distSqOuter > conef ? distSqOuter : conef; }; } - public static Func InvertedCone(WPos origin, float radius, Angle centerDir, Angle halfAngle) => p => -Cone(origin, radius, centerDir, halfAngle)(p); + public static Func InvertedCone(WPos origin, float radius, Angle centerDir, Angle halfAngle) + { + if (halfAngle.Rad <= 0 || radius <= 0) + return _ => float.MaxValue; + if (halfAngle.Rad >= MathF.PI) + return Circle(origin, radius); + // for <= 180-degree cone: result = intersection of circle and two half-planes with normals pointing outside cone sides + // for > 180-degree cone: result = intersection of circle and negated intersection of two half-planes with inverted normals + // both normals point outside + float coneFactor = halfAngle.Rad > Angle.HalfPi ? -1 : 1; + var nl = coneFactor * (centerDir + halfAngle).ToDirection().OrthoL(); + var nr = coneFactor * (centerDir - halfAngle).ToDirection().OrthoR(); + var radiusSq = radius * radius; + var originX = origin.X; + var originZ = origin.Z; + var nlX = nl.X; + var nlZ = nl.Z; + var nrX = nr.X; + var nrZ = nr.Z; + return p => + { + var pXoriginX = p.X - originX; + var pZoriginZ = p.Z - originZ; + var distSqOrigin = pXoriginX * pXoriginX + pZoriginZ * pZoriginZ; + var distSqOuter = distSqOrigin - radiusSq; + var distLeft = pXoriginX * nlX + pZoriginZ * nlZ; + var distRight = pXoriginX * nrX + pZoriginZ * nrZ; + + var maxSideDist = distLeft > distRight ? distLeft : distRight; + var conef = coneFactor * maxSideDist; + return distSqOuter > conef ? -distSqOuter : -conef; + }; + } public static Func DonutSector(WPos origin, float innerRadius, float outerRadius, Angle centerDir, Angle halfAngle) { if (halfAngle.Rad <= 0 || outerRadius <= 0 || innerRadius >= outerRadius) return _ => float.MaxValue; + if (halfAngle.Rad >= MathF.PI) return Donut(origin, innerRadius, outerRadius); + if (innerRadius <= 0) return Cone(origin, outerRadius, centerDir, halfAngle); + float coneFactor = halfAngle.Rad > Angle.HalfPi ? -1 : 1; var nl = coneFactor * (centerDir + halfAngle + 90.Degrees()).ToDirection(); var nr = coneFactor * (centerDir - halfAngle - 90.Degrees()).ToDirection(); + var innerSq = innerRadius * innerRadius; + var outerSq = outerRadius * outerRadius; + var originX = origin.X; + var originZ = origin.Z; + var nlX = nl.X; + var nlZ = nl.Z; + var nrX = nr.X; + var nrZ = nr.Z; + return p => { - var off = p - origin; - var distOrigin = off.Length(); - var distOuter = distOrigin - outerRadius; - var distInner = innerRadius - distOrigin; - var distLeft = off.Dot(nl); - var distRight = off.Dot(nr); - return Math.Max(Math.Max(distOuter, distInner), coneFactor * Math.Max(distLeft, distRight)); + var pXoriginX = p.X - originX; + var pZoriginZ = p.Z - originZ; + var distSqOrigin = pXoriginX * pXoriginX + pZoriginZ * pZoriginZ; + var distOuter = outerSq - outerRadius; + var distInner = innerSq - distSqOrigin; + var distLeft = pXoriginX * nlX + pZoriginZ * nlZ; + var distRight = pXoriginX * nrX + pZoriginZ * nrZ; + + var maxRadial = distOuter > distInner ? distOuter : distInner; + var maxCone = distLeft > distRight ? distLeft : distRight; + var conef = coneFactor * maxCone; + return maxRadial > conef ? maxRadial : conef; }; } public static Func InvertedDonutSector(WPos origin, float innerRadius, float outerRadius, Angle centerDir, Angle halfAngle) - => p => -DonutSector(origin, innerRadius, outerRadius, centerDir, halfAngle)(p); + { + if (halfAngle.Rad <= 0 || outerRadius <= 0 || innerRadius >= outerRadius) + return _ => float.MaxValue; + + if (halfAngle.Rad >= MathF.PI) + return Donut(origin, innerRadius, outerRadius); + + if (innerRadius <= 0) + return Cone(origin, outerRadius, centerDir, halfAngle); + + float coneFactor = halfAngle.Rad > Angle.HalfPi ? -1 : 1; + var nl = coneFactor * (centerDir + halfAngle + 90.Degrees()).ToDirection(); + var nr = coneFactor * (centerDir - halfAngle - 90.Degrees()).ToDirection(); + var innerSq = innerRadius * innerRadius; + var outerSq = outerRadius * outerRadius; + var originX = origin.X; + var originZ = origin.Z; + var nlX = nl.X; + var nlZ = nl.Z; + var nrX = nr.X; + var nrZ = nr.Z; + + return p => + { + var pXoriginX = p.X - originX; + var pZoriginZ = p.Z - originZ; + var distSqOrigin = pXoriginX * pXoriginX + pZoriginZ * pZoriginZ; + var distOuter = outerSq - outerRadius; + var distInner = innerSq - distSqOrigin; + var distLeft = pXoriginX * nlX + pZoriginZ * nlZ; + var distRight = pXoriginX * nrX + pZoriginZ * nrZ; + + var maxRadial = distOuter > distInner ? distOuter : distInner; + var maxCone = distLeft > distRight ? distLeft : distRight; + var conef = coneFactor * maxCone; + return maxRadial > conef ? -maxRadial : -conef; + }; + } public static Func Tri(WPos origin, RelTriangle tri) { @@ -92,57 +241,340 @@ public static Func Tri(WPos origin, RelTriangle tri) var a = origin + tri.A; var b = origin + tri.B; var c = origin + tri.C; + + var n1X = n1.X; + var n1Z = n1.Z; + var n2X = n2.X; + var n2Z = n2.Z; + var n3X = n3.X; + var n3Z = n3.Z; + var aX = a.X; + var aZ = a.Z; + var bX = b.X; + var bZ = b.Z; + var cX = c.X; + var cZ = c.Z; + return p => { - var d1 = n1.Dot(p - a); - var d2 = n2.Dot(p - b); - var d3 = n3.Dot(p - c); - return Math.Max(Math.Max(d1, d2), d3); + var d1 = n1X * (p.X - aX) + n1Z * (p.Z - aZ); + var d2 = n2X * (p.X - bX) + n2Z * (p.Z - bZ); + var d3 = n3X * (p.X - cX) + n3Z * (p.Z - cZ); + var max1 = d1 > d2 ? d1 : d2; + return max1 > d3 ? max1 : d3; }; } - public static Func InvertedTri(WPos origin, RelTriangle tri) => p => -Tri(origin, tri)(p); + public static Func InvertedTri(WPos origin, RelTriangle tri) + { + var ab = tri.B - tri.A; + var bc = tri.C - tri.B; + var ca = tri.A - tri.C; + var n1 = ab.OrthoL().Normalized(); + var n2 = bc.OrthoL().Normalized(); + var n3 = ca.OrthoL().Normalized(); + if (ab.Cross(bc) < 0) + { + n1 = -n1; + n2 = -n2; + n3 = -n3; + } + var a = origin + tri.A; + var b = origin + tri.B; + var c = origin + tri.C; + + var n1X = n1.X; + var n1Z = n1.Z; + var n2X = n2.X; + var n2Z = n2.Z; + var n3X = n3.X; + var n3Z = n3.Z; + var aX = a.X; + var aZ = a.Z; + var bX = b.X; + var bZ = b.Z; + var cX = c.X; + var cZ = c.Z; - public static Func TriList(WPos origin, List tris) => Union([.. tris.Select(tri => Tri(origin, tri))]); + return p => + { + var d1 = n1X * (p.X - aX) + n1Z * (p.Z - aZ); + var d2 = n2X * (p.X - bX) + n2Z * (p.Z - bZ); + var d3 = n3X * (p.X - cX) + n3Z * (p.Z - cZ); + var max1 = d1 > d2 ? d1 : d2; + return max1 > d3 ? -max1 : -d3; + }; + } public static Func Rect(WPos origin, WDir dir, float lenFront, float lenBack, float halfWidth) { // dir points outside far side var normal = dir.OrthoL(); // points outside left side + var originX = origin.X; + var originZ = origin.Z; + var dirX = dir.X; + var dirZ = dir.Z; + var normalX = normal.X; + var normalZ = normal.Z; + return p => { - var offset = p - origin; - var distParr = offset.Dot(dir); - var distOrtho = offset.Dot(normal); + var pXoriginX = p.X - originX; + var pZoriginZ = p.Z - originZ; + var distParr = pXoriginX * dirX + pZoriginZ * dirZ; + var distOrtho = pXoriginX * normalX + pZoriginZ * normalZ; + var distFront = distParr - lenFront; + var distBack = -distParr - lenBack; + var distLeft = distOrtho - halfWidth; + var distRight = -distOrtho - halfWidth; + + var maxParr = distFront > distBack ? distFront : distBack; + var maxOrtho = distLeft > distRight ? distLeft : distRight; + + return maxParr > maxOrtho ? maxParr : maxOrtho; + }; + } + + public static Func Rect(WPos origin, Angle direction, float lenFront, float lenBack, float halfWidth) + { + // dir points outside far side + var dir = direction.ToDirection(); + var normal = dir.OrthoL(); // points outside left side + var originX = origin.X; + var originZ = origin.Z; + var dirX = dir.X; + var dirZ = dir.Z; + var normalX = normal.X; + var normalZ = normal.Z; + + return p => + { + var pXoriginX = p.X - originX; + var pZoriginZ = p.Z - originZ; + var distParr = pXoriginX * dirX + pZoriginZ * dirZ; + var distOrtho = pXoriginX * normalX + pZoriginZ * normalZ; var distFront = distParr - lenFront; var distBack = -distParr - lenBack; var distLeft = distOrtho - halfWidth; var distRight = -distOrtho - halfWidth; - return Math.Max(Math.Max(distFront, distBack), Math.Max(distLeft, distRight)); + + var maxParr = distFront > distBack ? distFront : distBack; + var maxOrtho = distLeft > distRight ? distLeft : distRight; + + return maxParr > maxOrtho ? maxParr : maxOrtho; }; } - public static Func Rect(WPos origin, Angle direction, float lenFront, float lenBack, float halfWidth) => Rect(origin, direction.ToDirection(), lenFront, lenBack, halfWidth); + public static Func Rect(WPos from, WPos to, float halfWidth) { var dir = to - from; var l = dir.Length(); - return Rect(from, dir / l, l, 0, halfWidth); + var normalizedDir = dir / l; + var normal = normalizedDir.OrthoL(); + + var originX = from.X; + var originZ = from.Z; + var dirX = normalizedDir.X; + var dirZ = normalizedDir.Z; + var normalX = normal.X; + var normalZ = normal.Z; + + return p => + { + var pXoriginX = p.X - originX; + var pZoriginZ = p.Z - originZ; + var distParr = pXoriginX * dirX + pZoriginZ * dirZ; + var distOrtho = pXoriginX * normalX + pZoriginZ * normalZ; + var distFront = distParr - l; + var distBack = -distParr; + var distLeft = distOrtho - halfWidth; + var distRight = -distOrtho - halfWidth; + + var maxParr = distFront > distBack ? distFront : distBack; + var maxOrtho = distLeft > distRight ? distLeft : distRight; + + return maxParr > maxOrtho ? maxParr : maxOrtho; + }; + } + + public static Func InvertedRect(WPos origin, WDir dir, float lenFront, float lenBack, float halfWidth) + { + // dir points outside far side + var normal = dir.OrthoL(); // points outside left side + + var originX = origin.X; + var originZ = origin.Z; + var dirX = dir.X; + var dirZ = dir.Z; + var normalX = normal.X; + var normalZ = normal.Z; + + return p => + { + var pXoriginX = p.X - originX; + var pZoriginZ = p.Z - originZ; + var distParr = pXoriginX * dirX + pZoriginZ * dirZ; + var distOrtho = pXoriginX * normalX + pZoriginZ * normalZ; + var distFront = distParr - lenFront; + var distBack = -distParr - lenBack; + var distLeft = distOrtho - halfWidth; + var distRight = -distOrtho - halfWidth; + + var maxParr = distFront > distBack ? distFront : distBack; + var maxOrtho = distLeft > distRight ? distLeft : distRight; + + return maxParr > maxOrtho ? -maxParr : -maxOrtho; + }; + } + + public static Func InvertedRect(WPos origin, Angle direction, float lenFront, float lenBack, float halfWidth) + { + // dir points outside far side + var dir = direction.ToDirection(); + var normal = dir.OrthoL(); // points outside left side + + var originX = origin.X; + var originZ = origin.Z; + var dirX = dir.X; + var dirZ = dir.Z; + var normalX = normal.X; + var normalZ = normal.Z; + + return p => + { + var pXoriginX = p.X - originX; + var pZoriginZ = p.Z - originZ; + var distParr = pXoriginX * dirX + pZoriginZ * dirZ; + var distOrtho = pXoriginX * normalX + pZoriginZ * normalZ; + var distFront = distParr - lenFront; + var distBack = -distParr - lenBack; + var distLeft = distOrtho - halfWidth; + var distRight = -distOrtho - halfWidth; + + var maxParr = distFront > distBack ? distFront : distBack; + var maxOrtho = distLeft > distRight ? distLeft : distRight; + + return maxParr > maxOrtho ? -maxParr : -maxOrtho; + }; + } + + public static Func InvertedRect(WPos from, WPos to, float halfWidth) + { + var dir = to - from; + var l = dir.Length(); + var normalizedDir = dir / l; + var normal = normalizedDir.OrthoL(); + + var originX = from.X; + var originZ = from.Z; + var dirX = normalizedDir.X; + var dirZ = normalizedDir.Z; + var normalX = normal.X; + var normalZ = normal.Z; + + return p => + { + var pXoriginX = p.X - originX; + var pZoriginZ = p.Z - originZ; + var distParr = pXoriginX * dirX + pZoriginZ * dirZ; + var distOrtho = pXoriginX * normalX + pZoriginZ * normalZ; + var distFront = distParr - l; + var distBack = -distParr; + var distLeft = distOrtho - halfWidth; + var distRight = -distOrtho - halfWidth; + + var maxParr = distFront > distBack ? distFront : distBack; + var maxOrtho = distLeft > distRight ? distLeft : distRight; + + return maxParr > maxOrtho ? -maxParr : -maxOrtho; + }; + } + + public static Func Capsule(WPos origin, WDir dir, float length, float radius) + { + var radiusSq = radius * radius; + var originX = origin.X; + var originZ = origin.Z; + var dirX = dir.X; + var dirZ = dir.Z; + + return p => + { + var pXoriginX = p.X - originX; + var pZoriginZ = p.Z - originZ; + var t = pXoriginX * dirX + pZoriginZ * dirZ; + t = (t < 0) ? 0 : (t > length ? length : t); + var proj = origin + t * dir; + var pXprojX = p.X - proj.X; + var pZprojZ = p.Z - proj.Z; + return pXprojX * pXprojX + pZprojZ * pZprojZ - radiusSq; + }; + } + + public static Func Capsule(WPos origin, Angle direction, float length, float radius) + { + var radiusSq = radius * radius; + var dir = direction.ToDirection(); + var originX = origin.X; + var originZ = origin.Z; + var dirX = dir.X; + var dirZ = dir.Z; + + return p => + { + var pXoriginX = p.X - originX; + var pZoriginZ = p.Z - originZ; + var t = pXoriginX * dirX + pZoriginZ * dirZ; + t = (t < 0) ? 0 : (t > length ? length : t); + var proj = origin + t * dir; + var pXprojX = p.X - proj.X; + var pZprojZ = p.Z - proj.Z; + return pXprojX * pXprojX + pZprojZ * pZprojZ - radiusSq; + }; } - public static Func InvertedRect(WPos origin, WDir dir, float lenFront, float lenBack, float halfWidth) => p => -Rect(origin, dir, lenFront, lenBack, halfWidth)(p); + public static Func InvertedCapsule(WPos origin, WDir dir, float length, float radius) + { + var radiusSq = radius * radius; + var originX = origin.X; + var originZ = origin.Z; + var dirX = dir.X; + var dirZ = dir.Z; - public static Func InvertedRect(WPos origin, Angle direction, float lenFront, float lenBack, float halfWidth) => p => -Rect(origin, direction.ToDirection(), lenFront, lenBack, halfWidth)(p); - public static Func InvertedRect(WPos from, WPos to, float halfWidth) => p => -Rect(from, to, halfWidth)(p); + return p => + { + var pXoriginX = p.X - originX; + var pZoriginZ = p.Z - originZ; + var t = pXoriginX * dirX + pZoriginZ * dirZ; + t = (t < 0) ? 0 : (t > length ? length : t); + var proj = origin + t * dir; + var pXprojX = p.X - proj.X; + var pZprojZ = p.Z - proj.Z; + return radiusSq - (pXprojX * pXprojX + pZprojZ * pZprojZ); + }; + } - public static Func Capsule(WPos origin, WDir dir, float length, float radius) => p => + public static Func InvertedCapsule(WPos origin, Angle direction, float length, float radius) { - var offset = p - origin; - var t = Math.Clamp(offset.Dot(dir), 0, length); - var proj = origin + t * dir; - return (p - proj).Length() - radius; - }; - public static Func Capsule(WPos origin, Angle direction, float length, float radius) => Capsule(origin, direction.ToDirection(), length, radius); - public static Func InvertedCapsule(WPos origin, Angle direction, float length, float radius) => p => -Capsule(origin, direction.ToDirection(), length, radius)(p); + var radiusSq = radius * radius; + var dir = direction.ToDirection(); + var originX = origin.X; + var originZ = origin.Z; + var dirX = dir.X; + var dirZ = dir.Z; + + return p => + { + var pXoriginX = p.X - originX; + var pZoriginZ = p.Z - originZ; + var t = pXoriginX * dirX + pZoriginZ * dirZ; + t = (t < 0) ? 0 : (t > length ? length : t); + var proj = origin + t * dir; + var pXprojX = p.X - proj.X; + var pZprojZ = p.Z - proj.Z; + return radiusSq - (pXprojX * pXprojX + pZprojZ * pZprojZ); + }; + } public static Func Cross(WPos origin, Angle direction, float length, float halfWidth) { @@ -161,13 +593,48 @@ public static Func Cross(WPos origin, Angle direction, float length var distOBack = -distOrtho - length; var distOLeft = distParr - halfWidth; var distORight = -distParr - halfWidth; - var distP = Math.Max(Math.Max(distPFront, distPBack), Math.Max(distPLeft, distPRight)); - var distO = Math.Max(Math.Max(distOFront, distOBack), Math.Max(distOLeft, distORight)); - return Math.Min(distP, distO); + + var distPMax1 = distPFront > distPBack ? distPFront : distPBack; + var distPMax2 = distPLeft > distPRight ? distPLeft : distPRight; + var distP = distPMax1 > distPMax2 ? distPMax1 : distPMax2; + + var distOMax1 = distOFront > distOBack ? distOFront : distOBack; + var distOMax2 = distOLeft > distORight ? distOLeft : distORight; + var distO = distOMax1 > distOMax2 ? distOMax1 : distOMax2; + + return distP < distO ? distP : distO; }; } - public static Func InvertedCross(WPos origin, Angle direction, float length, float halfWidth) => p => -Cross(origin, direction, length, halfWidth)(p); + public static Func InvertedCross(WPos origin, Angle direction, float length, float halfWidth) + { + var dir = direction.ToDirection(); + var normal = dir.OrthoL(); + return p => + { + var offset = p - origin; + var distParr = offset.Dot(dir); + var distOrtho = offset.Dot(normal); + var distPFront = distParr - length; + var distPBack = -distParr - length; + var distPLeft = distOrtho - halfWidth; + var distPRight = -distOrtho - halfWidth; + var distOFront = distOrtho - length; + var distOBack = -distOrtho - length; + var distOLeft = distParr - halfWidth; + var distORight = -distParr - halfWidth; + + var distPMax1 = distPFront > distPBack ? distPFront : distPBack; + var distPMax2 = distPLeft > distPRight ? distPLeft : distPRight; + var distP = distPMax1 > distPMax2 ? distPMax1 : distPMax2; + + var distOMax1 = distOFront > distOBack ? distOFront : distOBack; + var distOMax2 = distOLeft > distORight ? distOLeft : distORight; + var distO = distOMax1 > distOMax2 ? distOMax1 : distOMax2; + + return distP < distO ? -distP : -distO; + }; + } // positive offset increases area public static Func ConvexPolygon(List<(WPos, WPos)> edges, bool cw, float offset = 0)