Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

SnapWindows - StickyWindows reloaded

0.00/5 (No votes)
15 Feb 2010 1  
A new approach to 2D - geometric computation replaces Point, Size, and Rectangle.

Introduction

In my previous article SizerPanel and CaptionPanel, I had mentioned how it annoys me that System.Drawing.Point, .Size, and .Rectangle always force me to write the same code twice: first for the horizontal direction, and then for the vertical. And I had also mentioned about the structure Vector that I wrote to replace System.Drawing.Point as well as System.Drawing.Size. I also extended my geometric stuff with a Rect structure to replace Rectangle.

The basic idea of this is to make horizontal and vertical access indexed: index 0 accesses horizontal, index 1 vertical. Now I can write geometric code for only one direction, and the other will be covered by swapping the indices 0 and 1.

So my primary goal was not to create a remake of StickyWindows, but to test, proof, and improve the power of my geometric approach. I took StickyWindows as the topic because I liked the idea, and also the implementation-challenge, especially the geometric part is not that trivial (in the original, the pure geometric computing-code took about 250 lines). It's an excellent "sparring-partner" to become fit in geometric computations, and to try out my "weapons".

What sticky/snap windows do

Mainform.gif

They make forms perform a kind of "magnetic behavior": when you resize or move a form, and it gets close enough to another form, the near edge snaps to the edge of the other. That really makes it easier to arrange multiple forms on screen in a clear manner.

Implementation challenges

Subclass the Form

You can't use the Resize or SizeChanged event to perform form-snapping, because that implies changing the size within the SizeChanged event itself. My first trials were in that direction, and they ended up in some real puzzling behavior. Nor will you get MouseMove events, because a Form is just resized, when especially the non-client-area is hit, and there is no MouseMove available. So you must access the required data before it appears on the .NET-managed level - in other words: you must subclass the Form. To achieve this, .NET provides two ways: the simpler one is to override the WndProc Sub in each Form you want to behave as a SnapWindow. The second way is to instantiate a NativeWindow, assign the target form's handle, and override the NativeWindow's WndProc(). The WndProc() will handle all the window messages to the target form. And I still believe as in my previous article, that this is the more flexible way, because you don't need to modify each form's user code, but can simply register a Form as a SnapWindow, from the outside.

Now, take a look at the subclassing code:

Protected Overrides Sub WndProc(ByRef m As Message)
   Const WM_NCLBUTTONDOWN As Integer = 161
   Const WM_MOVING As Integer = &H216
   Const WM_SIZING As Integer = &H214
   Select Case m.Msg
      Case WM_NCLBUTTONDOWN
         'init a VirtualForm
         Dim snapRects As New List(Of Rect)
         _VForm = VirtualForm.TryCreate(_Form, m.WParam.ToInt32, snapRects)
         If _VForm.Null Then Exit Select
         '...
         '... set up snapRects
         '...
      Case WM_MOVING, WM_SIZING
         'write the computed Rect to LParam
         Marshal.StructureToPtr(_VForm.GetSnapFormRect, m.LParam, False)
   End Select
   MyBase.WndProc(m)
End Sub

As it often happens: It's easy when you know how... ;) - in this case, we need to know how to handle the members of the window message:

m.Msg indicates the type of event to perform: WM_NCLBUTTONDOWN - WindowMessage about a Non-Client-area-Left-ButtonDown - indicates the beginning of a Form move or resize. WM_MOVING, and WM_SIZING are self-explanatory, aren't they?

When the form-bounds start to change (WM_NCLBUTTONDOWN), I initialize an instance of my VirtualForm class, passing three things to it: the Form, the snapRects (a list of Rectangles which can be snapped), and the window message's .WParam - uh?

m.WParam - this is more tricky than m.Msg. In general, the WParam can mean everything. But in combination with the WM_NCLBUTTONDOWN message, it passes one of the following HT (Hit-Test) constants: HTCAPTION, HTLEFT, HTRIGHT, HTTOP, HTTOPLEFT, HTTOPRIGHT, HTBOTTOM, HTBOTTOMLEFT, HTBOTTOMRIGHT (and some other, which are out of interest - TryCreate() rejects them, if they are there). Now, the VirtualForm knows exactly what kind of bounds-changing is requested. (You will already suspect: VirtualForm is designed to compute snapping form-bounds).

