65.9K
CodeProject 正在变化。 阅读更多。
Home

OpenS-CAD,一个简单的二维CAD应用程序

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (88投票s)

2007年12月30日

CPOL

14分钟阅读

viewsIcon

361238

downloadIcon

31616

一个简单的二维CAD应用程序。

OpenSCAD_src

引言

您是否曾想过一个二维CAD应用程序是如何设计和实现的?我曾想过,所以我决定坐下来写一个。现在,实现一个功能齐全的二维CAD对一个人来说是一项艰巨的任务,而且不是几个月就能完成的事情,因为我只是偶尔在晚上写。所以,到目前为止,我实现的是基本框架和最基本工具,但它确实展示了CAD应用程序是如何实现的。

这个程序演示了

  • 使用世界单位和坐标系,而不是屏幕单位和坐标系。
  • 网格层和绘图层。
  • 缩放和平移。
  • 选择矩形。选择包含的对象(从左到右移动)或选择任何部分包含的对象(从右到左移动)。
  • 基本绘图工具:直线、圆和弧。
  • 基本编辑工具。连接两条线,将线延伸到另一条线。
  • 实时捕捉和快速捕捉。捕捉用于将绘图对象精确地附加到另一个对象。
  • 移动选定对象。复制选定对象(目前仅在移动时)。

注意!此时我意识到代码中有几个错误,但这些错误与捕捉和选择工具有关,并不影响整体设计。

世界单位

在CAD绘图中常用的坐标系统是世界坐标系,其原点位于左下角,X轴正方向向右,Y轴正方向向上,这与默认的屏幕坐标系相反,后者原点位于左上角,Y轴正方向向下。

(注意,坐标系的正确名称是笛卡尔坐标系,维基百科解释了坐标系。)

为了在屏幕单位和数据单位之间保持清晰的划分,我创建了UnitPoint类。任何绘图工具、编辑工具、捕捉点等使用的所有点都使用单位点。

什么是单位?在这种情况下,单位是什么并不重要,它可以是英寸、厘米、英里。只有在按比例打印、更改基准测量值或导出到库或另一个绘图(目前均未实现)时,它才重要。

高级设计

尽管这是一个非常简单和基础的CAD应用程序,但它仍然包含相当多的代码,难以详细解释;因此,我将只解释主要接口和高级设计。

这些是主要接口

  • public interface ICanvas
  • 提供从单位点到屏幕点以及反向的转换。

  • public interface IModel
  • 所有数据对象的集合,如绘图工具对象、编辑工具对象和图层。

  • public interface ICanvasLayer
  • 包含给定图层的绘图对象集合。

  • public interface IDrawObject
  • 每个绘图工具类都必须实现此接口。

  • public interface IEditTool
  • 每个编辑工具类都必须实现此接口。

  • CanvasCtrl
  • CanvasCtrl是实际绘制到屏幕的地方。如前所述,所有绘图对象都使用单位点作为其位置,而Canvas控件提供单位点和屏幕点之间的转换。此控件还提供平移和缩放功能,这基本上只是偏移原点和缩放单位/屏幕比率的问题。

    如果您查看CanvasCtrlOnPaint代码,您会注意到我使用位图来绘制所谓的“静态”对象,其中静态对象是未选中的现有对象。这样做的原因是加快绘制新对象或移动现有对象的绘图速度。您可以将此静态位图视为背景,其中静态绘图对象仅绘制一次或直到背景失效(由几个不同事件引起)到背景(位图)中。

    然后,在这个背景之上,会绘制新对象或选定对象的绘图。这样,只需要重绘先前被选定对象覆盖的区域,并且由于它已经绘制到位图,所以只需要将失效区域从位图复制到屏幕。

    通常,建议在需要重绘时调用控件的Invalidate(rectangle)。这将导致一个绘制事件被排队,并且“稍后”将执行重绘。另一种选择是调用Refresh(),这将立即调用OnPaint,但不幸的是,此API不允许指定需要失效的区域,因此必须重绘整个控件。

    Invalidate(rect)调用导致绘图工具行为迟缓,因为更新发生在鼠标移动“稍后”之后,而Refresh()导致高CPU利用率,因为每次鼠标移动时都会重绘整个区域。对我来说,解决方案是实现几个立即执行绘制的绘制方法。

    • RepaintStatic(Rectangle r)
    • RepaintSnappoint(ISnapPoint snappoint)
    • RepaintObject(IDrawObject obj)

    ICanvas接口不是由CanvasCtrl直接实现的,而是由包装类ICanvasCtrlWrapper实现的。原因是,在OnPaint期间,当静态位图被标记为脏时,会使用两个不同的Graphics,因此我需要两个不同的ICanvas对象。

    ICanvasIModel设计支持多层。图层在CanvasCtrl中绘制的顺序如图所示。绘制从下往上进行。

    <active layer >
    <draw layer n – if not the active layer >
    …
    <draw layer 0 – if not the active layer>
    <grid layer>
    <background>
  • DataModel : IModel
  • DataModel是所有数据所在的地方。当然,这包括所有绘图层及其绘图对象,以及背景层、网格层,以及所有绘图和编辑工具。

    数据导入导出也在DataModel中通过Load(filename)Save(filename)完成。我选择使用XML格式作为数据文件(.cadxml)。原因是它更容易验证数据,并且允许您在不连接GUI部分的情况下修改数据。例如,目前添加或删除图层的唯一方法是手动修改cadxml文件。

    您现在期望即使是最简单的应用程序也支持撤销/重做,并且DataModel也支持这一点。

  • DrawingLayer : ICanvasLayer
  • 这个层没有什么好解释的。这个类包含绘图对象的列表和一些属性,如线宽、线颜色和启用标志。

  • 绘图工具及更多
  • 还支持以下内容,并将进行更详细的解释

    • 绘图工具:直线、两个圆工具和四个弧工具。
    • 编辑工具:连接两条线,延伸线。
    • 实时捕捉和快速捕捉。

