Silverlight数学引擎(8)——尺规作图之交点2
深圳又到了一年中最为尴尬的天气,盖被子又热不盖又冷,带伞又不下雨不带的话可能就成落汤鸡,就连夏天觅不找踪影的蚊子,这个季节也纷纷出来劫色了,不禁回忆起老家那种四季分明的气候,春花秋月夏雨冬雪…
我们继续来研究下交点,由于线和圆的交点相对比较简单我们就只讨论圆和圆相交的情况吧,其实也不是很难就是代数太多太繁琐,只要一步步理清了就好了。看看圆的方程:
(x-a)2 - (y-b)2 = r2
其中(a,b)就是圆心,r就是半径,很直观。计算两圆交点就是解这样的方程组,首先,我们按照这个公式定义一个圆:
//圆:(x-a)2+(y-b)2=r2 public class LogicalCircle { public LogicalCircle(LogicalPoint center, LogicalLine radius) { Center = center; Radius = radius; } public LogicalPoint Center { get; set; } public LogicalLine Radius { get; set; } public double a { get { return Center.X; } } public double b { get { return Center.Y; } } public double r { get { return Radius.Length; } } }
解二元二次方程组就是先把它转化成一元二次方程:
ax2+bx+c = 0
虽然离开初中很久了,但大家对这个东西一定还似曾相识吧,当年的题海战术中,几乎每次都有它的影子,尤其是它的求根公式:
至于这样的求根公式是怎么来的,我也懒得去想了,当年老师就是叫我们这样背下来的,这样解题就不用动脑子啦。
要计算两圆交点,我们不防先把方程组转化成一元二次方程(当然还有其他方法),看看代码是怎么实现的:
//圆:(x-a)2+(y-b)2=r2 //圆:(x-a)2+(y-b)2=r2 public static Tuple<LogicalPoint, LogicalPoint> CircleAndCircle(LogicalCircle circle1, LogicalCircle circle2) { var p1 = new LogicalPoint(double.PositiveInfinity, double.PositiveInfinity); var p2 = new LogicalPoint(double.PositiveInfinity, double.PositiveInfinity); if (((circle1.r + circle2.r) - circle1.Center.Distance(circle2.Center)) < 0) { //两圆相离,无交点 } else { //左右相减可得x、y的二元一次方程:y=mx+n; var m = (circle1.a - circle2.a)/(circle2.b - circle1.b); var n = (circle1.r.Sqr() - circle2.r.Sqr() + circle2.a.Sqr() - circle1.a.Sqr() + circle2.b.Sqr() - circle1.b.Sqr())/(2*(circle2.b - circle1.b)); //y=mx+n代入circle的方程:(x-a)2+(mx+n-b)2=r2 //转化成一元二次方程的标准式:ax2+bx2+c=0 var q = n - circle1.b; var a = m.Sqr() + 1; var b = 2*(m*q - circle1.a); var c = circle1.a.Sqr() + q.Sqr() - circle1.r.Sqr(); //求方程ax2+bx+c=0的两个根 var d = b*b - 4*a*c; if (a == 0 && b != 0) p1.X = -c/b; if (d == 0) p1.X = -b/(2*a); else if (d > 0) { d = d.SquareRoot(); p1.X = (-b - d)/(2*a); p2.X = (-b + d)/(2*a); } //代入x到直线方程求y: p1.Y = m*p1.X + n; p2.Y = m*p2.X + n; if (p1.Distance(p2).IsZero()) { p2.X = double.PositiveInfinity;//两交点重合,认为P2不存在 } } return new Tuple<LogicalPoint, LogicalPoint>(p1, p2); }
代码中我们处理了两个特殊情况——两圆相离和相切。相离直接把交点置为不存在了事,对于相切,其实是两交点重叠,按理说不处理就OK了,因为在屏幕上也是重叠显示的看起来也是一个点,但是为什么要把P2置为不存在呢?原因是如果我们给点加了名称如A、B并在屏幕上显示,当两点重合时,A、B也重叠了,不好看,所以选择去掉一个,此外别无他意!
这里又有另外一个问题,既然有两个交点,那如果我只要其中一个,那应该是取哪个呢?不理解吗?来看看下图,当两圆交于E、F,那E的坐标究竟是以上解中的P1还是P2呢?这里经过测试,我们发现按照顺时针的方向连接A、E、B、F,则始终有E是P1,F是P2。这为我们以后的鼠标画交点操作提供了理论依据,从而当鼠标在F点单击时,不会让交点跑到E点去。
此外,直线也圆也存在类似的问题,假设线AB和圆有两个交点E、F,其中E对应P1,F对应P2,则EF与AB的方向是一致的,如图:
OK,分析完毕,下面来完善我们的测试程序:
- 利用我们以前测试的元素(自由点P1、P2,自由线P1P2)
- 创建两个新的自由点(PO1,PO2)
- 以PO1、PO2为圆心,以P1P2为半径作两个圆
- 创建线P1P2和圆PO1的两个交点
- 创建圆PO1、PO2的两个交点
- 添加鼠标事件让自由点都可以拖拽。
一切就绪后,两个圆也出来了,但是圆心不能被拖动,调试进去返现我们的HitTest方法有问题了:
public static T HitTest<T>(this Panel panel, Point p) where T : FrameworkElement { var t = VisualTreeHelper.FindElementsInHostCoordinates(p, panel).FirstOrDefault(); if (t != null && t is T) return (T)t; return null; }
该方法没有按我们预期地返回自由点,而是返回了圆(因为点和圆都是Ellipse),为什么呢,因为圆是后创建的,其位于最上层,而HitTest只返回了一个Ellipse,当然是最上层的那个了。解决方法是在PointShape和CircleShape的构造函数中指定不同的ZIndex(保证圆在点的下面就OK了),如下:
public PointShape(LogicalPoint center) { Center = center; Shape = new Ellipse { Width = Size, Height = Size, Fill = Brushes.Green, Stroke = Brushes.Black }; Shape.Tag = this; Canvas.SetZIndex(Shape, 100); }
搞定,运行,来看看运行效果图吧:
下一节我们介绍一下线上的点和圆上的点的情况,新的曙光即将到来!