Well, and the next time we meet our VirtualForm is when a moving or a sizing occurs - and the code-comment already mentions this: the computing result is written to the LParam pointer - and all of the form-bounds-change-task is done! You see:

m.LParam is tricky: combined with a move/size message, it is a pointer which points to the RECT structure which defines the form-bounds after the change. And, changing the RECT structure changes the bounds.

At this point, unmanaged code is directly accessed - since LParam is an IntPtr, you cannot simply assign a Rectangle to it. You must copy the Rect structure byte wise to the place in memory where the pointer points to. This does Marshal.StructureToPtr() for us. And we hope that the data structure the WinAPI expects at that place in memory is compatible with the structure you submitted. Believe it or not - my Rect structure is compatible ;).

Note that the WinAPI RECT structure is different to the System.Drawing.Rectangle structure. Drawing.Rectangle defines a rectangle by a location-point and a size, while the WinAPI RECT defines the top-left point (equals location) and the bottom-right point (not equal to Size). I, for instance, found it more useful to see those things the "API-way", and that's why my Rect structure fits better to WinAPI than Drawing.Rectangle does.

From where did I get to know about the subclassing stuff?

(To be honest, I picked it up from the internet.) A more systematic method to find the info would be to open the Help-Index (offline-MSDN), set the filter to "Platform SDK", and type "WM_MOVING", "WM_SIZING" or "WM_NCLBUTTONDOWN" in the search-textbox. This brings up informative articles about these constants, and how you can work with them.

Unfortunately, the values of the constants aren't mentioned anywhere. You can download the VS2008 Platform SDK. This will install in your Program Files, and you will find a 'C:\Program Files\Microsoft SDKs\Windows\v6.0A\Include\WinUser.h' on your system, where (among other things) all constants of the user32.dll are defined.

Unfortunately, Help for the "Platform SDK" isn't available for Express editions (not sure about this, though).

Vector and Rect

A Vector is a Size and a Point in one. It provides X, Y and usual features to perform simple 2D-geometric math. I made a bunch of CType operators to make it as flexible and compatible as possible. Its special feature is that you can access X and Y by using an Integer index in order to avoid writing each piece of geometric code in two variants.

Rect simply is two Vectors. As I mentioned before, it doesn't define the described rectangle as a composition of a Point and a Size, but defines the top-left Point and the bottom-right one. I call them .Near and .Far, as a homage to the System.Text.StringAlignment enumeration, which is the only item I know in the Framework which also works in both dimensions (you can align a text-drawing horizontally by setting StringFormat.Alignment.Near/.Center/.Far, and align it vertically by setting StringFormat.LineAlignment.Near/.Center/.Far). In following sections, I will use the terms Near or Far to distinguish between a top or left entity from a bottom or right one. And, of course, Near and Far are accessible by index too.

VirtualForm

It only gets data once, in the constructor: Rects and Vectors. When new form-bounds are requested, it first computes the normal form-bounds, only using the globally accessible Control.MousePosition as additional data. In the second step, all "snap-candidate-rectangles" are compared with the computed normal-form-bounds, and the edge with the minimum distance gets snapped. This is done with the horizontal edges as well as with the vertical ones.

VirtualForm must compute the NormalFormRect without dependence to the real form, because the real bounds are different when another form's edge is snapped.

The NormalFormRect must be known to perform the "snapping off", when the distance between the NormalFormRect and a snapped edge exceeds the "magnetic distance".

So first, we have to understand how the NormalFormRect is computed, that means: comprehend how a mouse moves or resizes the form.

In fact, there is no difference between moving a form and resizing it, if you understand the process a bit more in general. You will see that it is always done by dragging edges: you can drag one edge (mouse grabs a form-edge), two edges (mouse grabs a corner), or four edges (mouse grabs the caption (=title-bar)). (To drag three edges is a theoretical option, but not provided by the infrastructure.)

Edge-dragging must be initialized by storing the information about which edges are affected, and also storing the "Grab Offset", which is the distances of the affected edges to the mouse-position. The VirtualForm constructor does this in two lines of code:

_SizeType = HT2SizeOption(HT)
_GrabOffs = Rect.From(Control.MousePosition).Subtract(_FormRect)

(For simplicity in code, _GrabOffs stores all edge-distances, although only the affected ones are needed.)