一切都与数学有关

CAD程序的基本功能之一是在绘图时能够精确地将一个对象捕捉到另一个对象。最简单的例子是在现有线的精确端点处开始一条新线。要找到线的端点(顶点)的捕捉点很简单,因为它只是定义线的两个UnitPoint之一。但是,如果我想捕捉到线的中心点,或者我想捕捉到鼠标所在位置线上最近的点,或者我想让线捕捉到圆的切点——我该怎么做?

答案是数学和三角学。

使用三角学

  • 所有绘图工具在计算对象是否包含在选择中时都使用。每个绘图工具都必须实现PointInObject(unitpoint)ObjectInRectangle(...),其中PointInObject在通过鼠标单击进行选择时调用,而ObjectInRectangle在通过选择矩形进行选择时调用——“橡皮筋”选择。
  • 有些工具在绘图时使用它。例如,当按下Ctrl键时,直线工具以正交模式绘图。在此模式下,直线的角度限制为45度步长。
  • 所有捕捉点类都用于计算捕捉点的位置。
  • 所有编辑工具,例如,“两条线相交”工具会计算两条选定线的交点,然后将这两条线的顶点移动到该交点。

现在,我刚开始写这个的时候,对我的数学或三角学几乎没有任何记忆,但我找到了很多很好的在线资源,甚至找到了一些工具所需的精确方程式。

我将所有数学实用方法都保存在HitUtil类中。我知道其中有几个错误,我需要回去修复一些方法并增强其他方法,使它们能够与Arc工具正确配合。

绘图工具

所有绘图工具都必须实现IDrawObject接口,该接口由Canvas控件使用。可用的绘图工具在数据模型中注册,并通过ID引用。当从菜单中选择一个绘图工具时,Canvas会调用CommandSelectDrawTool(string drawobjectid)。这会将Canvas置于绘图模式,现在它已准备好在鼠标单击或执行快速捕捉时创建新的绘图工具对象。

Canvas调用Modelm_model.CreateObject来创建给定类型的新工具。然后,Model会找到注册的工具对象并返回该对象的克隆。之所以注册实际对象而不是仅类型,是因为某些绘图对象(圆和弧)具有不同的模式(两点、圆心-半径和三点模式),因此注册同一类的两个对象但模式不同比创建两个派生类(如果仅注册类型则需要)更简单。

我实现的绘图工具确实支持单独的宽度和颜色,但默认情况下使用从图层继承的颜色和宽度,并且禁用此默认行为的标志尚未在GUI中公开。

所有绘图工具都在DrawTools文件夹中,每个工具都有一个单独的源文件。

从创建绘图工具到完成使用该工具的绘图,该绘图工具一直处于活动状态。当绘图工具处于活动状态时,键盘和鼠标事件会被转发到该工具。确定工具何时完成取决于OnMouseDown调用的返回值。

此方法可以返回

  • Done,表示工具已完成,将被添加到数据模型中。
  • DoneRepeat,表示工具已完成,将其添加到数据并创建同类型的另一个工具。
  • Continue,表示工具需要更多输入才能完成。

节点编辑

当选中现有对象时,其节点将可供编辑。当鼠标单击节点时,选中的对象将返回一个实现INodePoint的节点编辑对象。就像绘图工具处于活动状态时一样,Canvas会将鼠标和键盘事件转发到节点编辑对象,然后节点编辑对象负责相应地修改其所有者(绘图对象)。

编辑工具

编辑工具类似于绘图工具。它们必须实现IEditTool接口。它们在数据模型中用ID注册,并通过在Canvas上调用CommandEdit(string editid)来激活。

捕捉

