diff --git a/BossMod/BossModule/ArenaBounds.cs b/BossMod/BossModule/ArenaBounds.cs index ff9ff7cc5d..7c417ae5aa 100644 --- a/BossMod/BossModule/ArenaBounds.cs +++ b/BossMod/BossModule/ArenaBounds.cs @@ -14,33 +14,23 @@ public abstract class ArenaBounds(WPos center, float halfSize) private readonly Clip2D _clipper = new(); public IEnumerable ClipPoly => _clipper.ClipPoly; - private readonly Dictionary> _polyCache = []; - private readonly Dictionary, List<(WPos, WPos, WPos)>> _polyCache2 = []; - private readonly Dictionary<(WPos, float), List<(WPos, WPos, WPos)>> _triangulateCircle = []; - private readonly Dictionary<(WPos, float, float, Angle, Angle), List<(WPos, WPos, WPos)>> _triangulateCone = []; - private readonly Dictionary<(WPos, float, float), List<(WPos, WPos, WPos)>> _triangulateDonut = []; - private readonly Dictionary<(WPos, WPos, WPos), List<(WPos, WPos, WPos)>> _triangulateTri = []; - private readonly Dictionary<(WPos, WDir, WDir), List<(WPos, WPos, WPos)>> _triangulateTri2 = []; - private readonly Dictionary<(WPos, Angle, Angle, float), List<(WPos, WPos, WPos)>> _triangulateTri3 = []; - private readonly Dictionary<(WPos, WDir, float, float, float), List<(WPos, WPos, WPos)>> _triangulateRect = []; - private readonly Dictionary<(WPos, Angle, float, float, float), List<(WPos, WPos, WPos)>> _triangulateRect2 = []; - private readonly Dictionary<(WPos, WPos, float), List<(WPos, WPos, WPos)>> _triangulateRect3 = []; + private readonly Dictionary> cache = []; public List<(WPos, WPos, WPos)> ClipAndTriangulate(ClipperLib.PolyTree poly) { - if (_polyCache.TryGetValue(poly, out var cachedResult)) + if (cache.TryGetValue(poly, out var cachedResult)) return cachedResult; var result = _clipper.ClipAndTriangulate(poly); - _polyCache[poly] = result; + cache[poly] = result; return result; - } + } public List<(WPos, WPos, WPos)> ClipAndTriangulate(IEnumerable poly) { - if (_polyCache2.TryGetValue(poly, out var cachedResult)) + if (cache.TryGetValue(poly, out var cachedResult)) return cachedResult; var result = _clipper.ClipAndTriangulate(poly); - _polyCache2[poly] = result; - return result; + cache[poly] = result; + return result; } private float _screenHalfSize; @@ -73,7 +63,7 @@ public float ScreenHalfSize // TODO: think of a better way to do that (analytical clipping?) if (innerRadius >= outerRadius || innerRadius < 0 || halfAngle.Rad <= 0) return []; - if (_triangulateCone.TryGetValue((center, innerRadius, outerRadius, centerDirection, halfAngle), out var cachedResult)) + if (cache.TryGetValue((center, innerRadius, outerRadius, centerDirection, halfAngle), out var cachedResult)) return cachedResult; bool fullCircle = halfAngle.Rad >= MathF.PI; bool donut = innerRadius > 0; @@ -85,16 +75,16 @@ public float ScreenHalfSize (true, true) => CurveApprox.Donut(center, innerRadius, outerRadius, MaxApproxError), }; var result = ClipAndTriangulate(points); - _triangulateCone[(center, innerRadius, outerRadius, centerDirection, halfAngle)] = result; + cache[(center, innerRadius, outerRadius, centerDirection, halfAngle)] = result; return result; } public List<(WPos, WPos, WPos)> ClipAndTriangulateCircle(WPos center, float radius) { - if (_triangulateCircle.TryGetValue((center, radius), out var cachedResult)) + if (cache.TryGetValue((center, radius), out var cachedResult)) return cachedResult; var result = ClipAndTriangulate(CurveApprox.Circle(center, radius, MaxApproxError)); - _triangulateCircle[(center, radius)] = result; + cache[(center, radius)] = result; return result; } @@ -102,71 +92,71 @@ public float ScreenHalfSize { if (innerRadius >= outerRadius || innerRadius < 0) return []; - if (_triangulateDonut.TryGetValue((center, innerRadius, outerRadius), out var cachedResult)) + if (cache.TryGetValue((center, innerRadius, outerRadius), out var cachedResult)) return cachedResult; var result = ClipAndTriangulate(CurveApprox.Donut(center, innerRadius, outerRadius, MaxApproxError)); - _triangulateDonut[(center, innerRadius, outerRadius)] = result; + cache[(center, innerRadius, outerRadius)] = result; return result; } public List<(WPos, WPos, WPos)> ClipAndTriangulateTri(WPos a, WPos b, WPos c) { - if (_triangulateTri.TryGetValue((a, b, c), out var cachedResult)) + if (cache.TryGetValue((a, b, c), out var cachedResult)) return cachedResult; var result = ClipAndTriangulate([a, b, c]); - _triangulateTri[(a, b, c)] = result; + cache[(a, b, c)] = result; return result; } public List<(WPos, WPos, WPos)> ClipAndTriangulateIsoscelesTri(WPos apex, WDir height, WDir halfBase) { - if (_triangulateTri2.TryGetValue((apex, height, halfBase), out var cachedResult)) + if (cache.TryGetValue((apex, height, halfBase), out var cachedResult)) return cachedResult; var result = ClipAndTriangulateTri(apex, apex + height + halfBase, apex + height - halfBase); - _triangulateTri2[(apex, height, halfBase)] = result; + cache[(apex, height, halfBase)] = result; return result; } public List<(WPos, WPos, WPos)> ClipAndTriangulateIsoscelesTri(WPos apex, Angle direction, Angle halfAngle, float height) { - if (_triangulateTri3.TryGetValue((apex, direction, halfAngle, height), out var cachedResult)) + if (cache.TryGetValue((apex, direction, halfAngle, height), out var cachedResult)) return cachedResult; var dir = direction.ToDirection(); var normal = dir.OrthoL(); var result = ClipAndTriangulateIsoscelesTri(apex, height * dir, height * halfAngle.Tan() * normal); - _triangulateTri3[(apex, direction, halfAngle, height)] = result; + cache[(apex, direction, halfAngle, height)] = result; return result; } public List<(WPos, WPos, WPos)> ClipAndTriangulateRect(WPos origin, WDir direction, float lenFront, float lenBack, float halfWidth) { - if (_triangulateRect.TryGetValue((origin, direction, lenFront, lenBack, halfWidth), out var cachedResult)) + if (cache.TryGetValue((origin, direction, lenFront, lenBack, halfWidth), out var cachedResult)) return cachedResult; var side = halfWidth * direction.OrthoR(); var front = origin + lenFront * direction; var back = origin - lenBack * direction; var result = ClipAndTriangulate([front + side, front - side, back - side, back + side]); - _triangulateRect[(origin, direction, lenFront, lenBack, halfWidth)] = result; + cache[(origin, direction, lenFront, lenBack, halfWidth)] = result; return result; } public List<(WPos, WPos, WPos)> ClipAndTriangulateRect(WPos origin, Angle direction, float lenFront, float lenBack, float halfWidth) { - if (_triangulateRect2.TryGetValue((origin, direction, lenFront, lenBack, halfWidth), out var cachedResult)) + if (cache.TryGetValue((origin, direction, lenFront, lenBack, halfWidth), out var cachedResult)) return cachedResult; var result = ClipAndTriangulateRect(origin, direction.ToDirection(), lenFront, lenBack, halfWidth); - _triangulateRect2[(origin, direction, lenFront, lenBack, halfWidth)] = result; + cache[(origin, direction, lenFront, lenBack, halfWidth)] = result; return result; } public List<(WPos, WPos, WPos)> ClipAndTriangulateRect(WPos start, WPos end, float halfWidth) { - if (_triangulateRect3.TryGetValue((start, end, halfWidth), out var cachedResult)) + if (cache.TryGetValue((start, end, halfWidth), out var cachedResult)) return cachedResult; var dir = (end - start).Normalized(); var side = halfWidth * dir.OrthoR(); var result = ClipAndTriangulate([start + side, start - side, end - side, end + side]); - _triangulateRect3[(start, end, halfWidth)] = result; + cache[(start, end, halfWidth)] = result; return result; } } @@ -258,12 +248,23 @@ public override WDir ClampToBounds(WDir offset, float scale) public class ArenaBoundsPolygon : ArenaBounds { private (WPos Center, float HalfHeight, float HalfWidth)? _cachedPolygonProperties; - private readonly Dictionary _containsCache = []; - private readonly Dictionary<(WPos, WDir), float> _intersectRayCache = []; - private readonly Dictionary, Pathfinding.Map> _buildMapCache = []; - private readonly Dictionary<(WPos, WDir), WDir> _clampToBoundsCache = []; + private static readonly Dictionary staticCache = []; + private readonly Dictionary cache = []; - public readonly IEnumerable Points; + public IEnumerable Points; + public IEnumerable Points2 + { + get => Points; + set + { + if (!ReferenceEquals(Points, value)) + { + Points = value; + cache.Clear(); + _cachedPolygonProperties = null; + } + } + } public ArenaBoundsPolygon(IEnumerable points) : base(default, default) { @@ -308,28 +309,28 @@ private static (WPos center, float HalfHeight, float Halfwidth) CalculatePolygon public override bool Contains(WPos position) { - if (_containsCache.TryGetValue(position, out var cachedResult)) - return cachedResult; + if (cache.TryGetValue(position, out var cachedResult)) + return (bool)cachedResult; var result = position.InPolygon(Points); - _containsCache[position] = result; + cache[position] = result; return result; } public override Pathfinding.Map BuildMap(float resolution) { - if (_buildMapCache.TryGetValue(Points, out var cachedResult)) - return cachedResult; + if (staticCache.TryGetValue(Points, out var cachedResult)) + return (Pathfinding.Map)cachedResult; var unionProperties = CalculatePolygonProperties(); var map = new Pathfinding.Map(resolution, unionProperties.Center, unionProperties.HalfWidth, unionProperties.HalfHeight); map.BlockPixelsInside(ShapeDistance.InvertedPolygon(Points), 0, 0); - _buildMapCache[Points] = map; + staticCache[Points] = map; return map; } public override float IntersectRay(WPos origin, WDir dir) { - if (_intersectRayCache.TryGetValue((origin, dir), out var cachedResult)) - return cachedResult; + if (cache.TryGetValue((origin, dir), out var cachedResult)) + return (float)cachedResult; float minDistance = float.MaxValue; int i = 0; foreach (var point in Points) @@ -342,15 +343,17 @@ public override float IntersectRay(WPos origin, WDir dir) minDistance = distance; ++i; } - _intersectRayCache[(origin, dir)] = minDistance; + cache[(origin, dir)] = minDistance; return minDistance; } public override WDir ClampToBounds(WDir offset, float scale) { WPos position = Center + offset; - if (_clampToBoundsCache.TryGetValue((position, offset), out var cachedResult)) - return cachedResult; + if (Contains(position)) + return offset; + if (cache.TryGetValue((position, offset), out var cachedResult)) + return (WDir)cachedResult; float minDistance = float.MaxValue; WPos closestPoint = default; @@ -371,7 +374,7 @@ public override WDir ClampToBounds(WDir offset, float scale) ++i; } var result = closestPoint - Center; - _clampToBoundsCache[(position, offset)] = result; + cache[(position, offset)] = result; return result; } } @@ -398,11 +401,21 @@ public override bool Contains(WPos position) public override float IntersectRay(WPos origin, WDir dir) { - float outerIntersection = Intersect.RayCircle(origin, dir, Center, OuterRadius); - float innerIntersection = Intersect.RayCircle(origin, dir, Center, InnerRadius); - float minOuter = outerIntersection >= 0 ? outerIntersection : float.MaxValue; - float minInner = innerIntersection >= 0 ? innerIntersection : float.MaxValue; - return Math.Min(minOuter, minInner); + dir = dir.Normalized(); + + float intersectOuter = Intersect.RayCircle(origin, dir, Center, OuterRadius); + float intersectInner = Intersect.RayCircle(origin, dir, Center, InnerRadius); + + bool isValidOuter = intersectOuter >= 0; + bool isValidInner = intersectInner >= 0; + + if (isValidOuter && isValidInner) + return Math.Min(intersectOuter, intersectInner); + else if (isValidOuter) + return intersectOuter; + else if (isValidInner) + return intersectInner; + return -1; } public override WDir ClampToBounds(WDir offset, float scale = 1) @@ -433,9 +446,7 @@ public List BoundsList if (!ReferenceEquals(_boundsList, value)) //clear caches for clamptobounds, intersectray and contains if union composition gets modified, BuildClipPolygon and BuildMap stays incase map gets reverted later { _boundsList = value; - _containsCache.Clear(); - _intersectRayCache.Clear(); - _clampToBoundsCache.Clear(); + cache.Clear(); _cachedUnionProperties = null; } } @@ -452,11 +463,8 @@ public ArenaBoundsUnion(IEnumerable bounds) : base(default, default } private (WPos Center, float HalfHeight, float HalfWidth)? _cachedUnionProperties; - private readonly Dictionary, IEnumerable> _polyCache = []; - private readonly Dictionary _containsCache = []; - private readonly Dictionary<(WPos, WDir), float> _intersectRayCache = []; - private readonly Dictionary, Pathfinding.Map> _buildMapCache = []; - private readonly Dictionary<(WPos, WDir), WDir> _clampToBoundsCache = []; + private static readonly Dictionary staticCache = []; + private readonly Dictionary cache = []; private (WPos Center, float HalfHeight, float HalfWidth) CalculateUnionProperties() { @@ -496,8 +504,8 @@ private static (WPos Center, float HalfHeight, float HalfWidth) CalculateUnionPr public override IEnumerable BuildClipPoly(float offset = 0) { - if (_polyCache.TryGetValue(_boundsList, out var cachedResult)) - return cachedResult; + if (staticCache.TryGetValue(_boundsList, out var cachedResult)) + return (IEnumerable)cachedResult; var result = new List(); var clipper = new ClipperLib.Clipper(0); @@ -526,14 +534,14 @@ public override IEnumerable BuildClipPoly(float offset = 0) result.Add(polyResult.First()); } - _polyCache[_boundsList] = result; + staticCache[_boundsList] = result; return result; } public override Pathfinding.Map BuildMap(float resolution) { - if (_buildMapCache.TryGetValue(_boundsList, out var cachedResult)) - return cachedResult; + if (staticCache.TryGetValue(_boundsList, out var cachedResult)) + return (Pathfinding.Map)cachedResult; var unionProperties = CalculateUnionProperties(); var map = new Pathfinding.Map(resolution, unionProperties.Center, unionProperties.HalfWidth, unionProperties.HalfHeight); @@ -550,23 +558,23 @@ public override Pathfinding.Map BuildMap(float resolution) } map.Pixels[y * map.Width + x].MaxG = isInside ? float.MaxValue : 0; } - _buildMapCache[_boundsList] = map; + staticCache[_boundsList] = map; return map; } public override bool Contains(WPos position) { - if (_containsCache.TryGetValue(position, out var cachedResult)) - return cachedResult; + if (cache.TryGetValue(position, out var cachedResult)) + return (bool)cachedResult; var result = _boundsList.Any(bound => bound.Contains(position)); - _containsCache[position] = result; + cache[position] = result; return result; } public override float IntersectRay(WPos origin, WDir dir) { - if (_intersectRayCache.TryGetValue((origin, dir), out var cachedResult)) - return cachedResult; + if (cache.TryGetValue((origin, dir), out var cachedResult)) + return (float)cachedResult; float nearestIntersection = float.MaxValue; @@ -598,15 +606,17 @@ public override float IntersectRay(WPos origin, WDir dir) } } - _intersectRayCache[(origin, dir)] = nearestIntersection; + cache[(origin, dir)] = nearestIntersection; return nearestIntersection; } public override WDir ClampToBounds(WDir offset, float scale = 1) { WPos position = Center + offset; - if (_clampToBoundsCache.TryGetValue((position, offset), out var cachedResult)) - return cachedResult; + if (Contains(position)) + return offset; + if (cache.TryGetValue((position, offset), out var cachedResult)) + return (WDir)cachedResult; var clipper = new ClipperLib.Clipper(0); foreach (var bound in _boundsList) @@ -620,34 +630,30 @@ public override WDir ClampToBounds(WDir offset, float scale = 1) var solution = new ClipperLib.PolyTree(); clipper.Execute(ClipperLib.ClipType.ctUnion, solution, ClipperLib.PolyFillType.pftNonZero, ClipperLib.PolyFillType.pftNonZero); - if (!Contains(position)) - { - float closestDist = float.MaxValue; - WPos closestPoint = default; - var polygons = ClipperLib.Clipper.ClosedPathsFromPolyTree(solution); - foreach (var poly in polygons) + float closestDist = float.MaxValue; + WPos closestPoint = default; + + var polygons = ClipperLib.Clipper.ClosedPathsFromPolyTree(solution); + foreach (var poly in polygons) + { + for (int i = 0; i < poly.Count; i++) { - for (int i = 0; i < poly.Count; i++) - { - WPos p1 = new(poly[i].X / ScalingFactor, poly[i].Y / ScalingFactor); - WPos p2 = new(poly[(i + 1) % poly.Count].X / ScalingFactor, poly[(i + 1) % poly.Count].Y / ScalingFactor); + WPos p1 = new(poly[i].X / ScalingFactor, poly[i].Y / ScalingFactor); + WPos p2 = new(poly[(i + 1) % poly.Count].X / ScalingFactor, poly[(i + 1) % poly.Count].Y / ScalingFactor); - WPos currentClosest = Intersect.ClosestPointOnSegment(p1, p2, position); - float currentDist = (currentClosest - position).Length(); + WPos currentClosest = Intersect.ClosestPointOnSegment(p1, p2, position); + float currentDist = (currentClosest - position).Length(); - if (currentDist < closestDist) - { - closestDist = currentDist; - closestPoint = currentClosest; - } + if (currentDist < closestDist) + { + closestDist = currentDist; + closestPoint = currentClosest; } } - _clampToBoundsCache[(position, offset)] = closestPoint - Center; - return closestPoint - Center; } - _clampToBoundsCache[(position, offset)] = offset; - return offset; + cache[(position, offset)] = closestPoint - Center; + return closestPoint - Center; } } @@ -662,12 +668,10 @@ public ArenaBounds BaseBound get => _baseBound; set { - if (!ReferenceEquals(_baseBound, value)) //clear caches for clamptobounds, intersectray and contains if base bound gets modified, BuildClipPolygon and BuildMap stays incase map gets reverted later + if (!ReferenceEquals(_baseBound, value)) { _baseBound = value; - _containsCache.Clear(); - _intersectRayCache.Clear(); - _clampToBoundsCache.Clear(); + cache.Clear(); _cachedDifferenceProperties = null; } } @@ -677,22 +681,17 @@ public List BoundsList get => _subtractBounds; set { - if (!ReferenceEquals(_subtractBounds, value)) //clear caches for clamptobounds, intersectray and contains if subtracted bounds gets modified, BuildClipPolygon and BuildMap stays incase map gets reverted later + if (!ReferenceEquals(_subtractBounds, value)) { _subtractBounds = value; - _containsCache.Clear(); - _intersectRayCache.Clear(); - _clampToBoundsCache.Clear(); + cache.Clear(); _cachedDifferenceProperties = null; } } } private (WPos Center, float HalfHeight, float HalfWidth)? _cachedDifferenceProperties; - private readonly Dictionary<(ArenaBounds, List), IEnumerable> _polyCache = []; - private readonly Dictionary _containsCache = []; - private readonly Dictionary<(WPos, WDir), float> _intersectRayCache = []; - private readonly Dictionary<(ArenaBounds, List), Pathfinding.Map> _buildMapCache = []; - private readonly Dictionary<(WPos, WDir), WDir> _clampToBoundsCache = []; + private static readonly Dictionary staticCache = []; + private readonly Dictionary cache = []; public ArenaBoundsDifference(ArenaBounds baseBound, IEnumerable subtractBounds) : base(default, default) { @@ -739,8 +738,8 @@ private static (WPos Center, float HalfHeight, float HalfWidth) CalculateDiffere public override IEnumerable BuildClipPoly(float offset = 0) { - if (_polyCache.TryGetValue((BaseBound, _subtractBounds), out var cachedResult)) - return cachedResult; + if (staticCache.TryGetValue((BaseBound, _subtractBounds), out var cachedResult)) + return (IEnumerable)cachedResult; var clipperBase = new ClipperLib.Clipper(); var basePoly = _baseBound.BuildClipPoly(offset).Select(p => new ClipperLib.IntPoint(p.X * ScalingFactor, p.Z * ScalingFactor)).ToList(); @@ -767,20 +766,20 @@ public override IEnumerable BuildClipPoly(float offset = 0) foreach (var polygon in resultPolygons) { var polyResult = polygon.Select(pt => new WPos(pt.X / ScalingFactor, pt.Y / ScalingFactor)).ToList(); - if (polyResult.Any()) + if (polyResult.Count != 0) { polyResult.Add(polyResult.First()); finalResult.AddRange(polyResult); } } - _polyCache[(BaseBound, _subtractBounds)] = finalResult; + staticCache[(BaseBound, _subtractBounds)] = finalResult; return finalResult; } public override Pathfinding.Map BuildMap(float resolution = 0.5f) { - if (_buildMapCache.TryGetValue((BaseBound, _subtractBounds), out var cachedResult)) - return cachedResult; + if (staticCache.TryGetValue((BaseBound, _subtractBounds), out var cachedResult)) + return (Pathfinding.Map)cachedResult; var props = CalculateDifferenceProperties(); var map = new Pathfinding.Map(resolution, props.Center, props.HalfWidth, props.HalfHeight); @@ -791,25 +790,23 @@ public override Pathfinding.Map BuildMap(float resolution = 0.5f) bool insideSubtract = _subtractBounds.Any(sub => sub.Contains(pos)); map.Pixels[y * map.Width + x].MaxG = insideBase && !insideSubtract ? float.MaxValue : 0; } - _buildMapCache[(BaseBound, _subtractBounds)] = map; + staticCache[(BaseBound, _subtractBounds)] = map; return map; } public override bool Contains(WPos position) { - if (_containsCache.TryGetValue(position, out var cachedResult)) - return cachedResult; + if (cache.TryGetValue(position, out var cachedResult)) + return (bool)cachedResult; var result = _baseBound.Contains(position) && !_subtractBounds.Any(bound => bound.Contains(position)); - _containsCache[position] = result; + cache[position] = result; return result; } public override float IntersectRay(WPos origin, WDir dir) { - if (_intersectRayCache.TryGetValue((origin, dir), out var cachedResult)) - return cachedResult; - - var props = CalculateDifferenceProperties(); + if (cache.TryGetValue((origin, dir), out var cachedResult)) + return (float)cachedResult; var clipperBase = new ClipperLib.Clipper(); var basePoly = _baseBound.BuildClipPoly().Select(p => new ClipperLib.IntPoint(p.X * ScalingFactor, p.Z * ScalingFactor)).ToList(); @@ -847,15 +844,17 @@ public override float IntersectRay(WPos origin, WDir dir) nearestIntersection = distance; } } - _intersectRayCache[(origin, dir)] = nearestIntersection; + cache[(origin, dir)] = nearestIntersection; return nearestIntersection; } public override WDir ClampToBounds(WDir offset, float scale = 1) { WPos position = Center + offset; - if (_clampToBoundsCache.TryGetValue((position, offset), out var cachedResult)) - return cachedResult; + if (Contains(position)) + return offset; + if (cache.TryGetValue((position, offset), out var cachedResult)) + return (WDir)cachedResult; var clipperBase = new ClipperLib.Clipper(); var basePoly = _baseBound.BuildClipPoly().Select(p => new ClipperLib.IntPoint(p.X * ScalingFactor, p.Z * ScalingFactor)).ToList(); clipperBase.AddPath(basePoly, ClipperLib.PolyType.ptSubject, true); @@ -897,7 +896,7 @@ public override WDir ClampToBounds(WDir offset, float scale = 1) } } var result = closestPoint - Center; - _clampToBoundsCache[(position, offset)] = result; + cache[(position, offset)] = result; return result; } } diff --git a/BossMod/Modules/Endwalker/Alliance/A31Thaliak/ThaliakHieroglyphika.cs b/BossMod/Modules/Endwalker/Alliance/A31Thaliak/ThaliakHieroglyphika.cs index 7e454f0514..49b8971452 100644 --- a/BossMod/Modules/Endwalker/Alliance/A31Thaliak/ThaliakHieroglyphika.cs +++ b/BossMod/Modules/Endwalker/Alliance/A31Thaliak/ThaliakHieroglyphika.cs @@ -7,7 +7,7 @@ class Hieroglyphika(BossModule module) : Components.GenericAOEs(module) private static readonly WPos[] StartingCoords = [new(-963, 939), new(-963, 963), new(-939, 927), new(-951, 939), new(-951, 927), new(-939, 963), new(-927, 939), new(-939, 951), new(-939, 939), new(-927, 963), new(-963, 951), new(-927, 951), new(-951, 951), new(-963, 927)]; private byte currentIndex; - public override IEnumerable ActiveAOEs(int slot, Actor actor) => _aoes.Take(14); + public override IEnumerable ActiveAOEs(int slot, Actor actor) => _aoes; public override void OnEventEnvControl(byte index, uint state) { diff --git a/BossMod/Modules/Endwalker/Alliance/A34Eulogia/EulogiaHieroglyphika.cs b/BossMod/Modules/Endwalker/Alliance/A34Eulogia/EulogiaHieroglyphika.cs index fce741521c..66dcd8bd78 100644 --- a/BossMod/Modules/Endwalker/Alliance/A34Eulogia/EulogiaHieroglyphika.cs +++ b/BossMod/Modules/Endwalker/Alliance/A34Eulogia/EulogiaHieroglyphika.cs @@ -6,7 +6,7 @@ class Hieroglyphika(BossModule module) : Components.GenericAOEs(module) private readonly List _aoes = []; private static readonly WPos[] StartingCoords = [new(951, -933), new(939, -933), new(951, -957), new(939, -957), new(927, -933), new(963, -957), new(963, -933), new(951, -945), new(939, -945), new(927, -921), new(939, -921), new(927, -945), new(963, -945), new(963, -921)]; - public override IEnumerable ActiveAOEs(int slot, Actor actor) => _aoes.Take(14); + public override IEnumerable ActiveAOEs(int slot, Actor actor) => _aoes; public override void OnEventIcon(Actor actor, uint iconID) { diff --git a/BossMod/ThirdParty/Clipper2Lib/Clipper.Core.cs b/BossMod/ThirdParty/Clipper2Lib/Clipper.Core.cs new file mode 100644 index 0000000000..6e35fb8a21 --- /dev/null +++ b/BossMod/ThirdParty/Clipper2Lib/Clipper.Core.cs @@ -0,0 +1,784 @@ +/******************************************************************************* +* Author : Angus Johnson * +* Date : 1 October 2023 * +* Website : http://www.angusj.com * +* Copyright : Angus Johnson 2010-2023 * +* Purpose : Core structures and functions for the Clipper Library * +* License : http://www.boost.org/LICENSE_1_0.txt * +*******************************************************************************/ + +#nullable enable +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace Clipper2Lib +{ + public struct Point64 + { + public long X; + public long Y; + +#if USINGZ + public long Z; + + public Point64(Point64 pt) + { + X = pt.X; + Y = pt.Y; + Z = pt.Z; + } + + public Point64(Point64 pt, double scale) + { + X = (long) Math.Round(pt.X * scale, MidpointRounding.AwayFromZero); + Y = (long) Math.Round(pt.Y * scale, MidpointRounding.AwayFromZero); + Z = (long) Math.Round(pt.Z * scale, MidpointRounding.AwayFromZero); + } + + public Point64(long x, long y, long z = 0) + { + X = x; + Y = y; + Z = z; + } + + public Point64(double x, double y, double z = 0.0) + { + X = (long) Math.Round(x, MidpointRounding.AwayFromZero); + Y = (long) Math.Round(y, MidpointRounding.AwayFromZero); + Z = (long) Math.Round(z, MidpointRounding.AwayFromZero); + } + + public Point64(PointD pt) + { + X = (long) Math.Round(pt.x, MidpointRounding.AwayFromZero); + Y = (long) Math.Round(pt.y, MidpointRounding.AwayFromZero); + Z = pt.z; + } + + public Point64(PointD pt, double scale) + { + X = (long) Math.Round(pt.x * scale, MidpointRounding.AwayFromZero); + Y = (long) Math.Round(pt.y * scale, MidpointRounding.AwayFromZero); + Z = pt.z; + } + + public static bool operator ==(Point64 lhs, Point64 rhs) + { + return lhs.X == rhs.X && lhs.Y == rhs.Y; + } + + public static bool operator !=(Point64 lhs, Point64 rhs) + { + return lhs.X != rhs.X || lhs.Y != rhs.Y; + } + + public static Point64 operator +(Point64 lhs, Point64 rhs) + { + return new Point64(lhs.X + rhs.X, lhs.Y + rhs.Y, lhs.Z + rhs.Z); + } + + public static Point64 operator -(Point64 lhs, Point64 rhs) + { + return new Point64(lhs.X - rhs.X, lhs.Y - rhs.Y, lhs.Z - rhs.Z); + } + + public override string ToString() + { + return $"{X},{Y},{Z} "; // nb: trailing space + } + +#else + public Point64(Point64 pt) + { + X = pt.X; + Y = pt.Y; + } + + public Point64(long x, long y) + { + X = x; + Y = y; + } + + public Point64(double x, double y) + { + X = (long) Math.Round(x, MidpointRounding.AwayFromZero); + Y = (long) Math.Round(y, MidpointRounding.AwayFromZero); + } + + public Point64(PointD pt) + { + X = (long) Math.Round(pt.x, MidpointRounding.AwayFromZero); + Y = (long) Math.Round(pt.y, MidpointRounding.AwayFromZero); + } + + public Point64(Point64 pt, double scale) + { + X = (long) Math.Round(pt.X * scale, MidpointRounding.AwayFromZero); + Y = (long) Math.Round(pt.Y * scale, MidpointRounding.AwayFromZero); + } + + public Point64(PointD pt, double scale) + { + X = (long) Math.Round(pt.x * scale, MidpointRounding.AwayFromZero); + Y = (long) Math.Round(pt.y * scale, MidpointRounding.AwayFromZero); + } + + public static bool operator ==(Point64 lhs, Point64 rhs) + { + return lhs.X == rhs.X && lhs.Y == rhs.Y; + } + + public static bool operator !=(Point64 lhs, Point64 rhs) + { + return lhs.X != rhs.X || lhs.Y != rhs.Y; + } + + public static Point64 operator +(Point64 lhs, Point64 rhs) + { + return new Point64(lhs.X + rhs.X, lhs.Y + rhs.Y); + } + + public static Point64 operator -(Point64 lhs, Point64 rhs) + { + return new Point64(lhs.X - rhs.X, lhs.Y - rhs.Y); + } + public readonly override string ToString() + { + return $"{X},{Y} "; // nb: trailing space + } + +#endif + public readonly override bool Equals(object? obj) + { + if (obj != null && obj is Point64 p) + return this == p; + return false; + } + + public readonly override int GetHashCode() + { + return HashCode.Combine(X, Y); //#599 + } + + } + + public struct PointD + { + public double x; + public double y; + +#if USINGZ + public long z; + + public PointD(PointD pt) + { + x = pt.x; + y = pt.y; + z = pt.z; + } + + public PointD(Point64 pt) + { + x = pt.X; + y = pt.Y; + z = pt.Z; + } + + public PointD(Point64 pt, double scale) + { + x = pt.X * scale; + y = pt.Y * scale; + z = pt.Z; + } + + public PointD(PointD pt, double scale) + { + x = pt.x * scale; + y = pt.y * scale; + z = pt.z; + } + + public PointD(long x, long y, long z = 0) + { + this.x = x; + this.y = y; + this.z = z; + } + + public PointD(double x, double y, long z = 0) + { + this.x = x; + this.y = y; + this.z = z; + } + + public string ToString(int precision = 2) + { + return string.Format($"{{0:F{precision}}},{{1:F{precision}}},{{2:D}}", x,y,z); + } + +#else + public PointD(PointD pt) + { + x = pt.x; + y = pt.y; + } + + public PointD(Point64 pt) + { + x = pt.X; + y = pt.Y; + } + + public PointD(PointD pt, double scale) + { + x = pt.x * scale; + y = pt.y * scale; + } + + public PointD(Point64 pt, double scale) + { + x = pt.X * scale; + y = pt.Y * scale; + } + + public PointD(long x, long y) + { + this.x = x; + this.y = y; + } + + public PointD(double x, double y) + { + this.x = x; + this.y = y; + } + + public readonly string ToString(int precision = 2) + { + return string.Format($"{{0:F{precision}}},{{1:F{precision}}}", x,y); + } + +#endif + public static bool operator ==(PointD lhs, PointD rhs) + { + return InternalClipper.IsAlmostZero(lhs.x - rhs.x) && + InternalClipper.IsAlmostZero(lhs.y - rhs.y); + } + + public static bool operator !=(PointD lhs, PointD rhs) + { + return !InternalClipper.IsAlmostZero(lhs.x - rhs.x) || + !InternalClipper.IsAlmostZero(lhs.y - rhs.y); + } + + public readonly override bool Equals(object? obj) + { + if (obj != null && obj is PointD p) + return this == p; + return false; + } + + public void Negate() { x = -x; y = -y; } + + public readonly override int GetHashCode() + { + return HashCode.Combine(x, y); //#599 + } + + } + + public struct Rect64 + { + public long left; + public long top; + public long right; + public long bottom; + + public Rect64(long l, long t, long r, long b) + { + left = l; + top = t; + right = r; + bottom = b; + } + + public Rect64(bool isValid) + { + if (isValid) + { + left = 0; top = 0; right = 0; bottom = 0; + } + else + { + left = long.MaxValue; top = long.MaxValue; + right = long.MinValue; bottom = long.MinValue; + } + } + + public Rect64(Rect64 rec) + { + left = rec.left; + top = rec.top; + right = rec.right; + bottom = rec.bottom; + } + + public long Width + { readonly get => right - left; + set => right = left + value; + } + + public long Height + { readonly get => bottom - top; + set => bottom = top + value; + } + + public readonly bool IsEmpty() + { + return bottom <= top || right <= left; + } + + public readonly bool IsValid() + { + return left < long.MaxValue; + } + + public readonly Point64 MidPoint() + { + return new Point64((left + right) /2, (top + bottom)/2); + } + + public readonly bool Contains(Point64 pt) + { + return pt.X > left && pt.X < right && + pt.Y > top && pt.Y < bottom; + } + + public readonly bool Contains(Rect64 rec) + { + return rec.left >= left && rec.right <= right && + rec.top >= top && rec.bottom <= bottom; + } + + public readonly bool Intersects(Rect64 rec) + { + return (Math.Max(left, rec.left) <= Math.Min(right, rec.right)) && + (Math.Max(top, rec.top) <= Math.Min(bottom, rec.bottom)); + } + + public readonly Path64 AsPath() + { + Path64 result = new Path64(4) + { + new Point64(left, top), + new Point64(right, top), + new Point64(right, bottom), + new Point64(left, bottom) + }; + return result; + } + + } + + public struct RectD + { + public double left; + public double top; + public double right; + public double bottom; + + public RectD(double l, double t, double r, double b) + { + left = l; + top = t; + right = r; + bottom = b; + } + + public RectD(RectD rec) + { + left = rec.left; + top = rec.top; + right = rec.right; + bottom = rec.bottom; + } + + public RectD(bool isValid) + { + if (isValid) + { + left = 0; top = 0; right = 0; bottom = 0; + } + else + { + left = double.MaxValue; top = double.MaxValue; + right = -double.MaxValue; bottom = -double.MaxValue; + } + } + public double Width + { readonly get => right - left; + set => right = left + value; + } + + public double Height + { readonly get => bottom - top; + set => bottom = top + value; + } + + public readonly bool IsEmpty() + { + return bottom <= top || right <= left; + } + + public readonly PointD MidPoint() + { + return new PointD((left + right) / 2, (top + bottom) / 2); + } + + public readonly bool Contains(PointD pt) + { + return pt.x > left && pt.x < right && + pt.y > top && pt.y < bottom; + } + + public readonly bool Contains(RectD rec) + { + return rec.left >= left && rec.right <= right && + rec.top >= top && rec.bottom <= bottom; + } + + public readonly bool Intersects(RectD rec) + { + return (Math.Max(left, rec.left) < Math.Min(right, rec.right)) && + (Math.Max(top, rec.top) < Math.Min(bottom, rec.bottom)); + } + + public readonly PathD AsPath() + { + PathD result = new PathD(4) + { + new PointD(left, top), + new PointD(right, top), + new PointD(right, bottom), + new PointD(left, bottom) + }; + return result; + } + + } + + public class Path64 : List + { + private Path64() : base() { } + public Path64(int capacity = 0) : base(capacity) { } + public Path64(IEnumerable path) : base(path) { } + public override string ToString() + { + string s = ""; + foreach (Point64 p in this) + s = s + p.ToString() + " "; + return s; + } + } + + public class Paths64 : List + { + private Paths64() : base() { } + public Paths64(int capacity = 0) : base(capacity) { } + public Paths64(IEnumerable paths) : base(paths) { } + public override string ToString() + { + string s = ""; + foreach (Path64 p in this) + s = s + p.ToString() + "\n"; + return s; + } + } + + public class PathD : List + { + private PathD() : base() { } + public PathD(int capacity = 0) : base(capacity) { } + public PathD(IEnumerable path) : base(path) { } + public string ToString(int precision = 2) + { + string s = ""; + foreach (PointD p in this) + s = s + p.ToString(precision) + " "; + return s; + } + } + + public class PathsD : List + { + private PathsD() : base() { } + public PathsD(int capacity = 0) : base(capacity) { } + public PathsD(IEnumerable paths) : base(paths) { } + public string ToString(int precision = 2) + { + string s = ""; + foreach (PathD p in this) + s = s + p.ToString(precision) + "\n"; + return s; + } + } + + // Note: all clipping operations except for Difference are commutative. + public enum ClipType + { + None, + Intersection, + Union, + Difference, + Xor + }; + + public enum PathType + { + Subject, + Clip + }; + + // By far the most widely used filling rules for polygons are EvenOdd + // and NonZero, sometimes called Alternate and Winding respectively. + // https://en.wikipedia.org/wiki/Nonzero-rule + public enum FillRule + { + EvenOdd, + NonZero, + Positive, + Negative + }; + + // PointInPolygon + internal enum PipResult + { + Inside, + Outside, + OnEdge + }; + + public static class InternalClipper + { + internal const long MaxInt64 = 9223372036854775807; + internal const long MaxCoord = MaxInt64 / 4; + internal const double max_coord = MaxCoord; + internal const double min_coord = -MaxCoord; + internal const long Invalid64 = MaxInt64; + + internal const double defaultArcTolerance = 0.25; + internal const double floatingPointTolerance = 1E-12; + internal const double defaultMinimumEdgeLength = 0.1; + + private static readonly string + precision_range_error = "Error: Precision is out of range."; + +#if USINGZ + public static Path64 SetZ(Path64 path, long Z) + { + Path64 result = new Path64(path.Count); + foreach (Point64 pt in path) result.Add(new Point64(pt.X, pt.Y, Z)); + return result; + } +#endif + + internal static void CheckPrecision(int precision) + { + if (precision < -8 || precision > 8) + throw new Exception(precision_range_error); + } + + internal static bool IsAlmostZero(double value) + { + return (Math.Abs(value) <= floatingPointTolerance); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static double CrossProduct(Point64 pt1, Point64 pt2, Point64 pt3) + { + // typecast to double to avoid potential int overflow + return ((double) (pt2.X - pt1.X) * (pt3.Y - pt2.Y) - + (double) (pt2.Y - pt1.Y) * (pt3.X - pt2.X)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static double DotProduct(Point64 pt1, Point64 pt2, Point64 pt3) + { + // typecast to double to avoid potential int overflow + return ((double) (pt2.X - pt1.X) * (pt3.X - pt2.X) + + (double) (pt2.Y - pt1.Y) * (pt3.Y - pt2.Y)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static double CrossProduct(PointD vec1, PointD vec2) + { + return (vec1.y * vec2.x - vec2.y * vec1.x); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static double DotProduct(PointD vec1, PointD vec2) + { + return (vec1.x * vec2.x + vec1.y * vec2.y); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static long CheckCastInt64(double val) + { + if ((val >= max_coord) || (val <= min_coord)) return Invalid64; + return (long)Math.Round(val, MidpointRounding.AwayFromZero); + } + + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool GetIntersectPoint(Point64 ln1a, + Point64 ln1b, Point64 ln2a, Point64 ln2b, out Point64 ip) + { + double dy1 = (ln1b.Y - ln1a.Y); + double dx1 = (ln1b.X - ln1a.X); + double dy2 = (ln2b.Y - ln2a.Y); + double dx2 = (ln2b.X - ln2a.X); + double det = dy1 * dx2 - dy2 * dx1; + if (det == 0.0) + { + ip = new Point64(); + return false; + } + + double t = ((ln1a.X - ln2a.X) * dy2 - (ln1a.Y - ln2a.Y) * dx2) / det; + if (t <= 0.0) ip = ln1a; + else if (t >= 1.0) ip = ln1b; + else { + // avoid using constructor (and rounding too) as they affect performance //664 + ip.X = (long) (ln1a.X + t * dx1); + ip.Y = (long) (ln1a.Y + t * dy1); +#if USINGZ + ip.Z = 0; +#endif + } + return true; + } + + internal static bool SegsIntersect(Point64 seg1a, + Point64 seg1b, Point64 seg2a, Point64 seg2b, bool inclusive = false) + { + if (inclusive) + { + double res1 = CrossProduct(seg1a, seg2a, seg2b); + double res2 = CrossProduct(seg1b, seg2a, seg2b); + if (res1 * res2 > 0) return false; + double res3 = CrossProduct(seg2a, seg1a, seg1b); + double res4 = CrossProduct(seg2b, seg1a, seg1b); + if (res3 * res4 > 0) return false; + // ensure NOT collinear + return (res1 != 0 || res2 != 0 || res3 != 0 || res4 != 0); + } + else + { + return (CrossProduct(seg1a, seg2a, seg2b) * + CrossProduct(seg1b, seg2a, seg2b) < 0) && + (CrossProduct(seg2a, seg1a, seg1b) * + CrossProduct(seg2b, seg1a, seg1b) < 0); + } + } + public static Point64 GetClosestPtOnSegment(Point64 offPt, + Point64 seg1, Point64 seg2) + { + if (seg1.X == seg2.X && seg1.Y == seg2.Y) return seg1; + double dx = (seg2.X - seg1.X); + double dy = (seg2.Y - seg1.Y); + double q = ((offPt.X - seg1.X) * dx + + (offPt.Y - seg1.Y) * dy) / ((dx*dx) + (dy*dy)); + if (q < 0) q = 0; else if (q > 1) q = 1; + return new Point64( + // use MidpointRounding.ToEven in order to explicitly match the nearbyint behaviour on the C++ side + seg1.X + Math.Round(q * dx, MidpointRounding.ToEven), + seg1.Y + Math.Round(q * dy, MidpointRounding.ToEven) + ); + } + + public static PointInPolygonResult PointInPolygon(Point64 pt, Path64 polygon) + { + int len = polygon.Count, start = 0; + if (len < 3) return PointInPolygonResult.IsOutside; + + while (start < len && polygon[start].Y == pt.Y) start++; + if (start == len) return PointInPolygonResult.IsOutside; + + double d; + bool isAbove = polygon[start].Y < pt.Y, startingAbove = isAbove; + int val = 0, i = start + 1, end = len; + while (true) + { + if (i == end) + { + if (end == 0 || start == 0) break; + end = start; + i = 0; + } + + if (isAbove) + { + while (i < end && polygon[i].Y < pt.Y) i++; + if (i == end) continue; + } + else + { + while (i < end && polygon[i].Y > pt.Y) i++; + if (i == end) continue; + } + + Point64 curr = polygon[i], prev; + if (i > 0) prev = polygon[i - 1]; + else prev = polygon[len - 1]; + + if (curr.Y == pt.Y) + { + if (curr.X == pt.X || (curr.Y == prev.Y && + ((pt.X < prev.X) != (pt.X < curr.X)))) + return PointInPolygonResult.IsOn; + i++; + if (i == start) break; + continue; + } + + if (pt.X < curr.X && pt.X < prev.X) + { + // we're only interested in edges crossing on the left + } + else if (pt.X > prev.X && pt.X > curr.X) + { + val = 1 - val; // toggle val + } + else + { + d = CrossProduct(prev, curr, pt); + if (d == 0) return PointInPolygonResult.IsOn; + if ((d < 0) == isAbove) val = 1 - val; + } + isAbove = !isAbove; + i++; + } + + if (isAbove != startingAbove) + { + if (i == len) i = 0; + if (i == 0) + d = CrossProduct(polygon[len - 1], polygon[0], pt); + else + d = CrossProduct(polygon[i - 1], polygon[i], pt); + if (d == 0) return PointInPolygonResult.IsOn; + if ((d < 0) == isAbove) val = 1 - val; + } + + if (val == 0) + return PointInPolygonResult.IsOutside; + return PointInPolygonResult.IsInside; + } + + } // InternalClipper + +} // namespace \ No newline at end of file diff --git a/BossMod/ThirdParty/Clipper2Lib/Clipper.Engine.cs b/BossMod/ThirdParty/Clipper2Lib/Clipper.Engine.cs new file mode 100644 index 0000000000..eaee6a4066 --- /dev/null +++ b/BossMod/ThirdParty/Clipper2Lib/Clipper.Engine.cs @@ -0,0 +1,3620 @@ +/******************************************************************************* +* Author : Angus Johnson * +* Date : 22 November 2023 * +* Website : http://www.angusj.com * +* Copyright : Angus Johnson 2010-2023 * +* Purpose : This is the main polygon clipping module * +* Thanks : Special thanks to Thong Nguyen, Guus Kuiper, Phil Stopford, * +* : and Daniel Gosnell for their invaluable assistance with C#. * +* License : http://www.boost.org/LICENSE_1_0.txt * +*******************************************************************************/ + +#nullable enable +using System; +using System.Collections; +using System.Collections.Generic; +using System.Reflection.Emit; +using System.Runtime.CompilerServices; + +namespace Clipper2Lib +{ + + // Vertex: a pre-clipping data structure. It is used to separate polygons + // into ascending and descending 'bounds' (or sides) that start at local + // minima and ascend to a local maxima, before descending again. + [Flags] + public enum PointInPolygonResult + { + IsOn = 0, + IsInside = 1, + IsOutside = 2 + }; + + [Flags] + internal enum VertexFlags + { + None = 0, + OpenStart = 1, + OpenEnd = 2, + LocalMax = 4, + LocalMin = 8 + }; + + internal class Vertex + { + public readonly Point64 pt; + public Vertex? next; + public Vertex? prev; + public VertexFlags flags; + + public Vertex(Point64 pt, VertexFlags flags, Vertex? prev) + { + this.pt = pt; + this.flags = flags; + next = null; + this.prev = prev; + } + }; + + internal readonly struct LocalMinima + { + public readonly Vertex vertex; + public readonly PathType polytype; + public readonly bool isOpen; + + public LocalMinima(Vertex vertex, PathType polytype, bool isOpen = false) + { + this.vertex = vertex; + this.polytype = polytype; + this.isOpen = isOpen; + } + + public static bool operator ==(LocalMinima lm1, LocalMinima lm2) + { + return ReferenceEquals(lm1.vertex, lm2.vertex); + } + + public static bool operator !=(LocalMinima lm1, LocalMinima lm2) + { + return !(lm1 == lm2); + } + + public override bool Equals(object? obj) + { + return obj is LocalMinima minima && this == minima; + } + + public override int GetHashCode() + { + return vertex.GetHashCode(); + } + }; + + // IntersectNode: a structure representing 2 intersecting edges. + // Intersections must be sorted so they are processed from the largest + // Y coordinates to the smallest while keeping edges adjacent. + internal readonly struct IntersectNode + { + public readonly Point64 pt; + public readonly Active edge1; + public readonly Active edge2; + + public IntersectNode(Point64 pt, Active edge1, Active edge2) + { + this.pt = pt; + this.edge1 = edge1; + this.edge2 = edge2; + } + }; + + internal struct LocMinSorter : IComparer + { + public readonly int Compare(LocalMinima locMin1, LocalMinima locMin2) + { + return locMin2.vertex.pt.Y.CompareTo(locMin1.vertex.pt.Y); + } + } + + // OutPt: vertex data structure for clipping solutions + internal class OutPt + { + public Point64 pt; + public OutPt? next; + public OutPt prev; + public OutRec outrec; + public HorzSegment? horz; + + public OutPt(Point64 pt, OutRec outrec) + { + this.pt = pt; + this.outrec = outrec; + next = this; + prev = this; + horz = null; + } + }; + + internal enum JoinWith { None, Left, Right }; + internal enum HorzPosition { Bottom, Middle, Top }; + + + // OutRec: path data structure for clipping solutions + internal class OutRec + { + public int idx; + public OutRec? owner; + public Active? frontEdge; + public Active? backEdge; + public OutPt? pts; + public PolyPathBase? polypath; + public Rect64 bounds = new Rect64(); + public Path64 path = new Path64(); + public bool isOpen; + public List? splits = null; + public OutRec? recursiveSplit; + }; + + internal struct HorzSegSorter : IComparer + { + public readonly int Compare(HorzSegment? hs1, HorzSegment? hs2) + { + if (hs1 == null || hs2 == null) return 0; + if (hs1.rightOp == null) + { + return hs2.rightOp == null ? 0 : 1; + } + else if (hs2.rightOp == null) + return -1; + else + return hs1.leftOp!.pt.X.CompareTo(hs2.leftOp!.pt.X); + } + } + + internal class HorzSegment + { + public OutPt? leftOp; + public OutPt? rightOp; + public bool leftToRight; + public HorzSegment(OutPt op) + { + leftOp = op; + rightOp = null; + leftToRight = true; + } + } + + internal class HorzJoin + { + public OutPt? op1; + public OutPt? op2; + public HorzJoin(OutPt ltor, OutPt rtol) + { + op1 = ltor; + op2 = rtol; + } + } + + /////////////////////////////////////////////////////////////////// + // Important: UP and DOWN here are premised on Y-axis positive down + // displays, which is the orientation used in Clipper's development. + /////////////////////////////////////////////////////////////////// + + internal class Active + { + public Point64 bot; + public Point64 top; + public long curX; // current (updated at every new scanline) + public double dx; + public int windDx; // 1 or -1 depending on winding direction + public int windCount; + public int windCount2; // winding count of the opposite polytype + public OutRec? outrec; + + // AEL: 'active edge list' (Vatti's AET - active edge table) + // a linked list of all edges (from left to right) that are present + // (or 'active') within the current scanbeam (a horizontal 'beam' that + // sweeps from bottom to top over the paths in the clipping operation). + public Active? prevInAEL; + public Active? nextInAEL; + + // SEL: 'sorted edge list' (Vatti's ST - sorted table) + // linked list used when sorting edges into their new positions at the + // top of scanbeams, but also (re)used to process horizontals. + public Active? prevInSEL; + public Active? nextInSEL; + public Active? jump; + public Vertex? vertexTop; + public LocalMinima localMin; // the bottom of an edge 'bound' (also Vatti) + internal bool isLeftBound; + internal JoinWith joinWith; + }; + + internal static class ClipperEngine + { + internal static void AddLocMin(Vertex vert, PathType polytype, bool isOpen, + List minimaList) + { + // make sure the vertex is added only once ... + if ((vert.flags & VertexFlags.LocalMin) != VertexFlags.None) return; + vert.flags |= VertexFlags.LocalMin; + + LocalMinima lm = new LocalMinima(vert, polytype, isOpen); + minimaList.Add(lm); + } + + internal static void AddPathsToVertexList(Paths64 paths, PathType polytype, bool isOpen, + List minimaList, List vertexList) + { + int totalVertCnt = 0; + foreach (Path64 path in paths) totalVertCnt += path.Count; + vertexList.Capacity = vertexList.Count + totalVertCnt; + + foreach (Path64 path in paths) + { + Vertex? v0 = null, prev_v = null, curr_v; + foreach (Point64 pt in path) + { + if (v0 == null) + { + v0 = new Vertex(pt, VertexFlags.None, null); + vertexList.Add(v0); + prev_v = v0; + } + else if (prev_v!.pt != pt) // ie skips duplicates + { + curr_v = new Vertex(pt, VertexFlags.None, prev_v); + vertexList.Add(curr_v); + prev_v.next = curr_v; + prev_v = curr_v; + } + } + if (prev_v == null || prev_v.prev == null) continue; + if (!isOpen && prev_v.pt == v0!.pt) prev_v = prev_v.prev; + prev_v.next = v0; + v0!.prev = prev_v; + if (!isOpen && prev_v.next == prev_v) continue; + + // OK, we have a valid path + bool going_up, going_up0; + if (isOpen) + { + curr_v = v0.next; + while (curr_v != v0 && curr_v!.pt.Y == v0.pt.Y) + curr_v = curr_v.next; + going_up = curr_v.pt.Y <= v0.pt.Y; + if (going_up) + { + v0.flags = VertexFlags.OpenStart; + AddLocMin(v0, polytype, true, minimaList); + } + else + v0.flags = VertexFlags.OpenStart | VertexFlags.LocalMax; + } + else // closed path + { + prev_v = v0.prev; + while (prev_v != v0 && prev_v!.pt.Y == v0.pt.Y) + prev_v = prev_v.prev; + if (prev_v == v0) + continue; // only open paths can be completely flat + going_up = prev_v.pt.Y > v0.pt.Y; + } + + going_up0 = going_up; + prev_v = v0; + curr_v = v0.next; + while (curr_v != v0) + { + if (curr_v!.pt.Y > prev_v.pt.Y && going_up) + { + prev_v.flags |= VertexFlags.LocalMax; + going_up = false; + } + else if (curr_v.pt.Y < prev_v.pt.Y && !going_up) + { + going_up = true; + AddLocMin(prev_v, polytype, isOpen, minimaList); + } + prev_v = curr_v; + curr_v = curr_v.next; + } + + if (isOpen) + { + prev_v.flags |= VertexFlags.OpenEnd; + if (going_up) + prev_v.flags |= VertexFlags.LocalMax; + else + AddLocMin(prev_v, polytype, isOpen, minimaList); + } + else if (going_up != going_up0) + { + if (going_up0) AddLocMin(prev_v, polytype, false, minimaList); + else prev_v.flags |= VertexFlags.LocalMax; + } + } + } + } + + public class ReuseableDataContainer64 + { + internal readonly List _minimaList; + internal readonly List _vertexList; + public ReuseableDataContainer64() + { + _minimaList = new List(); + _vertexList = new List(); + } + public void Clear() + { + _minimaList.Clear(); + _vertexList.Clear(); + } + + public void AddPaths(Paths64 paths, PathType pt, bool isOpen) + { + ClipperEngine.AddPathsToVertexList(paths, pt, isOpen, _minimaList, _vertexList); + } + } + + public class ClipperBase + { + private ClipType _cliptype; + private FillRule _fillrule; + private Active? _actives; + private Active? _sel; + private readonly List _minimaList; + private readonly List _intersectList; + private readonly List _vertexList; + private readonly List _outrecList; + private readonly List _scanlineList; + private readonly List _horzSegList; + private readonly List _horzJoinList; + private int _currentLocMin; + private long _currentBotY; + private bool _isSortedMinimaList; + private bool _hasOpenPaths; + internal bool _using_polytree; + internal bool _succeeded; + public bool PreserveCollinear { get; set; } + public bool ReverseSolution { get; set; } + +#if USINGZ + public delegate void ZCallback64(Point64 bot1, Point64 top1, + Point64 bot2, Point64 top2, ref Point64 intersectPt); + + public long DefaultZ { get; set; } + protected ZCallback64? _zCallback; +#endif + public ClipperBase() + { + _minimaList = new List(); + _intersectList = new List(); + _vertexList = new List(); + _outrecList = new List(); + _scanlineList = new List(); + _horzSegList = new List(); + _horzJoinList = new List(); + PreserveCollinear = true; + } + +#if USINGZ + private bool XYCoordsEqual(Point64 pt1, Point64 pt2) + { + return (pt1.X == pt2.X && pt1.Y == pt2.Y); + } + + private void SetZ(Active e1, Active e2, ref Point64 intersectPt) + { + if (_zCallback == null) return; + + // prioritize subject vertices over clip vertices + // and pass the subject vertices before clip vertices in the callback + if (GetPolyType(e1) == PathType.Subject) + { + if (XYCoordsEqual(intersectPt, e1.bot)) + intersectPt = new Point64(intersectPt.X, intersectPt.Y, e1.bot.Z); + else if (XYCoordsEqual(intersectPt, e1.top)) + intersectPt = new Point64(intersectPt.X, intersectPt.Y, e1.top.Z); + else if (XYCoordsEqual(intersectPt, e2.bot)) + intersectPt = new Point64(intersectPt.X, intersectPt.Y, e2.bot.Z); + else if (XYCoordsEqual(intersectPt, e2.top)) + intersectPt = new Point64(intersectPt.X, intersectPt.Y, e2.top.Z); + else + intersectPt = new Point64(intersectPt.X, intersectPt.Y, DefaultZ); + _zCallback(e1.bot, e1.top, e2.bot, e2.top, ref intersectPt); + } + else + { + if (XYCoordsEqual(intersectPt, e2.bot)) + intersectPt = new Point64(intersectPt.X, intersectPt.Y, e2.bot.Z); + else if (XYCoordsEqual(intersectPt, e2.top)) + intersectPt = new Point64(intersectPt.X, intersectPt.Y, e2.top.Z); + else if (XYCoordsEqual(intersectPt, e1.bot)) + intersectPt = new Point64(intersectPt.X, intersectPt.Y, e1.bot.Z); + else if (XYCoordsEqual(intersectPt, e1.top)) + intersectPt = new Point64(intersectPt.X, intersectPt.Y, e1.top.Z); + else + intersectPt = new Point64(intersectPt.X, intersectPt.Y, DefaultZ); + _zCallback(e2.bot, e2.top, e1.bot, e1.top, ref intersectPt); + } + } +#endif + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsOdd(int val) + { + return ((val & 1) != 0); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsHotEdge(Active ae) + { + return ae.outrec != null; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsOpen(Active ae) + { + return ae.localMin.isOpen; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsOpenEnd(Active ae) + { + return ae.localMin.isOpen && IsOpenEnd(ae.vertexTop!); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsOpenEnd(Vertex v) + { + return (v.flags & (VertexFlags.OpenStart | VertexFlags.OpenEnd)) != VertexFlags.None; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Active? GetPrevHotEdge(Active ae) + { + Active? prev = ae.prevInAEL; + while (prev != null && (IsOpen(prev) || !IsHotEdge(prev))) + prev = prev.prevInAEL; + return prev; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsFront(Active ae) + { + return (ae == ae.outrec!.frontEdge); + } + + /******************************************************************************* + * Dx: 0(90deg) * + * | * + * +inf (180deg) <--- o --. -inf (0deg) * + *******************************************************************************/ + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static double GetDx(Point64 pt1, Point64 pt2) + { + double dy = pt2.Y - pt1.Y; + if (dy != 0) + return (pt2.X - pt1.X) / dy; + if (pt2.X > pt1.X) + return double.NegativeInfinity; + return double.PositiveInfinity; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static long TopX(Active ae, long currentY) + { + if ((currentY == ae.top.Y) || (ae.top.X == ae.bot.X)) return ae.top.X; + if (currentY == ae.bot.Y) return ae.bot.X; + + // use MidpointRounding.ToEven in order to explicitly match the nearbyint behaviour on the C++ side + return ae.bot.X + (long) Math.Round(ae.dx * (currentY - ae.bot.Y), MidpointRounding.ToEven); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsHorizontal(Active ae) + { + return (ae.top.Y == ae.bot.Y); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsHeadingRightHorz(Active ae) + { + return (double.IsNegativeInfinity(ae.dx)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsHeadingLeftHorz(Active ae) + { + return (double.IsPositiveInfinity(ae.dx)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void SwapActives(ref Active ae1, ref Active ae2) + { + (ae2, ae1) = (ae1, ae2); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static PathType GetPolyType(Active ae) + { + return ae.localMin.polytype; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsSamePolyType(Active ae1, Active ae2) + { + return ae1.localMin.polytype == ae2.localMin.polytype; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void SetDx(Active ae) + { + ae.dx = GetDx(ae.bot, ae.top); + } + + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vertex NextVertex(Active ae) + { + if (ae.windDx > 0) + return ae.vertexTop!.next!; + return ae.vertexTop!.prev!; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vertex PrevPrevVertex(Active ae) + { + if (ae.windDx > 0) + return ae.vertexTop!.prev!.prev!; + return ae.vertexTop!.next!.next!; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsMaxima(Vertex vertex) + { + return ((vertex.flags & VertexFlags.LocalMax) != VertexFlags.None); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsMaxima(Active ae) + { + return IsMaxima(ae.vertexTop!); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Active? GetMaximaPair(Active ae) + { + Active? ae2; + ae2 = ae.nextInAEL; + while (ae2 != null) + { + if (ae2.vertexTop == ae.vertexTop) return ae2; // Found! + ae2 = ae2.nextInAEL; + } + return null; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vertex? GetCurrYMaximaVertex_Open(Active ae) + { + Vertex? result = ae.vertexTop; + if (ae.windDx > 0) + while (result!.next!.pt.Y == result.pt.Y && + ((result.flags & (VertexFlags.OpenEnd | + VertexFlags.LocalMax)) == VertexFlags.None)) + result = result.next; + else + while (result!.prev!.pt.Y == result.pt.Y && + ((result.flags & (VertexFlags.OpenEnd | + VertexFlags.LocalMax)) == VertexFlags.None)) + result = result.prev; + if (!IsMaxima(result)) result = null; // not a maxima + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vertex? GetCurrYMaximaVertex(Active ae) + { + Vertex? result = ae.vertexTop; + if (ae.windDx > 0) + while (result!.next!.pt.Y == result.pt.Y) result = result.next; + else + while (result!.prev!.pt.Y == result.pt.Y) result = result.prev; + if (!IsMaxima(result)) result = null; // not a maxima + return result; + } + + private struct IntersectListSort : IComparer + { + public readonly int Compare(IntersectNode a, IntersectNode b) + { + if (a.pt.Y == b.pt.Y) + { + if (a.pt.X == b.pt.X) return 0; + return (a.pt.X < b.pt.X) ? -1 : 1; + } + return (a.pt.Y > b.pt.Y) ? -1 : 1; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void SetSides(OutRec outrec, Active startEdge, Active endEdge) + { + outrec.frontEdge = startEdge; + outrec.backEdge = endEdge; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void SwapOutrecs(Active ae1, Active ae2) + { + OutRec? or1 = ae1.outrec; // at least one edge has + OutRec? or2 = ae2.outrec; // an assigned outrec + if (or1 == or2) + { + Active? ae = or1!.frontEdge; + or1.frontEdge = or1.backEdge; + or1.backEdge = ae; + return; + } + + if (or1 != null) + { + if (ae1 == or1.frontEdge) + or1.frontEdge = ae2; + else + or1.backEdge = ae2; + } + + if (or2 != null) + { + if (ae2 == or2.frontEdge) + or2.frontEdge = ae1; + else + or2.backEdge = ae1; + } + + ae1.outrec = or2; + ae2.outrec = or1; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void SetOwner(OutRec outrec, OutRec newOwner) + { + //precondition1: new_owner is never null + while (newOwner.owner != null && newOwner.owner.pts == null) + newOwner.owner = newOwner.owner.owner; + + //make sure that outrec isn't an owner of newOwner + OutRec? tmp = newOwner; + while (tmp != null && tmp != outrec) + tmp = tmp.owner; + if (tmp != null) + newOwner.owner = outrec.owner; + outrec.owner = newOwner; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static double Area(OutPt op) + { + // https://en.wikipedia.org/wiki/Shoelace_formula + double area = 0.0; + OutPt op2 = op; + do + { + area += (double) (op2.prev.pt.Y + op2.pt.Y) * + (op2.prev.pt.X - op2.pt.X); + op2 = op2.next!; + } while (op2 != op); + return area * 0.5; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static double AreaTriangle(Point64 pt1, Point64 pt2, Point64 pt3) + { + return (double) (pt3.Y + pt1.Y) * (pt3.X - pt1.X) + + (double) (pt1.Y + pt2.Y) * (pt1.X - pt2.X) + + (double) (pt2.Y + pt3.Y) * (pt2.X - pt3.X); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static OutRec? GetRealOutRec(OutRec? outRec) + { + while ((outRec != null) && (outRec.pts == null)) + outRec = outRec.owner; + return outRec; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsValidOwner(OutRec? outRec, OutRec? testOwner) + { + while ((testOwner != null) && (testOwner != outRec)) + testOwner = testOwner.owner; + return testOwner == null; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void UncoupleOutRec(Active ae) + { + OutRec? outrec = ae.outrec; + if (outrec == null) return; + outrec.frontEdge!.outrec = null; + outrec.backEdge!.outrec = null; + outrec.frontEdge = null; + outrec.backEdge = null; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool OutrecIsAscending(Active hotEdge) + { + return (hotEdge == hotEdge.outrec!.frontEdge); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void SwapFrontBackSides(OutRec outrec) + { + // while this proc. is needed for open paths + // it's almost never needed for closed paths + Active ae2 = outrec.frontEdge!; + outrec.frontEdge = outrec.backEdge; + outrec.backEdge = ae2; + outrec.pts = outrec.pts!.next; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool EdgesAdjacentInAEL(IntersectNode inode) + { + return (inode.edge1.nextInAEL == inode.edge2) || (inode.edge1.prevInAEL == inode.edge2); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected void ClearSolutionOnly() + { + while (_actives != null) DeleteFromAEL(_actives); + _scanlineList.Clear(); + DisposeIntersectNodes(); + _outrecList.Clear(); + _horzSegList.Clear(); + _horzJoinList.Clear(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Clear() + { + ClearSolutionOnly(); + _minimaList.Clear(); + _vertexList.Clear(); + _currentLocMin = 0; + _isSortedMinimaList = false; + _hasOpenPaths = false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected void Reset() + { + if (!_isSortedMinimaList) + { + _minimaList.Sort(new LocMinSorter()); + _isSortedMinimaList = true; + } + + _scanlineList.Capacity = _minimaList.Count; + for (int i = _minimaList.Count - 1; i >= 0; i--) + _scanlineList.Add(_minimaList[i].vertex.pt.Y); + + _currentBotY = 0; + _currentLocMin = 0; + _actives = null; + _sel = null; + _succeeded = true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void InsertScanline(long y) + { + int index = _scanlineList.BinarySearch(y); + if (index >= 0) return; + index = ~index; + _scanlineList.Insert(index, y); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool PopScanline(out long y) + { + int cnt = _scanlineList.Count - 1; + if (cnt < 0) + { + y = 0; + return false; + } + + y = _scanlineList[cnt]; + _scanlineList.RemoveAt(cnt--); + while (cnt >= 0 && y == _scanlineList[cnt]) + _scanlineList.RemoveAt(cnt--); + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool HasLocMinAtY(long y) + { + return (_currentLocMin < _minimaList.Count && _minimaList[_currentLocMin].vertex.pt.Y == y); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private LocalMinima PopLocalMinima() + { + return _minimaList[_currentLocMin++]; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void AddSubject(Path64 path) + { + AddPath(path, PathType.Subject); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void AddOpenSubject(Path64 path) + { + AddPath(path, PathType.Subject, true); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void AddClip(Path64 path) + { + AddPath(path, PathType.Clip); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected void AddPath(Path64 path, PathType polytype, bool isOpen = false) + { + Paths64 tmp = new Paths64(1) { path }; + AddPaths(tmp, polytype, isOpen); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected void AddPaths(Paths64 paths, PathType polytype, bool isOpen = false) + { + if (isOpen) _hasOpenPaths = true; + _isSortedMinimaList = false; + ClipperEngine.AddPathsToVertexList(paths, polytype, isOpen, _minimaList, _vertexList); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected void AddReuseableData(ReuseableDataContainer64 reuseableData) + { + if (reuseableData._minimaList.Count == 0) return; + // nb: reuseableData will continue to own the vertices, so it's important + // that the reuseableData object isn't destroyed before the Clipper object + // that's using the data. + _isSortedMinimaList = false; + foreach (LocalMinima lm in reuseableData._minimaList) + { + _minimaList.Add(new LocalMinima(lm.vertex, lm.polytype, lm.isOpen)); + if (lm.isOpen) _hasOpenPaths = true; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool IsContributingClosed(Active ae) + { + switch (_fillrule) + { + case FillRule.Positive: + if (ae.windCount != 1) return false; + break; + case FillRule.Negative: + if (ae.windCount != -1) return false; + break; + case FillRule.NonZero: + if (Math.Abs(ae.windCount) != 1) return false; + break; + } + + switch (_cliptype) + { + case ClipType.Intersection: + return _fillrule switch + { + FillRule.Positive => ae.windCount2 > 0, + FillRule.Negative => ae.windCount2 < 0, + _ => ae.windCount2 != 0, + }; + + case ClipType.Union: + return _fillrule switch + { + FillRule.Positive => ae.windCount2 <= 0, + FillRule.Negative => ae.windCount2 >= 0, + _ => ae.windCount2 == 0, + }; + + case ClipType.Difference: + bool result = _fillrule switch + { + FillRule.Positive => (ae.windCount2 <= 0), + FillRule.Negative => (ae.windCount2 >= 0), + _ => (ae.windCount2 == 0), + }; + return (GetPolyType(ae) == PathType.Subject) ? result : !result; + + case ClipType.Xor: + return true; // XOr is always contributing unless open + + default: + return false; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool IsContributingOpen(Active ae) + { + bool isInClip, isInSubj; + switch (_fillrule) + { + case FillRule.Positive: + isInSubj = ae.windCount > 0; + isInClip = ae.windCount2 > 0; + break; + case FillRule.Negative: + isInSubj = ae.windCount < 0; + isInClip = ae.windCount2 < 0; + break; + default: + isInSubj = ae.windCount != 0; + isInClip = ae.windCount2 != 0; + break; + } + + bool result = _cliptype switch + { + ClipType.Intersection => isInClip, + ClipType.Union => !isInSubj && !isInClip, + _ => !isInClip + }; + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void SetWindCountForClosedPathEdge(Active ae) + { + // Wind counts refer to polygon regions not edges, so here an edge's WindCnt + // indicates the higher of the wind counts for the two regions touching the + // edge. (nb: Adjacent regions can only ever have their wind counts differ by + // one. Also, open paths have no meaningful wind directions or counts.) + + Active? ae2 = ae.prevInAEL; + // find the nearest closed path edge of the same PolyType in AEL (heading left) + PathType pt = GetPolyType(ae); + while (ae2 != null && (GetPolyType(ae2) != pt || IsOpen(ae2))) ae2 = ae2.prevInAEL; + + if (ae2 == null) + { + ae.windCount = ae.windDx; + ae2 = _actives; + } + else if (_fillrule == FillRule.EvenOdd) + { + ae.windCount = ae.windDx; + ae.windCount2 = ae2.windCount2; + ae2 = ae2.nextInAEL; + } + else + { + // NonZero, positive, or negative filling here ... + // when e2's WindCnt is in the SAME direction as its WindDx, + // then polygon will fill on the right of 'e2' (and 'e' will be inside) + // nb: neither e2.WindCnt nor e2.WindDx should ever be 0. + if (ae2.windCount * ae2.windDx < 0) + { + // opposite directions so 'ae' is outside 'ae2' ... + if (Math.Abs(ae2.windCount) > 1) + { + // outside prev poly but still inside another. + if (ae2.windDx * ae.windDx < 0) + // reversing direction so use the same WC + ae.windCount = ae2.windCount; + else + // otherwise keep 'reducing' the WC by 1 (i.e. towards 0) ... + ae.windCount = ae2.windCount + ae.windDx; + } + else + // now outside all polys of same polytype so set own WC ... + ae.windCount = (IsOpen(ae) ? 1 : ae.windDx); + } + else + { + //'ae' must be inside 'ae2' + if (ae2.windDx * ae.windDx < 0) + // reversing direction so use the same WC + ae.windCount = ae2.windCount; + else + // otherwise keep 'increasing' the WC by 1 (i.e. away from 0) ... + ae.windCount = ae2.windCount + ae.windDx; + } + + ae.windCount2 = ae2.windCount2; + ae2 = ae2.nextInAEL; // i.e. get ready to calc WindCnt2 + } + + // update windCount2 ... + if (_fillrule == FillRule.EvenOdd) + while (ae2 != ae) + { + if (GetPolyType(ae2!) != pt && !IsOpen(ae2!)) + ae.windCount2 = (ae.windCount2 == 0 ? 1 : 0); + ae2 = ae2!.nextInAEL; + } + else + while (ae2 != ae) + { + if (GetPolyType(ae2!) != pt && !IsOpen(ae2!)) + ae.windCount2 += ae2!.windDx; + ae2 = ae2!.nextInAEL; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void SetWindCountForOpenPathEdge(Active ae) + { + Active? ae2 = _actives; + if (_fillrule == FillRule.EvenOdd) + { + int cnt1 = 0, cnt2 = 0; + while (ae2 != ae) + { + if (GetPolyType(ae2!) == PathType.Clip) + cnt2++; + else if (!IsOpen(ae2!)) + cnt1++; + ae2 = ae2!.nextInAEL; + } + + ae.windCount = (IsOdd(cnt1) ? 1 : 0); + ae.windCount2 = (IsOdd(cnt2) ? 1 : 0); + } + else + { + while (ae2 != ae) + { + if (GetPolyType(ae2!) == PathType.Clip) + ae.windCount2 += ae2!.windDx; + else if (!IsOpen(ae2!)) + ae.windCount += ae2!.windDx; + ae2 = ae2!.nextInAEL; + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsValidAelOrder(Active resident, Active newcomer) + { + if (newcomer.curX != resident.curX) + return newcomer.curX > resident.curX; + + // get the turning direction a1.top, a2.bot, a2.top + double d = InternalClipper.CrossProduct(resident.top, newcomer.bot, newcomer.top); + if (d != 0) return (d < 0); + + // edges must be collinear to get here + + // for starting open paths, place them according to + // the direction they're about to turn + if (!IsMaxima(resident) && (resident.top.Y > newcomer.top.Y)) + { + return InternalClipper.CrossProduct(newcomer.bot, + resident.top, NextVertex(resident).pt) <= 0; + } + + if (!IsMaxima(newcomer) && (newcomer.top.Y > resident.top.Y)) + { + return InternalClipper.CrossProduct(newcomer.bot, + newcomer.top, NextVertex(newcomer).pt) >= 0; + } + + long y = newcomer.bot.Y; + bool newcomerIsLeft = newcomer.isLeftBound; + + if (resident.bot.Y != y || resident.localMin.vertex.pt.Y != y) + return newcomer.isLeftBound; + // resident must also have just been inserted + if (resident.isLeftBound != newcomerIsLeft) + return newcomerIsLeft; + if (InternalClipper.CrossProduct(PrevPrevVertex(resident).pt, + resident.bot, resident.top) == 0) return true; + // compare turning direction of the alternate bound + return (InternalClipper.CrossProduct(PrevPrevVertex(resident).pt, + newcomer.bot, PrevPrevVertex(newcomer).pt) > 0) == newcomerIsLeft; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void InsertLeftEdge(Active ae) + { + Active ae2; + + if (_actives == null) + { + ae.prevInAEL = null; + ae.nextInAEL = null; + _actives = ae; + } + else if (!IsValidAelOrder(_actives, ae)) + { + ae.prevInAEL = null; + ae.nextInAEL = _actives; + _actives.prevInAEL = ae; + _actives = ae; + } + else + { + ae2 = _actives; + while (ae2.nextInAEL != null && IsValidAelOrder(ae2.nextInAEL, ae)) + ae2 = ae2.nextInAEL; + //don't separate joined edges + if (ae2.joinWith == JoinWith.Right) ae2 = ae2.nextInAEL!; + ae.nextInAEL = ae2.nextInAEL; + if (ae2.nextInAEL != null) ae2.nextInAEL.prevInAEL = ae; + ae.prevInAEL = ae2; + ae2.nextInAEL = ae; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void InsertRightEdge(Active ae, Active ae2) + { + ae2.nextInAEL = ae.nextInAEL; + if (ae.nextInAEL != null) ae.nextInAEL.prevInAEL = ae2; + ae2.prevInAEL = ae; + ae.nextInAEL = ae2; + } + + private void InsertLocalMinimaIntoAEL(long botY) + { + LocalMinima localMinima; + Active? leftBound, rightBound; + // Add any local minima (if any) at BotY ... + // NB horizontal local minima edges should contain locMin.vertex.prev + while (HasLocMinAtY(botY)) + { + localMinima = PopLocalMinima(); + if ((localMinima.vertex.flags & VertexFlags.OpenStart) != VertexFlags.None) + { + leftBound = null; + } + else + { + leftBound = new Active + { + bot = localMinima.vertex.pt, + curX = localMinima.vertex.pt.X, + windDx = -1, + vertexTop = localMinima.vertex.prev, + top = localMinima.vertex.prev!.pt, + outrec = null, + localMin = localMinima + }; + SetDx(leftBound); + } + + if ((localMinima.vertex.flags & VertexFlags.OpenEnd) != VertexFlags.None) + { + rightBound = null; + } + else + { + rightBound = new Active + { + bot = localMinima.vertex.pt, + curX = localMinima.vertex.pt.X, + windDx = 1, + vertexTop = localMinima.vertex.next, // i.e. ascending + top = localMinima.vertex.next!.pt, + outrec = null, + localMin = localMinima + }; + SetDx(rightBound); + } + + // Currently LeftB is just the descending bound and RightB is the ascending. + // Now if the LeftB isn't on the left of RightB then we need swap them. + if (leftBound != null && rightBound != null) + { + if (IsHorizontal(leftBound)) + { + if (IsHeadingRightHorz(leftBound)) SwapActives(ref leftBound, ref rightBound); + } + else if (IsHorizontal(rightBound)) + { + if (IsHeadingLeftHorz(rightBound)) SwapActives(ref leftBound, ref rightBound); + } + else if (leftBound.dx < rightBound.dx) + SwapActives(ref leftBound, ref rightBound); + //so when leftBound has windDx == 1, the polygon will be oriented + //counter-clockwise in Cartesian coords (clockwise with inverted Y). + } + else if (leftBound == null) + { + leftBound = rightBound; + rightBound = null; + } + + bool contributing; + leftBound!.isLeftBound = true; + InsertLeftEdge(leftBound); + + if (IsOpen(leftBound)) + { + SetWindCountForOpenPathEdge(leftBound); + contributing = IsContributingOpen(leftBound); + } + else + { + SetWindCountForClosedPathEdge(leftBound); + contributing = IsContributingClosed(leftBound); + } + + if (rightBound != null) + { + rightBound.windCount = leftBound.windCount; + rightBound.windCount2 = leftBound.windCount2; + InsertRightEdge(leftBound, rightBound); /////// + + if (contributing) + { + AddLocalMinPoly(leftBound, rightBound, leftBound.bot, true); + if (!IsHorizontal(leftBound)) + CheckJoinLeft(leftBound, leftBound.bot); + } + + while (rightBound.nextInAEL != null && + IsValidAelOrder(rightBound.nextInAEL, rightBound)) + { + IntersectEdges(rightBound, rightBound.nextInAEL, rightBound.bot); + SwapPositionsInAEL(rightBound, rightBound.nextInAEL); + } + + if (IsHorizontal(rightBound)) + PushHorz(rightBound); + else + { + CheckJoinRight(rightBound, rightBound.bot); + InsertScanline(rightBound.top.Y); + } + } + else if (contributing) + StartOpenPath(leftBound, leftBound.bot); + + if (IsHorizontal(leftBound)) + PushHorz(leftBound); + else + InsertScanline(leftBound.top.Y); + } // while (HasLocMinAtY()) + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void PushHorz(Active ae) + { + ae.nextInSEL = _sel; + _sel = ae; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool PopHorz(out Active? ae) + { + ae = _sel; + if (_sel == null) return false; + _sel = _sel.nextInSEL; + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private OutPt AddLocalMinPoly(Active ae1, Active ae2, Point64 pt, bool isNew = false) + { + OutRec outrec = NewOutRec(); + ae1.outrec = outrec; + ae2.outrec = outrec; + + if (IsOpen(ae1)) + { + outrec.owner = null; + outrec.isOpen = true; + if (ae1.windDx > 0) + SetSides(outrec, ae1, ae2); + else + SetSides(outrec, ae2, ae1); + } + else + { + outrec.isOpen = false; + Active? prevHotEdge = GetPrevHotEdge(ae1); + // e.windDx is the winding direction of the **input** paths + // and unrelated to the winding direction of output polygons. + // Output orientation is determined by e.outrec.frontE which is + // the ascending edge (see AddLocalMinPoly). + if (prevHotEdge != null) + { + if (_using_polytree) + SetOwner(outrec, prevHotEdge.outrec!); + outrec.owner = prevHotEdge.outrec; + if (OutrecIsAscending(prevHotEdge) == isNew) + SetSides(outrec, ae2, ae1); + else + SetSides(outrec, ae1, ae2); + } + else + { + outrec.owner = null; + if (isNew) + SetSides(outrec, ae1, ae2); + else + SetSides(outrec, ae2, ae1); + } + } + + OutPt op = new OutPt(pt, outrec); + outrec.pts = op; + return op; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private OutPt? AddLocalMaxPoly(Active ae1, Active ae2, Point64 pt) + { + if (IsJoined(ae1)) Split(ae1, pt); + if (IsJoined(ae2)) Split(ae2, pt); + + if (IsFront(ae1) == IsFront(ae2)) + { + if (IsOpenEnd(ae1)) + SwapFrontBackSides(ae1.outrec!); + else if (IsOpenEnd(ae2)) + SwapFrontBackSides(ae2.outrec!); + else + { + _succeeded = false; + return null; + } + } + + OutPt result = AddOutPt(ae1, pt); + if (ae1.outrec == ae2.outrec) + { + OutRec outrec = ae1.outrec!; + outrec.pts = result; + + if (_using_polytree) + { + Active? e = GetPrevHotEdge(ae1); + if (e == null) + outrec.owner = null; + else + SetOwner(outrec, e.outrec!); + // nb: outRec.owner here is likely NOT the real + // owner but this will be fixed in DeepCheckOwner() + } + UncoupleOutRec(ae1); + } + // and to preserve the winding orientation of outrec ... + else if (IsOpen(ae1)) + { + if (ae1.windDx < 0) + JoinOutrecPaths(ae1, ae2); + else + JoinOutrecPaths(ae2, ae1); + } + else if (ae1.outrec!.idx < ae2.outrec!.idx) + JoinOutrecPaths(ae1, ae2); + else + JoinOutrecPaths(ae2, ae1); + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void JoinOutrecPaths(Active ae1, Active ae2) + { + // join ae2 outrec path onto ae1 outrec path and then delete ae2 outrec path + // pointers. (NB Only very rarely do the joining ends share the same coords.) + OutPt p1Start = ae1.outrec!.pts!; + OutPt p2Start = ae2.outrec!.pts!; + OutPt p1End = p1Start.next!; + OutPt p2End = p2Start.next!; + if (IsFront(ae1)) + { + p2End.prev = p1Start; + p1Start.next = p2End; + p2Start.next = p1End; + p1End.prev = p2Start; + ae1.outrec.pts = p2Start; + // nb: if IsOpen(e1) then e1 & e2 must be a 'maximaPair' + ae1.outrec.frontEdge = ae2.outrec.frontEdge; + if (ae1.outrec.frontEdge != null) + ae1.outrec.frontEdge!.outrec = ae1.outrec; + } + else + { + p1End.prev = p2Start; + p2Start.next = p1End; + p1Start.next = p2End; + p2End.prev = p1Start; + + ae1.outrec.backEdge = ae2.outrec.backEdge; + if (ae1.outrec.backEdge != null) + ae1.outrec.backEdge!.outrec = ae1.outrec; + } + + // after joining, the ae2.OutRec must contains no vertices ... + ae2.outrec.frontEdge = null; + ae2.outrec.backEdge = null; + ae2.outrec.pts = null; + SetOwner(ae2.outrec, ae1.outrec); + + if (IsOpenEnd(ae1)) + { + ae2.outrec.pts = ae1.outrec.pts; + ae1.outrec.pts = null; + } + + // and ae1 and ae2 are maxima and are about to be dropped from the Actives list. + ae1.outrec = null; + ae2.outrec = null; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static OutPt AddOutPt(Active ae, Point64 pt) + { + + // Outrec.OutPts: a circular doubly-linked-list of POutPt where ... + // opFront[.Prev]* ~~~> opBack & opBack == opFront.Next + OutRec outrec = ae.outrec!; + bool toFront = IsFront(ae); + OutPt opFront = outrec.pts!; + OutPt opBack = opFront.next!; + + if (toFront && (pt == opFront.pt)) return opFront; + else if (!toFront && (pt == opBack.pt)) return opBack; + + OutPt newOp = new OutPt(pt, outrec); + opBack.prev = newOp; + newOp.prev = opFront; + newOp.next = opBack; + opFront.next = newOp; + if (toFront) outrec.pts = newOp; + return newOp; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private OutRec NewOutRec() + { + OutRec result = new OutRec + { + idx = _outrecList.Count + }; + _outrecList.Add(result); + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private OutPt StartOpenPath(Active ae, Point64 pt) + { + OutRec outrec = NewOutRec(); + outrec.isOpen = true; + if (ae.windDx > 0) + { + outrec.frontEdge = ae; + outrec.backEdge = null; + } + else + { + outrec.frontEdge = null; + outrec.backEdge = ae; + } + + ae.outrec = outrec; + OutPt op = new OutPt(pt, outrec); + outrec.pts = op; + return op; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void UpdateEdgeIntoAEL(Active ae) + { + ae.bot = ae.top; + ae.vertexTop = NextVertex(ae); + ae.top = ae.vertexTop!.pt; + ae.curX = ae.bot.X; + SetDx(ae); + + if (IsJoined(ae)) Split(ae, ae.bot); + + if (IsHorizontal(ae)) + { + if (!IsOpen(ae)) TrimHorz(ae, PreserveCollinear); + return; + } + InsertScanline(ae.top.Y); + + CheckJoinLeft(ae, ae.bot); + CheckJoinRight(ae, ae.bot, true); // (#500) + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Active? FindEdgeWithMatchingLocMin(Active e) + { + Active? result = e.nextInAEL; + while (result != null) + { + if (result.localMin == e.localMin) return result; + if (!IsHorizontal(result) && e.bot != result.bot) result = null; + else result = result.nextInAEL; + } + result = e.prevInAEL; + while (result != null) + { + if (result.localMin == e.localMin) return result; + if (!IsHorizontal(result) && e.bot != result.bot) return null; + result = result.prevInAEL; + } + return result; + } + + private OutPt? IntersectEdges(Active ae1, Active ae2, Point64 pt) + { + OutPt? resultOp = null; + + // MANAGE OPEN PATH INTERSECTIONS SEPARATELY ... + if (_hasOpenPaths && (IsOpen(ae1) || IsOpen(ae2))) + { + if (IsOpen(ae1) && IsOpen(ae2)) return null; + // the following line avoids duplicating quite a bit of code + if (IsOpen(ae2)) SwapActives(ref ae1, ref ae2); + if (IsJoined(ae2)) Split(ae2, pt); // needed for safety + + if (_cliptype == ClipType.Union) + { + if (!IsHotEdge(ae2)) return null; + } + else if (ae2.localMin.polytype == PathType.Subject) + return null; + + switch (_fillrule) + { + case FillRule.Positive: + if (ae2.windCount != 1) return null; break; + case FillRule.Negative: + if (ae2.windCount != -1) return null; break; + default: + if (Math.Abs(ae2.windCount) != 1) return null; break; + } + + // toggle contribution ... + if (IsHotEdge(ae1)) + { + resultOp = AddOutPt(ae1, pt); +#if USINGZ + SetZ(ae1, ae2, ref resultOp.pt); +#endif + if (IsFront(ae1)) + ae1.outrec!.frontEdge = null; + else + ae1.outrec!.backEdge = null; + ae1.outrec = null; + } + + // horizontal edges can pass under open paths at a LocMins + else if (pt == ae1.localMin.vertex.pt && + !IsOpenEnd(ae1.localMin.vertex)) + { + // find the other side of the LocMin and + // if it's 'hot' join up with it ... + Active? ae3 = FindEdgeWithMatchingLocMin(ae1); + if (ae3 != null && IsHotEdge(ae3)) + { + ae1.outrec = ae3.outrec; + if (ae1.windDx > 0) + SetSides(ae3.outrec!, ae1, ae3); + else + SetSides(ae3.outrec!, ae3, ae1); + return ae3.outrec!.pts; + } + + resultOp = StartOpenPath(ae1, pt); + } + else + resultOp = StartOpenPath(ae1, pt); + +#if USINGZ + SetZ(ae1, ae2, ref resultOp.pt); +#endif + return resultOp; + } + + // MANAGING CLOSED PATHS FROM HERE ON + if (IsJoined(ae1)) Split(ae1, pt); + if (IsJoined(ae2)) Split(ae2, pt); + + // UPDATE WINDING COUNTS... + + int oldE1WindCount, oldE2WindCount; + if (ae1.localMin.polytype == ae2.localMin.polytype) + { + if (_fillrule == FillRule.EvenOdd) + { + oldE1WindCount = ae1.windCount; + ae1.windCount = ae2.windCount; + ae2.windCount = oldE1WindCount; + } + else + { + if (ae1.windCount + ae2.windDx == 0) + ae1.windCount = -ae1.windCount; + else + ae1.windCount += ae2.windDx; + if (ae2.windCount - ae1.windDx == 0) + ae2.windCount = -ae2.windCount; + else + ae2.windCount -= ae1.windDx; + } + } + else + { + if (_fillrule != FillRule.EvenOdd) + ae1.windCount2 += ae2.windDx; + else + ae1.windCount2 = (ae1.windCount2 == 0 ? 1 : 0); + if (_fillrule != FillRule.EvenOdd) + ae2.windCount2 -= ae1.windDx; + else + ae2.windCount2 = (ae2.windCount2 == 0 ? 1 : 0); + } + + switch (_fillrule) + { + case FillRule.Positive: + oldE1WindCount = ae1.windCount; + oldE2WindCount = ae2.windCount; + break; + case FillRule.Negative: + oldE1WindCount = -ae1.windCount; + oldE2WindCount = -ae2.windCount; + break; + default: + oldE1WindCount = Math.Abs(ae1.windCount); + oldE2WindCount = Math.Abs(ae2.windCount); + break; + } + + bool e1WindCountIs0or1 = oldE1WindCount == 0 || oldE1WindCount == 1; + bool e2WindCountIs0or1 = oldE2WindCount == 0 || oldE2WindCount == 1; + + if ((!IsHotEdge(ae1) && !e1WindCountIs0or1) || (!IsHotEdge(ae2) && !e2WindCountIs0or1)) return null; + + // NOW PROCESS THE INTERSECTION ... + + // if both edges are 'hot' ... + if (IsHotEdge(ae1) && IsHotEdge(ae2)) + { + if ((oldE1WindCount != 0 && oldE1WindCount != 1) || (oldE2WindCount != 0 && oldE2WindCount != 1) || + (ae1.localMin.polytype != ae2.localMin.polytype && _cliptype != ClipType.Xor)) + { + resultOp = AddLocalMaxPoly(ae1, ae2, pt); +#if USINGZ + if (resultOp != null) + SetZ(ae1, ae2, ref resultOp.pt); +#endif + } + else if (IsFront(ae1) || (ae1.outrec == ae2.outrec)) + { + // this 'else if' condition isn't strictly needed but + // it's sensible to split polygons that ony touch at + // a common vertex (not at common edges). + resultOp = AddLocalMaxPoly(ae1, ae2, pt); +#if USINGZ + OutPt op2 = AddLocalMinPoly(ae1, ae2, pt); + if (resultOp != null) + SetZ(ae1, ae2, ref resultOp.pt); + SetZ(ae1, ae2, ref op2.pt); +#else + AddLocalMinPoly(ae1, ae2, pt); +#endif + } + else + { + // can't treat as maxima & minima + resultOp = AddOutPt(ae1, pt); +#if USINGZ + OutPt op2 = AddOutPt(ae2, pt); + SetZ(ae1, ae2, ref resultOp.pt); + SetZ(ae1, ae2, ref op2.pt); +#else + AddOutPt(ae2, pt); +#endif + SwapOutrecs(ae1, ae2); + } + } + + // if one or other edge is 'hot' ... + else if (IsHotEdge(ae1)) + { + resultOp = AddOutPt(ae1, pt); +#if USINGZ + SetZ(ae1, ae2, ref resultOp.pt); +#endif + SwapOutrecs(ae1, ae2); + } + else if (IsHotEdge(ae2)) + { + resultOp = AddOutPt(ae2, pt); +#if USINGZ + SetZ(ae1, ae2, ref resultOp.pt); +#endif + SwapOutrecs(ae1, ae2); + } + + // neither edge is 'hot' + else + { + long e1Wc2, e2Wc2; + switch (_fillrule) + { + case FillRule.Positive: + e1Wc2 = ae1.windCount2; + e2Wc2 = ae2.windCount2; + break; + case FillRule.Negative: + e1Wc2 = -ae1.windCount2; + e2Wc2 = -ae2.windCount2; + break; + default: + e1Wc2 = Math.Abs(ae1.windCount2); + e2Wc2 = Math.Abs(ae2.windCount2); + break; + } + + if (!IsSamePolyType(ae1, ae2)) + { + resultOp = AddLocalMinPoly(ae1, ae2, pt); +#if USINGZ + SetZ(ae1, ae2, ref resultOp.pt); +#endif + } + else if (oldE1WindCount == 1 && oldE2WindCount == 1) + { + resultOp = null; + switch (_cliptype) + { + case ClipType.Union: + if (e1Wc2 > 0 && e2Wc2 > 0) return null; + resultOp = AddLocalMinPoly(ae1, ae2, pt); + break; + + case ClipType.Difference: + if (((GetPolyType(ae1) == PathType.Clip) && (e1Wc2 > 0) && (e2Wc2 > 0)) || + ((GetPolyType(ae1) == PathType.Subject) && (e1Wc2 <= 0) && (e2Wc2 <= 0))) + { + resultOp = AddLocalMinPoly(ae1, ae2, pt); + } + + break; + + case ClipType.Xor: + resultOp = AddLocalMinPoly(ae1, ae2, pt); + break; + + default: // ClipType.Intersection: + if (e1Wc2 <= 0 || e2Wc2 <= 0) return null; + resultOp = AddLocalMinPoly(ae1, ae2, pt); + break; + } +#if USINGZ + if (resultOp != null) SetZ(ae1, ae2, ref resultOp.pt); +#endif + } + } + + return resultOp; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DeleteFromAEL(Active ae) + { + Active? prev = ae.prevInAEL; + Active? next = ae.nextInAEL; + if (prev == null && next == null && (ae != _actives)) return; // already deleted + if (prev != null) + prev.nextInAEL = next; + else + _actives = next; + if (next != null) next.prevInAEL = prev; + // delete &ae; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void AdjustCurrXAndCopyToSEL(long topY) + { + Active? ae = _actives; + _sel = ae; + while (ae != null) + { + ae.prevInSEL = ae.prevInAEL; + ae.nextInSEL = ae.nextInAEL; + ae.jump = ae.nextInSEL; + if (ae.joinWith == JoinWith.Left) + ae.curX = ae.prevInAEL!.curX; // this also avoids complications + else + ae.curX = TopX(ae, topY); + // NB don't update ae.curr.Y yet (see AddNewIntersectNode) + ae = ae.nextInAEL; + } + } + + protected void ExecuteInternal(ClipType ct, FillRule fillRule) + { + if (ct == ClipType.None) return; + _fillrule = fillRule; + _cliptype = ct; + Reset(); + if (!PopScanline(out long y)) return; + while (_succeeded) + { + InsertLocalMinimaIntoAEL(y); + Active? ae; + while (PopHorz(out ae)) DoHorizontal(ae!); + if (_horzSegList.Count > 0) + { + ConvertHorzSegsToJoins(); + _horzSegList.Clear(); + } + _currentBotY = y; // bottom of scanbeam + if (!PopScanline(out y)) + break; // y new top of scanbeam + DoIntersections(y); + DoTopOfScanbeam(y); + while (PopHorz(out ae)) DoHorizontal(ae!); + } + if (_succeeded) ProcessHorzJoins(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DoIntersections(long topY) + { + if (BuildIntersectList(topY)) + { + ProcessIntersectList(); + DisposeIntersectNodes(); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DisposeIntersectNodes() + { + _intersectList.Clear(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void AddNewIntersectNode(Active ae1, Active ae2, long topY) + { + if (!InternalClipper.GetIntersectPoint( + ae1.bot, ae1.top, ae2.bot, ae2.top, out Point64 ip)) + ip = new Point64(ae1.curX, topY); + + if (ip.Y > _currentBotY || ip.Y < topY) + { + double absDx1 = Math.Abs(ae1.dx); + double absDx2 = Math.Abs(ae2.dx); + if (absDx1 > 100 && absDx2 > 100) + { + if (absDx1 > absDx2) + ip = InternalClipper.GetClosestPtOnSegment(ip, ae1.bot, ae1.top); + else + ip = InternalClipper.GetClosestPtOnSegment(ip, ae2.bot, ae2.top); + } + else if (absDx1 > 100) + ip = InternalClipper.GetClosestPtOnSegment(ip, ae1.bot, ae1.top); + else if (absDx2 > 100) + ip = InternalClipper.GetClosestPtOnSegment(ip, ae2.bot, ae2.top); + else + { + if (ip.Y < topY) ip.Y = topY; + else ip.Y = _currentBotY; + if (absDx1 < absDx2) ip.X = TopX(ae1, ip.Y); + else ip.X = TopX(ae2, ip.Y); + } + } + IntersectNode node = new IntersectNode(ip, ae1, ae2); + _intersectList.Add(node); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Active? ExtractFromSEL(Active ae) + { + Active? res = ae.nextInSEL; + if (res != null) + res.prevInSEL = ae.prevInSEL; + ae.prevInSEL!.nextInSEL = res; + return res; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Insert1Before2InSEL(Active ae1, Active ae2) + { + ae1.prevInSEL = ae2.prevInSEL; + if (ae1.prevInSEL != null) + ae1.prevInSEL.nextInSEL = ae1; + ae1.nextInSEL = ae2; + ae2.prevInSEL = ae1; + } + + private bool BuildIntersectList(long topY) + { + if (_actives == null || _actives.nextInAEL == null) return false; + + // Calculate edge positions at the top of the current scanbeam, and from this + // we will determine the intersections required to reach these new positions. + AdjustCurrXAndCopyToSEL(topY); + + // Find all edge intersections in the current scanbeam using a stable merge + // sort that ensures only adjacent edges are intersecting. Intersect info is + // stored in FIntersectList ready to be processed in ProcessIntersectList. + // Re merge sorts see https://stackoverflow.com/a/46319131/359538 + + Active? left = _sel, right, lEnd, rEnd, currBase, prevBase, tmp; + + while (left!.jump != null) + { + prevBase = null; + while (left != null && left.jump != null) + { + currBase = left; + right = left.jump; + lEnd = right; + rEnd = right.jump; + left.jump = rEnd; + while (left != lEnd && right != rEnd) + { + if (right!.curX < left!.curX) + { + tmp = right.prevInSEL!; + for (; ; ) + { + AddNewIntersectNode(tmp, right, topY); + if (tmp == left) break; + tmp = tmp.prevInSEL!; + } + + tmp = right; + right = ExtractFromSEL(tmp); + lEnd = right; + Insert1Before2InSEL(tmp, left); + if (left == currBase) + { + currBase = tmp; + currBase.jump = rEnd; + if (prevBase == null) _sel = currBase; + else prevBase.jump = currBase; + } + } + else left = left.nextInSEL; + } + + prevBase = currBase; + left = rEnd; + } + left = _sel; + } + + return _intersectList.Count > 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ProcessIntersectList() + { + // We now have a list of intersections required so that edges will be + // correctly positioned at the top of the scanbeam. However, it's important + // that edge intersections are processed from the bottom up, but it's also + // crucial that intersections only occur between adjacent edges. + + // First we do a quicksort so intersections proceed in a bottom up order ... + _intersectList.Sort(new IntersectListSort()); + + // Now as we process these intersections, we must sometimes adjust the order + // to ensure that intersecting edges are always adjacent ... + for (int i = 0; i < _intersectList.Count; ++i) + { + if (!EdgesAdjacentInAEL(_intersectList[i])) + { + int j = i + 1; + while (!EdgesAdjacentInAEL(_intersectList[j])) j++; + // swap + (_intersectList[j], _intersectList[i]) = + (_intersectList[i], _intersectList[j]); + } + + IntersectNode node = _intersectList[i]; + IntersectEdges(node.edge1, node.edge2, node.pt); + SwapPositionsInAEL(node.edge1, node.edge2); + + node.edge1.curX = node.pt.X; + node.edge2.curX = node.pt.X; + CheckJoinLeft(node.edge2, node.pt, true); + CheckJoinRight(node.edge1, node.pt, true); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void SwapPositionsInAEL(Active ae1, Active ae2) + { + // preconditon: ae1 must be immediately to the left of ae2 + Active? next = ae2.nextInAEL; + if (next != null) next.prevInAEL = ae1; + Active? prev = ae1.prevInAEL; + if (prev != null) prev.nextInAEL = ae2; + ae2.prevInAEL = prev; + ae2.nextInAEL = ae1; + ae1.prevInAEL = ae2; + ae1.nextInAEL = next; + if (ae2.prevInAEL == null) _actives = ae2; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool ResetHorzDirection(Active horz, Vertex? vertexMax, + out long leftX, out long rightX) + { + if (horz.bot.X == horz.top.X) + { + // the horizontal edge is going nowhere ... + leftX = horz.curX; + rightX = horz.curX; + Active? ae = horz.nextInAEL; + while (ae != null && ae.vertexTop != vertexMax) + ae = ae.nextInAEL; + return ae != null; + } + + if (horz.curX < horz.top.X) + { + leftX = horz.curX; + rightX = horz.top.X; + return true; + } + leftX = horz.top.X; + rightX = horz.curX; + return false; // right to left + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void TrimHorz(Active horzEdge, bool preserveCollinear) + { + bool wasTrimmed = false; + Point64 pt = NextVertex(horzEdge).pt; + + while (pt.Y == horzEdge.top.Y) + { + // always trim 180 deg. spikes (in closed paths) + // but otherwise break if preserveCollinear = true + if (preserveCollinear && + (pt.X < horzEdge.top.X) != (horzEdge.bot.X < horzEdge.top.X)) + break; + + horzEdge.vertexTop = NextVertex(horzEdge); + horzEdge.top = pt; + wasTrimmed = true; + if (IsMaxima(horzEdge)) break; + pt = NextVertex(horzEdge).pt; + } + if (wasTrimmed) SetDx(horzEdge); // +/-infinity + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void AddToHorzSegList(OutPt op) + { + if (op.outrec.isOpen) return; + _horzSegList.Add(new HorzSegment(op)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private OutPt GetLastOp(Active hotEdge) + { + OutRec outrec = hotEdge.outrec!; + return (hotEdge == outrec.frontEdge) ? + outrec.pts! : outrec.pts!.next!; + } + +private void DoHorizontal(Active horz) + /******************************************************************************* + * Notes: Horizontal edges (HEs) at scanline intersections (i.e. at the top or * + * bottom of a scanbeam) are processed as if layered.The order in which HEs * + * are processed doesn't matter. HEs intersect with the bottom vertices of * + * other HEs[#] and with non-horizontal edges [*]. Once these intersections * + * are completed, intermediate HEs are 'promoted' to the next edge in their * + * bounds, and they in turn may be intersected[%] by other HEs. * + * * + * eg: 3 horizontals at a scanline: / | / / * + * | / | (HE3)o ========%========== o * + * o ======= o(HE2) / | / / * + * o ============#=========*======*========#=========o (HE1) * + * / | / | / * + *******************************************************************************/ + { + Point64 pt; + bool horzIsOpen = IsOpen(horz); + long Y = horz.bot.Y; + + Vertex? vertex_max = horzIsOpen ? + GetCurrYMaximaVertex_Open(horz) : + GetCurrYMaximaVertex(horz); + + bool isLeftToRight = + ResetHorzDirection(horz, vertex_max, out long leftX, out long rightX); + + if (IsHotEdge(horz)) + { +#if USINGZ + OutPt op = AddOutPt(horz, new Point64(horz.curX, Y, horz.bot.Z)); +#else + OutPt op = AddOutPt(horz, new Point64(horz.curX, Y)); +#endif + AddToHorzSegList(op); + } + + for (; ; ) + { + // loops through consec. horizontal edges (if open) + Active? ae = isLeftToRight ? horz.nextInAEL : horz.prevInAEL; + + while (ae != null) + { + if (ae.vertexTop == vertex_max) + { + // do this first!! + if (IsHotEdge(horz) && IsJoined(ae!)) Split(ae, ae.top); + + if (IsHotEdge(horz)) + { + while (horz.vertexTop != vertex_max) + { + AddOutPt(horz, horz.top); + UpdateEdgeIntoAEL(horz); + } + if (isLeftToRight) + AddLocalMaxPoly(horz, ae, horz.top); + else + AddLocalMaxPoly(ae, horz, horz.top); + } + DeleteFromAEL(ae); + DeleteFromAEL(horz); + return; + } + + // if horzEdge is a maxima, keep going until we reach + // its maxima pair, otherwise check for break conditions + if (vertex_max != horz.vertexTop || IsOpenEnd(horz)) + { + // otherwise stop when 'ae' is beyond the end of the horizontal line + if ((isLeftToRight && ae.curX > rightX) || + (!isLeftToRight && ae.curX < leftX)) break; + + if (ae.curX == horz.top.X && !IsHorizontal(ae)) + { + pt = NextVertex(horz).pt; + + // to maximize the possibility of putting open edges into + // solutions, we'll only break if it's past HorzEdge's end + if (IsOpen(ae) && !IsSamePolyType(ae, horz) && !IsHotEdge(ae)) + { + if ((isLeftToRight && (TopX(ae, pt.Y) > pt.X)) || + (!isLeftToRight && (TopX(ae, pt.Y) < pt.X))) break; + } + // otherwise for edges at horzEdge's end, only stop when horzEdge's + // outslope is greater than e's slope when heading right or when + // horzEdge's outslope is less than e's slope when heading left. + else if ((isLeftToRight && (TopX(ae, pt.Y) >= pt.X)) || + (!isLeftToRight && (TopX(ae, pt.Y) <= pt.X))) break; + } + } + + pt = new Point64(ae.curX, Y); + + if (isLeftToRight) + { + IntersectEdges(horz, ae, pt); + SwapPositionsInAEL(horz, ae); + CheckJoinLeft(ae, pt); + horz.curX = ae.curX; + ae = horz.nextInAEL; + } + else + { + IntersectEdges(ae, horz, pt); + SwapPositionsInAEL(ae, horz); + CheckJoinRight(ae, pt); + horz.curX = ae.curX; + ae = horz.prevInAEL; + } + + if (IsHotEdge(horz)) + AddToHorzSegList(GetLastOp(horz)); + + } // we've reached the end of this horizontal + + // check if we've finished looping + // through consecutive horizontals + if (horzIsOpen && IsOpenEnd(horz)) // ie open at top + { + if (IsHotEdge(horz)) + { + AddOutPt(horz, horz.top); + if (IsFront(horz)) + horz.outrec!.frontEdge = null; + else + horz.outrec!.backEdge = null; + horz.outrec = null; + } + DeleteFromAEL(horz); + return; + } + else if (NextVertex(horz).pt.Y != horz.top.Y) + break; + + //still more horizontals in bound to process ... + if (IsHotEdge(horz)) + AddOutPt(horz, horz.top); + + UpdateEdgeIntoAEL(horz); + + isLeftToRight = ResetHorzDirection(horz, + vertex_max, out leftX, out rightX); + + } // end for loop and end of (possible consecutive) horizontals + + if (IsHotEdge(horz)) + { + OutPt op = AddOutPt(horz, horz.top); + AddToHorzSegList(op); + } + + UpdateEdgeIntoAEL(horz); // this is the end of an intermediate horiz. + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DoTopOfScanbeam(long y) + { + _sel = null; // sel_ is reused to flag horizontals (see PushHorz below) + Active? ae = _actives; + while (ae != null) + { + // NB 'ae' will never be horizontal here + if (ae.top.Y == y) + { + ae.curX = ae.top.X; + if (IsMaxima(ae)) + { + ae = DoMaxima(ae); // TOP OF BOUND (MAXIMA) + continue; + } + + // INTERMEDIATE VERTEX ... + if (IsHotEdge(ae)) + AddOutPt(ae, ae.top); + UpdateEdgeIntoAEL(ae); + if (IsHorizontal(ae)) + PushHorz(ae); // horizontals are processed later + } + else // i.e. not the top of the edge + ae.curX = TopX(ae, y); + + ae = ae.nextInAEL; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private Active? DoMaxima(Active ae) + { + Active? prevE; + Active? nextE, maxPair; + prevE = ae.prevInAEL; + nextE = ae.nextInAEL; + + if (IsOpenEnd(ae)) + { + if (IsHotEdge(ae)) AddOutPt(ae, ae.top); + if (!IsHorizontal(ae)) + { + if (IsHotEdge(ae)) + { + if (IsFront(ae)) + ae.outrec!.frontEdge = null; + else + ae.outrec!.backEdge = null; + ae.outrec = null; + } + DeleteFromAEL(ae); + } + return nextE; + } + + maxPair = GetMaximaPair(ae); + if (maxPair == null) return nextE; // eMaxPair is horizontal + + if (IsJoined(ae)) Split(ae, ae.top); + if (IsJoined(maxPair)) Split(maxPair, maxPair.top); + + // only non-horizontal maxima here. + // process any edges between maxima pair ... + while (nextE != maxPair) + { + IntersectEdges(ae, nextE!, ae.top); + SwapPositionsInAEL(ae, nextE!); + nextE = ae.nextInAEL; + } + + if (IsOpen(ae)) + { + if (IsHotEdge(ae)) + AddLocalMaxPoly(ae, maxPair, ae.top); + DeleteFromAEL(maxPair); + DeleteFromAEL(ae); + return (prevE != null ? prevE.nextInAEL : _actives); + } + + // here ae.nextInAel == ENext == EMaxPair ... + if (IsHotEdge(ae)) + AddLocalMaxPoly(ae, maxPair, ae.top); + + DeleteFromAEL(ae); + DeleteFromAEL(maxPair); + return (prevE != null ? prevE.nextInAEL : _actives); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsJoined(Active e) + { + return e.joinWith != JoinWith.None; + } + + private void Split(Active e, Point64 currPt) + { + if (e.joinWith == JoinWith.Right) + { + e.joinWith = JoinWith.None; + e.nextInAEL!.joinWith = JoinWith.None; + AddLocalMinPoly(e, e.nextInAEL, currPt, true); + } + else + { + e.joinWith = JoinWith.None; + e.prevInAEL!.joinWith = JoinWith.None; + AddLocalMinPoly(e.prevInAEL, e, currPt, true); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void CheckJoinLeft(Active e, + Point64 pt, bool checkCurrX = false) + { + Active? prev = e.prevInAEL; + if (prev == null || IsOpen(e) || IsOpen(prev) || + !IsHotEdge(e) || !IsHotEdge(prev)) return; + if ((pt.Y < e.top.Y + 2 || pt.Y < prev.top.Y + 2) && // avoid trivial joins + ((e.bot.Y > pt.Y) || (prev.bot.Y > pt.Y))) return; // (#490) + + if (checkCurrX) + { + if (Clipper.PerpendicDistFromLineSqrd(pt, prev.bot, prev.top) > 0.25) return; + } + else if (e.curX != prev.curX) return; + if (InternalClipper.CrossProduct(e.top, pt, prev.top) != 0) return; + + if (e.outrec!.idx == prev.outrec!.idx) + AddLocalMaxPoly(prev, e, pt); + else if (e.outrec.idx < prev.outrec.idx) + JoinOutrecPaths(e, prev); + else + JoinOutrecPaths(prev, e); + prev.joinWith = JoinWith.Right; + e.joinWith = JoinWith.Left; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void CheckJoinRight(Active e, + Point64 pt, bool checkCurrX = false) + { + Active? next = e.nextInAEL; + if (IsOpen(e) || !IsHotEdge(e) || IsJoined(e) || + next == null || IsOpen(next) || !IsHotEdge(next)) return; + if ((pt.Y < e.top.Y + 2 || pt.Y < next.top.Y + 2) && // avoid trivial joins + ((e.bot.Y > pt.Y) || (next.bot.Y > pt.Y))) return; // (#490) + + if (checkCurrX) + { + if (Clipper.PerpendicDistFromLineSqrd(pt, next.bot, next.top) > 0.25) return; + } + else if (e.curX != next.curX) return; + if (InternalClipper.CrossProduct(e.top, pt, next.top) != 0) + return; + + if (e.outrec!.idx == next.outrec!.idx) + AddLocalMaxPoly(e, next, pt); + else if (e.outrec.idx < next.outrec.idx) + JoinOutrecPaths(e, next); + else + JoinOutrecPaths(next, e); + e.joinWith = JoinWith.Right; + next.joinWith = JoinWith.Left; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void FixOutRecPts(OutRec outrec) + { + OutPt op = outrec.pts!; + do + { + op!.outrec = outrec; + op = op.next!; + } while (op != outrec.pts); + } + + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool SetHorzSegHeadingForward(HorzSegment hs, OutPt opP, OutPt opN) + { + if (opP.pt.X == opN.pt.X) return false; + if (opP.pt.X < opN.pt.X) + { + hs.leftOp = opP; + hs.rightOp = opN; + hs.leftToRight = true; + } + else + { + hs.leftOp = opN; + hs.rightOp = opP; + hs.leftToRight = false; + } + return true; + } + + private static bool UpdateHorzSegment(HorzSegment hs) + { + OutPt op = hs.leftOp!; + OutRec outrec = GetRealOutRec(op.outrec)!; + bool outrecHasEdges = outrec.frontEdge != null; + long curr_y = op.pt.Y; + OutPt opP = op, opN = op; + if (outrecHasEdges) + { + OutPt opA = outrec.pts!, opZ = opA.next!; + while (opP != opZ && opP.prev.pt.Y == curr_y) + opP = opP.prev; + while (opN != opA && opN.next!.pt.Y == curr_y) + opN = opN.next; + } + else + { + while (opP.prev != opN && opP.prev.pt.Y == curr_y) + opP = opP.prev; + while (opN.next != opP && opN.next!.pt.Y == curr_y) + opN = opN.next; + } + bool result = + SetHorzSegHeadingForward(hs, opP, opN) && + hs.leftOp!.horz == null; + + if (result) + hs.leftOp!.horz = hs; + else + hs.rightOp = null; // (for sorting) + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static OutPt DuplicateOp(OutPt op, bool insert_after) + { + OutPt result = new OutPt(op.pt, op.outrec); + if (insert_after) + { + result.next = op.next; + result.next!.prev = result; + result.prev = op; + op.next = result; + } + else + { + result.prev = op.prev; + result.prev.next = result; + result.next = op; + op.prev = result; + } + return result; + } + + private void ConvertHorzSegsToJoins() + { + int k = 0; + foreach (HorzSegment hs in _horzSegList) + if (UpdateHorzSegment(hs)) k++; + if (k < 2) return; + _horzSegList.Sort(new HorzSegSorter()); + + for (int i = 0; i < k -1; i++) + { + HorzSegment hs1 = _horzSegList[i]; + // for each HorzSegment, find others that overlap + for (int j = i + 1; j < k; j++) + { + HorzSegment hs2 = _horzSegList[j]; + if ((hs2.leftOp!.pt.X >= hs1.rightOp!.pt.X) || + (hs2.leftToRight == hs1.leftToRight) || + (hs2.rightOp!.pt.X <= hs1.leftOp!.pt.X)) continue; + long curr_y = hs1.leftOp.pt.Y; + if ((hs1).leftToRight) + { + while (hs1.leftOp.next!.pt.Y == curr_y && + hs1.leftOp.next.pt.X <= hs2.leftOp.pt.X) + hs1.leftOp = hs1.leftOp.next; + while (hs2.leftOp.prev.pt.Y == curr_y && + hs2.leftOp.prev.pt.X <= hs1.leftOp.pt.X) + (hs2).leftOp = (hs2).leftOp.prev; + HorzJoin join = new HorzJoin( + DuplicateOp((hs1).leftOp, true), + DuplicateOp((hs2).leftOp, false)); + _horzJoinList.Add(join); + } + else + { + while (hs1.leftOp.prev.pt.Y == curr_y && + hs1.leftOp.prev.pt.X <= hs2.leftOp.pt.X) + hs1.leftOp = hs1.leftOp.prev; + while (hs2.leftOp.next!.pt.Y == curr_y && + hs2.leftOp.next.pt.X <= (hs1).leftOp.pt.X) + hs2.leftOp = (hs2).leftOp.next; + HorzJoin join = new HorzJoin( + DuplicateOp((hs2).leftOp, true), + DuplicateOp((hs1).leftOp, false)); + _horzJoinList.Add(join); + } + } + } + } + + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Path64 GetCleanPath(OutPt op) + { + Path64 result = new Path64(); + OutPt op2 = op; + while (op2.next != op && + ((op2.pt.X == op2.next!.pt.X && op2.pt.X == op2.prev.pt.X) || + (op2.pt.Y == op2.next.pt.Y && op2.pt.Y == op2.prev.pt.Y))) op2 = op2.next; + result.Add(op2.pt); + OutPt prevOp = op2; + op2 = op2.next; + while (op2 != op) + { + if ((op2.pt.X != op2.next!.pt.X || op2.pt.X != prevOp.pt.X) && + (op2.pt.Y != op2.next.pt.Y || op2.pt.Y != prevOp.pt.Y)) + { + result.Add(op2.pt); + prevOp = op2; + } + op2 = op2.next; + } + return result; + } + + + private static PointInPolygonResult PointInOpPolygon(Point64 pt, OutPt op) + { + if (op == op.next || op.prev == op.next) + return PointInPolygonResult.IsOutside; + + OutPt op2 = op; + do + { + if (op.pt.Y != pt.Y) break; + op = op.next!; + } while (op != op2); + if (op.pt.Y == pt.Y) // not a proper polygon + return PointInPolygonResult.IsOutside; + + // must be above or below to get here + bool isAbove = op.pt.Y < pt.Y, startingAbove = isAbove; + int val = 0; + + op2 = op.next!; + while (op2 != op) + { + if (isAbove) + while (op2 != op && op2.pt.Y < pt.Y) op2 = op2.next!; + else + while (op2 != op && op2.pt.Y > pt.Y) op2 = op2.next!; + if (op2 == op) break; + + // must have touched or crossed the pt.Y horizonal + // and this must happen an even number of times + + if (op2.pt.Y == pt.Y) // touching the horizontal + { + if (op2.pt.X == pt.X || (op2.pt.Y == op2.prev.pt.Y && + (pt.X < op2.prev.pt.X) != (pt.X < op2.pt.X))) + return PointInPolygonResult.IsOn; + op2 = op2.next!; + if (op2 == op) break; + continue; + } + + if (op2.pt.X <= pt.X || op2.prev.pt.X <= pt.X) + { + if ((op2.prev.pt.X < pt.X && op2.pt.X < pt.X)) + val = 1 - val; // toggle val + else + { + double d = InternalClipper.CrossProduct(op2.prev.pt, op2.pt, pt); + if (d == 0) return PointInPolygonResult.IsOn; + if ((d < 0) == isAbove) val = 1 - val; + } + } + isAbove = !isAbove; + op2 = op2.next!; + } + + if (isAbove != startingAbove) + { + double d = InternalClipper.CrossProduct(op2.prev.pt, op2.pt, pt); + if (d == 0) return PointInPolygonResult.IsOn; + if ((d < 0) == isAbove) val = 1 - val; + } + + if (val == 0) return PointInPolygonResult.IsOutside; + else return PointInPolygonResult.IsInside; + } + + private static bool Path1InsidePath2(OutPt op1, OutPt op2) + { + // we need to make some accommodation for rounding errors + // so we won't jump if the first vertex is found outside + PointInPolygonResult result; + int outside_cnt = 0; + OutPt op = op1; + do + { + result = PointInOpPolygon(op.pt, op2); + if (result == PointInPolygonResult.IsOutside) ++outside_cnt; + else if (result == PointInPolygonResult.IsInside) --outside_cnt; + op = op.next!; + } while (op != op1 && Math.Abs(outside_cnt) < 2); + if (Math.Abs(outside_cnt) > 1) return (outside_cnt < 0); + // since path1's location is still equivocal, check its midpoint + Point64 mp = GetBounds(GetCleanPath(op1)).MidPoint(); + Path64 path2 = GetCleanPath(op2); + return InternalClipper.PointInPolygon(mp, path2) != PointInPolygonResult.IsOutside; + } + + private void MoveSplits(OutRec fromOr, OutRec toOr) + { + if (fromOr.splits == null) return; + toOr.splits ??= new List(); + foreach (int i in fromOr.splits) + toOr.splits.Add(i); + fromOr.splits = null; + } + + private void ProcessHorzJoins() + { + foreach (HorzJoin j in _horzJoinList) + { + OutRec or1 = GetRealOutRec(j.op1!.outrec)!; + OutRec or2 = GetRealOutRec(j.op2!.outrec)!; + + OutPt op1b = j.op1.next!; + OutPt op2b = j.op2.prev!; + j.op1.next = j.op2; + j.op2.prev = j.op1; + op1b.prev = op2b; + op2b.next = op1b; + + if (or1 == or2) // 'join' is really a split + { + or2 = NewOutRec(); + or2.pts = op1b; + FixOutRecPts(or2); + + //if or1->pts has moved to or2 then update or1->pts!! + if (or1.pts!.outrec == or2) + { + or1.pts = j.op1; + or1.pts.outrec = or1; + } + + if (_using_polytree) //#498, #520, #584, D#576, #618 + { + if (Path1InsidePath2(or1.pts, or2.pts)) + { + //swap or1's & or2's pts + (or2.pts, or1.pts) = (or1.pts, or2.pts); + FixOutRecPts(or1); + FixOutRecPts(or2); + //or2 is now inside or1 + or2.owner = or1; + } + else if (Path1InsidePath2(or2.pts, or1.pts)) + or2.owner = or1; + else + or2.owner = or1.owner; + + or1.splits ??= new List(); + or1.splits.Add(or2.idx); + } + else + or2.owner = or1; + } + else + { + or2.pts = null; + if (_using_polytree) + { + SetOwner(or2, or1); + MoveSplits(or2, or1); //#618 + } + else + or2.owner = or1; + } + } + } + + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool PtsReallyClose(Point64 pt1, Point64 pt2) + { + return (Math.Abs(pt1.X - pt2.X) < 2) && (Math.Abs(pt1.Y - pt2.Y) < 2); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsVerySmallTriangle(OutPt op) + { + return op.next!.next == op.prev && + (PtsReallyClose(op.prev.pt, op.next.pt) || + PtsReallyClose(op.pt, op.next.pt) || + PtsReallyClose(op.pt, op.prev.pt)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsValidClosedPath(OutPt? op) + { + return (op != null && op.next != op && + (op.next != op.prev || !IsVerySmallTriangle(op))); + } + + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static OutPt? DisposeOutPt(OutPt op) + { + OutPt? result = (op.next == op ? null : op.next); + op.prev.next = op.next; + op.next!.prev = op.prev; + // op == null; + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void CleanCollinear(OutRec? outrec) + { + outrec = GetRealOutRec(outrec); + + if (outrec == null || outrec.isOpen) return; + + if(!IsValidClosedPath(outrec.pts)) + { + outrec.pts = null; + return; + } + + OutPt startOp = outrec.pts!; + OutPt? op2 = startOp; + for (; ; ) + { + // NB if preserveCollinear == true, then only remove 180 deg. spikes + if ((InternalClipper.CrossProduct(op2!.prev.pt, op2.pt, op2.next!.pt) == 0) && + ((op2.pt == op2.prev.pt) || (op2.pt == op2.next.pt) || !PreserveCollinear || + (InternalClipper.DotProduct(op2.prev.pt, op2.pt, op2.next.pt) < 0))) + { + if (op2 == outrec.pts) + outrec.pts = op2.prev; + op2 = DisposeOutPt(op2); + if (!IsValidClosedPath(op2)) + { + outrec.pts = null; + return; + } + startOp = op2!; + continue; + } + op2 = op2.next; + if (op2 == startOp) break; + } + FixSelfIntersects(outrec); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DoSplitOp(OutRec outrec, OutPt splitOp) + { + // splitOp.prev <=> splitOp && + // splitOp.next <=> splitOp.next.next are intersecting + OutPt prevOp = splitOp.prev; + OutPt nextNextOp = splitOp.next!.next!; + outrec.pts = prevOp; + OutPt result = prevOp; + + InternalClipper.GetIntersectPoint( + prevOp.pt, splitOp.pt, splitOp.next.pt, nextNextOp.pt, out Point64 ip); + +#if USINGZ + if (_zCallback != null) + _zCallback(prevOp.pt, splitOp.pt, splitOp.next.pt, nextNextOp.pt, ref ip); +#endif + + double area1 = Area(prevOp); + double absArea1 = Math.Abs(area1); + + if (absArea1 < 2) + { + outrec.pts = null; + return; + } + + double area2 = AreaTriangle(ip, splitOp.pt, splitOp.next.pt); + double absArea2 = Math.Abs(area2); + + // de-link splitOp and splitOp.next from the path + // while inserting the intersection point + if (ip == prevOp.pt || ip == nextNextOp.pt) + { + nextNextOp.prev = prevOp; + prevOp.next = nextNextOp; + } + else + { + OutPt newOp2 = new OutPt(ip, outrec) { prev = prevOp, next = nextNextOp }; + nextNextOp.prev = newOp2; + prevOp.next = newOp2; + } + + // nb: area1 is the path's area *before* splitting, whereas area2 is + // the area of the triangle containing splitOp & splitOp.next. + // So the only way for these areas to have the same sign is if + // the split triangle is larger than the path containing prevOp or + // if there's more than one self=intersection. + if (absArea2 > 1 && + (absArea2 > absArea1 || + ((area2 > 0) == (area1 > 0)))) + { + OutRec newOutRec = NewOutRec(); + newOutRec.owner = outrec.owner; + splitOp.outrec = newOutRec; + splitOp.next.outrec = newOutRec; + + OutPt newOp = new OutPt(ip, newOutRec) { prev = splitOp.next, next = splitOp }; + newOutRec.pts = newOp; + splitOp.prev = newOp; + splitOp.next.next = newOp; + + if (_using_polytree) + { + if (Path1InsidePath2(prevOp, newOp)) + { + newOutRec.splits ??= new List(); + newOutRec.splits.Add(outrec.idx); + } + else + { + outrec.splits ??= new List(); + outrec.splits.Add(newOutRec.idx); + } + } + } + //else { splitOp = null; splitOp.next = null; } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void FixSelfIntersects(OutRec outrec) + { + OutPt op2 = outrec.pts!; + for (; ; ) + { + // triangles can't self-intersect + if (op2.prev == op2.next!.next) break; + if (InternalClipper.SegsIntersect(op2.prev.pt, + op2.pt, op2.next.pt, op2.next.next!.pt)) + { + DoSplitOp(outrec, op2); + if (outrec.pts == null) return; + op2 = outrec.pts; + continue; + } + else + op2 = op2.next; + if (op2 == outrec.pts) break; + } + } + + internal static bool BuildPath(OutPt? op, bool reverse, bool isOpen, Path64 path) + { + if (op == null || op.next == op || (!isOpen && op.next == op.prev)) return false; + path.Clear(); + + Point64 lastPt; + OutPt op2; + if (reverse) + { + lastPt = op.pt; + op2 = op.prev; + } + else + { + op = op.next!; + lastPt = op.pt; + op2 = op.next!; + } + path.Add(lastPt); + + while (op2 != op) + { + if (op2.pt != lastPt) + { + lastPt = op2.pt; + path.Add(lastPt); + } + if (reverse) + op2 = op2.prev; + else + op2 = op2.next!; + } + + if (path.Count == 3 && IsVerySmallTriangle(op2)) return false; + else return true; + } + + protected bool BuildPaths(Paths64 solutionClosed, Paths64 solutionOpen) + { + solutionClosed.Clear(); + solutionOpen.Clear(); + solutionClosed.Capacity = _outrecList.Count; + solutionOpen.Capacity = _outrecList.Count; + + int i = 0; + // _outrecList.Count is not static here because + // CleanCollinear can indirectly add additional OutRec + while (i < _outrecList.Count) + { + OutRec outrec = _outrecList[i++]; + if (outrec.pts == null) continue; + + Path64 path = new Path64(); + if (outrec.isOpen) + { + if (BuildPath(outrec.pts, ReverseSolution, true, path)) + solutionOpen.Add(path); + } + else + { + CleanCollinear(outrec); + // closed paths should always return a Positive orientation + // except when ReverseSolution == true + if (BuildPath(outrec.pts, ReverseSolution, false, path)) + solutionClosed.Add(path); + } + } + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Rect64 GetBounds(Path64 path) + { + if (path.Count == 0) return new Rect64(); + Rect64 result = Clipper.InvalidRect64; + foreach (Point64 pt in path) + { + if (pt.X < result.left) result.left = pt.X; + if (pt.X > result.right) result.right = pt.X; + if (pt.Y < result.top) result.top = pt.Y; + if (pt.Y > result.bottom) result.bottom = pt.Y; + } + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool CheckBounds(OutRec outrec) + { + if (outrec.pts == null) return false; + if (!outrec.bounds.IsEmpty()) return true; + CleanCollinear(outrec); + if (outrec.pts == null || + !BuildPath(outrec.pts, ReverseSolution, false, outrec.path)) + return false; + outrec.bounds = GetBounds(outrec.path); + return true; + } + + private bool CheckSplitOwner(OutRec outrec, List? splits) + { + foreach (int i in splits!) + { + OutRec? split = GetRealOutRec(_outrecList[i]); + if (split == null || split == outrec || split.recursiveSplit == outrec) continue; + split.recursiveSplit = outrec; //#599 + if (split!.splits != null && CheckSplitOwner(outrec, split.splits)) return true; + if (IsValidOwner(outrec, split) && + CheckBounds(split) && + split.bounds.Contains(outrec.bounds) && + Path1InsidePath2(outrec.pts!, split.pts!)) + { + outrec.owner = split; //found in split + return true; + } + } + return false; + } + private void RecursiveCheckOwners(OutRec outrec, PolyPathBase polypath) + { + // pre-condition: outrec will have valid bounds + // post-condition: if a valid path, outrec will have a polypath + + if (outrec.polypath != null || outrec.bounds.IsEmpty()) return; + + while (outrec.owner != null) + { + if (outrec.owner.splits != null && + CheckSplitOwner(outrec, outrec.owner.splits)) break; + else if (outrec.owner.pts != null && CheckBounds(outrec.owner) && + Path1InsidePath2(outrec.pts!, outrec.owner.pts!)) break; + outrec.owner = outrec.owner.owner; + } + + if (outrec.owner != null) + { + if (outrec.owner.polypath == null) + RecursiveCheckOwners(outrec.owner, polypath); + outrec.polypath = outrec.owner.polypath!.AddChild(outrec.path); + } + else + outrec.polypath = polypath.AddChild(outrec.path); + } + + protected void BuildTree(PolyPathBase polytree, Paths64 solutionOpen) + { + polytree.Clear(); + solutionOpen.Clear(); + if (_hasOpenPaths) + solutionOpen.Capacity = _outrecList.Count; + + int i = 0; + // _outrecList.Count is not static here because + // CheckBounds below can indirectly add additional + // OutRec (via FixOutRecPts & CleanCollinear) + while (i < _outrecList.Count) + { + OutRec outrec = _outrecList[i++]; + if (outrec.pts == null) continue; + + if (outrec.isOpen) + { + Path64 open_path = new Path64(); + if (BuildPath(outrec.pts, ReverseSolution, true, open_path)) + solutionOpen.Add(open_path); + continue; + } + if (CheckBounds(outrec)) + RecursiveCheckOwners(outrec, polytree); + } + } + + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Rect64 GetBounds() + { + Rect64 bounds = Clipper.InvalidRect64; + foreach (Vertex t in _vertexList) + { + Vertex v = t; + do + { + if (v.pt.X < bounds.left) bounds.left = v.pt.X; + if (v.pt.X > bounds.right) bounds.right = v.pt.X; + if (v.pt.Y < bounds.top) bounds.top = v.pt.Y; + if (v.pt.Y > bounds.bottom) bounds.bottom = v.pt.Y; + v = v.next!; + } while (v != t); + } + return bounds.IsEmpty() ? new Rect64(0, 0, 0, 0) : bounds; + } + + } // ClipperBase class + + + public class Clipper64 : ClipperBase + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal new void AddPath(Path64 path, PathType polytype, bool isOpen = false) + { + base.AddPath(path, polytype, isOpen); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public new void AddReuseableData(ReuseableDataContainer64 reuseableData) + { + base.AddReuseableData(reuseableData); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal new void AddPaths(Paths64 paths, PathType polytype, bool isOpen = false) + { + base.AddPaths(paths, polytype, isOpen); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void AddSubject(Paths64 paths) + { + AddPaths(paths, PathType.Subject); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void AddOpenSubject(Paths64 paths) + { + AddPaths(paths, PathType.Subject, true); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void AddClip(Paths64 paths) + { + AddPaths(paths, PathType.Clip); + } + + public bool Execute(ClipType clipType, FillRule fillRule, + Paths64 solutionClosed, Paths64 solutionOpen) + { + solutionClosed.Clear(); + solutionOpen.Clear(); + try + { + ExecuteInternal(clipType, fillRule); + BuildPaths(solutionClosed, solutionOpen); + } + catch + { + _succeeded = false; + } + + ClearSolutionOnly(); + return _succeeded; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Execute(ClipType clipType, FillRule fillRule, Paths64 solutionClosed) + { + return Execute(clipType, fillRule, solutionClosed, new Paths64()); + } + + public bool Execute(ClipType clipType, FillRule fillRule, PolyTree64 polytree, Paths64 openPaths) + { + polytree.Clear(); + openPaths.Clear(); + _using_polytree = true; + try + { + ExecuteInternal(clipType, fillRule); + BuildTree(polytree, openPaths); + } + catch + { + _succeeded = false; + } + + ClearSolutionOnly(); + return _succeeded; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Execute(ClipType clipType, FillRule fillRule, PolyTree64 polytree) + { + return Execute(clipType, fillRule, polytree, new Paths64()); + } + +#if USINGZ + public ZCallback64? ZCallback { + get { return this._zCallback; } + set { this._zCallback = value; } + } +#endif + + } // Clipper64 class + + public class ClipperD : ClipperBase + { + private readonly string precision_range_error = "Error: Precision is out of range."; + + private readonly double _scale; + private readonly double _invScale; + +#if USINGZ + public delegate void ZCallbackD(PointD bot1, PointD top1, + PointD bot2, PointD top2, ref PointD intersectPt); + + public ZCallbackD? ZCallback { get; set; } + + private void CheckZCallback() + { + if (ZCallback != null) + _zCallback = ZCB; + else + _zCallback = null; + } +#endif + + public ClipperD(int roundingDecimalPrecision = 2) + { + if (roundingDecimalPrecision < -8 || roundingDecimalPrecision > 8) + throw new ClipperLibException(precision_range_error); + _scale = Math.Pow(10, roundingDecimalPrecision); + _invScale = 1 / _scale; + } + +#if USINGZ + private void ZCB(Point64 bot1, Point64 top1, + Point64 bot2, Point64 top2, ref Point64 intersectPt) + { + // de-scale (x & y) + // temporarily convert integers to their initial float values + // this will slow clipping marginally but will make it much easier + // to understand the coordinates passed to the callback function + PointD tmp = Clipper.ScalePointD(intersectPt, _invScale); + //do the callback + ZCallback?.Invoke( + Clipper.ScalePointD(bot1, _invScale), + Clipper.ScalePointD(top1, _invScale), + Clipper.ScalePointD(bot2, _invScale), + Clipper.ScalePointD(top2, _invScale), ref tmp); + intersectPt = new Point64(intersectPt.X, + intersectPt.Y, tmp.z); + } +#endif + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void AddPath(PathD path, PathType polytype, bool isOpen = false) + { + base.AddPath(Clipper.ScalePath64(path, _scale), polytype, isOpen); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void AddPaths(PathsD paths, PathType polytype, bool isOpen = false) + { + base.AddPaths(Clipper.ScalePaths64(paths, _scale), polytype, isOpen); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void AddSubject(PathD path) + { + AddPath(path, PathType.Subject); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void AddOpenSubject(PathD path) + { + AddPath(path, PathType.Subject, true); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void AddClip(PathD path) + { + AddPath(path, PathType.Clip); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void AddSubject(PathsD paths) + { + AddPaths(paths, PathType.Subject); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void AddOpenSubject(PathsD paths) + { + AddPaths(paths, PathType.Subject, true); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void AddClip(PathsD paths) + { + AddPaths(paths, PathType.Clip); + } + + public bool Execute(ClipType clipType, FillRule fillRule, + PathsD solutionClosed, PathsD solutionOpen) + { + Paths64 solClosed64 = new Paths64(), solOpen64 = new Paths64(); +#if USINGZ + CheckZCallback(); +#endif + + bool success = true; + solutionClosed.Clear(); + solutionOpen.Clear(); + try + { + ExecuteInternal(clipType, fillRule); + BuildPaths(solClosed64, solOpen64); + } + catch + { + success = false; + } + + ClearSolutionOnly(); + if (!success) return false; + + solutionClosed.Capacity = solClosed64.Count; + foreach (Path64 path in solClosed64) + solutionClosed.Add(Clipper.ScalePathD(path, _invScale)); + solutionOpen.Capacity = solOpen64.Count; + foreach (Path64 path in solOpen64) + solutionOpen.Add(Clipper.ScalePathD(path, _invScale)); + + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Execute(ClipType clipType, FillRule fillRule, PathsD solutionClosed) + { + return Execute(clipType, fillRule, solutionClosed, new PathsD()); + } + + public bool Execute(ClipType clipType, FillRule fillRule, PolyTreeD polytree, PathsD openPaths) + { + polytree.Clear(); + openPaths.Clear(); + _using_polytree = true; + (polytree as PolyPathD).Scale = _scale; +#if USINGZ + CheckZCallback(); +#endif + Paths64 oPaths = new Paths64(); + bool success = true; + try + { + ExecuteInternal(clipType, fillRule); + BuildTree(polytree, oPaths); + } + catch + { + success = false; + } + ClearSolutionOnly(); + if (!success) return false; + if (oPaths.Count > 0) + { + openPaths.Capacity = oPaths.Count; + foreach (Path64 path in oPaths) + openPaths.Add(Clipper.ScalePathD(path, _invScale)); + } + + return true; + } + + public bool Execute(ClipType clipType, FillRule fillRule, PolyTreeD polytree) + { + return Execute(clipType, fillRule, polytree, new PathsD()); + } + } // ClipperD class + + public abstract class PolyPathBase : IEnumerable + { + internal PolyPathBase? _parent; + internal List _childs = new List(); + + public IEnumerator GetEnumerator() + { + return new NodeEnumerator(_childs); + } + private class NodeEnumerator : IEnumerator + { + private int position = -1; + private readonly List _nodes; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public NodeEnumerator(List nodes) + { + _nodes = new List(nodes); + } + + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool MoveNext() + { + position++; + return (position < _nodes.Count); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Reset() + { + position = -1; + } + + public object Current + { + get + { + if (position < 0 || position >= _nodes.Count) + throw new InvalidOperationException(); + return _nodes[position]; + } + } + + }; + + public bool IsHole => GetIsHole(); + + public PolyPathBase(PolyPathBase? parent = null) { _parent = parent; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int GetLevel() + { + int result = 0; + PolyPathBase? pp = _parent; + while (pp != null) { ++result; pp = pp._parent; } + return result; + } + + public int Level => GetLevel(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool GetIsHole() + { + int lvl = GetLevel(); + return lvl != 0 && (lvl & 1) == 0; + } + + public int Count => _childs.Count; + public abstract PolyPathBase AddChild(Path64 p); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Clear() + { + _childs.Clear(); + } + + internal string ToStringInternal(int idx, int level) + { + string result = "", padding = "", plural = "s"; + if (_childs.Count == 1) plural = ""; + padding = padding.PadLeft(level * 2); + if ((level & 1) == 0) + result += string.Format("{0}+- hole ({1}) contains {2} nested polygon{3}.\n", padding, idx, _childs.Count, plural); + else + result += string.Format("{0}+- polygon ({1}) contains {2} hole{3}.\n", padding, idx, _childs.Count, plural); + + for (int i = 0; i < Count; i++) + if (_childs[i].Count > 0) + result += _childs[i].ToStringInternal(i, level +1); + return result; + } + + public override string ToString() + { + if (Level > 0) return ""; //only accept tree root + string plural = "s"; + if (_childs.Count == 1) plural = ""; + string result = string.Format("Polytree with {0} polygon{1}.\n", _childs.Count, plural); + for (int i = 0; i < Count; i++) + if (_childs[i].Count > 0) + result += _childs[i].ToStringInternal(i, 1); + return result + '\n'; + } + +} // PolyPathBase class + +public class PolyPath64 : PolyPathBase + { + public Path64? Polygon { get; private set; } // polytree root's polygon == null + + public PolyPath64(PolyPathBase? parent = null) : base(parent) {} + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override PolyPathBase AddChild(Path64 p) + { + PolyPathBase newChild = new PolyPath64(this); + (newChild as PolyPath64)!.Polygon = p; + _childs.Add(newChild); + return newChild; + } + + public PolyPath64 this[int index] + { + get + { + if (index < 0 || index >= _childs.Count) + throw new InvalidOperationException(); + return (PolyPath64) _childs[index]; + } + } + + public PolyPath64 Child(int index) + { + if (index < 0 || index >= _childs.Count) + throw new InvalidOperationException(); + return (PolyPath64) _childs[index]; + } + + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public double Area() + { + double result = Polygon == null ? 0 : Clipper.Area(Polygon); + foreach (PolyPathBase polyPathBase in _childs) + { + PolyPath64 child = (PolyPath64) polyPathBase; + result += child.Area(); + } + return result; + } + } + public class PolyPathD : PolyPathBase + { + internal double Scale { get; set; } + public PathD? Polygon { get; private set; } + + public PolyPathD(PolyPathBase? parent = null) : base(parent) {} + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override PolyPathBase AddChild(Path64 p) + { + PolyPathBase newChild = new PolyPathD(this); + (newChild as PolyPathD)!.Scale = Scale; + (newChild as PolyPathD)!.Polygon = Clipper.ScalePathD(p, 1 / Scale); + _childs.Add(newChild); + return newChild; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public PolyPathBase AddChild(PathD p) + { + PolyPathBase newChild = new PolyPathD(this); + (newChild as PolyPathD)!.Scale = Scale; + (newChild as PolyPathD)!.Polygon = p; + _childs.Add(newChild); + return newChild; + } + + [IndexerName("Child")] + public PolyPathD this[int index] + { + get + { + if (index < 0 || index >= _childs.Count) + throw new InvalidOperationException(); + return (PolyPathD) _childs[index]; + } + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public double Area() + { + double result = Polygon == null ? 0 : Clipper.Area(Polygon); + foreach (PolyPathBase polyPathBase in _childs) + { + PolyPathD child = (PolyPathD) polyPathBase; + result += child.Area(); + } + return result; + } + } + + public class PolyTree64 : PolyPath64 {} + + public class PolyTreeD : PolyPathD + { + public new double Scale => base.Scale; + } + + public class ClipperLibException : Exception + { + public ClipperLibException(string description) : base(description) {} + } +} // namespace \ No newline at end of file diff --git a/BossMod/ThirdParty/Clipper2Lib/Clipper.Minkowski.cs b/BossMod/ThirdParty/Clipper2Lib/Clipper.Minkowski.cs new file mode 100644 index 0000000000..5b38c7395f --- /dev/null +++ b/BossMod/ThirdParty/Clipper2Lib/Clipper.Minkowski.cs @@ -0,0 +1,90 @@ +/******************************************************************************* +* Author : Angus Johnson * +* Date : 15 October 2022 * +* Website : http://www.angusj.com * +* Copyright : Angus Johnson 2010-2022 * +* Purpose : Minkowski Sum and Difference * +* License : http://www.boost.org/LICENSE_1_0.txt * +*******************************************************************************/ + +#nullable enable +using System; + +namespace Clipper2Lib +{ + public class Minkowski + { + private static Paths64 MinkowskiInternal(Path64 pattern, Path64 path, bool isSum, bool isClosed) + { + int delta = isClosed ? 0 : 1; + int patLen = pattern.Count, pathLen = path.Count; + Paths64 tmp = new Paths64(pathLen); + + foreach (Point64 pathPt in path) + { + Path64 path2 = new Path64(patLen); + if (isSum) + { + foreach (Point64 basePt in pattern) + path2.Add(pathPt + basePt); + } + else + { + foreach (Point64 basePt in pattern) + path2.Add(pathPt - basePt); + } + tmp.Add(path2); + } + + Paths64 result = new Paths64((pathLen - delta) * patLen); + int g = isClosed ? pathLen - 1 : 0; + + int h = patLen - 1; + for (int i = delta; i < pathLen; i++) + { + for (int j = 0; j < patLen; j++) + { + Path64 quad = new Path64(4) + { + tmp[g][h], tmp[i][h], tmp[i][j], tmp[g][j] + }; + if (!Clipper.IsPositive(quad)) + result.Add(Clipper.ReversePath(quad)); + else + result.Add(quad); + h = j; + } + g = i; + } + return result; + } + + public static Paths64 Sum(Path64 pattern, Path64 path, bool isClosed) + { + return Clipper.Union(MinkowskiInternal(pattern, path, true, isClosed), FillRule.NonZero); + } + + public static PathsD Sum(PathD pattern, PathD path, bool isClosed, int decimalPlaces = 2) + { + double scale = Math.Pow(10, decimalPlaces); + Paths64 tmp = Clipper.Union(MinkowskiInternal(Clipper.ScalePath64(pattern, scale), + Clipper.ScalePath64(path, scale), true, isClosed), FillRule.NonZero); + return Clipper.ScalePathsD(tmp, 1 / scale); + } + + public static Paths64 Diff(Path64 pattern, Path64 path, bool isClosed) + { + return Clipper.Union(MinkowskiInternal(pattern, path, false, isClosed), FillRule.NonZero); + } + + public static PathsD Diff(PathD pattern, PathD path, bool isClosed, int decimalPlaces = 2) + { + double scale = Math.Pow(10, decimalPlaces); + Paths64 tmp = Clipper.Union(MinkowskiInternal(Clipper.ScalePath64(pattern, scale), + Clipper.ScalePath64(path, scale), false, isClosed), FillRule.NonZero); + return Clipper.ScalePathsD(tmp, 1 / scale); + } + + } + +} // namespace \ No newline at end of file diff --git a/BossMod/ThirdParty/Clipper2Lib/Clipper.Offset.cs b/BossMod/ThirdParty/Clipper2Lib/Clipper.Offset.cs new file mode 100644 index 0000000000..6c95210d2a --- /dev/null +++ b/BossMod/ThirdParty/Clipper2Lib/Clipper.Offset.cs @@ -0,0 +1,789 @@ +/******************************************************************************* +* Author : Angus Johnson * +* Date : 28 November 2023 * +* Website : http://www.angusj.com * +* Copyright : Angus Johnson 2010-2023 * +* Purpose : Path Offset (Inflate/Shrink) * +* License : http://www.boost.org/LICENSE_1_0.txt * +*******************************************************************************/ + +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace Clipper2Lib +{ + public enum JoinType + { + Miter, + Square, + Bevel, + Round + }; + + public enum EndType + { + Polygon, + Joined, + Butt, + Square, + Round + }; + + public class ClipperOffset + { + + private class Group + { + internal Paths64 inPaths; + internal List boundsList; + internal List isHoleList; + internal JoinType joinType; + internal EndType endType; + internal bool pathsReversed; + internal int lowestPathIdx; + + public Group(Paths64 paths, JoinType joinType, EndType endType = EndType.Polygon) + { + this.joinType = joinType; + this.endType = endType; + + bool isJoined = ((endType == EndType.Polygon) || (endType == EndType.Joined)); + inPaths = new Paths64(paths.Count); + foreach(Path64 path in paths) + inPaths.Add(Clipper.StripDuplicates(path, isJoined)); + + // get bounds of each path --> boundsList + boundsList = new List(inPaths.Count); + GetMultiBounds(inPaths, boundsList); + + if (endType == EndType.Polygon) + { + lowestPathIdx = GetLowestPathIdx(boundsList); + isHoleList = new List(inPaths.Count); + + foreach (Path64 path in inPaths) + isHoleList.Add(Clipper.Area(path) < 0); + // the lowermost path must be an outer path, so if its orientation is negative, + // then flag that the whole group is 'reversed' (will negate delta etc.) + // as this is much more efficient than reversing every path. + pathsReversed = (lowestPathIdx >= 0) && isHoleList[lowestPathIdx]; + if (pathsReversed) + for (int i = 0; i < isHoleList.Count; i++) isHoleList[i] = !isHoleList[i]; + } + else + { + lowestPathIdx = -1; + isHoleList = new List(new bool[inPaths.Count]); + pathsReversed = false; + } + } + } + + private static readonly double Tolerance = 1.0E-12; + private static readonly Rect64 InvalidRect64 = + new Rect64(long.MaxValue, long.MaxValue, long.MinValue, long.MinValue); + private static readonly RectD InvalidRectD = + new RectD(double.MaxValue, double.MaxValue, double.MinValue, double.MinValue); + private static readonly long MAX_COORD = long.MaxValue >> 2; + private static readonly long MIN_COORD = -MAX_COORD; + + private static readonly string + coord_range_error = "Error: Coordinate range."; + + + private readonly List _groupList = new List(); + private Path64 pathOut = new Path64(); + private readonly PathD _normals = new PathD(); + private readonly Paths64 _solution = new Paths64(); + private double _groupDelta; //*0.5 for open paths; *-1.0 for negative areas + private double _delta; + private double _mitLimSqr; + private double _stepsPerRad; + private double _stepSin; + private double _stepCos; + private JoinType _joinType; + private EndType _endType; + public double ArcTolerance { get; set; } + public bool MergeGroups { get; set; } + public double MiterLimit { get; set; } + public bool PreserveCollinear { get; set; } + public bool ReverseSolution { get; set; } + + public delegate double DeltaCallback64(Path64 path, + PathD path_norms, int currPt, int prevPt); + public ClipperOffset.DeltaCallback64? DeltaCallback { get; set; } + +#if USINGZ + public ClipperBase.ZCallback64? ZCallback { get; set; } +#endif + public ClipperOffset(double miterLimit = 2.0, + double arcTolerance = 0.0, bool + preserveCollinear = false, bool reverseSolution = false) + { + MiterLimit = miterLimit; + ArcTolerance = arcTolerance; + MergeGroups = true; + PreserveCollinear = preserveCollinear; + ReverseSolution = reverseSolution; +#if USINGZ + ZCallback = null; +#endif + } + public void Clear() + { + _groupList.Clear(); + } + + public void AddPath(Path64 path, JoinType joinType, EndType endType) + { + int cnt = path.Count; + if (cnt == 0) return; + Paths64 pp = new Paths64(1) { path }; + AddPaths(pp, joinType, endType); + } + + public void AddPaths(Paths64 paths, JoinType joinType, EndType endType) + { + int cnt = paths.Count; + if (cnt == 0) return; + _groupList.Add(new Group(paths, joinType, endType)); + } + + private int CalcSolutionCapacity() + { + int result = 0; + foreach (Group g in _groupList) + result += (g.endType == EndType.Joined) ? g.inPaths.Count * 2 : g.inPaths.Count; + return result; + } + + private void ExecuteInternal(double delta) + { + _solution.Clear(); + if (_groupList.Count == 0) return; + _solution.Capacity = CalcSolutionCapacity(); + + // make sure the offset delta is significant + if (Math.Abs(delta) < 0.5) + { + foreach (Group group in _groupList) + foreach (Path64 path in group.inPaths) + _solution.Add(path); + return; + } + + _delta = delta; + _mitLimSqr = (MiterLimit <= 1 ? + 2.0 : 2.0 / Clipper.Sqr(MiterLimit)); + + foreach (Group group in _groupList) + DoGroupOffset(group); + } + + internal bool CheckPathsReversed() + { + bool result = false; + foreach (Group g in _groupList) + if (g.endType == EndType.Polygon) + { + result = g.pathsReversed; + break; + } + return result; + } + + public void Execute(double delta, Paths64 solution) + { + solution.Clear(); + ExecuteInternal(delta); + if (_groupList.Count == 0) return; + + bool pathsReversed = CheckPathsReversed(); + FillRule fillRule = pathsReversed ? FillRule.Negative : FillRule.Positive; + + // clean up self-intersections ... + Clipper64 c = new Clipper64(); + c.PreserveCollinear = PreserveCollinear; + // the solution should retain the orientation of the input + c.ReverseSolution = ReverseSolution != pathsReversed; +#if USINGZ + c.ZCallback = ZCallback; +#endif + c.AddSubject(_solution); + c.Execute(ClipType.Union, fillRule, solution); + } + + public void Execute(double delta, PolyTree64 solutionTree) + { + solutionTree.Clear(); + ExecuteInternal(delta); + if (_groupList.Count == 0) return; + + bool pathsReversed = CheckPathsReversed(); + FillRule fillRule = pathsReversed ? FillRule.Negative : FillRule.Positive; + // clean up self-intersections ... + Clipper64 c = new Clipper64(); + c.PreserveCollinear = PreserveCollinear; + // the solution should normally retain the orientation of the input + c.ReverseSolution = ReverseSolution != pathsReversed; +#if USINGZ + c.ZCallback = ZCallback; +#endif + c.AddSubject(_solution); + c.Execute(ClipType.Union, fillRule, solutionTree); + } + + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static PointD GetUnitNormal(Point64 pt1, Point64 pt2) + { + double dx = (pt2.X - pt1.X); + double dy = (pt2.Y - pt1.Y); + if ((dx == 0) && (dy == 0)) return new PointD(); + + double f = 1.0 / Math.Sqrt(dx * dx + dy * dy); + dx *= f; + dy *= f; + + return new PointD(dy, -dx); + } + + public void Execute(DeltaCallback64 deltaCallback, Paths64 solution) + { + DeltaCallback = deltaCallback; + Execute(1.0, solution); + } + + internal static void GetMultiBounds(Paths64 paths, List boundsList) + { + boundsList.Capacity = paths.Count; + foreach (Path64 path in paths) + { + if (path.Count < 1) + { + boundsList.Add(InvalidRect64); + continue; + } + + Point64 pt1 = path[0]; + Rect64 r = new Rect64(pt1.X, pt1.Y, pt1.X, pt1.Y); + foreach (Point64 pt in path) + { + if (pt.Y > r.bottom) r.bottom = pt.Y; + else if (pt.Y < r.top) r.top = pt.Y; + if (pt.X > r.right) r.right = pt.X; + else if (pt.X < r.left) r.left = pt.X; + } + boundsList.Add(r); + } + } + + internal static bool ValidateBounds(List boundsList, double delta) + { + int int_delta = (int)delta; + + Point64 botPt = new Point64(int.MaxValue, int.MinValue); + foreach (Rect64 r in boundsList) + { + if (!r.IsValid()) continue; // ignore invalid paths + else if (r.left < MIN_COORD + int_delta || + r.right > MAX_COORD + int_delta || + r.top < MIN_COORD + int_delta || + r.bottom > MAX_COORD + int_delta) return false; + } + return true; + } + + internal static int GetLowestPathIdx(List boundsList) + { + int result = -1; + Point64 botPt = new Point64(long.MaxValue, long.MinValue); + for (int i = 0; i < boundsList.Count; i++) + { + Rect64 r = boundsList[i]; + if (!r.IsValid()) continue; // ignore invalid paths + else if (r.bottom > botPt.Y || (r.bottom == botPt.Y && r.left < botPt.X)) + { + botPt = new Point64(r.left, r.bottom); + result = i; + } + } + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static PointD TranslatePoint(PointD pt, double dx, double dy) + { +#if USINGZ + return new PointD(pt.x + dx, pt.y + dy, pt.z); +#else + return new PointD(pt.x + dx, pt.y + dy); +#endif + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static PointD ReflectPoint(PointD pt, PointD pivot) + { +#if USINGZ + return new PointD(pivot.x + (pivot.x - pt.x), pivot.y + (pivot.y - pt.y), pt.z); +#else + return new PointD(pivot.x + (pivot.x - pt.x), pivot.y + (pivot.y - pt.y)); +#endif + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool AlmostZero(double value, double epsilon = 0.001) + { + return Math.Abs(value) < epsilon; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static double Hypotenuse(double x, double y) + { + return Math.Sqrt(Math.Pow(x, 2) + Math.Pow(y, 2)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static PointD NormalizeVector(PointD vec) + { + double h = Hypotenuse(vec.x, vec.y); + if (AlmostZero(h)) return new PointD(0,0); + double inverseHypot = 1 / h; + return new PointD(vec.x* inverseHypot, vec.y* inverseHypot); + } + + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static PointD GetAvgUnitVector(PointD vec1, PointD vec2) + { + return NormalizeVector(new PointD(vec1.x + vec2.x, vec1.y + vec2.y)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static PointD IntersectPoint(PointD pt1a, PointD pt1b, PointD pt2a, PointD pt2b) + { + if (InternalClipper.IsAlmostZero(pt1a.x - pt1b.x)) //vertical + { + if (InternalClipper.IsAlmostZero(pt2a.x - pt2b.x)) return new PointD(0, 0); + double m2 = (pt2b.y - pt2a.y) / (pt2b.x - pt2a.x); + double b2 = pt2a.y - m2 * pt2a.x; + return new PointD(pt1a.x, m2* pt1a.x + b2); + } + + if (InternalClipper.IsAlmostZero(pt2a.x - pt2b.x)) //vertical + { + double m1 = (pt1b.y - pt1a.y) / (pt1b.x - pt1a.x); + double b1 = pt1a.y - m1 * pt1a.x; + return new PointD(pt2a.x, m1* pt2a.x + b1); + } + else + { + double m1 = (pt1b.y - pt1a.y) / (pt1b.x - pt1a.x); + double b1 = pt1a.y - m1 * pt1a.x; + double m2 = (pt2b.y - pt2a.y) / (pt2b.x - pt2a.x); + double b2 = pt2a.y - m2 * pt2a.x; + if (InternalClipper.IsAlmostZero(m1 - m2)) return new PointD(0, 0); + double x = (b2 - b1) / (m1 - m2); + return new PointD(x, m1 * x + b1); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private Point64 GetPerpendic(Point64 pt, PointD norm) + { +#if USINGZ + return new Point64(pt.X + norm.x * _groupDelta, + pt.Y + norm.y * _groupDelta, pt.Z); +#else + return new Point64(pt.X + norm.x * _groupDelta, + pt.Y + norm.y * _groupDelta); +#endif + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private PointD GetPerpendicD(Point64 pt, PointD norm) + { +#if USINGZ + return new PointD(pt.X + norm.x * _groupDelta, + pt.Y + norm.y * _groupDelta, pt.Z); +#else + return new PointD(pt.X + norm.x * _groupDelta, + pt.Y + norm.y * _groupDelta); +#endif + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DoBevel(Path64 path, int j, int k) + { + Point64 pt1, pt2; + if (j == k) + { + double absDelta = Math.Abs(_groupDelta); + pt1 = new Point64(path[j].X - absDelta * _normals[j].x, path[j].Y - absDelta * _normals[j].y); + pt2 = new Point64(path[j].X + absDelta * _normals[j].x, path[j].Y + absDelta * _normals[j].y); + } + else + { + pt1 = new Point64(path[j].X + _groupDelta * _normals[k].x, path[j].Y + _groupDelta * _normals[k].y); + pt2 = new Point64(path[j].X + _groupDelta * _normals[j].x, path[j].Y + _groupDelta * _normals[j].y); + } + pathOut.Add(pt1); + pathOut.Add(pt2); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DoSquare(Path64 path, int j, int k) + { + PointD vec; + if (j == k) + { + vec = new PointD(_normals[j].y, -_normals[j].x); + } + else + { + vec = GetAvgUnitVector( + new PointD(-_normals[k].y, _normals[k].x), + new PointD(_normals[j].y, -_normals[j].x)); + } + + double absDelta = Math.Abs(_groupDelta); + // now offset the original vertex delta units along unit vector + PointD ptQ = new PointD(path[j]); + ptQ = TranslatePoint(ptQ, absDelta * vec.x, absDelta * vec.y); + + // get perpendicular vertices + PointD pt1 = TranslatePoint(ptQ, _groupDelta * vec.y, _groupDelta * -vec.x); + PointD pt2 = TranslatePoint(ptQ, _groupDelta * -vec.y, _groupDelta * vec.x); + // get 2 vertices along one edge offset + PointD pt3 = GetPerpendicD(path[k], _normals[k]); + + if (j == k) + { + PointD pt4 = new PointD( + pt3.x + vec.x * _groupDelta, + pt3.y + vec.y * _groupDelta); + PointD pt = IntersectPoint(pt1, pt2, pt3, pt4); +#if USINGZ + pt.z = ptQ.z; +#endif + //get the second intersect point through reflecion + pathOut.Add(new Point64(ReflectPoint(pt, ptQ))); + pathOut.Add(new Point64(pt)); + } + else + { + PointD pt4 = GetPerpendicD(path[j], _normals[k]); + PointD pt = IntersectPoint(pt1, pt2, pt3, pt4); +#if USINGZ + pt.z = ptQ.z; +#endif + pathOut.Add(new Point64(pt)); + //get the second intersect point through reflecion + pathOut.Add(new Point64(ReflectPoint(pt, ptQ))); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DoMiter(Group group, Path64 path, int j, int k, double cosA) + { + double q = _groupDelta / (cosA + 1); +#if USINGZ + pathOut.Add(new Point64( + path[j].X + (_normals[k].x + _normals[j].x) * q, + path[j].Y + (_normals[k].y + _normals[j].y) * q, + path[j].Z)); +#else + pathOut.Add(new Point64( + path[j].X + (_normals[k].x + _normals[j].x) * q, + path[j].Y + (_normals[k].y + _normals[j].y) * q)); +#endif + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DoRound(Path64 path, int j, int k, double angle) + { + if (DeltaCallback != null) + { + // when DeltaCallback is assigned, _groupDelta won't be constant, + // so we'll need to do the following calculations for *every* vertex. + double absDelta = Math.Abs(_groupDelta); + double arcTol = ArcTolerance > 0.01 ? + ArcTolerance : + Math.Log10(2 + absDelta) * InternalClipper.defaultArcTolerance; + double stepsPer360 = Math.PI / Math.Acos(1 - arcTol / absDelta); + _stepSin = Math.Sin((2 * Math.PI) / stepsPer360); + _stepCos = Math.Cos((2 * Math.PI) / stepsPer360); + if (_groupDelta < 0.0) _stepSin = -_stepSin; + _stepsPerRad = stepsPer360 / (2 * Math.PI); + } + + Point64 pt = path[j]; + PointD offsetVec = new PointD(_normals[k].x * _groupDelta, _normals[k].y * _groupDelta); + if (j == k) offsetVec.Negate(); +#if USINGZ + pathOut.Add(new Point64(pt.X + offsetVec.x, pt.Y + offsetVec.y, pt.Z)); +#else + pathOut.Add(new Point64(pt.X + offsetVec.x, pt.Y + offsetVec.y)); +#endif + int steps = (int) Math.Ceiling(_stepsPerRad * Math.Abs(angle)); + for (int i = 1; i < steps; i++) // ie 1 less than steps + { + offsetVec = new PointD(offsetVec.x * _stepCos - _stepSin * offsetVec.y, + offsetVec.x * _stepSin + offsetVec.y * _stepCos); +#if USINGZ + pathOut.Add(new Point64(pt.X + offsetVec.x, pt.Y + offsetVec.y, pt.Z)); +#else + pathOut.Add(new Point64(pt.X + offsetVec.x, pt.Y + offsetVec.y)); +#endif + } + pathOut.Add(GetPerpendic(pt, _normals[j])); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void BuildNormals(Path64 path) + { + int cnt = path.Count; + _normals.Clear(); + _normals.Capacity = cnt; + + for (int i = 0; i < cnt - 1; i++) + _normals.Add(GetUnitNormal(path[i], path[i + 1])); + _normals.Add(GetUnitNormal(path[cnt - 1], path[0])); + } + + private void OffsetPoint(Group group, Path64 path, int j, ref int k) + { + // Let A = change in angle where edges join + // A == 0: ie no change in angle (flat join) + // A == PI: edges 'spike' + // sin(A) < 0: right turning + // cos(A) < 0: change in angle is more than 90 degree + double sinA = InternalClipper.CrossProduct(_normals[j], _normals[k]); + double cosA = InternalClipper.DotProduct(_normals[j], _normals[k]); + if (sinA > 1.0) sinA = 1.0; + else if (sinA < -1.0) sinA = -1.0; + + if (DeltaCallback != null) + { + _groupDelta = DeltaCallback(path, _normals, j, k); + if (group.pathsReversed) _groupDelta = -_groupDelta; + } + if (Math.Abs(_groupDelta) < Tolerance) + { + pathOut.Add(path[j]); + return; + } + + if (cosA > -0.99 && (sinA * _groupDelta < 0)) // test for concavity first (#593) + { + // is concave + pathOut.Add(GetPerpendic(path[j], _normals[k])); + // this extra point is the only (simple) way to ensure that + // path reversals are fully cleaned with the trailing clipper + pathOut.Add(path[j]); // (#405) + pathOut.Add(GetPerpendic(path[j], _normals[j])); + } + else if ((cosA > 0.999) && (_joinType != JoinType.Round)) + { + // almost straight - less than 2.5 degree (#424, #482, #526 & #724) + DoMiter(group, path, j, k, cosA); + } + else if (_joinType == JoinType.Miter) + { + // miter unless the angle is sufficiently acute to exceed ML + if (cosA > _mitLimSqr - 1) DoMiter(group, path, j, k, cosA); + else DoSquare(path, j, k); + } + else if (_joinType == JoinType.Round) + DoRound(path, j, k, Math.Atan2(sinA, cosA)); + else if (_joinType == JoinType.Bevel) + DoBevel(path, j, k); + else + DoSquare(path, j, k); + + k = j; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void OffsetPolygon(Group group, Path64 path) + { + pathOut = new Path64(); + int cnt = path.Count, prev = cnt - 1; + for (int i = 0; i < cnt; i++) + OffsetPoint(group, path, i, ref prev); + _solution.Add(pathOut); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void OffsetOpenJoined(Group group, Path64 path) + { + OffsetPolygon(group, path); + path = Clipper.ReversePath(path); + BuildNormals(path); + OffsetPolygon(group, path); + } + + private void OffsetOpenPath(Group group, Path64 path) + { + pathOut = new Path64(); + int highI = path.Count - 1; + + if (DeltaCallback != null) + _groupDelta = DeltaCallback(path, _normals, 0, 0); + + // do the line start cap + if (Math.Abs(_groupDelta) < Tolerance) + pathOut.Add(path[0]); + else + switch (_endType) + { + case EndType.Butt: + DoBevel(path, 0, 0); + break; + case EndType.Round: + DoRound(path, 0, 0, Math.PI); + break; + default: + DoSquare(path, 0, 0); + break; + } + + // offset the left side going forward + for (int i = 1, k = 0; i < highI; i++) + OffsetPoint(group, path, i, ref k); + + // reverse normals ... + for (int i = highI; i > 0; i--) + _normals[i] = new PointD(-_normals[i - 1].x, -_normals[i - 1].y); + _normals[0] = _normals[highI]; + + if (DeltaCallback != null) + _groupDelta = DeltaCallback(path, _normals, highI, highI); + // do the line end cap + if (Math.Abs(_groupDelta) < Tolerance) + pathOut.Add(path[highI]); + else + switch (_endType) + { + case EndType.Butt: + DoBevel(path, highI, highI); + break; + case EndType.Round: + DoRound(path, highI, highI, Math.PI); + break; + default: + DoSquare(path, highI, highI); + break; + } + + // offset the left side going back + for (int i = highI, k = 0; i > 0; i--) + OffsetPoint(group, path, i, ref k); + + _solution.Add(pathOut); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool ToggleBoolIf(bool val, bool condition) + { + return condition ? !val : val; + } + private void DoGroupOffset(Group group) + { + if (group.endType == EndType.Polygon) + { + // a straight path (2 points) can now also be 'polygon' offset + // where the ends will be treated as (180 deg.) joins + if (group.lowestPathIdx < 0) _delta = Math.Abs(_delta); + _groupDelta = (group.pathsReversed) ? -_delta : _delta; + } + else + _groupDelta = Math.Abs(_delta); + + double absDelta = Math.Abs(_groupDelta); + if (!ValidateBounds(group.boundsList, absDelta)) + throw new Exception(coord_range_error); + + _joinType = group.joinType; + _endType = group.endType; + + if (group.joinType == JoinType.Round || group.endType == EndType.Round) + { + // calculate a sensible number of steps (for 360 deg for the given offset + // arcTol - when fArcTolerance is undefined (0), the amount of + // curve imprecision that's allowed is based on the size of the + // offset (delta). Obviously very large offsets will almost always + // require much less precision. See also offset_triginometry2.svg + double arcTol = ArcTolerance > 0.01 ? + ArcTolerance : + Math.Log10(2 + absDelta) * InternalClipper.defaultArcTolerance; + double stepsPer360 = Math.PI / Math.Acos(1 - arcTol / absDelta); + _stepSin = Math.Sin((2 * Math.PI) / stepsPer360); + _stepCos = Math.Cos((2 * Math.PI) / stepsPer360); + if (_groupDelta < 0.0) _stepSin = -_stepSin; + _stepsPerRad = stepsPer360 / (2 * Math.PI); + } + + using List.Enumerator pathIt = group.inPaths.GetEnumerator(); + using List.Enumerator boundsIt = group.boundsList.GetEnumerator(); + using List.Enumerator isHoleIt = group.isHoleList.GetEnumerator(); + while (pathIt.MoveNext() && boundsIt.MoveNext() && isHoleIt.MoveNext()) + { + Rect64 pathBounds = boundsIt.Current; + if (!pathBounds.IsValid()) continue; + + Path64 p = pathIt.Current; + bool isHole = isHoleIt.Current; + + pathOut = new Path64(); + int cnt = p.Count; + + if (cnt == 1) + { + Point64 pt = p[0]; + + // single vertex so build a circle or square ... + if (group.endType == EndType.Round) + { + double r = absDelta; + int steps = (int) Math.Ceiling(_stepsPerRad * 2 * Math.PI); + pathOut = Clipper.Ellipse(pt, r, r, steps); +#if USINGZ + pathOut = InternalClipper.SetZ(pathOut, pt.Z); +#endif + } + else + { + int d = (int) Math.Ceiling(_groupDelta); + Rect64 r = new Rect64(pt.X - d, pt.Y - d, pt.X + d, pt.Y + d); + pathOut = r.AsPath(); +#if USINGZ + pathOut = InternalClipper.SetZ(pathOut, pt.Z); +#endif + } + _solution.Add(pathOut); + continue; + } // end of offsetting a single point + + + // when shrinking outer paths, make sure they can shrink this far (#593) + // also when shrinking holes, make sure they too can shrink this far (#715) + if (((_groupDelta > 0) == ToggleBoolIf(isHole, group.pathsReversed)) && + (Math.Min(pathBounds.Width, pathBounds.Height) <= -_groupDelta * 2)) + continue; + + if (cnt == 2 && group.endType == EndType.Joined) + _endType = (group.joinType == JoinType.Round) ? + EndType.Round : + EndType.Square; + + BuildNormals(p); + if (_endType == EndType.Polygon) OffsetPolygon(group, p); + else if (_endType == EndType.Joined) OffsetOpenJoined(group, p); + else OffsetOpenPath(group, p); + } + } + } + +} // namespace \ No newline at end of file diff --git a/BossMod/ThirdParty/Clipper2Lib/Clipper.RectClip.cs b/BossMod/ThirdParty/Clipper2Lib/Clipper.RectClip.cs new file mode 100644 index 0000000000..ea4f8623aa --- /dev/null +++ b/BossMod/ThirdParty/Clipper2Lib/Clipper.RectClip.cs @@ -0,0 +1,1066 @@ +/******************************************************************************* +* Author : Angus Johnson * +* Date : 8 September 2023 * +* Website : http://www.angusj.com * +* Copyright : Angus Johnson 2010-2023 * +* Purpose : FAST rectangular clipping * +* License : http://www.boost.org/LICENSE_1_0.txt * +*******************************************************************************/ + +#nullable enable +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace Clipper2Lib +{ + public class OutPt2 + { + public OutPt2? next; + public OutPt2? prev; + + public Point64 pt; + public int ownerIdx; + public List? edge; + public OutPt2(Point64 pt) + { + this.pt = pt; + } + } + + public class RectClip64 + { + protected enum Location + { + left, top, right, bottom, inside + }; + + readonly protected Rect64 rect_; + readonly protected Point64 mp_; + readonly protected Path64 rectPath_; + protected Rect64 pathBounds_; + protected List results_; + protected List[] edges_; + protected int currIdx_ = -1; + internal RectClip64(Rect64 rect) + { + currIdx_ = -1; + rect_ = rect; + mp_ = rect.MidPoint(); + rectPath_ = rect_.AsPath(); + results_ = new List(); + edges_ = new List[8]; + for (int i = 0; i < 8; i++) + edges_[i] = new List(); + } + + internal OutPt2 Add(Point64 pt, bool startingNewPath = false) + { // this method is only called by InternalExecute. + // Later splitting and rejoining won't create additional op's, + // though they will change the (non-storage) fResults count. + int currIdx = results_.Count; + OutPt2 result; + if ((currIdx == 0) || startingNewPath) + { + result = new OutPt2(pt); + results_.Add(result); + result.ownerIdx = currIdx; + result.prev = result; + result.next = result; + } + else + { + currIdx--; + OutPt2? prevOp = results_[currIdx]; + if (prevOp!.pt == pt) return prevOp; + result = new OutPt2(pt) + { + ownerIdx = currIdx, + next = prevOp.next + }; + prevOp.next!.prev = result; + prevOp.next = result; + result.prev = prevOp; + results_[currIdx] = result; + } + return result; + } + + private static bool Path1ContainsPath2(Path64 path1, Path64 path2) + { + // nb: occasionally, due to rounding, path1 may + // appear (momentarily) inside or outside path2. + int ioCount = 0; + foreach (Point64 pt in path2) + { + PointInPolygonResult pip = + InternalClipper.PointInPolygon(pt, path1); + switch(pip) + { + case PointInPolygonResult.IsInside: + ioCount--; break; + case PointInPolygonResult.IsOutside: + ioCount++; break; + } + if (Math.Abs(ioCount) > 1) break; + } + return ioCount <= 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsClockwise(Location prev, Location curr, + Point64 prevPt, Point64 currPt, Point64 rectMidPoint) + { + if (AreOpposites(prev, curr)) + return InternalClipper.CrossProduct(prevPt, rectMidPoint, currPt) < 0; + else + return HeadingClockwise(prev, curr); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool AreOpposites(Location prev, Location curr) + { + return Math.Abs((int)prev - (int) curr) == 2; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool HeadingClockwise(Location prev, Location curr) + { + return ((int) prev + 1) % 4 == (int) curr; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Location GetAdjacentLocation(Location loc, bool isClockwise) + { + int delta = (isClockwise) ? 1 : 3; + return (Location)(((int) loc + delta) % 4); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static OutPt2? UnlinkOp(OutPt2 op) + { + if (op.next == op) return null; + op.prev!.next = op.next; + op.next!.prev = op.prev; + return op.next; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static OutPt2? UnlinkOpBack(OutPt2 op) + { + if (op.next == op) return null; + op.prev!.next = op.next; + op.next!.prev = op.prev; + return op.prev; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint GetEdgesForPt(Point64 pt, Rect64 rec) + { + uint result = 0; + if (pt.X == rec.left) result = 1; + else if (pt.X == rec.right) result = 4; + if (pt.Y == rec.top) result += 2; + else if (pt.Y == rec.bottom) result += 8; + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsHeadingClockwise(Point64 pt1, Point64 pt2, int edgeIdx) + { + return edgeIdx switch + { + 0 => pt2.Y < pt1.Y, + 1 => pt2.X > pt1.X, + 2 => pt2.Y > pt1.Y, + _ => pt2.X < pt1.X, + }; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool HasHorzOverlap(Point64 left1, Point64 right1, + Point64 left2, Point64 right2) + { + return (left1.X < right2.X) && (right1.X > left2.X); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool HasVertOverlap(Point64 top1, Point64 bottom1, + Point64 top2, Point64 bottom2) + { + return (top1.Y < bottom2.Y) && (bottom1.Y > top2.Y); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void AddToEdge(List edge, OutPt2 op) + { + if (op.edge != null) return; + op.edge = edge!; + edge.Add(op); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void UncoupleEdge(OutPt2 op) + { + if (op.edge == null) return; + for (int i = 0; i < op.edge.Count; i++) + { + OutPt2? op2 = op.edge[i]; + if (op2 == op) + { + op.edge[i] = null; + break; + } + } + op.edge = null; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void SetNewOwner(OutPt2 op, int newIdx) + { + op.ownerIdx = newIdx; + OutPt2 op2 = op.next!; + while (op2 != op) + { + op2.ownerIdx = newIdx; + op2 = op2.next!; + } + } + + private void AddCorner(Location prev, Location curr) + { + if (HeadingClockwise(prev, curr)) + Add(rectPath_[(int) prev]); + else + Add(rectPath_[(int) curr]); + } + + private void AddCorner(ref Location loc, bool isClockwise) + { + if (isClockwise) + { + Add(rectPath_[(int) loc]); + loc = GetAdjacentLocation(loc, true); + } + else + { + loc = GetAdjacentLocation(loc, false); + Add(rectPath_[(int) loc]); + } + } + + static protected bool GetLocation(Rect64 rec, Point64 pt, out Location loc) + { + if (pt.X == rec.left && pt.Y >= rec.top && pt.Y <= rec.bottom) + { + loc = Location.left; return false; // pt on rec + } + if (pt.X == rec.right && pt.Y >= rec.top && pt.Y <= rec.bottom) + { + loc = Location.right; return false; // pt on rec + } + if (pt.Y == rec.top && pt.X >= rec.left && pt.X <= rec.right) + { + loc = Location.top; return false; // pt on rec + } + if (pt.Y == rec.bottom && pt.X >= rec.left && pt.X <= rec.right) + { + loc = Location.bottom; return false; // pt on rec + } + if (pt.X < rec.left) loc = Location.left; + else if (pt.X > rec.right) loc = Location.right; + else if (pt.Y < rec.top) loc = Location.top; + else if (pt.Y > rec.bottom) loc = Location.bottom; + else loc = Location.inside; + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsHorizontal(Point64 pt1, Point64 pt2) + { + return pt1.Y == pt2.Y; + } + + private static bool GetSegmentIntersection(Point64 p1, + Point64 p2, Point64 p3, Point64 p4, out Point64 ip) + { + double res1 = InternalClipper.CrossProduct(p1, p3, p4); + double res2 = InternalClipper.CrossProduct(p2, p3, p4); + if (res1 == 0) + { + ip = p1; + if (res2 == 0) return false; // segments are collinear + else if (p1 == p3 || p1 == p4) return true; + //else if (p2 == p3 || p2 == p4) { ip = p2; return true; } + else if (IsHorizontal(p3, p4)) return ((p1.X > p3.X) == (p1.X < p4.X)); + else return ((p1.Y > p3.Y) == (p1.Y < p4.Y)); + } + else if (res2 == 0) + { + ip = p2; + if (p2 == p3 || p2 == p4) return true; + else if (IsHorizontal(p3, p4)) return ((p2.X > p3.X) == (p2.X < p4.X)); + else return ((p2.Y > p3.Y) == (p2.Y < p4.Y)); + } + + if ((res1 > 0) == (res2 > 0)) + { + ip = new Point64(0, 0); + return false; + } + + double res3 = InternalClipper.CrossProduct(p3, p1, p2); + double res4 = InternalClipper.CrossProduct(p4, p1, p2); + if (res3 == 0) + { + ip = p3; + if (p3 == p1 || p3 == p2) return true; + else if (IsHorizontal(p1, p2)) return ((p3.X > p1.X) == (p3.X < p2.X)); + else return ((p3.Y > p1.Y) == (p3.Y < p2.Y)); + } + else if (res4 == 0) + { + ip = p4; + if (p4 == p1 || p4 == p2) return true; + else if (IsHorizontal(p1, p2)) return ((p4.X > p1.X) == (p4.X < p2.X)); + else return ((p4.Y > p1.Y) == (p4.Y < p2.Y)); + } + if ((res3 > 0) == (res4 > 0)) + { + ip = new Point64(0, 0); + return false; + } + + // segments must intersect to get here + return InternalClipper.GetIntersectPoint(p1, p2, p3, p4, out ip); + } + + + static protected bool GetIntersection(Path64 rectPath, Point64 p, Point64 p2, ref Location loc, out Point64 ip) + { + // gets the pt of intersection between rectPath and segment(p, p2) that's closest to 'p' + // when result == false, loc will remain unchanged + ip = new Point64(); + switch (loc) + { + case Location.left: + if (GetSegmentIntersection(p, p2, rectPath[0], rectPath[3], out ip)) + return true; + else if (p.Y < rectPath[0].Y && GetSegmentIntersection(p, p2, rectPath[0], rectPath[1], out ip)) + { + loc = Location.top; + return true; + } + else if (GetSegmentIntersection(p, p2, rectPath[2], rectPath[3], out ip)) + { + loc = Location.bottom; + return true; + } + else return false; + + case Location.right: + if (GetSegmentIntersection(p, p2, rectPath[1], rectPath[2], out ip)) + return true; + else if (p.Y < rectPath[0].Y && GetSegmentIntersection(p, p2, rectPath[0], rectPath[1], out ip)) + { + loc = Location.top; + return true; + } + else if (GetSegmentIntersection(p, p2, rectPath[2], rectPath[3], out ip)) + { + loc = Location.bottom; + return true; + } + else return false; + + case Location.top: + if (GetSegmentIntersection(p, p2, rectPath[0], rectPath[1], out ip)) + return true; + else if (p.X < rectPath[0].X && GetSegmentIntersection(p, p2, rectPath[0], rectPath[3], out ip)) + { + loc = Location.left; + return true; + } + else if (p.X > rectPath[1].X && GetSegmentIntersection(p, p2, rectPath[1], rectPath[2], out ip)) + { + loc = Location.right; + return true; + } + else return false; + + case Location.bottom: + if (GetSegmentIntersection(p, p2, rectPath[2], rectPath[3], out ip)) + return true; + else if (p.X < rectPath[3].X && GetSegmentIntersection(p, p2, rectPath[0], rectPath[3], out ip)) + { + loc = Location.left; + return true; + } + else if (p.X > rectPath[2].X && GetSegmentIntersection(p, p2, rectPath[1], rectPath[2], out ip)) + { + loc = Location.right; + return true; + } + else return false; + + default: + if (GetSegmentIntersection(p, p2, rectPath[0], rectPath[3], out ip)) + { + loc = Location.left; + return true; + } + else if (GetSegmentIntersection(p, p2, rectPath[0], rectPath[1], out ip)) + { + loc = Location.top; + return true; + } + else if (GetSegmentIntersection(p, p2, rectPath[1], rectPath[2], out ip)) + { + loc = Location.right; + return true; + } + else if (GetSegmentIntersection(p, p2, rectPath[2], rectPath[3], out ip)) + { + loc = Location.bottom; + return true; + } + else return false; + } + } + + protected void GetNextLocation(Path64 path, + ref Location loc, ref int i, int highI) + { + switch (loc) + { + case Location.left: + { + while (i <= highI && path[i].X <= rect_.left) i++; + if (i > highI) break; + if (path[i].X >= rect_.right) loc = Location.right; + else if (path[i].Y <= rect_.top) loc = Location.top; + else if (path[i].Y >= rect_.bottom) loc = Location.bottom; + else loc = Location.inside; + } + break; + + case Location.top: + { + while (i <= highI && path[i].Y <= rect_.top) i++; + if (i > highI) break; + if (path[i].Y >= rect_.bottom) loc = Location.bottom; + else if (path[i].X <= rect_.left) loc = Location.left; + else if (path[i].X >= rect_.right) loc = Location.right; + else loc = Location.inside; + } + break; + + case Location.right: + { + while (i <= highI && path[i].X >= rect_.right) i++; + if (i > highI) break; + if (path[i].X <= rect_.left) loc = Location.left; + else if (path[i].Y <= rect_.top) loc = Location.top; + else if (path[i].Y >= rect_.bottom) loc = Location.bottom; + else loc = Location.inside; + } + break; + + case Location.bottom: + { + while (i <= highI && path[i].Y >= rect_.bottom) i++; + if (i > highI) break; + if (path[i].Y <= rect_.top) loc = Location.top; + else if (path[i].X <= rect_.left) loc = Location.left; + else if (path[i].X >= rect_.right) loc = Location.right; + else loc = Location.inside; + } + break; + + case Location.inside: + { + while (i <= highI) + { + if (path[i].X < rect_.left) loc = Location.left; + else if (path[i].X > rect_.right) loc = Location.right; + else if (path[i].Y > rect_.bottom) loc = Location.bottom; + else if (path[i].Y < rect_.top) loc = Location.top; + else + { + Add(path[i]); + i++; + continue; + } + break; + } + } + break; + } // switch + } + + private void ExecuteInternal(Path64 path) + { + if (path.Count < 3 || rect_.IsEmpty()) return; + List startLocs = new List(); + + Location firstCross = Location.inside; + Location crossingLoc = firstCross, prev = firstCross; + + int i, highI = path.Count - 1; + if (!GetLocation(rect_, path[highI], out Location loc)) + { + i = highI - 1; + while (i >= 0 && !GetLocation(rect_, path[i], out prev)) i--; + if (i < 0) + { + foreach(Point64 pt in path) { Add(pt); } + return; + } + if (prev == Location.inside) loc = Location.inside; + } + Location startingLoc = loc; + + /////////////////////////////////////////////////// + i = 0; + while (i <= highI) + { + prev = loc; + Location prevCrossLoc = crossingLoc; + GetNextLocation(path, ref loc, ref i, highI); + if (i > highI) break; + + Point64 prevPt = (i == 0) ? path[highI] : path[i - 1]; + crossingLoc = loc; + if (!GetIntersection(rectPath_, + path[i], prevPt, ref crossingLoc, out Point64 ip)) + { + // ie remaining outside + if (prevCrossLoc == Location.inside) + { + bool isClockw = IsClockwise(prev, loc, prevPt, path[i], mp_); + do + { + startLocs.Add(prev); + prev = GetAdjacentLocation(prev, isClockw); + } while (prev != loc); + crossingLoc = prevCrossLoc; // still not crossed + } + + else if (prev != Location.inside && prev != loc) + { + bool isClockw = IsClockwise(prev, loc, prevPt, path[i], mp_); + do + { + AddCorner(ref prev, isClockw); + } while (prev != loc); + } + ++i; + continue; + } + + //////////////////////////////////////////////////// + // we must be crossing the rect boundary to get here + //////////////////////////////////////////////////// + + if (loc == Location.inside) // path must be entering rect + { + if (firstCross == Location.inside) + { + firstCross = crossingLoc; + startLocs.Add(prev); + } + else if (prev != crossingLoc) + { + bool isClockw = IsClockwise(prev, crossingLoc, prevPt, path[i], mp_); + do + { + AddCorner(ref prev, isClockw); + } while (prev != crossingLoc); + } + } + else if (prev != Location.inside) + { + // passing right through rect. 'ip' here will be the second + // intersect pt but we'll also need the first intersect pt (ip2) + loc = prev; + GetIntersection(rectPath_, + prevPt, path[i], ref loc, out Point64 ip2); + if (prevCrossLoc != Location.inside && prevCrossLoc != loc) //#597 + AddCorner(prevCrossLoc, loc); + + if (firstCross == Location.inside) + { + firstCross = loc; + startLocs.Add(prev); + } + + loc = crossingLoc; + Add(ip2); + if (ip == ip2) + { + // it's very likely that path[i] is on rect + GetLocation(rect_, path[i], out loc); + AddCorner(crossingLoc, loc); + crossingLoc = loc; + continue; + } + } + else // path must be exiting rect + { + loc = crossingLoc; + if (firstCross == Location.inside) + firstCross = crossingLoc; + } + + Add(ip); + } //while i <= highI + /////////////////////////////////////////////////// + + if (firstCross == Location.inside) + { + // path never intersects + if (startingLoc != Location.inside) + { + if (pathBounds_.Contains(rect_) && + Path1ContainsPath2(path, rectPath_)) + { + for (int j = 0; j < 4; j++) + { + Add(rectPath_[j]); + AddToEdge(edges_[j * 2], results_[0]!); + } + } + } + } + else if (loc != Location.inside && + (loc != firstCross || startLocs.Count > 2)) + { + if (startLocs.Count > 0) + { + prev = loc; + foreach (Location loc2 in startLocs) + { + if (prev == loc2) continue; + AddCorner(ref prev, HeadingClockwise(prev, loc2)); + prev = loc2; + } + loc = prev; + } + if (loc != firstCross) + AddCorner(ref loc, HeadingClockwise(loc, firstCross)); + } + } + + public Paths64 Execute(Paths64 paths) + { + Paths64 result = new Paths64(); + if (rect_.IsEmpty()) return result; + foreach (Path64 path in paths) + { + if (path.Count < 3) continue; + pathBounds_ = Clipper.GetBounds(path); + if (!rect_.Intersects(pathBounds_)) + continue; // the path must be completely outside fRect + else if (rect_.Contains(pathBounds_)) + { + // the path must be completely inside rect_ + result.Add(path); + continue; + } + ExecuteInternal(path); + CheckEdges(); + for (int i = 0; i < 4; ++i) + TidyEdgePair(i, edges_[i * 2], edges_[i * 2 + 1]); + + foreach (OutPt2? op in results_) + { + Path64 tmp = GetPath(op); + if (tmp.Count > 0) result.Add(tmp); + } + + //clean up after every loop + results_.Clear(); + for (int i = 0; i < 8; i++) + edges_[i].Clear(); + } + return result; + } + + private void CheckEdges() + { + for (int i = 0; i < results_.Count; i++) + { + OutPt2? op = results_[i], op2 = op; + if (op == null) continue; + do + { + if (InternalClipper.CrossProduct( + op2!.prev!.pt, op2.pt, op2.next!.pt) == 0) + { + if (op2 == op) + { + op2 = UnlinkOpBack(op2); + if (op2 == null) break; + op = op2.prev; + } + else + { + op2 = UnlinkOpBack(op2); + if (op2 == null) break; + } + } + else + op2 = op2.next; + } while (op2 != op); + + if (op2 == null) + { + results_[i] = null; + continue; + } + results_[i] = op2; // safety first + + uint edgeSet1 = GetEdgesForPt(op!.prev!.pt, rect_); + op2 = op; + do + { + uint edgeSet2 = GetEdgesForPt(op2!.pt, rect_); + if (edgeSet2 != 0 && op2.edge == null) + { + uint combinedSet = (edgeSet1 & edgeSet2); + for (int j = 0; j < 4; ++j) + { + if ((combinedSet & (1 << j)) != 0) + { + if (IsHeadingClockwise(op2.prev!.pt, op2.pt, j)) + AddToEdge(edges_[j * 2], op2); + else + AddToEdge(edges_[j * 2 + 1], op2); + } + } + } + edgeSet1 = edgeSet2; + op2 = op2.next; + } while (op2 != op); + } + } + + private void TidyEdgePair(int idx, List cw, List ccw) + { + if (ccw.Count == 0) return; + bool isHorz = ((idx == 1) || (idx == 3)); + bool cwIsTowardLarger = ((idx == 1) || (idx == 2)); + int i = 0, j = 0; + OutPt2? p1, p2, p1a, p2a, op, op2; + + while (i < cw.Count) + { + p1 = cw[i]; + if (p1 == null || p1.next == p1.prev) + { + cw[i++] = null; + j = 0; + continue; + } + + int jLim = ccw.Count; + while (j < jLim && + (ccw[j] == null || ccw[j]!.next == ccw[j]!.prev)) ++j; + + if (j == jLim) + { + ++i; + j = 0; + continue; + } + + if (cwIsTowardLarger) + { + // p1 >>>> p1a; + // p2 <<<< p2a; + p1 = cw[i]!.prev!; + p1a = cw[i]; + p2 = ccw[j]; + p2a = ccw[j]!.prev!; + } + else + { + // p1 <<<< p1a; + // p2 >>>> p2a; + p1 = cw[i]; + p1a = cw[i]!.prev!; + p2 = ccw[j]!.prev!; + p2a = ccw[j]; + } + + if ((isHorz && !HasHorzOverlap(p1!.pt, p1a!.pt, p2!.pt, p2a!.pt)) || + (!isHorz && !HasVertOverlap(p1!.pt, p1a!.pt, p2!.pt, p2a!.pt))) + { + ++j; + continue; + } + + // to get here we're either splitting or rejoining + bool isRejoining = cw[i]!.ownerIdx != ccw[j]!.ownerIdx; + + if (isRejoining) + { + results_[p2!.ownerIdx] = null; + SetNewOwner(p2, p1!.ownerIdx); + } + + // do the split or re-join + if (cwIsTowardLarger) + { + // p1 >> | >> p1a; + // p2 << | << p2a; + p1!.next = p2; + p2!.prev = p1; + p1a!.prev = p2a; + p2a!.next = p1a; + } + else + { + // p1 << | << p1a; + // p2 >> | >> p2a; + p1!.prev = p2; + p2!.next = p1; + p1a!.next = p2a; + p2a!.prev = p1a; + } + + if (!isRejoining) + { + int new_idx = results_.Count; + results_.Add(p1a); + SetNewOwner(p1a, new_idx); + } + + if (cwIsTowardLarger) + { + op = p2; + op2 = p1a; + } + else + { + op = p1; + op2 = p2a; + } + results_[op.ownerIdx] = op; + results_[op2.ownerIdx] = op2; + + // and now lots of work to get ready for the next loop + + bool opIsLarger, op2IsLarger; + if (isHorz) // X + { + opIsLarger = op.pt.X > op.prev!.pt.X; + op2IsLarger = op2.pt.X > op2.prev!.pt.X; + } + else // Y + { + opIsLarger = op.pt.Y > op.prev!.pt.Y; + op2IsLarger = op2.pt.Y > op2.prev!.pt.Y; + } + + if ((op.next == op.prev) || + (op.pt == op.prev.pt)) + { + if (op2IsLarger == cwIsTowardLarger) + { + cw[i] = op2; + ccw[j++] = null; + } + else + { + ccw[j] = op2; + cw[i++] = null; + } + } + else if ((op2.next == op2.prev) || + (op2.pt == op2.prev.pt)) + { + if (opIsLarger == cwIsTowardLarger) + { + cw[i] = op; + ccw[j++] = null; + } + else + { + ccw[j] = op; + cw[i++] = null; + } + } + else if (opIsLarger == op2IsLarger) + { + if (opIsLarger == cwIsTowardLarger) + { + cw[i] = op; + UncoupleEdge(op2); + AddToEdge(cw, op2); + ccw[j++] = null; + } + else + { + cw[i++] = null; + ccw[j] = op2; + UncoupleEdge(op); + AddToEdge(ccw, op); + j = 0; + } + } + else + { + if (opIsLarger == cwIsTowardLarger) + cw[i] = op; + else + ccw[j] = op; + if (op2IsLarger == cwIsTowardLarger) + cw[i] = op2; + else + ccw[j] = op2; + } + } + } + + private Path64 GetPath(OutPt2? op) + { + Path64 result = new Path64(); + if (op == null || op.prev == op.next) return result; + OutPt2? op2 = op.next; + while (op2 != null && op2 != op) + { + if (InternalClipper.CrossProduct( + op2!.prev!.pt, op2.pt, op2!.next!.pt) == 0) + { + op = op2.prev; + op2 = UnlinkOp(op2); + } + else + op2 = op2.next; + } + if (op2 == null) return new Path64(); + + result.Add(op.pt); + op2 = op.next; + while (op2 != op) + { + result.Add(op2!.pt); + op2 = op2.next; + } + return result; + } + + } // RectClip class + + public class RectClipLines64 : RectClip64 + { + internal RectClipLines64(Rect64 rect) : base(rect) { } + + public new Paths64 Execute(Paths64 paths) + { + Paths64 result = new Paths64(); + if (rect_.IsEmpty()) return result; + foreach (Path64 path in paths) + { + if (path.Count < 2) continue; + pathBounds_ = Clipper.GetBounds(path); + if (!rect_.Intersects(pathBounds_)) + continue; // the path must be completely outside fRect + // Apart from that, we can't be sure whether the path + // is completely outside or completed inside or intersects + // fRect, simply by comparing path bounds with fRect. + ExecuteInternal(path); + + foreach (OutPt2? op in results_) + { + Path64 tmp = GetPath(op); + if (tmp.Count > 0) result.Add(tmp); + } + + //clean up after every loop + results_.Clear(); + for(int i = 0; i < 8; i++) + edges_[i].Clear(); + } + return result; + } + + private Path64 GetPath(OutPt2? op) + { + Path64 result = new Path64(); + if (op == null || op == op.next) return result; + op = op.next; // starting at path beginning + result.Add(op!.pt); + OutPt2 op2 = op.next!; + while (op2 != op) + { + result.Add(op2!.pt); + op2 = op2.next!; + } + return result; + } + + private void ExecuteInternal(Path64 path) + { + results_.Clear(); + if (path.Count < 2 || rect_.IsEmpty()) return; + + Location prev = Location.inside; + int i = 1, highI = path.Count - 1; + if (!GetLocation(rect_, path[0], out Location loc)) + { + while (i <= highI && !GetLocation(rect_, path[i], out prev)) i++; + if (i > highI) + { + foreach (Point64 pt in path) Add(pt); + } + if (prev == Location.inside) loc = Location.inside; + i = 1; + } + if (loc == Location.inside) Add(path[0]); + + /////////////////////////////////////////////////// + while (i <= highI) + { + prev = loc; + GetNextLocation(path, ref loc, ref i, highI); + if (i > highI) break; + Point64 prevPt = path[i - 1]; + + Location crossingLoc = loc; + if (!GetIntersection(rectPath_, path[i], prevPt, ref crossingLoc, out Point64 ip)) + { + // ie remaining outside (& crossingLoc still == loc) + ++i; + continue; + } + + //////////////////////////////////////////////////// + // we must be crossing the rect boundary to get here + //////////////////////////////////////////////////// + + if (loc == Location.inside) // path must be entering rect + { + Add(ip, true); + } + else if (prev != Location.inside) + { + // passing right through rect. 'ip' here will be the second + // intersect pt but we'll also need the first intersect pt (ip2) + crossingLoc = prev; + GetIntersection(rectPath_, prevPt, path[i], ref crossingLoc, out Point64 ip2); + Add(ip2); + Add(ip); + } + else // path must be exiting rect + { + Add(ip); + } + } //while i <= highI + /////////////////////////////////////////////////// + } // RectClipLines.ExecuteInternal + + } // RectClipLines class + +} // namespace \ No newline at end of file diff --git a/BossMod/ThirdParty/Clipper2Lib/Clipper.cs b/BossMod/ThirdParty/Clipper2Lib/Clipper.cs new file mode 100644 index 0000000000..6b63f05d6b --- /dev/null +++ b/BossMod/ThirdParty/Clipper2Lib/Clipper.cs @@ -0,0 +1,1156 @@ +/******************************************************************************* +* Author : Angus Johnson * +* Date : 18 October 2023 * +* Website : http://www.angusj.com * +* Copyright : Angus Johnson 2010-2023 * +* Purpose : This module contains simple functions that will likely cover * +* most polygon boolean and offsetting needs, while also avoiding * +* the inherent complexities of the other modules. * +* Thanks : Special thanks to Thong Nguyen, Guus Kuiper, Phil Stopford, * +* : and Daniel Gosnell for their invaluable assistance with C#. * +* License : http://www.boost.org/LICENSE_1_0.txt * +*******************************************************************************/ + +#nullable enable +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace Clipper2Lib +{ + + // PRE-COMPILER CONDITIONAL ... + // USINGZ: For user defined Z-coordinates. See Clipper.SetZ + + public static class Clipper + { + private static Rect64 invalidRect64 = new Rect64(false); + public static Rect64 InvalidRect64 => invalidRect64; + + private static RectD invalidRectD = new RectD(false); + public static RectD InvalidRectD => invalidRectD; + + public static Paths64 Intersect(Paths64 subject, Paths64 clip, FillRule fillRule) + { + return BooleanOp(ClipType.Intersection, subject, clip, fillRule); + } + + public static PathsD Intersect(PathsD subject, PathsD clip, + FillRule fillRule, int precision = 2) + { + return BooleanOp(ClipType.Intersection, + subject, clip, fillRule, precision); + } + + public static Paths64 Union(Paths64 subject, FillRule fillRule) + { + return BooleanOp(ClipType.Union, subject, null, fillRule); + } + + public static Paths64 Union(Paths64 subject, Paths64 clip, FillRule fillRule) + { + return BooleanOp(ClipType.Union, subject, clip, fillRule); + } + + public static PathsD Union(PathsD subject, FillRule fillRule) + { + return BooleanOp(ClipType.Union, subject, null, fillRule); + } + + public static PathsD Union(PathsD subject, PathsD clip, + FillRule fillRule, int precision = 2) + { + return BooleanOp(ClipType.Union, + subject, clip, fillRule, precision); + } + + public static Paths64 Difference(Paths64 subject, Paths64 clip, FillRule fillRule) + { + return BooleanOp(ClipType.Difference, subject, clip, fillRule); + } + + public static PathsD Difference(PathsD subject, PathsD clip, + FillRule fillRule, int precision = 2) + { + return BooleanOp(ClipType.Difference, + subject, clip, fillRule, precision); + } + + public static Paths64 Xor(Paths64 subject, Paths64 clip, FillRule fillRule) + { + return BooleanOp(ClipType.Xor, subject, clip, fillRule); + } + + public static PathsD Xor(PathsD subject, PathsD clip, + FillRule fillRule, int precision = 2) + { + return BooleanOp(ClipType.Xor, + subject, clip, fillRule, precision); + } + + public static Paths64 BooleanOp(ClipType clipType, + Paths64? subject, Paths64? clip, FillRule fillRule) + { + Paths64 solution = new Paths64(); + if (subject == null) return solution; + Clipper64 c = new Clipper64(); + c.AddPaths(subject, PathType.Subject); + if (clip != null) + c.AddPaths(clip, PathType.Clip); + c.Execute(clipType, fillRule, solution); + return solution; + } + + public static void BooleanOp(ClipType clipType, + Paths64? subject, Paths64? clip, + PolyTree64 polytree, FillRule fillRule) + { + if (subject == null) return; + Clipper64 c = new Clipper64(); + c.AddPaths(subject, PathType.Subject); + if (clip != null) + c.AddPaths(clip, PathType.Clip); + c.Execute(clipType, fillRule, polytree); + } + + public static PathsD BooleanOp(ClipType clipType, PathsD subject, PathsD? clip, + FillRule fillRule, int precision = 2) + { + PathsD solution = new PathsD(); + ClipperD c = new ClipperD(precision); + c.AddSubject(subject); + if (clip != null) + c.AddClip(clip); + c.Execute(clipType, fillRule, solution); + return solution; + } + + public static void BooleanOp(ClipType clipType, + PathsD? subject, PathsD? clip, + PolyTreeD polytree, FillRule fillRule, int precision = 2) + { + if (subject == null) return; + ClipperD c = new ClipperD(precision); + c.AddPaths(subject, PathType.Subject); + if (clip != null) + c.AddPaths(clip, PathType.Clip); + c.Execute(clipType, fillRule, polytree); + } + + public static Paths64 InflatePaths(Paths64 paths, double delta, JoinType joinType, + EndType endType, double miterLimit = 2.0) + { + ClipperOffset co = new ClipperOffset(miterLimit); + co.AddPaths(paths, joinType, endType); + Paths64 solution = new Paths64(); + co.Execute(delta, solution); + return solution; + } + + public static PathsD InflatePaths(PathsD paths, double delta, JoinType joinType, + EndType endType, double miterLimit = 2.0, int precision = 2) + { + InternalClipper.CheckPrecision(precision); + double scale = Math.Pow(10, precision); + Paths64 tmp = ScalePaths64(paths, scale); + ClipperOffset co = new ClipperOffset(miterLimit); + co.AddPaths(tmp, joinType, endType); + co.Execute(delta * scale, tmp); // reuse 'tmp' to receive (scaled) solution + return ScalePathsD(tmp, 1 / scale); + } + + public static Paths64 RectClip(Rect64 rect, Paths64 paths) + { + if (rect.IsEmpty() || paths.Count == 0) return new Paths64(); + RectClip64 rc = new RectClip64(rect); + return rc.Execute(paths); + } + + public static Paths64 RectClip(Rect64 rect, Path64 path) + { + if (rect.IsEmpty() || path.Count == 0) return new Paths64(); + Paths64 tmp = new Paths64 { path }; + return RectClip(rect, tmp); + } + + public static PathsD RectClip(RectD rect, PathsD paths, int precision = 2) + { + InternalClipper.CheckPrecision(precision); + if (rect.IsEmpty() || paths.Count == 0) return new PathsD(); + double scale = Math.Pow(10, precision); + Rect64 r = ScaleRect(rect, scale); + Paths64 tmpPath = ScalePaths64(paths, scale); + RectClip64 rc = new RectClip64(r); + tmpPath = rc.Execute(tmpPath); + return ScalePathsD(tmpPath, 1 / scale); + } + + public static PathsD RectClip(RectD rect, PathD path, int precision = 2) + { + if (rect.IsEmpty() || path.Count == 0) return new PathsD(); + PathsD tmp = new PathsD { path }; + return RectClip(rect, tmp, precision); + } + public static Paths64 RectClipLines(Rect64 rect, Paths64 paths) + { + if (rect.IsEmpty() || paths.Count == 0) return new Paths64(); + RectClipLines64 rc = new RectClipLines64(rect); + return rc.Execute(paths); + } + + public static Paths64 RectClipLines(Rect64 rect, Path64 path) + { + if (rect.IsEmpty() || path.Count == 0) return new Paths64(); + Paths64 tmp = new Paths64 { path }; + return RectClipLines(rect, tmp); + } + + public static PathsD RectClipLines(RectD rect, + PathsD paths, int precision = 2) + { + InternalClipper.CheckPrecision(precision); + if (rect.IsEmpty() || paths.Count == 0) return new PathsD(); + double scale = Math.Pow(10, precision); + Rect64 r = ScaleRect(rect, scale); + Paths64 tmpPath = ScalePaths64(paths, scale); + RectClipLines64 rc = new RectClipLines64(r); + tmpPath = rc.Execute(tmpPath); + return ScalePathsD(tmpPath, 1 / scale); + } + public static PathsD RectClipLines(RectD rect, PathD path, int precision = 2) + { + if (rect.IsEmpty() || path.Count == 0) return new PathsD(); + PathsD tmp = new PathsD { path }; + return RectClipLines(rect, tmp, precision); + } + public static Paths64 MinkowskiSum(Path64 pattern, Path64 path, bool isClosed) + { + return Minkowski.Sum(pattern, path, isClosed); + } + + public static PathsD MinkowskiSum(PathD pattern, PathD path, bool isClosed) + { + return Minkowski.Sum(pattern, path, isClosed); + } + + public static Paths64 MinkowskiDiff(Path64 pattern, Path64 path, bool isClosed) + { + return Minkowski.Diff(pattern, path, isClosed); + } + + public static PathsD MinkowskiDiff(PathD pattern, PathD path, bool isClosed) + { + return Minkowski.Diff(pattern, path, isClosed); + } + + public static double Area(Path64 path) + { + // https://en.wikipedia.org/wiki/Shoelace_formula + double a = 0.0; + int cnt = path.Count; + if (cnt < 3) return 0.0; + Point64 prevPt = path[cnt - 1]; + foreach (Point64 pt in path) + { + a += (double) (prevPt.Y + pt.Y) * (prevPt.X - pt.X); + prevPt = pt; + } + return a * 0.5; + } + + public static double Area(Paths64 paths) + { + double a = 0.0; + foreach (Path64 path in paths) + a += Area(path); + return a; + } + + public static double Area(PathD path) + { + double a = 0.0; + int cnt = path.Count; + if (cnt < 3) return 0.0; + PointD prevPt = path[cnt - 1]; + foreach (PointD pt in path) + { + a += (prevPt.y + pt.y) * (prevPt.x - pt.x); + prevPt = pt; + } + return a * 0.5; + } + + public static double Area(PathsD paths) + { + double a = 0.0; + foreach (PathD path in paths) + a += Area(path); + return a; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsPositive(Path64 poly) + { + return Area(poly) >= 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsPositive(PathD poly) + { + return Area(poly) >= 0; + } + + public static string Path64ToString(Path64 path) + { + string result = ""; + foreach (Point64 pt in path) + result += pt.ToString(); + return result + '\n'; + } + public static string Paths64ToString(Paths64 paths) + { + string result = ""; + foreach (Path64 path in paths) + result += Path64ToString(path); + return result; + } + public static string PathDToString(PathD path) + { + string result = ""; + foreach (PointD pt in path) + result += pt.ToString(); + return result + '\n'; + } + public static string PathsDToString(PathsD paths) + { + string result = ""; + foreach (PathD path in paths) + result += PathDToString(path); + return result; + } + public static Path64 OffsetPath(Path64 path, long dx, long dy) + { + Path64 result = new Path64(path.Count); + foreach (Point64 pt in path) + result.Add(new Point64(pt.X + dx, pt.Y + dy)); + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Point64 ScalePoint64(Point64 pt, double scale) + { + Point64 result = new Point64() + { + X = (long) Math.Round(pt.X * scale, MidpointRounding.AwayFromZero), + Y = (long) Math.Round(pt.Y * scale, MidpointRounding.AwayFromZero), +#if USINGZ + Z = pt.Z +#endif + }; + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static PointD ScalePointD(Point64 pt, double scale) + { + PointD result = new PointD() + { + x = pt.X * scale, + y = pt.Y * scale, +#if USINGZ + z = pt.Z, +#endif + }; + return result; + } + + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rect64 ScaleRect(RectD rec, double scale) + { + Rect64 result = new Rect64() + { + left = (long) (rec.left * scale), + top = (long) (rec.top * scale), + right = (long) (rec.right * scale), + bottom = (long) (rec.bottom * scale) + }; + return result; + } + + public static Path64 ScalePath(Path64 path, double scale) + { + if (InternalClipper.IsAlmostZero(scale - 1)) return path; + Path64 result = new Path64(path.Count); +#if USINGZ + foreach (Point64 pt in path) + result.Add(new Point64(pt.X * scale, pt.Y * scale, pt.Z)); +#else + foreach (Point64 pt in path) + result.Add(new Point64(pt.X * scale, pt.Y * scale)); +#endif + return result; + } + + public static Paths64 ScalePaths(Paths64 paths, double scale) + { + if (InternalClipper.IsAlmostZero(scale - 1)) return paths; + Paths64 result = new Paths64(paths.Count); + foreach (Path64 path in paths) + result.Add(ScalePath(path, scale)); + return result; + } + + public static PathD ScalePath(PathD path, double scale) + { + if (InternalClipper.IsAlmostZero(scale - 1)) return path; + PathD result = new PathD(path.Count); + foreach (PointD pt in path) + result.Add(new PointD(pt, scale)); + return result; + } + + public static PathsD ScalePaths(PathsD paths, double scale) + { + if (InternalClipper.IsAlmostZero(scale - 1)) return paths; + PathsD result = new PathsD(paths.Count); + foreach (PathD path in paths) + result.Add(ScalePath(path, scale)); + return result; + } + + // Unlike ScalePath, both ScalePath64 & ScalePathD also involve type conversion + public static Path64 ScalePath64(PathD path, double scale) + { + int cnt = path.Count; + Path64 res = new Path64(cnt); + foreach (PointD pt in path) + res.Add(new Point64(pt, scale)); + return res; + } + + public static Paths64 ScalePaths64(PathsD paths, double scale) + { + int cnt = paths.Count; + Paths64 res = new Paths64(cnt); + foreach (PathD path in paths) + res.Add(ScalePath64(path, scale)); + return res; + } + + public static PathD ScalePathD(Path64 path, double scale) + { + int cnt = path.Count; + PathD res = new PathD(cnt); + foreach (Point64 pt in path) + res.Add(new PointD(pt, scale)); + return res; + } + + public static PathsD ScalePathsD(Paths64 paths, double scale) + { + int cnt = paths.Count; + PathsD res = new PathsD(cnt); + foreach (Path64 path in paths) + res.Add(ScalePathD(path, scale)); + return res; + } + + // The static functions Path64 and PathD convert path types without scaling + public static Path64 Path64(PathD path) + { + Path64 result = new Path64(path.Count); + foreach (PointD pt in path) + result.Add(new Point64(pt)); + return result; + } + + public static Paths64 Paths64(PathsD paths) + { + Paths64 result = new Paths64(paths.Count); + foreach (PathD path in paths) + result.Add(Path64(path)); + return result; + } + + public static PathsD PathsD(Paths64 paths) + { + PathsD result = new PathsD(paths.Count); + foreach (Path64 path in paths) + result.Add(PathD(path)); + return result; + } + + public static PathD PathD(Path64 path) + { + PathD result = new PathD(path.Count); + foreach (Point64 pt in path) + result.Add(new PointD(pt)); + return result; + } + + public static Path64 TranslatePath(Path64 path, long dx, long dy) + { + Path64 result = new Path64(path.Count); + foreach (Point64 pt in path) + result.Add(new Point64(pt.X + dx, pt.Y + dy)); + return result; + } + + public static Paths64 TranslatePaths(Paths64 paths, long dx, long dy) + { + Paths64 result = new Paths64(paths.Count); + foreach (Path64 path in paths) + result.Add(OffsetPath(path, dx, dy)); + return result; + } + + public static PathD TranslatePath(PathD path, double dx, double dy) + { + PathD result = new PathD(path.Count); + foreach (PointD pt in path) + result.Add(new PointD(pt.x + dx, pt.y + dy)); + return result; + } + + public static PathsD TranslatePaths(PathsD paths, double dx, double dy) + { + PathsD result = new PathsD(paths.Count); + foreach (PathD path in paths) + result.Add(TranslatePath(path, dx, dy)); + return result; + } + + public static Path64 ReversePath(Path64 path) + { + Path64 result = new Path64(path); + result.Reverse(); + return result; + } + + public static PathD ReversePath(PathD path) + { + PathD result = new PathD(path); + result.Reverse(); + return result; + } + + public static Paths64 ReversePaths(Paths64 paths) + { + Paths64 result = new Paths64(paths.Count); + foreach (Path64 t in paths) + result.Add(ReversePath(t)); + + return result; + } + + public static PathsD ReversePaths(PathsD paths) + { + PathsD result = new PathsD(paths.Count); + foreach (PathD path in paths) + result.Add(ReversePath(path)); + return result; + } + + public static Rect64 GetBounds(Path64 path) + { + Rect64 result = InvalidRect64; + foreach (Point64 pt in path) + { + if (pt.X < result.left) result.left = pt.X; + if (pt.X > result.right) result.right = pt.X; + if (pt.Y < result.top) result.top = pt.Y; + if (pt.Y > result.bottom) result.bottom = pt.Y; + } + return result.left == long.MaxValue ? new Rect64() : result; + } + + public static Rect64 GetBounds(Paths64 paths) + { + Rect64 result = InvalidRect64; + foreach (Path64 path in paths) + foreach (Point64 pt in path) + { + if (pt.X < result.left) result.left = pt.X; + if (pt.X > result.right) result.right = pt.X; + if (pt.Y < result.top) result.top = pt.Y; + if (pt.Y > result.bottom) result.bottom = pt.Y; + } + return result.left == long.MaxValue ? new Rect64() : result; + } + + public static RectD GetBounds(PathD path) + { + RectD result = InvalidRectD; + foreach (PointD pt in path) + { + if (pt.x < result.left) result.left = pt.x; + if (pt.x > result.right) result.right = pt.x; + if (pt.y < result.top) result.top = pt.y; + if (pt.y > result.bottom) result.bottom = pt.y; + } + return result.left == double.MaxValue ? new RectD() : result; + } + + public static RectD GetBounds(PathsD paths) + { + RectD result = InvalidRectD; + foreach (PathD path in paths) + foreach (PointD pt in path) + { + if (pt.x < result.left) result.left = pt.x; + if (pt.x > result.right) result.right = pt.x; + if (pt.y < result.top) result.top = pt.y; + if (pt.y > result.bottom) result.bottom = pt.y; + } + return result.left == double.MaxValue ? new RectD() : result; + } + + public static Path64 MakePath(int[] arr) + { + int len = arr.Length / 2; + Path64 p = new Path64(len); + for (int i = 0; i < len; i++) + p.Add(new Point64(arr[i * 2], arr[i * 2 + 1])); + return p; + } + + public static Path64 MakePath(long[] arr) + { + int len = arr.Length / 2; + Path64 p = new Path64(len); + for (int i = 0; i < len; i++) + p.Add(new Point64(arr[i * 2], arr[i * 2 + 1])); + return p; + } + + public static PathD MakePath(double[] arr) + { + int len = arr.Length / 2; + PathD p = new PathD(len); + for (int i = 0; i < len; i++) + p.Add(new PointD(arr[i * 2], arr[i * 2 + 1])); + return p; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double Sqr(double value) + { + return value * value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool PointsNearEqual(PointD pt1, PointD pt2, double distanceSqrd) + { + return Sqr(pt1.x - pt2.x) + Sqr(pt1.y - pt2.y) < distanceSqrd; + } + + public static PathD StripNearDuplicates(PathD path, + double minEdgeLenSqrd, bool isClosedPath) + { + int cnt = path.Count; + PathD result = new PathD(cnt); + if (cnt == 0) return result; + PointD lastPt = path[0]; + result.Add(lastPt); + for (int i = 1; i < cnt; i++) + if (!PointsNearEqual(lastPt, path[i], minEdgeLenSqrd)) + { + lastPt = path[i]; + result.Add(lastPt); + } + + if (isClosedPath && PointsNearEqual(lastPt, result[0], minEdgeLenSqrd)) + { + result.RemoveAt(result.Count - 1); + } + + return result; + } + + public static Path64 StripDuplicates(Path64 path, bool isClosedPath) + { + int cnt = path.Count; + Path64 result = new Path64(cnt); + if (cnt == 0) return result; + Point64 lastPt = path[0]; + result.Add(lastPt); + for (int i = 1; i < cnt; i++) + if (lastPt != path[i]) + { + lastPt = path[i]; + result.Add(lastPt); + } + if (isClosedPath && lastPt == result[0]) + result.RemoveAt(result.Count - 1); + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void AddPolyNodeToPaths(PolyPath64 polyPath, Paths64 paths) + { + if (polyPath.Polygon!.Count > 0) + paths.Add(polyPath.Polygon); + for (int i = 0; i < polyPath.Count; i++) + AddPolyNodeToPaths((PolyPath64) polyPath._childs[i], paths); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Paths64 PolyTreeToPaths64(PolyTree64 polyTree) + { + Paths64 result = new Paths64(); + for (int i = 0; i < polyTree.Count; i++) + AddPolyNodeToPaths((PolyPath64) polyTree._childs[i], result); + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void AddPolyNodeToPathsD(PolyPathD polyPath, PathsD paths) + { + if (polyPath.Polygon!.Count > 0) + paths.Add(polyPath.Polygon); + for (int i = 0; i < polyPath.Count; i++) + AddPolyNodeToPathsD((PolyPathD) polyPath._childs[i], paths); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static PathsD PolyTreeToPathsD(PolyTreeD polyTree) + { + PathsD result = new PathsD(); + foreach (PolyPathD polyPathBase in polyTree) + { + PolyPathD p = (PolyPathD)polyPathBase; + AddPolyNodeToPathsD(p, result); + } + + return result; + } + + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double PerpendicDistFromLineSqrd(PointD pt, PointD line1, PointD line2) + { + double a = pt.x - line1.x; + double b = pt.y - line1.y; + double c = line2.x - line1.x; + double d = line2.y - line1.y; + if (c == 0 && d == 0) return 0; + return Sqr(a * d - c * b) / (c * c + d * d); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double PerpendicDistFromLineSqrd(Point64 pt, Point64 line1, Point64 line2) + { + double a = (double) pt.X - line1.X; + double b = (double) pt.Y - line1.Y; + double c = (double) line2.X - line1.X; + double d = (double) line2.Y - line1.Y; + if (c == 0 && d == 0) return 0; + return Sqr(a * d - c * b) / (c * c + d * d); + } + + internal static void RDP(Path64 path, int begin, int end, double epsSqrd, List flags) + { + int idx = 0; + double max_d = 0; + while (end > begin && path[begin] == path[end]) flags[end--] = false; + for (int i = begin + 1; i < end; ++i) + { + // PerpendicDistFromLineSqrd - avoids expensive Sqrt() + double d = PerpendicDistFromLineSqrd(path[i], path[begin], path[end]); + if (d <= max_d) continue; + max_d = d; + idx = i; + } + if (max_d <= epsSqrd) return; + flags[idx] = true; + if (idx > begin + 1) RDP(path, begin, idx, epsSqrd, flags); + if (idx < end - 1) RDP(path, idx, end, epsSqrd, flags); + } + + public static Path64 RamerDouglasPeucker(Path64 path, double epsilon) + { + int len = path.Count; + if (len < 5) return path; + List flags = new List(new bool[len]) { [0] = true, [len - 1] = true }; + RDP(path, 0, len - 1, Sqr(epsilon), flags); + Path64 result = new Path64(len); + for (int i = 0; i < len; ++i) + if (flags[i]) result.Add(path[i]); + return result; + } + + public static Paths64 RamerDouglasPeucker(Paths64 paths, double epsilon) + { + Paths64 result = new Paths64(paths.Count); + foreach (Path64 path in paths) + result.Add(RamerDouglasPeucker(path, epsilon)); + return result; + } + + internal static void RDP(PathD path, int begin, int end, double epsSqrd, List flags) + { + int idx = 0; + double max_d = 0; + while (end > begin && path[begin] == path[end]) flags[end--] = false; + for (int i = begin + 1; i < end; ++i) + { + // PerpendicDistFromLineSqrd - avoids expensive Sqrt() + double d = PerpendicDistFromLineSqrd(path[i], path[begin], path[end]); + if (d <= max_d) continue; + max_d = d; + idx = i; + } + if (max_d <= epsSqrd) return; + flags[idx] = true; + if (idx > begin + 1) RDP(path, begin, idx, epsSqrd, flags); + if (idx < end - 1) RDP(path, idx, end, epsSqrd, flags); + } + + public static PathD RamerDouglasPeucker(PathD path, double epsilon) + { + int len = path.Count; + if (len < 5) return path; + List flags = new List(new bool[len]) { [0] = true, [len - 1] = true }; + RDP(path, 0, len - 1, Sqr(epsilon), flags); + PathD result = new PathD(len); + for (int i = 0; i < len; ++i) + if (flags[i]) result.Add(path[i]); + return result; + } + + public static PathsD RamerDouglasPeucker(PathsD paths, double epsilon) + { + PathsD result = new PathsD(paths.Count); + foreach (PathD path in paths) + result.Add(RamerDouglasPeucker(path, epsilon)); + return result; + } + + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int GetNext(int current, int high, ref bool[] flags) + { + ++current; + while (current <= high && flags[current]) ++current; + if (current <= high) return current; + current = 0; + while (flags[current]) ++current; + return current; + } + + private static int GetPrior(int current, int high, ref bool[] flags) + { + if (current == 0) current = high; + else --current; + while (current > 0 && flags[current]) --current; + if (!flags[current]) return current; + current = high; + while (flags[current]) --current; + return current; + } + + public static Path64 SimplifyPath(Path64 path, + double epsilon, bool isClosedPath = true) + { + int len = path.Count, high = len - 1; + double epsSqr = Sqr(epsilon); + if (len < 4) return path; + + bool[] flags = new bool[len]; + double[] dsq = new double[len]; + int curr = 0, prev, start, next, prior2; + + if (isClosedPath) + { + dsq[0] = PerpendicDistFromLineSqrd(path[0], path[high], path[1]); + dsq[high] = PerpendicDistFromLineSqrd(path[high], path[0], path[high - 1]); + } + else + { + dsq[0] = double.MaxValue; + dsq[high] = double.MaxValue; + } + + for (int i = 1; i < high; ++i) + dsq[i] = PerpendicDistFromLineSqrd(path[i], path[i - 1], path[i + 1]); + + for (; ; ) + { + if (dsq[curr] > epsSqr) + { + start = curr; + do + { + curr = GetNext(curr, high, ref flags); + } while (curr != start && dsq[curr] > epsSqr); + if (curr == start) break; + } + + prev = GetPrior(curr, high, ref flags); + next = GetNext(curr, high, ref flags); + if (next == prev) break; + + if (dsq[next] < dsq[curr]) + { + prior2 = prev; + prev = curr; + curr = next; + next = GetNext(next, high, ref flags); + } + else + prior2 = GetPrior(prev, high, ref flags); + + flags[curr] = true; + curr = next; + next = GetNext(next, high, ref flags); + if (isClosedPath || ((curr != high) && (curr != 0))) + dsq[curr] = PerpendicDistFromLineSqrd(path[curr], path[prev], path[next]); + if (isClosedPath || ((prev != 0) && (prev != high))) + dsq[prev] = PerpendicDistFromLineSqrd(path[prev], path[prior2], path[curr]); + } + Path64 result = new Path64(len); + for (int i = 0; i < len; i++) + if (!flags[i]) result.Add(path[i]); + return result; + } + + public static Paths64 SimplifyPaths(Paths64 paths, + double epsilon, bool isClosedPaths = true) + { + Paths64 result = new Paths64(paths.Count); + foreach (Path64 path in paths) + result.Add(SimplifyPath(path, epsilon, isClosedPaths)); + return result; + } + + public static PathD SimplifyPath(PathD path, + double epsilon, bool isClosedPath = true) + { + int len = path.Count, high = len - 1; + double epsSqr = Sqr(epsilon); + if (len < 4) return path; + + bool[] flags = new bool[len]; + double[] dsq = new double[len]; + int curr = 0, prev, start, next, prior2; + if (isClosedPath) + { + dsq[0] = PerpendicDistFromLineSqrd(path[0], path[high], path[1]); + dsq[high] = PerpendicDistFromLineSqrd(path[high], path[0], path[high - 1]); + } + else + { + dsq[0] = double.MaxValue; + dsq[high] = double.MaxValue; + } + for (int i = 1; i < high; ++i) + dsq[i] = PerpendicDistFromLineSqrd(path[i], path[i - 1], path[i + 1]); + + for (; ; ) + { + if (dsq[curr] > epsSqr) + { + start = curr; + do + { + curr = GetNext(curr, high, ref flags); + } while (curr != start && dsq[curr] > epsSqr); + if (curr == start) break; + } + + prev = GetPrior(curr, high, ref flags); + next = GetNext(curr, high, ref flags); + if (next == prev) break; + + if (dsq[next] < dsq[curr]) + { + prior2 = prev; + prev = curr; + curr = next; + next = GetNext(next, high, ref flags); + } + else + prior2 = GetPrior(prev, high, ref flags); + + flags[curr] = true; + curr = next; + next = GetNext(next, high, ref flags); + if (isClosedPath || ((curr != high) && (curr != 0))) + dsq[curr] = PerpendicDistFromLineSqrd(path[curr], path[prev], path[next]); + if (isClosedPath || ((prev != 0) && (prev != high))) + dsq[prev] = PerpendicDistFromLineSqrd(path[prev], path[prior2], path[curr]); + } + PathD result = new PathD(len); + for (int i = 0; i < len; i++) + if (!flags[i]) result.Add(path[i]); + return result; + } + + public static PathsD SimplifyPaths(PathsD paths, + double epsilon, bool isClosedPath = true) + { + PathsD result = new PathsD(paths.Count); + foreach (PathD path in paths) + result.Add(SimplifyPath(path, epsilon, isClosedPath)); + return result; + } + + public static Path64 TrimCollinear(Path64 path, bool isOpen = false) + { + int len = path.Count; + int i = 0; + if (!isOpen) + { + while (i < len - 1 && InternalClipper.CrossProduct( + path[len - 1], path[i], path[i + 1]) == 0) i++; + while (i < len - 1 && InternalClipper.CrossProduct( + path[len - 2], path[len - 1], path[i]) == 0) len--; + } + + if (len - i < 3) + { + if (!isOpen || len < 2 || path[0] == path[1]) + return new Path64(); + return path; + } + + Path64 result = new Path64(len - i); + Point64 last = path[i]; + result.Add(last); + for (i++; i < len - 1; i++) + { + if (InternalClipper.CrossProduct( + last, path[i], path[i + 1]) == 0) continue; + last = path[i]; + result.Add(last); + } + + if (isOpen) + result.Add(path[len - 1]); + else if (InternalClipper.CrossProduct( + last, path[len - 1], result[0]) != 0) + result.Add(path[len - 1]); + else + { + while (result.Count > 2 && InternalClipper.CrossProduct( + result[result.Count - 1], result[result.Count - 2], result[0]) == 0) + result.RemoveAt(result.Count - 1); + if (result.Count < 3) + result.Clear(); + } + return result; + } + + public static PathD TrimCollinear(PathD path, int precision, bool isOpen = false) + { + InternalClipper.CheckPrecision(precision); + double scale = Math.Pow(10, precision); + Path64 p = ScalePath64(path, scale); + p = TrimCollinear(p, isOpen); + return ScalePathD(p, 1 / scale); + } + + public static PointInPolygonResult PointInPolygon(Point64 pt, Path64 polygon) + { + return InternalClipper.PointInPolygon(pt, polygon); + } + + public static PointInPolygonResult PointInPolygon(PointD pt, + PathD polygon, int precision = 2) + { + InternalClipper.CheckPrecision(precision); + double scale = Math.Pow(10, precision); + Point64 p = new Point64(pt, scale); + Path64 path = ScalePath64(polygon, scale); + return InternalClipper.PointInPolygon(p, path); + } + + public static Path64 Ellipse(Point64 center, + double radiusX, double radiusY = 0, int steps = 0) + { + if (radiusX <= 0) return new Path64(); + if (radiusY <= 0) radiusY = radiusX; + if (steps <= 2) + steps = (int) Math.Ceiling(Math.PI * Math.Sqrt((radiusX + radiusY) / 2)); + + double si = Math.Sin(2 * Math.PI / steps); + double co = Math.Cos(2 * Math.PI / steps); + double dx = co, dy = si; + Path64 result = new Path64(steps) { new Point64(center.X + radiusX, center.Y) }; + for (int i = 1; i < steps; ++i) + { + result.Add(new Point64(center.X + radiusX * dx, center.Y + radiusY * dy)); + double x = dx * co - dy * si; + dy = dy * co + dx * si; + dx = x; + } + return result; + } + + public static PathD Ellipse(PointD center, + double radiusX, double radiusY = 0, int steps = 0) + { + if (radiusX <= 0) return new PathD(); + if (radiusY <= 0) radiusY = radiusX; + if (steps <= 2) + steps = (int) Math.Ceiling(Math.PI * Math.Sqrt((radiusX + radiusY) / 2)); + + double si = Math.Sin(2 * Math.PI / steps); + double co = Math.Cos(2 * Math.PI / steps); + double dx = co, dy = si; + PathD result = new PathD(steps) { new PointD(center.x + radiusX, center.y) }; + for (int i = 1; i < steps; ++i) + { + result.Add(new PointD(center.x + radiusX * dx, center.y + radiusY * dy)); + double x = dx * co - dy * si; + dy = dy * co + dx * si; + dx = x; + } + return result; + } + + private static void ShowPolyPathStructure(PolyPath64 pp, int level) + { + string spaces = new string(' ', level * 2); + string caption = (pp.IsHole ? "Hole " : "Outer "); + if (pp.Count == 0) + { + Console.WriteLine(spaces + caption); + } + else + { + Console.WriteLine(spaces + caption + string.Format("({0})", pp.Count)); + foreach (PolyPath64 child in pp) { ShowPolyPathStructure(child, level + 1); } + } + } + + public static void ShowPolyTreeStructure(PolyTree64 polytree) + { + Console.WriteLine("Polytree Root"); + foreach (PolyPath64 child in polytree) { ShowPolyPathStructure(child, 1); } + } + + private static void ShowPolyPathStructure(PolyPathD pp, int level) + { + string spaces = new string(' ', level * 2); + string caption = (pp.IsHole ? "Hole " : "Outer "); + if (pp.Count == 0) + { + Console.WriteLine(spaces + caption); + } + else + { + Console.WriteLine(spaces + caption + string.Format("({0})", pp.Count)); + foreach (PolyPathD child in pp) { ShowPolyPathStructure(child, level + 1); } + } + } + + public static void ShowPolyTreeStructure(PolyTreeD polytree) + { + Console.WriteLine("Polytree Root"); + foreach (PolyPathD child in polytree) { ShowPolyPathStructure(child, 1); } + } + + } // Clipper +} // namespace \ No newline at end of file diff --git a/BossMod/ThirdParty/Clipper2Lib/HashCode.cs b/BossMod/ThirdParty/Clipper2Lib/HashCode.cs new file mode 100644 index 0000000000..e87e144a89 --- /dev/null +++ b/BossMod/ThirdParty/Clipper2Lib/HashCode.cs @@ -0,0 +1,126 @@ +using System; +using System.ComponentModel; +using System.Runtime.CompilerServices; +using System.Security.Cryptography; + +namespace Clipper2Lib +{ + /* + + Licensed to the .NET Foundation under one or more agreements. + // The .NET Foundation licenses this file to you under the MIT license. + + The xxHash32 implementation is based on the code published by Yann Collet: + https://raw.githubusercontent.com/Cyan4973/xxHash/5c174cfa4e45a42f94082dc0d4539b39696afea1/xxhash.c + + xxHash - Fast Hash algorithm + Copyright (C) 2012-2016, Yann Collet + + BSD 2-Clause License (http://www.opensource.org/licenses/bsd-license.php) + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following disclaimer + in the documentation and/or other materials provided with the + distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + You can contact the author at : + - xxHash homepage: http://www.xxhash.com + - xxHash source repository : https://github.com/Cyan4973/xxHash + */ + + public struct HashCode + { + private static readonly uint s_seed = GenerateGlobalSeed(); + + private const uint Prime1 = 2654435761U; + private const uint Prime2 = 2246822519U; + private const uint Prime3 = 3266489917U; + private const uint Prime4 = 668265263U; + private const uint Prime5 = 374761393U; + + private static uint GenerateGlobalSeed() + { + using RandomNumberGenerator randomNumberGenerator = RandomNumberGenerator.Create(); + byte[] data = new byte[sizeof(uint)]; + randomNumberGenerator.GetBytes(data); + return BitConverter.ToUInt32(data, 0); + } + + public static int Combine(T1 value1, T2 value2) + { + uint hc1 = (uint) (value1?.GetHashCode() ?? 0); + uint hc2 = (uint) (value2?.GetHashCode() ?? 0); + + uint hash = MixEmptyState(); + hash += 8; + + hash = QueueRound(hash, hc1); + hash = QueueRound(hash, hc2); + + hash = MixFinal(hash); + return (int) hash; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint QueueRound(uint hash, uint queuedValue) + { + return RotateLeft(hash + (queuedValue * Prime3), 17) * Prime4; + } + + private static uint MixEmptyState() + { + return s_seed + Prime5; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint MixFinal(uint hash) + { + hash ^= hash >> 15; + hash *= Prime2; + hash ^= hash >> 13; + hash *= Prime3; + hash ^= hash >> 16; + return hash; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint RotateLeft(uint value, int offset) + { + return (value << offset) | (value >> (32 - offset)); + } + +#pragma warning disable CS0809 // Obsolete member overrides non-obsolete member + [Obsolete("HashCode is a mutable struct and should not be compared with other HashCodes. Use ToHashCode to retrieve the computed hash code.", error: true)] + [EditorBrowsable(EditorBrowsableState.Never)] + public override int GetHashCode() + { + throw new NotSupportedException($"{nameof(HashCode)}.{nameof(GetHashCode)}() is not supported"); + } + + [Obsolete("HashCode is a mutable struct and should not be compared with other HashCodes.", error: true)] + [EditorBrowsable(EditorBrowsableState.Never)] + public override bool Equals(object? obj) + { + throw new NotSupportedException($"{nameof(HashCode)}.{nameof(Equals)}() is not supported"); + } +#pragma warning restore CS0809 // Obsolete member overrides non-obsolete member + } +} \ No newline at end of file diff --git a/BossMod/Util/HelperMethods.cs b/BossMod/Util/HelperMethods.cs index d5fc3e1a87..9a6d9fe58d 100644 --- a/BossMod/Util/HelperMethods.cs +++ b/BossMod/Util/HelperMethods.cs @@ -6,9 +6,12 @@ public static class Helpers { public const float RadianConversion = MathF.PI / 180; public static readonly float sqrt3 = MathF.Sqrt(3); + private static readonly Dictionary cache = []; public static (WPos p1, WPos p2, WPos p3) CalculateEquilateralTriangleVertices(WPos origin, Angle rotation, float SideLength, float offset = 0) { + if (cache.TryGetValue((origin, rotation, SideLength, offset), out var cachedResult)) + return ((WPos, WPos, WPos))cachedResult; var sideOffset = (SideLength + offset) / 2; var height = MathF.Sqrt(3) / 2 * (SideLength + offset); var direction = rotation.ToDirection(); @@ -17,12 +20,15 @@ public static (WPos p1, WPos p2, WPos p3) CalculateEquilateralTriangleVertices(W var p1 = origin; var p2 = origin + direction * height - ortho * sideOffset; var p3 = origin + direction * height + ortho * sideOffset; - - return (p1, p2, p3); + var result = (p1, p2, p3); + cache[(origin, rotation, SideLength, offset)] = result; + return result; } public static List CalculateEquilateralTriangleVertices(WPos Center, float HalfSize) { + if (cache.TryGetValue((Center, HalfSize), out var cachedResult)) + return (List)cachedResult; var halfSide = HalfSize; var height = halfSide * sqrt3; var center = Center + new WDir(0, height / 3); @@ -33,13 +39,18 @@ public static List CalculateEquilateralTriangleVertices(WPos Center, float center + new WDir(halfSide, height / 3), center + new WDir(0, -2 * height / 3) }; + cache[(Center, HalfSize)] = points; return points; } public static WPos RotateAroundOrigin(float rotatebydegrees, WPos origin, WPos caster) { + if (cache.TryGetValue((rotatebydegrees, origin, caster), out var cachedResult)) + return (WPos)cachedResult; float x = MathF.Cos(rotatebydegrees * RadianConversion) * (caster.X - origin.X) - MathF.Sin(rotatebydegrees * RadianConversion) * (caster.Z - origin.Z); float z = MathF.Sin(rotatebydegrees * RadianConversion) * (caster.X - origin.X) + MathF.Cos(rotatebydegrees * RadianConversion) * (caster.Z - origin.Z); - return new WPos(origin.X + x, origin.Z + z); + var result = new WPos(origin.X + x, origin.Z + z); + cache[(rotatebydegrees, origin, caster)] = result; + return result; } } \ No newline at end of file diff --git a/BossMod/Util/Intersect.cs b/BossMod/Util/Intersect.cs index 8e91f2dadb..2c58e818f8 100644 --- a/BossMod/Util/Intersect.cs +++ b/BossMod/Util/Intersect.cs @@ -6,13 +6,22 @@ public static class Intersect public static float RayCircle(WPos rayOrigin, WDir rayDir, WPos circleCenter, float circleRadius) { var ccToRO = rayOrigin - circleCenter; - // (ccToRO + t * rayDir) ^ 2 = R^2 => t^2 + 2 * t * ccToRO dot rayDir + ccToRO^2 - R^2 = 0 var halfB = ccToRO.Dot(rayDir); var halfDSq = halfB * halfB - ccToRO.LengthSq() + circleRadius * circleRadius; if (halfDSq < 0) return float.MaxValue; // never intersects - var t = -halfB + MathF.Sqrt(halfDSq); - return t >= 0 ? t : float.MaxValue; + + var sqrtHalfDSq = MathF.Sqrt(halfDSq); + var t1 = -halfB + sqrtHalfDSq; + var t2 = -halfB - sqrtHalfDSq; + if (t1 >= 0 && t2 >= 0) + return Math.Min(t1, t2); + else if (t1 >= 0) + return t1; + else if (t2 >= 0) + return t2; + else + return float.MaxValue; } public static float RaySegment(WPos rayOrigin, WDir rayDir, WPos a, WPos b)