Now, it can recompute the NormalFormBounds, whenever requested, since the drag-principle is the simple: always keep the offset constant. But recomputing isn't quite that simple, because I'm not allowed to change all edges, and must distinguish between the edges according to _SizeType.

To understand in depth how I achieve that distinction, let me do an egression about...

Enumeration advanced

Enumerations are useful to store and query multiple boolean statements. With the bit operators Or, And, and Xor, you can filter and modify all the 32 bool-statements of an (Int32-)enumeration-member with one command. One of the first things the VirtualForm constructer does is call HT2SizeOption() to map the passed HT-constant to an enumeration named SizeOption.

Public Enum SizeOption As Integer
  Top = 1
  Right = 2
  Bottom = 4
  Left = 8
End Enum

For brevity in code, I added the mainly used bit-combinations too so that the complete definition comes up as:

Public Enum SizeOption As Integer
  Top = 1
  Right = 2
  Bottom = 4
  Left = 8
  TopLeft = Top Or Left
  TopRight = Top Or Right
  BottomLeft = Bottom Or Left
  BottomRight = Bottom Or Right
  Caption = Top Or Left Or Bottom Or Right
End Enum

No rocket science, is it? And it defines clearly the nine options to drag a form.

Now you can check out whether a given (multiple) bool-statement (represented by an enumeration value) has one or more bits in common with a given other one:

(_SizeType And SizeOption.Top) = SizeOption.Top

will match, if _SizeType is one of the following: Top, TopLeft, TopRight, Caption.

Another way to query is:

(_SizeType And SizeOption.TopLeft) <> 0

This will match, if any of the _SizeType bits match any of the TopLeft bits. In particular: Top, Left, TopLeft, TopRight, BottomLeft, Caption.

If you query against a 1-bit-statement, the code for comparison to 0 is shorter than the first variant:

(_SizeType And SizeOption.Top) = SizeOption.Top

has the same results as:

(_SizeType And SizeOption.Top) = 0

Back to rectangle-computation

Indexable structures also imply that I can iterate them. In the case of Rect, I can iterate in two different ways: from near to far as well as from horizontal to vertical. I can nest in two ways and end up with a two-dimensional iteration.

'(Dim rct As Rect)
For far = 0 To 1
  For vertical = 0 To 1
     Dim edge As Integer = rct(far, vertical)
  Next
Next

This loops the four edges in the following order: Left, Top, Right, Bottom. And now, I build four "enumeration-queries", which can be iterated in the same way: I build a two-dimensional Array of Enums:

Private Shared _SizeOptions As SizeOption(,) = New SizeOption(,) { _
      {SizeOption.Left, SizeOption.Top}, _
      {SizeOption.Right, SizeOption.Bottom}}

Now I can iterate _SizeOptions, _FormRect-edges, and _GrabOffsets within a 2D-loop, select the affected edges, and assign values, to make the distances to the mouse position equal to the corresponding offset (principle of dragging).

Private Sub ComputeFormRect()
   Dim ptMouse = Vector.From(Control.MousePosition)
   For far = 0 To 1
      For vertical = 0 To 1
         If 0 <> (_SizeType And _SizeOptions(far, vertical)) Then
            _FormRect(far, vertical) = ptMouse(vertical) - _GrabOffs(far, vertical)
         End If
      Next
   Next
End Sub

Computing the SnapRect

The whole story:

Public Function GetSnapFormRect() As Rect
   ComputeFormRect()
   Dim result = _FormRect
   Dim snapSize = Vector.From(SnapWindow.SnapSize)
   Dim inflated = _FormRect.Add(-snapSize, snapSize)
   For vertical = 0 To 1
      Dim horizontal = vertical Xor 1
      'get min-distance between _FormRects dragged 
      'edges and any edge of any _SnapRect
      Dim minDist = Integer.MaxValue
      For far As Integer = 0 To 1
         'filter dragged edge(s)
         If 0 = (_SizeType And _SizeOptions(far, vertical)) Then Continue For 
         For Each rct2 In _SnapRects.Where( _
                     Function(r2) Intersect1D(r2, inflated, horizontal))
            For far2 As Integer = 0 To 1
               Dim distance = _FormRect(far, vertical) - rct2(far2, vertical)
               If distance.Abs() < minDist.Abs() Then minDist = distance
            Next
         Next
      Next
      If minDist.Abs() > SnapWindow.SnapSize Then Continue For
      For far = 0 To 1
         'filter dragged edges 
         If 0 <> (_SizeType And _SizeOptions(far, vertical)) Then _
              result(far, vertical) -= minDist
      Next
   Next
   If _SizeType <> SizeOption.Caption Then
      'clip near-edges (WinApi cares for the far-edge-restriction)
      result.Near = result.Near.Intersect(_NearClip.Far).Union(_NearClip.Near)
   End If
   Return result
