Silverlight数学引擎(10)——尺规作图设计
前面的分析被总结为两个字——依赖,我们的设计就从依赖开始。如果将相互依赖的各个元素画成一幅图,这种图就有两种形式,一种是树(像我们前面做的表达式解析),另一种就是网。很明显几何作图各个元素之间的关系是一张网,因此我们很难用一个树状的职能型的组织结构来类比地说明它,或者用人际关系之网来比如更恰当一点。
不少人都会谈网色变,因为即是网就必然存在闭路,我们的网络短路或者程序死循环大多都拜他所赐,所以首先我们就要注意这个问题。举个例子,比如我们屏幕上有两个自由点A和B,以及依赖于它们的线段AB,我们用鼠标可以拖动自由点A,然后线段AB会跟着发生变化。如果我们用鼠标拖动线段,则A也会跟着移动,A的移动又带动着线AB的变化,好,等着翘辫子了。
所以为了防止出现这样的情况,我们就必须严厉禁止鼠标拖动非自由的元素,如线和圆以及交点之类的,如此,相应的接口随之诞生:
public interface IMovable { void Move(LogicalPoint newPosition); }
我们只允许继承了IMovable的元素才能被鼠标控制,很显然只有FreePoint和PointOnLine、PointOnCircle有这样的属性,然后我们的鼠标事件只需要这样写就OK了:
this.MouseLeftButtonDown += (s, e) => { var item = this.HitTest<Ellipse>(e.GetPosition(null)); if (item != null && item.Tag != null && item.Tag is IMovable) { SeletedItem = item.Tag as IMovable; } };
有了这一点,我们基本上就从源头把像死循环这样的大bug给办了,就是说我们已经避免了依赖被颠倒的问题,下来就来看看如何处理正常的依赖关系,由于都是坐标系上的图形元素,我们用ICoordinate接口来规范它,首先分析下各个元素共有的一些性质:
- 有关联的坐标系,否则逻辑坐标与屏幕坐标无法转化
- 有更新显示的行为(UpdateVisual)
- 有中心位置(Center)
- 有关联的图形界面元素如(Ellipse、Line)
- 有依赖(Dependencies)
- 有叠放顺序
此外还有颜色、可见性等其他属性,暂时先不考了,按照这些规则定义我们的各个类,为了简化代码,我们还是先定义一个抽象的基类,之后各个具体类从基类扩展即可;此外因为点有很多种且大部分性质相同,我们将其定义为一个抽象类PointShape,具体的点再继承它,由此关系就变得比较清晰:
因为各个元素的共同点是非常多的,所以基本上的功能都在集中在CoordinateBase类中实现,下面主要看看CoordinateBase是如何处理依赖的:
首先是向父元素们注册依赖,比如线AB依赖于A和B,则把AB分别添加到A、B的依赖列表中:
public List<ICoordinate> Dependencies { get; set; } public void RegisterDependencies(params ICoordinate[] parents) { foreach (var parent in parents) { if (parent.Dependencies == null) parent.Dependencies = new List<ICoordinate>(); parent.Dependencies.Add(this); } }
再来看看依赖的使用,只发生在UpdateVisual()函数中,任何子类都必须调用基类的该方法,且只能在各自的UpdateVisual()的末尾调用,这样保证了依赖能够按照顺序处理:
public virtual void UpdateVisual() { if (!Center.Exists()) Shape.Opacity = 0.1; else if (visible) Shape.Opacity = Opacity; if (Dependencies != null) foreach (var dep in Dependencies) dep.UpdateVisual(); }
来看看PointShape的实现就知道了:
public abstract class PointShape : CoordinateBase { private const double Size = 20; public override FrameworkElement CreateShape() { return new Ellipse { Width = Size, Height = Size, Fill = Fill, Stroke = Brushes.Black, Tag = this }; } public override int ZIndex { get { return (int)MathEngine.ZIndex.FreePoint; } } public override void UpdateVisual() { Shape.CenterTo(Center.ToPhysical(CS).Coordinate); base.UpdateVisual(); } }
具体类的代码实现都是挺简单的,其实我们在分析的时候基本上都实现了,这里只是进行了必要的重构和相关的性能优化。在重构过程中我将各个Demo的代码分开,做到了新的Demo的出现不会覆盖旧的版本,例如本次的Demo就是以一个三角形外接圆示例的实例来展现的,放在单独的静态类中:
//三角形外接圆示例 public static class TriangleCircumcircleDemo { public static void Play(CoordinateSystem cs) { cs.Children.Clear(); //创建三个自由点A、B、C var A = cs.DrawFreePoint(new LogicalPoint(1, 2)); var B = cs.DrawFreePoint(new LogicalPoint(-2, 3)); var C = cs.DrawFreePoint(new LogicalPoint(0, 0)); //创建三角形ABC的三条边 var AB = cs.DrawLine(A, B); var BC = cs.DrawLine(B, C); var CA = cs.DrawLine(C, A); //创建AB边中垂线 var O1 = cs.DrawCircle(A, AB); var O2 = cs.DrawCircle(B, AB); var D = cs.DrawIntersectionPoint(O1, O2, 1); var E = cs.DrawIntersectionPoint(O1, O2, 2); var DE = cs.DrawLine(D, E); //var F = cs.DrawIntersectionPoint(AB, DE);//AB边中点 //创建BC边中垂线 var O3 = cs.DrawCircle(B, BC); var O4 = cs.DrawCircle(C, BC); var G = cs.DrawIntersectionPoint(O3, O4, 1); var H = cs.DrawIntersectionPoint(O3, O4, 2); var GH = cs.DrawLine(G, H); //var I = cs.DrawIntersectionPoint(BC, GH);//BC边中点 var O = cs.DrawIntersectionPoint(DE, GH);//中垂线交点(外接圆圆心) var OA = cs.DrawLine(O, A);//外接圆半径 var circleABC = cs.DrawCircle(O, OA);//三角形ABC外接圆 // var auxiliaryFigures = new CoordinateBase[] {O1, O2, D, E, DE, O3, O4, G, H, GH, OA}; foreach (var figure in auxiliaryFigures) figure.Opacity = 0.2; } }
从示例中可以看出,除了A、B、C三个点是自由点外,其他的全都是靠依赖产生的,呵呵,三生万物实在不假哦。从三角形的外接圆我有所感触:其实自由是相对的,因为A、B、C名义上是自由点,却始终逃不出自己构造的外接圆,有点意思吧!
几何作图的原理就是这些了,除了外接圆,你还可以画出它的内切圆、旁切圆、伪内切圆、九点圆等等等等,总之,可以作茧自缚哈哈!理论结束了下次我们就来点实际有用的吧,如何用鼠标交互地去画图而不是用代码写呢,涉及到界面编程都是有点挑战的,后续章节会给出方案!