Conspicuously absent from the .NET Framework and GDI+ are three-dimensional (3D) shapes. Sure, you can draw lines, ellipses, rectangles, arcs, Bezier curves, and polygons, but things as obvious as spheres and cubes are absent. A sphere is a circle with volume, and a cube is a rectangle with a z-dimension or a depth. Unfortunately, 3D primitive shapes just don’t exist.
Recently, when working at a logistics company, I was asked if we could render loads as 3D images, showing the load as various stacks of bins and pallets. Such an implementation would permit suppliers, carriers, dockworkers, and plants to visualize what was in a particular shipment. With all of the shippers in the world, this seems like it would be a common enough problem. Unfortunately, to do this you have to write your own primitives or leap up to something more advanced like DirectX 9. What I decided to do was experiment with GDI+ and implement a 3D cube primitive to determine how much work was involved in capturing the graphics aspect of this problem. This article more or less describes the results.
Defining the Cube Structure
A 3D cube, for the purposes of this example, has height, width, and depth values. When each of the six sides of the primitive has the same dimension as the others, you have a cube. Additionally, the graphics primitive needs to know about its Cartesian location, and the ability to divine the center of the cube also seems useful. The result is six rectangles connected along their respective edges.
An additional objective was that the cube be “renderable” using GDI+. This meant that the class or structure had to work with one of the GDI+ draw methods. Graphics.DrawPolygon turned out to be an option. However, imitating the implementation of the Rectangle structure, defining the 3D Cube as a structure, using the Rectangle structure as a pattern, and emulating the style of properties and fields of the Rectangle structure in my Cube structure seemed like a practical choice.
Defining Cube Fields and Properties
Following the Rectangle structure defined in the System.Drawing namespace, I added location, height, width, depth, center, path, and rotateX and rotateY fields. Location and center were defined as point structures; a point is a single X,Y Cartesian pair. Location represents the upper-left corner of the cube. Height, depth, and width are self-explanatory; these were defined as integers. Path was defined as a GraphicsPath, which is more or less an array of points. RotateX and rotateY are enumerated types representing the aspect ratio. These values are used to create a horizontal left, right, or center, and vertical up, down, or center viewpoint perspective. Collectively, rotateX and rotateY help give the illusion of viewing the cube from different angles.
You can define the project as a Class Library and add a reference to the System.Drawing.dll assembly. If you include the necessary Imports statement and the members mentioned thus far, your structure could be implemented as shown in Listing 1.
Listing 1: Defining the enumerations and fields for the Cube structure
Imports System Imports System.Drawing Imports System.Drawing.Drawing2D Public Enum RotateHorizontal Left = -1 Center = 0 Right = 1 End Enum Public Enum RotateVertical Up = -1 Center = 0 Down = 1 End Enum Public Structure Cube Private FLocation As Point Private FHeight As Integer Private FWidth As Integer Private FDepth As Integer Private FCenter As Point Private FPath As GraphicsPath Private FRotateX As RotateHorizontal Private FRotateY As RotateVertical End Structure
To avoid getting lost, note that you will be adding code to the Cube structure, thereby growing the structure and the code in each successive listing until you have all of the code in the final listing.
Note: Using camel-cased names for fields and Pascal-cased names for properties (for example, location and Location) won’t work in VB.NET because it is case insensitive. I prefer to use an F-prefix to mean field because it is easy and not as atrocious as m_, mvar_, or str_. Also, even though a language like C# is case sensitive, using case to distinguish members is not CLS compliant. This means that cross-language use of such classes may cause problems, and tools such as FxCop (http://www.gotdotnet.com/team/fxcop/) will report an error in your code due to such usage. Whichever convention you use, try to be consistent.
The next thing you need to do is add the symmetric properties for your fields. Convention fields are always private, and properties are the public means by which you permit consumers to change a field’s value in a constrained way. Listing 2 shows the Cube Structure with the associated properties and a few extra convenience properties whose values are derived from the previously defined fields.
Listing 2: The Cube structure with properties
Imports System Imports System.Drawing Imports System.Drawing.Drawing2D Public Enum RotateHorizontal Left = -1 Center = 0 Right = 1 End Enum Public Enum RotateVertical Up = -1 Center = 0 Down = 1 End Enum Public Enum CubeSides Left Right Top Bottom Front Back End Enum Public Structure Cube Private FLocation As Point Private FHeight As Integer Private FWidth As Integer Private FDepth As Integer Private FCenter As Point Private FPath As GraphicsPath Private FRotateX As RotateHorizontal Private FRotateY As RotateVertical Public Property Location() As Point Get Return FLocation End Get Set(ByVal Value As Point) FLocation = Value End Set End Property Public Property Height() As Integer Get Return FHeight End Get Set(ByVal Value As Integer) FHeight = Value End Set End Property Public Property Width() As Integer Get Return FWidth End Get Set(ByVal Value As Integer) FWidth = Value End Set End Property Public Property Depth() As Integer Get Return FDepth End Get Set(ByVal Value As Integer) FDepth = Value End Set End Property Public Property Center() As Point Get Return FCenter End Get Set(ByVal Value As Point) FCenter = Value End Set End Property Public ReadOnly Property Path() As GraphicsPath Get Return FPath End Get End Property Public Property RotateX() As RotateHorizontal Get Return FRotateX End Get Set(ByVal Value As RotateHorizontal) FRotateX = Value End Set End Property Public Property RotateY() As RotateVertical Get Return FRotateY End Get Set(ByVal Value As RotateVertical) FRotateY = Value End Set End Property Public ReadOnly Property X() As Integer Get Return FLocation.X End Get End Property Public ReadOnly Property Y() As Integer Get Return FLocation.Y End Get End Property 'Return the rectangle that bounds the entire polygon 'representing the cube Public ReadOnly Property BoundsRect() As Rectangle Get If (FPath Is Nothing) Then Return New Rectangle(0, 0, 0, 0) Else Dim r As RectangleF = Path.GetBounds() ' Implicit conversion from single to integer, ' really only available in VB Return New Rectangle(r.X, r.Y, r.Width, r.Height) End If End Get End Property Public Property Size() As CubeSize Get Return New CubeSize(FWidth, FHeight, FDepth) End Get Set(ByVal Value As CubeSize) FWidth = Value.Width FHeight = Value.Height FDepth = Value.Depth End Set End Property Public ReadOnly Property Item(ByVal index As CubeSides) _ As Point() Get Select Case index Case CubeSides.Back Return Back Case CubeSides.Front Return Front Case CubeSides.Left Return Left Case CubeSides.Right Return Right Case CubeSides.Top Return Top Case CubeSides.Bottom Return Bottom Case Else Return Front End Select End Get End Property Public ReadOnly Property Top() As Point() Get ' TODO: Need to implement behavior here Return New Point() {} ' Return GetTop(location, height, width, depth, ' rotateX, rotateY) End Get End Property Public ReadOnly Property Bottom() As Point() Get ' TODO: Need to implement behavior here Return New Point() {} ' Return GetBottom(location, height, width, depth, ' rotateX, rotateY) End Get End Property Public ReadOnly Property Left() As Point() Get ' TODO: Need to implement behavior here Return New Point() {} ' Return GetLeft(location, height, width, depth, ' rotateX, rotateY) End Get End Property Public ReadOnly Property Right() As Point() Get ' TODO: Need to implement behavior here Return New Point() {} ' Return GetRight(location, height, width, depth, ' rotateX, rotateY) End Get End Property Public ReadOnly Property Front() As Point() Get ' TODO: Need to implement behavior here Return New Point() {} ' Return GetFront(location, height, width, depth, ' rotateX, rotateY) End Get End Property Public ReadOnly Property Back() As Point() Get ' TODO: Need to implement behavior here Return New Point() {} ' Return GetBack(location, height, width, depth, ' rotateX, rotateY) End Get End Property End Structure
I added an enumeration (CubeSides) so I could refer to the sides of a cube by name. I defined a property indexer to return an array of points representing a specific size, added a property that returns the enclosing bounds of the cube by getting the bounds of a GraphicsPath object, and defined read-only properties representing each side. I stubbed out these properties in terms of as yet unimplemented methods.