using System; namespace EllipseTruncate { public struct AngleRange { public double angle, size; public AngleRange(double angle, double size) { this.angle = angle; this.size = size; } public bool isEmpty() { return size <= Geometry.precision; } public bool isFull() { return size >= 2.0*Math.PI - Geometry.precision; } public double end() { return wrap(angle + size); } public bool contains(double a) { return diff(a, angle) < size; } public bool intersects(AngleRange r) { return contains(r.angle) || r.contains(angle); } public AngleRange union(AngleRange r) { double da = diff(r.angle, angle); if (da <= size + Geometry.precision) return new AngleRange(angle, Math.Max(size, da + r.size)); da = 2.0*Math.PI - da; if (da <= r.size + Geometry.precision) return new AngleRange(r.angle, Math.Max(r.size, da + size)); da = Math.Min(angle, r.angle); return new AngleRange(da, Math.Max(angle + size, r.angle + r.size) - da); } public static AngleRange byAngles(double a0, double a1) { return new AngleRange(a0, diff(a1, a0)); } public static double diff(double a1, double a0) { return a0 < a1 ? a1 - a0 : a1 - a0 + 2.0*Math.PI; } public static double wrap(double angle) { return angle > Math.PI ? angle - 2.0*Math.PI : angle < -Math.PI ? angle + 2.0*Math.PI : angle; } } public class Ellipse { public Point center; public Point p1; public Point p2; public double r1; public double r2; public double angle; public Matrix matrix; public Matrix matrixInv; public Ellipse(Point p0, Point p1, Point p2) { center = p0; this.p1 = p1; Point d = p1 - p0; r1 = d.len(); angle = d.atan(); Point dp = d.rotate90()/r1; r2 = Math.Abs(dp*(p2 - p0)); this.p2 = p0 + dp*r2; matrix = Matrix.identity() .translate(center) .rotate(angle) .scale(r1, r2); matrixInv = matrix.invert(); } public void drawFull(Cairo.Context context) { int segments = 100; double da = 2.0*Math.PI/segments; double s = Math.Sin(da); double c = Math.Cos(da); Point r = new Point(1.0, 0.0); context.Save(); context.Matrix = (new Matrix(context.Matrix)*matrix).toCairo(); context.SetSourceRGBA(1.0, 0.0, 0.0, 0.1); context.Arc(0.0, 0.0, 1.0, 0.0, 2.0*Math.PI); context.ClosePath(); context.Fill(); context.Restore(); context.Save(); context.LineWidth = 0.5; context.SetSourceRGBA(1.0, 0.0, 0.0, 0.5); Point p = matrix*r; for(int i = 0; i < segments; ++i) { r = new Point(r.x*c - r.y*s, r.y*c + r.x*s); p = matrix*r; context.LineTo(p.x, p.y); } context.ClosePath(); context.Stroke(); context.Restore(); } private static void swap(ref double a, ref double b) { double c = a; a = b; b = c; } private static void swap(ref int a, ref int b) { int c = a; a = b; b = c; } private bool cutRange(ref int index, AngleRange[] ranges, double da, double h) { if (h <= Geometry.precision - 1.0) { return true; } else if (h >= 1.0 - Geometry.precision) { return false; } else { double a = Math.Asin(h); ranges[index].angle = AngleRange.wrap( a < 0.0 ? da - a - Math.PI : da - a + Math.PI ); ranges[index].size = Math.PI + a + a; int j = index; for(int i = 0, r = 0; i < index;) { if (r > 0) ranges[i] = ranges[i + r]; if (ranges[i].intersects(ranges[j])) { if (j < i) { ranges[j] = ranges[i].union(ranges[j]); ++r; --index; } else { ranges[i] = ranges[i].union(ranges[j]); j = i; ++i; } if (ranges[j].isFull()) return false; } else ++i; } if (j == index) ++index; } return true; } public void drawTruncated(Cairo.Context context, Point b0, Point b1, Point b2) { Point dx = matrixInv.turn(b1 - b0); Point dy = matrixInv.turn(b2 - b0); Point nx = dx.rotate90().normalize(); Point ny = dy.rotate90().normalize(); Point o = matrixInv*b0; double ax = dx.atan(); double ay = dy.atan(); double aax = AngleRange.wrap(ax + Math.PI); double aay = AngleRange.wrap(ay + Math.PI); double sign = nx*dy; if (Math.Abs(sign) <= Geometry.precision) return; if (sign < 0.0) { double a; nx *= -1.0; ny *= -1.0; a = ax; ax = aax; aax = a; a = ay; ay = aay; aay = a; } context.Save(); context.Matrix = (new Matrix(context.Matrix)*matrix).toCairo(); context.SetSourceRGBA(1.0, 0.0, 0.0, 0.1); context.MoveTo(o.x, o.y); context.RelLineTo(dx.x, dx.y); context.RelLineTo(dy.x, dy.y); context.RelLineTo(-dx.x, -dx.y); context.ClosePath(); context.Fill(); context.Restore(); // gather invisible ranges AngleRange[] cutRanges = new AngleRange[4]; int count = 0; if ( !cutRange(ref count, cutRanges, ax , (o*nx)) ) return; if ( !cutRange(ref count, cutRanges, aax, -((o+dx+dy)*nx)) ) return; if ( !cutRange(ref count, cutRanges, ay , (o+dx)*ny) ) return; if ( !cutRange(ref count, cutRanges, aay, -((o+dy)*ny)) ) return; // sort bounds for(int i = 0; i < count; ++i) for(int j = i+1; j < count; ++j) if (cutRanges[j].angle < cutRanges[i].angle) { AngleRange r = cutRanges[i]; cutRanges[i] = cutRanges[j]; cutRanges[j] = r; } // invert bounds AngleRange[] ranges = new AngleRange[4]; for(int i = 0; i < count; ++i) ranges[i] = AngleRange.byAngles(cutRanges[(i > 0 ? i : count) - 1].end(), cutRanges[i].angle); // dummy if (count == 0) ranges[count++] = new AngleRange(0.0, 2.0*Math.PI); // draw int segments = 100; double da = 2.0*Math.PI/segments; double s = Math.Sin(da); double c = Math.Cos(da); context.Save(); context.LineWidth = 2.0; context.SetSourceRGBA(0.0, 0.0, 1.0, 1.0); for(int i = 0; i < count; ++i) { double angle = ranges[i].angle; double size = ranges[i].size; int cnt = (int)Math.Floor(size/da); Point r = new Point(1.0, 0.0).rotate(angle); Point p = matrix*r; context.MoveTo(p.x, p.y); for(int j = 0; j < cnt; ++j) { r = new Point(r.x*c - r.y*s, r.y*c + r.x*s); p = matrix*r; context.LineTo(p.x, p.y); } r = new Point(1.0, 0.0).rotate(angle + size); p = matrix*r; context.LineTo(p.x, p.y); } context.Stroke(); context.Restore(); } } }