End Function

Now we will examine it step by step:

ComputeFormRect()
Dim result = _FormRect
Dim snapSize = Vector.From(SnapWindow.SnapSize)
Dim inflated = _FormRect.Add(-snapSize, snapSize)

is not that complicated: compute the normal _FormRect (a class member) and set it as the default return value. Then, create inflated, as _FormRect, inflated by the magnetic distance.

Then, enter the two-dimensional rectangle-loop:

For vertical = 0 To 1
   Dim horizontal = vertical Xor 1
   Dim minDist = Integer.MaxValue
   For far As Integer = 0 To 1
      '...

Note the second line: Dim horizontal = vertical Xor 1. valueToSwitch Xor 1 - the easiest way to switch an index from 0 to 1 and reverse.

Now we come to the kernel. First, skip the computation if the _FormRect edge in question isn't moveable at all (check against _SizeType):

If (_SizeType And _SizeOptions(far, vertical)) = 0 Then Continue For

If _SizeType matches with the (far, vertical) addressed _SizeOption, we loop _SnapRects in order to search the minimum-distance-edge to the (far, vertical) addressed _FormRect edge:

For Each rct2 In _SnapRects.Where(Function(r2) Intersect1D(r2, inflated, horizontal))
  For far2 As Integer = 0 To 1
     Dim distance = _FormRect(far, vertical) - rct2(far2, vertical)
     If distance.Abs() < minDist.Abs() Then minDist = distance
  Next
Next

Again, there is a filter: SnapRects.Where(Function(r2) Intersect1D(r2, inflated, horizontal)) only takes those Rects to examine, which intersect in the other direction with the inflated FormRect (mentioned before). Because, it doesn't make sense to snap an edge 15 pixels to the left, when it is located about 500 pixels below ;). Note: Here, I get an one-dimensional understanding of intersection: a rectangle may intersect in the horizontal direction, but not vertical (e.g., words in a line intersect horizontal, but not vertical ). See the intersection computation code:

Private Function Intersect1D(ByVal rct1 As Rect, _
      ByVal rct2 As Rect, ByVal vertical As Integer) As Boolean
   Return rct1.Far(vertical) >= rct2.Near(vertical) _
      AndAlso rct2.Far(vertical) >= rct1.Near(vertical)
End Function

OK, now we have our minDist, and can apply it to our return-result (if minDist is small enough):

If minDist.Abs() > SnapWindow.SnapSize Then Continue For
For far = 0 To 1
   'apply minDist only to dragged edges
   If 0 <> (_SizeType And _SizeOptions(far, vertical)) Then _
       result(far, vertical) -= minDist
Next

followed by the (I hope so) last point of surprise:

If _SizeType <> SizeOption.Caption Then
   'clip near-edges (WinApi cares for the far-edge-restriction) 
   result.Near = result.Near.Intersect(_NearClip.Far).Union(_NearClip.Near)
End If

Eh? Why does WinAPI only care for far-edge-restriction, but not restrict the near-edges? What does it mean at all: "caring for edge-restriction"? To illustrate (and for your pleasure), I created a little demo, to show what happens if you comment out that final clip:

NearClipDemo.gif

Got it? Violating the MinimumSize from above is prevented by setting the form's Height back to the minimum amount. But it was the Top which changed! - and so the form moves, although you're actually sizing the top edge.

Some points of interest left open

Vector.Intersect() and Vector.Union()

Let's begin with the above mentioned clip:

result.Near = result.Near.Intersect(_NearClip.Far).Union(_NearClip.Near)

Intersect() and Union() are not applied to Rects, but to Vectors! How can a Vector intersect with another - it has no size! And that's the point. Since a Vector represents a point as well as a size, it is a size. And when I intersect two Vectors, I take them as two Rects with .Near-Vector= (0,0). In another view: Vector1.Intersect(Vector2) returns the minimum amount of both in each direction. And, that's exactly the one (the ceiling-) part of clipping a Vector inside a Rect. The other (floor-) part of clipping is a Union with the Rect's .Near-Vector, which means the maximum amount in each direction. In summary: you clip the Vector v within the Rect rct by Intersecting the union(v, rct.Near) with rct.Far.