捕捉用于在绘图模式下精确地“捕捉”到另一个对象。支持三种类型的捕捉:网格捕捉、实时捕捉和快速捕捉。

  • 网格捕捉很容易理解。
  • 实时捕捉是鼠标移到现有对象上时显示的捕捉点,例如,当鼠标移到线的中心点的端点上时,您会看到显示捕捉矩形。可以通过Ctrl + S切换实时捕捉的开关。
  • 快速捕捉是通过键盘命令执行的。此捕捉用于通过单个字母命令捕捉到对象上的特定点。例如,如果您想从现有线的最近端点(顶点)开始画一条线,您可以将鼠标移到现有线上然后按“V”,这将找到两个端点中最近的一个并将其作为捕捉点返回,然后新线将使用此位置作为其起点。

那么它是如何工作的?

支持的捕捉点类型取决于绘图对象,因此每个绘图对象都负责返回捕捉对象。

对于实时捕捉,Canvas在鼠标移动时调用m_model.SnapPoint。Model会根据鼠标位置查找可能的目標对象,然后对每个目标对象调用obj.SnapPoint,直到找到捕捉点。

快速捕捉在Canvas的OnKeyDown中进行检查。首先,它检查按键是否已注册为捕捉,如果已注册,则调用Model获取注册类型的捕捉点。

每种捕捉点类型都实现为派生自SnapPointBase的类,并且传递给绘图对象的SnapPoint方法的参数之一是请求的实时捕捉类型列表,另一个参数是快速捕捉类型。

如果您查看DrawTools.Line.SnapPoint,您会注意到对于实时捕捉,它会遍历各种类型,并为每种类型检查鼠标点是否在捕捉距离内。而对于快速捕捉,无论鼠标点在哪里,都会计算并返回捕捉点。

捕捉点的注册在主视图DocumentForm中完成。注册捕捉点列表如下

m_canvas.RunningSnaps = new Type[] 
{ 
    typeof(VertextSnapPoint), 
    typeof(MidpointSnapPoint),
    typeof(IntersectSnapPoint), 
    typeof(QuadrantSnapPoint), 
    typeof(CenterSnapPoint), 
    typeof(DivisionSnapPoint), 
}; 
 
m_canvas.AddQuickSnapType(Keys.N, typeof(NearestSnapPoint)); 
m_canvas.AddQuickSnapType(Keys.M, typeof(MidpointSnapPoint));
m_canvas.AddQuickSnapType(Keys.I, typeof(IntersectSnapPoint)); 
m_canvas.AddQuickSnapType(Keys.V, typeof(VertextSnapPoint)); 
m_canvas.AddQuickSnapType(Keys.P, typeof(PerpendicularSnapPoint)); 
m_canvas.AddQuickSnapType(Keys.Q, typeof(QuadrantSnapPoint)); 
m_canvas.AddQuickSnapType(Keys.C, typeof(CenterSnapPoint)); 
m_canvas.AddQuickSnapType(Keys.T, typeof(TangentSnapPoint));
m_canvas.AddQuickSnapType(Keys.D, typeof(DivisionSnapPoint));

所有捕捉点类都在SnapPoints.cs中。

撤销 - 重做

撤销/重做通过UndoRedoBuffer类(位于Utils\Undo.cs)完成。每个可撤销命令都必须派生自EditCommandBase。已实现的命令包括:Add、Remove、Move、NodeMove、EditTool。

图层

目前,对图层设置的任何编辑都必须通过直接修改XML来完成。

增强功能

当然,我有一个长长的GUI功能和绘图/编辑工具列表,我想将它们实现到这个应用程序中,并且可能会随着时间的推移实现。这些都是应用程序实际可用作CAD应用程序所需的功能。但是,除了所有功能之外,当前设计中还有三个瓶颈,如果这个应用程序要处理任何大规模数据,也应该解决。

  1. 绘图性能。在平移时,每次都会重绘整个静态图像。这可以进行优化,以便只重绘图像的“新”部分,而其余部分只是移动。我尝试了20,000条线,在我的计算机上,重绘可见所有对象的图层大约需要130毫秒,但我在另一台计算机上尝试了相同的配置,它花费了近400毫秒,足以使平移显得迟缓。
  2. 查找给定点的对象,或给定区域内的对象。目前,这是通过迭代对象列表来完成的,如果每个图层包含数千个对象,这显然无法很好地扩展。解决方案可能是使用R树。但实现这样的算法会花费我太多时间,所以我决定暂时忽略它。
  3. GDI在放大圆形时的出现问题。我不知道为什么会这样,但我假设GDI实际上会尝试绘制整个圆,即使屏幕上只能看到圆的一小部分。

结论

这是一个有趣且令人愉快的小项目。我实现了我的目标,了解了一个二维CAD应用程序是如何设计的。我花了大约两个月的时间,在晚上偶尔工作,才达到了这个阶段,我可能会一点一点地继续添加功能,但我并不幻想能够独自开发一个功能齐全的应用程序,因为我根本没有足够的时间投入;相反,您需要分担工作量,也许可以在开源社区中。

现在,如果我能弄清楚像Google SketchUp这样的3D应用程序是如何工作的就好了!

© . All rights reserved.