/// <summary>
        /// https://en.wikipedia.org/wiki/Sutherland%E2%80%93Hodgman_algorithm#Pseudo_code
        /// Fails if result would be empty
        /// </summary>
        public static bool TryConvexClip(Polygon2 subject, Polygon2 clip, out Polygon2 result)
        {
            bool Inside(IntVector2 p, IntLineSegment2 edge) => GeometryOperations.Clockness(edge.First, edge.Second, p) != Clockness.CounterClockwise;

            List <IntVector2> outputList = subject.Points;

            for (var i = 0; i < clip.Points.Count - 1; i++)
            {
                var clipEdge = new IntLineSegment2(clip.Points[i], clip.Points[i + 1]);
                List <IntVector2> inputList = outputList;
                outputList = new List <IntVector2>();

                var S = inputList[inputList.Count - 2];
                for (var j = 0; j < inputList.Count - 1; j++)
                {
                    var E = inputList[j];
                    if (Inside(E, clipEdge))
                    {
                        if (!Inside(S, clipEdge))
                        {
                            var SE = new IntLineSegment2(S, E);
                            if (!GeometryOperations.TryFindLineLineIntersection(SE, clipEdge, out var intersection))
                            {
                                throw new NotImplementedException();
                            }
                            outputList.Add(intersection.LossyToIntVector2());
                        }
                        outputList.Add(E);
                    }
                    else if (Inside(S, clipEdge))
                    {
                        var SE = new IntLineSegment2(S, E);
                        if (!GeometryOperations.TryFindLineLineIntersection(SE, clipEdge, out var intersection))
                        {
                            throw new NotImplementedException();
                        }
                        outputList.Add(intersection.LossyToIntVector2());
                    }
                    S = E;
                }

                if (outputList.Count == 0)
                {
                    result = null;
                    return(false);
                }

                outputList.Add(outputList[0]);
            }

            result = new Polygon2(outputList);
            return(true);
        }
        public static PolyTree ExtrudePolygon(IReadOnlyList <IntVector2> points, int offset)
        {
            if (points.Count <= 1)
            {
                throw new NotImplementedException("Not implemented: extrude 0 or 1 points");
            }

            if (offset <= 0)
            {
                throw new ArgumentOutOfRangeException();
            }

            List <IntVector2> polygonPoints = new List <IntVector2>(points);

            // hack: fidget the last point slightly to enforce having area
            var last               = points[points.Count - 1];
            var secondToLast       = points[points.Count - 2];
            var secondToLastToLast = secondToLast.To(last);

            if (secondToLastToLast == IntVector2.Zero)
            {
                polygonPoints.Add(last + new IntVector2(1, 0));
            }
            else if (secondToLastToLast.X == 0)
            {
                polygonPoints.Add(last + new IntVector2(1, 0));
            }
            else
            {
                polygonPoints.Add(last + new IntVector2(0, 1));
            }

            for (int i = points.Count - 2; i >= 0; i--)
            {
                polygonPoints.Add(points[i]);
            }

            var inputHairlinePolygon = new Polygon2(polygonPoints);
            var outputPolygons       = PolygonOperations.Offset()
                                       .Include(inputHairlinePolygon)
                                       .Dilate(offset)
                                       .Execute();

            return(outputPolygons);
        }