Rects way of intersection

System.Drawing.Rectangle.Intersect(other As Rectangle) returns Rectangle.Empty, if the Rectangles do not intersect. Instead of that, Rect.Intersect() is computed as follows:

''' <summary>
''' a none-intersection is indicated by returning an imaginary Rect
''' </summary>
Public Function Intersect(ByVal other As Rect) As Rect
   For i = 0 To 1
      Intersect.Near(i) = Near(i).Max(other.Near(i))
      Intersect.Far(i) = Far(i).Min(other.Far(i))
   Next
End Function

''' <summary>true, if my size has a negative component</summary>
Public ReadOnly Property IsImaginary() As Boolean
   Get
      If Far.X - Near.X < 0 Then Return True
      Return Far.Y - Near.Y < 0
   End Get
End Property

The advantage is that an imaginary Rect still delivers a valuable information: namely, the distance between both Rects. Got it? The distance between two rectangles is understood as negative intersection.

The idea of "homogen structure"

Structures whose fields are only of one data type, I address them as "homogen". In my opinion, we can apply some principles to them, e.g., the indexablity I already explained.

In general, you can try to transfer features from the unit-types to the whole structure.

Inverting operator

Since Integer is invertable, I want Vector and Rect also to support it:

Public Shared Operator -(ByVal v1 As Vector) As Vector
    Return New Vector(-v1.X, -v1.Y)
End Operator 

Public Shared Operator -(ByVal r As Rect) As Rect
    Return New Rect(-r.Near, -r.Far)
End Operator

Data-stretching

This means several operations may be applicable, although there may be passed too few data to explicitly assign to each member.

For instance, the operation .From(). I take it as a public shared constructor, a bit more comfortable than the good old New():

Dim v = Vector.From(x As Integer, y As Integer)

That is the basic one. But also unambiguous is to do this:

Dim v = Vector.From(amount As Integer)

It only can mean to assign amount to v.X as well as to v.Y.

See a data stretching equality-comparison here:

Dim b = v.EachEquals(amount As Integer)

And this is convenient: Suddenly, I have a sort of "inferred constants" (inferred by Integer). That means, Vector.From(0) is a perfect substitute of Point.Empty. And I'm free to define other "special-points", e.g., Vector.From(Integer.MaxValue) or Vector.From(Integer.MinValue), to indicate (by convention) an invalid location without using the Nullable(Of T) structure.

Checking against each special-point is covered by Vector.EachEquals(), e.g., Vector.EachEquals(0) substitutes Point.IsEmpty, and Vector.EachEquals(Integer.MinValue) is a new feature with no equivalent in Point or Size, maybe at best the really ugly:

Dim pt = New Point(Integer.MinValue, Integer.MinValue) 
Dim b = pt = New Point(Integer.MinValue, Integer.MinValue)

But go on stretching data

Theoretically, I could overload the + operator as follows:

Public Shared Operator +(ByVal v As Vector, ByVal amount As Integer) As Vector
   Return New Vector(v.X + amount, v.Y + amount)
End Operator

to puzzle the audience with expressions like:

Dim v = Vector.From(4, 5) + 9

But that's too inconsiderate for even me. ;) Instead, see my Add() overloads, the second one does data stretching:

Public Function Add(ByVal x As Integer, ByVal y As Integer) As Vector
 Return New Vector(Me.X + x, Me.Y + y)
End Function

Public Function Add(ByVal amount As Integer) As Vector
 Return New Vector(X + amount, Y + amount)
End Function

Public Function Add(ByVal v As Vector) As Vector
 Return New Vector(X + v.X, Y + v.Y)
End Function

This performs Offset, Inflate/Deflate, and is compatible to Point and Size, because of some widening CType() operators. (.Subtract() works in the same manner.)

Datastretching with Rect

The benefits of .From() and .EachEquals(), of course, work as well, and in the case of .From(), I support one more stretch option:

Public Shared Function From(ByVal eachMember As Integer) As Rect
 Return Rect.From(Vector.From(eachMember))
End Function

