前面总结了尺规作图的三大元素(点、线、圆),而且得出其结论——所有图形也最终是依赖于一些自由点(FreePoint)。自由点是没有依赖的,可以在屏幕上随意绘制,因此除了基本的坐标转换(数学坐标系与屏幕坐标系)外没有复杂的数学计算,所以我们也就不讨论了。本节主要讨论线与线交点的坐标计算,本来想把三种交点(线与线,线与圆,圆与圆)放在一节介绍,后来写演示代码的时候发现工程量有点大,所以这一节还是只是介绍下线与线的交点(这叫做小步快跑,是TDD编程的推荐方法,因为步子迈太大容易扯着蛋啦,寡人这两天就觉得有点蛋疼)。
线与线的交点必须要有两条线,我们在后台计算坐标需要用到平面解析几何(就是代数与几何的结合体啦),用到的都是初中的数学知识,因此大家不要惊慌。
先来看看直线的方程:
y=ax+b
当我们用两个自由点确定一条直线时,我们把两个自由点A(x1,y1),B(x2,y2)的坐标代入改方程可以求解出a和b的值。为了直观起见,我们定义一个线的类(LogicLine)。(本节定义的类基本上都是为了演示计算和与数学概念相衔接,我们计算是用数学坐标系为参考的)。
//表示直线:y=ax+b public class LogicalLine { public LogicalLine(LogicalPoint p1, LogicalPoint p2) { P1 = p1; P2 = p2; } public LogicalPoint P1 { get; set; } public LogicalPoint P2 { get; set; } //直线(x1,y1)(x2,y2):y=ax+b:a=(y1-y2)/(x1-x2),b=y1-a*x1 private double dy { get { return P2.Y - P1.Y; } } private double dx { get { return P2.X - P1.X; } } public double a { get { return dy / dx; } } public double b { get { return P1.Y - a * P1.X; } } }
有了直线方程,那么求两线的交点顺理成章就变成了解方程组(二元一次方程组),假设有两条直线:
- y=a1x+b1
- y=a2x+b2
如果该方程组有一个解(x=x3,y=y3),则点(x3,y3)就是交点了。
有意思的是这个方程组中,如果a1=a2 且 b1=b2,则有无数个解。是不是就有无数个交点呢?这种情况是两条线重合,可以理解成两线平行的特殊情况,我们当做没有交点。解二元一次方程组很简单了是吧,都是代数带来带去,烦得很呐,所以寡人在初中时只喜欢纯几何,呵呵,眼根清净。
不管怎么说,一次方程还是挺简单的,逻辑都写在代码里了,如果你离开初中很多年了,需要温故一下的话就看一眼吧,否则完全可以忽略了:
//线与线的交点 public static LogicalPoint LineAndLine(LogicalLine line1, LogicalLine line2) { double x; double y; if (line1.dx.IsZero()) //与Y轴平行 { x = line1.P1.X; y = line2.a*x + line2.b; } else if (line2.dx.IsZero()) //与Y轴平行 { x = line2.P1.X; y = line1.a*x + line1.b; } else { if (line1.a.IsEqual(line2.a)) //两线平行,认为交点无穷远 { x = double.NegativeInfinity; y = double.NegativeInfinity; } else { var d = (line1.a - line2.a); x = (line2.b - line1.b)/d; y = line1.a*x + line1.b; } } return new LogicalPoint(x, y); }
可以看到代码里面用到了一些扩展的方法,例如IsZero()什么的,如果感觉莫名其妙可以看看源代码,过多地使用扩展方法可能会使代码看起来有点脱离.net了,毕竟.net的类库啊功能啊什么的已经很完备了,不过偶尔用用还是可以的,为了书写方便而已。
接下来就是要测试我们的计算逻辑是否正确,这里用TDD虽然必不可少但是太不直观了,我们还是写一个可交互的界面来验证。具体做法是这样子的:
- 使用我们前面实现的坐标系(CoordinateSystem)
- 在坐标系上画两个自由点P1、P2(红色)和四个角上的点(绿色)。
- 画四条固定的直线(坐标系的所在的Canvas的四边)。
- 画一条自由线FreeLine(依赖于P1、P2)。
- 添加四个交点(FreeLine和四个边的交点)(蓝色)
- 添加坐标系的鼠标事件,使得可以拖动P1、P2。
- 当P1或者P2移动时,重新计算FreeLine和四个交点的坐标并更新显示。
我们先来设计几个坐标系上的元素(点、线)来实现我们的测试,虽然元素很少很简单,我们还是按照从抽象到具体的设计方式,这是一种习惯啦。其实随便画几个圆啊线啊什么的搞一下测试也可以,但是以寡人的经历来看这样也省不了多少时间,反而不利于培养良好的编程习惯。
有了第一节面向对象的设计方法讲解,这里就不再详细介绍了,直接给出了图吧:
这样的设计体现了依赖的顺序,比如线依赖于两个点,交点又依赖于两条线,好处就是只要一个自由点发生变化了,就可以依照依赖找出其相关的所有元素,已达到牵一发而动的目的,呵呵。
因为显示和计算用到的坐标系不同,接口中定义CS(坐标系)是必须的,因为我们计算都是用的数学坐标系,而在界面上显示又必须是按照屏幕坐标系统。
UpdateVisual()是用于计算和更新位置的,当然不同的图形元素有不同的逻辑,在代码里看会比较清楚.
这里顺便提一下Canvas鼠标事件的一个常用功能——从当前单击点取元素,比如Canvas上有一个Ellipse,如果我在Ellipse上单击,可以找出这个元素。
作为游戏编程的人肯定都会想到用HitTest,对,我们就用它,这里我把它扩展成一个静态方法,而且还是泛型哦,这样如果Point下面有多个元素,你就可以方便地找出你要的类型,比如我们的测试用例中自由点和自由线是部分重叠的,而我们要拖动的仅仅是点而已。代码如下,其中VisualTreeHelper.FindElementsInHostCoordinates是Silverlight类库中的方法:
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; }
OK,这节就到这里了,来看看运行效果图吧:
下一节我们再讲解一下更复杂一点的交点的计算,以及存在多个交点的情况下,交点顺序如何确定的,两交点重合又将如何处理!
来源:https://www.cnblogs.com/fooeunfun/archive/2012/11/29/2795157.html