Introduction
Have you ever wondered how a 2D CAD application is designed and implemented? I have, so I decided to sit down and write one. Now, implementing a fully functional 2D CAD is a tremendous task for a single person, and not something that can be completed in just a couple of months when only working on it on and off in the evenings. So, what I have implemented so far is the basic framework and only the most basic tools, but it does demonstrate how a CAD application could be implemented.
What this program demonstrates is:
- Using world units and a coordinate system instead of screen units and a coordinate system.
- Grid layer and drawing layers.
- Zoom and pan.
- Selection rectangle. Select enclosed objects (when moving left to right) or select any partially enclosed objects (moving right to left).
- Basic draw tool: line, circle, and arc.
- Basic edit tool. Join two lines, extend line to line.
- Running snap, and quick snap. Snap is used to precisely attach a draw object to another object.
- Moving selected objects. Copy of selected object (for now, only when moving).
Note! I am aware of several bugs in the code at this point, but these bugs are related to the snap and selection tool, and do not affect the overall design.
World units
A commonly used coordinate system when drawing in CAD is the world coordinate system where the origin is located at the lower left corner and the positive X direction is right, and the positive Y direction is up, as opposed to the default screen coordinates where the origin is upper left and the Y direction is down.
(Note, The correct name for the coordinate system is the Cartesian system, Wikipedia explains the coordinate system.)
To keep a clear separation between the screen and the data units, I created the UnitPoint
class. All points used by any draw tools, edit tools, snap points etc., are done using the unit point.
What is a unit? In this case, it doesn’t really matter what a unit is, it could be an inch, a centimeter, a mile. It would matter only at the time of printing if printing to scale, changing the base measurement, or when exporting and importing to a library or another drawing (neither of which are implemented yet).
High level design
Although this is a very simple and basic CAD application, it still contains quite a bit of code, too much to explain in details; instead, I will just explain the main interfaces and the high level design.
These are the main interfaces:
public interface ICanvas
Provides the translation from unit points to screen points, and vice versa.
public interface IModel
Collection of all the data objects such as draw tool objects, edit tool objects, and layers.
public interface ICanvasLayer
Contains the collection of draw objects for the given layer.
public interface IDrawObject
Each draw tool class must implement this interface.
public interface IEditTool
Each edit tool class must implement this interface.
CanvasCtrl
The CanvasCtrl
is where the actual drawing to screen is done. As mentioned earlier, all drawing objects use unit points for their location, and the canvas control provides the translation between the unit point and the screen point. This control also provides the panning and zooming functionality, which basically is just a matter of offsetting the origin and scaling the unit/screen ratio.
If you look in the OnPaint
code for CanvasCtrl
, you will notice that I use a bitmap for drawing what I call the ‘static’ objects, where a static object is an existing object that is not selected. The reason for this is to speed up the drawing when drawing a new object or moving existing objects. You can think of this static bitmap as a background, where the static draw objects are painted to the background (bitmap) only once or until the background is invalidated which can be caused by several different events.
Then on top of this background is where the paint of the new or selected draw objects happens. This way, only the area previously covered by the selected draw object needs to be repainted, and since it has already been drawn to the bitmap, all that needs to happen is copy the invalidated area from the bitmap to the screen.
Usually, it is recommended to call Invalidate(rectangle)
on a control when a repaint is required. This will cause a paint event to be queued, and ‘after a while’, the repaint is performed. The other option is to call Refresh()
which will cause the OnPaint
to be called immediately, but unfortunately, this API doesn’t allow to specify the area that needs to be invalidated, so the entire control will have to be repainted.
The Invalidate(rect)
call was causing the draw tool to behave sluggish as the update would happen ‘a while after’ the mouse was moved, and Refresh()
was causing a high CPU utilization since the entire area was being repainted each time the mouse was moved. The solution for me was to implement a couple of paint methods which does the paint immediately.
RepaintStatic(Rectangle r)
RepaintSnappoint(ISnapPoint snappoint)
RepaintObject(IDrawObject obj)
The ICanvas
interface is not implemented directly by the CanvasCtrl
but instead by a wrapper class ICanvasCtrlWrapper
. The reason for this is during OnPaint
with the static bitmap marked as dirty, two different Graphics
are used, and therefore, I need two different ICanvas
objects.
The ICanvas
– IModel
design supports multiple layers. The order the layers are drawn in the CanvasCtrl
is shown here. The drawing happens from the bottom up.
<active layer >
<draw layer n – if not the active layer >
…
<draw layer 0 – if not the active layer>
<grid layer>
<background>
DataModel : IModel
The DataModel
is where all the data lives. This, of course, includes all the drawing layers and their drawing objects, and also the background layer, grid layer, and all the draw and edit tools.
Serializing data in and out are also done in the DataModel
with Load(filename)
and Save(filename)
. I chose to use XML format for the data file (.cadxml). The reasons are that it is easier to verify data, and it allows you to modify data without having the part of the GUI hooked up. E.g., the only way to add or remove layers for now is to manually modify the cadxml file.
You have come to expect undo/redo from even the simplest applications today, and this is also supported by the DataModel
.
DrawingLayer : ICanvasLayer
There is not much to explain about this layer. This class contains the list of draw objects and a few properties such as line width, line color, and the enabled flag.
DrawTools and more
The following are also supported, and will be explained in more details:
- Draw tools: lines, two circle tools, and four arc tools.
- Edit tools: join two lines, extend lines.
- Running snap and quick snap.
It is all about math
One of the basic features you expect from a CAD program is the ability to precisely snap one object to another when drawing. The simplest example is starting a new line at the exact endpoint of an existing line. To find the snap point for the end point (vertex point) of a line is simple, as it is simply one of the the two UnitPoint
s that defines the line. But, what if I want to snap to the center point of the line, or I want to snap to the nearest point on the line from where the mouse is located, or I want the line to snap to the tangent point on a circle – how can I do that?
The answer is math and trigonometry.
Trigonometry is used:
- By all draw tools when calculating if the object is included in the selection. Each draw tool must implement a
PointInObject(unitpoint)
and ObjectInRectangle(…)
, where PointInObject
is called when the selection is done by a mouse click, and ObjectInRectangle
is called when the selection is done by the selection rectangle – ‘rubberband’ selection.
- Some tools use it while drawing. E.g., the line tool draws in Ortho mode when the control key is pressed. In this mode, the angle of the line is limited to a 45 degree step.
- All the snap point classes to calculate the snap point location.
- All edit tools, e.g., the ‘2 Lines Meet’ tool calculates the intersection points of the two selected lines and then move both lines' vertex points to this intersection point.
Now, I didn’t remember any of my math or trigonometry when I started on this, but I found a lot of good information available online, and I was even able to find the exact equations needed for some of the tools.
I have kept all math utility methods in the HitUtil
class. I am aware of a couple of bugs in there, and will have to get back to fix some of the methods and enhance others so they work correct with the Arc tool.
Draw tools
All draw tools must implement the IDrawObject
interface which is used by the canvas control. The available draw tools are registered in the data model, and referenced by an ID. When a draw tool is selected from the menu, CommandSelectDrawTool(string drawobjectid)
is called on the canvas. This sets the canvas into drawing mode, and it is now ready to create a new draw tool object when either the mouse is clicked or a quick snap is performed.
The canvas calls into the model m_model.CreateObject
to create a new tool of the given type. The model then finds the registered tool object and returns a clone of the object. The reason for having the actual object and not just the type registered is because some of the draw objects (circle and arcs) has different modes (2 point, center – radius, and 3 point modes), and so it is simpler to register two objects of the same class but with the mode set differently than it is to create two derived classes which is required if only the type was registered.
The draw tools that I have implemented does support individual width and color, but the default is to use the color and width inherited from the layer, and the flag to disable this default behavior is not exposed in the GUI yet.
All draw tools are in the DrawTools folder, with each tool in its separate source file.
The draw tool is considered active from the time it is created and until the draw with the tool is complete. While the draw tool is active, keyboard and mouse events are forwarded to the tool. What determines when the tool is complete is the return value of the OnMouseDown
call.
This method can return:
Done
, meaning the tool is complete and will be added to the data model.
DoneRepeat
, meaning the tool is complete, add it to the data and create a new tool of the same type.
Continue
, meaning the tool needs more input in order to complete.
Node edit
When an existing object is selected, its nodes become available for edit. When the mouse is clicked over a node, the selected object returns a node edit object which implements INodePoint
. Like when the draw tool is active, the canvas forwards mouse and key events into the node edit object, and the node edit object is then responsible for modifying its owner (the draw object) accordingly.
Edit tools
The edit tools are similar to the draw tools. They must implement the IEditTool
interface. They are registered in the data model with an ID, and they are activated by calling CommandEdit(string editid)
on the canvas.
Snaps
Snaps are used while in drawing mode to precisely ‘snap’ to another object. There are three types of snaps supported, snap to grid, running snap, and quick snap.
- Snap to grid is quiet obvious.
- RunningSnap is the snap point that shows when the mouse is moved over an existing object, e.g., you will notice when moving the mouser over the end points of the center point of a line that the snap rectangle is shown. Running snap can be toggled on and off with Ctrl + S.
- QuickSnap is performed by a keyboard command. This snap is used to snap to specific points on the object by a single letter command. E.g., if you want to start a line from the nearest end point (vertex point) of an existing line, you will move the mouse over the existing line and press ‘V’, which will then find the nearest of the two end points and return that as the snap point, and the new line will then use this location as its starting point.
So how does it work?
The type of snap point supported depends on the draw object, so each draw object is responsible for returning the snap object.
For RunningSnap, the canvas calls m_model.SnapPoint
on mouse move. The model finds the possible target objects from the mouse location, and then calls obj.SnapPoint
for each of the target objects until a snap point is found.
QuickSnap is checked in the canvas on OnKeyDown
. First, it checks if the key has been registered for snap, and if it has, it calls the model to get the snap point of the registered type.
Each snap point type is implemented as a class derived from SnapPointBase
, and one of the parameters passed to the draw object's SnapPoint
method is a list of requested running snap types, and another parameter is the quick snap type.
If you look at DrawTools.Line.SnapPoint
, you will notice that for running snaps, it iterates through the types, and for each type, checks if the mouse point is within the snap distance. Whereas for QuickSnap, the snap point is calculated and returned regardless of the mouse point.
The registration of the snap points is done in the main view, DocumentForm
. This is how the list of snap points is registered:
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));
All snap point classes are in SnapPoints.cs.
Undo – Redo
Undo / Redo is done using the UndoRedoBuffer
class (located in Utils\Undo.cs). Each undo-able command must be derived from EditCommandBase
. The following commands have been implemented: Add, Remove, Move, NodeMove, EditTool.
Layers
For now, any edits of layer settings have to be done by modifying the XML directly.
Enhancements
Of course, I have a long list of GUI features and draw and edit tools I would like to implement into this application, and they might get implemented as time permits. These are all features required for the app to actually be useful as a CAD application. But, in addition to all the features, there are three bottlenecks in the current design which should also be addressed if this app is to be used with any large scale data.
- The drawing performance. While panning, the entire static image is redrawn each time. This could be optimized so only the ‘new’ part of the image is actually re-painted while the rest of the image is just moved. I have tried with 20,000 lines, and on my computer, it takes about 130ms to redraw the layer when all objects are visible, but I tried the same config on a different computer where it took almost 400ms, enough to make the panning appear sluggish.
- Finding the objects for a given point, or objects within a given area. Right now, this is done by iterating through the list of objects, which clearly does not scale well if each layer contains thousands of objects. The solution for this could be to use the R-tree. But implementing such an algorithm would take too much of my time, so I decided to ignore it for now.
- Issue with the GDI when zooming in on a circle. I don’t know why this happens, but I assume the GDI actually attempts to draw the entire circle, even though only a tiny fraction of the circle is visible on the screen.
Conclusion
This has been a fun and interesting little project to work on. I achieved my objective, to find out how a 2D CAD application could be designed. It took me about two months of on and off work in the evenings to get this far, and I will probably continue adding features little by little, but I have no illusions of being able to develop a full featured app on my own as I simply do not have enough time to dedicate; instead you would need to share the load, maybe in an open source community.
Now, if I could only figure out how a 3D app like Google SketchUp works!