Public Shared Function From(ByVal eachMember As Vector) As Rect
 Return New Rect(eachMember, eachMember)
End Function

Also .Add() becomes more interesting:

Public Function Add(ByVal toEach As Integer) As Rect
 Return Add(Vector.From(toEach))
End Function

''' <summary>performs offset </summary>
Public Function Add(ByVal toEach As Vector) As Rect
 Return New Rect(Near + toEach, Far + toEach)
End Function

''' <summary>
''' performs sophisticated offsets, eg. inflating/deflating (if v1 = -v2)
''' </summary>
Public Function Add(ByVal v1 As Vector, ByVal v2 As Vector) As Rect
 Return New Rect(Near + v1, Far + v2)
End Function

''' <summary>
''' performs sophisticated offsets, supports compatiblity to Rectangle
''' </summary>
Public Function Add(ByVal r As Rect) As Rect
 Return New Rect(Near + r.Near, Far + r.Far)
End Function

(.Subtract() ditto.)

The above works behind the scenes, when VirtualForm gets the four-edges-offset within one line of code:

_GrabOffs = Rect.From(Control.MousePosition).Subtract(_FormRect)

or when I inflate _FormRect with snapSize:

Dim inflated = _FormRect.Add(-snapSize, snapSize) 

LINQ

''' <summary>enables Linq-power to Vectors Elements</summary>
Public Function GetEnumerator() As IEnumerator(Of Integer) Implements _
      IEnumerable(Of Integer).GetEnumerator
   Return DirectCast(New Integer() {X, Y}, IEnumerable(Of Integer)) _
      .GetEnumerator
End Function

Private Function GetEnumerator1() As IEnumerator Implements _
      IEnumerable.GetEnumerator
   Return DirectCast(New Integer() {X, Y}, IEnumerable(Of Integer)) _
      .GetEnumerator
End Function

''' <summary>enables Linq-power to Rects elements</summary>
Public Function GetEnumerator() As IEnumerator(Of Vector) Implements _
      IEnumerable(Of Vector).GetEnumerator
   Return DirectCast(New Vector() {Near, Far}, IEnumerable(Of Vector)) _
      .GetEnumerator
End Function

Private Function GetEnumerator1() As IEnumerator Implements _
      IEnumerable.GetEnumerator
   Return DirectCast(New Vector() {Near, Far}, IEnumerable(Of Vector)) _
      .GetEnumerator
End Function

I don't know whether that is useful. I, for instance, don't use it. But it looks interesting to me, and it was easy to implement. ;)

Replacing public shared functions with extension functions

It's a sort of off-topic, but I'd like to mention one more principle, to make you understand all the shown code:

When it comes to extension functions, in many cases, there is no more need for Public Shared functions, which forces you to type full qualifiers to call them. Compare these clip-variants:

Public Function ClipIn(ByVal pt As Point, _
                ByVal rct As Rectangle) As Point
   Return New Point( _
      Math.Max(Math.Min(pt.X, rct.Right), rct.X), _
      Math.Max(Math.Min(pt.Y, rct.Bottom), rct.Y))
End Function

Public Function ClipIn2(ByVal pt As Point, _
                ByVal rct As Rectangle) As Point
   Return New Point(pt.X.Max(rct.X).Min(rct.Right), _
                    pt.Y.Max(rct.Y).Min(rct.Bottom))
End Function

In the second variant, Min()/Max() appears as an extension function, which is shorter, easier to type (help from intellisense), and which resolves the nasty nesting of functions to an easier to handle concatenation. The same concept I applied to Math.Abs() (maybe you have realized that already, seeing my previous code-samples).

Even tests to Nothing, I'd prefer as an extension:

<Extension()> _
Public Function Null(Of T As Class)(ByVal Subj As T) As Boolean
   Return Subj Is Nothing
End Function

<Extension()> _
Public Function NotNull(Of T As Class)(ByVal Subj As T) As Boolean
   Return Subj IsNot Nothing
End Function

More ideas - the third dimension

I don't really know much about DirectX, but I wonder how DirectX developers manage not to lose their minds. Aren't they forced to type each bit of code thrice? Or, worse, 2.Pow(2) times? I don't know the exact increase in complexity depending on the dimensions, but a square on a chess-board has 8 neighbors, while a packed cube touches 26 others!

I wonder what a "StickyCubes" application looks like? ;)

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here