Sign in TopomorphRSS Feed

There is a lot written about code, elegance and the quality of software. If it’s not a truth that elegant code is efficient, maintainable and a joy to write, its absence sure is a razor sharp tool to indicate you’re on the wrong track. 

I was working on a simple Silverlight control that shows a star and I was about to layout the segments.

Here’s one algorithm to create a star:
From the first point you go across the centre and take the point to the left or right.
Repeat until finished. 

 

image


It does make you a star (though not a bright one).It wont work for stars with an even number of points. These have 2 sub paths. So obviously we have two types of stars, the odds and the evens.

To handle these types we check the oddity of the NumberOfPoints and follow two code paths:
if (odd) makeOddPointArray else uh MakeTwoEvenPointArrays()

 

   1:  private Point[] makeOddPointArray()
   2:  {
   3:      Point[] points = new Point[NumberOfPoints];
   4:      for (int i = 0; i < NumberOfPoints; i++)
   5:      {
   6:          points[i] = FromPolarPoint(360.0 * i / NumberOfPoints, OuterRadius);
   7:      }
   8:   
   9:      Point[] mixedPoints = new Point[NumberOfPoints];
  10:      int middleIndex = NumberOfPoints / 2;
  11:      for (int i = 0; i < middleIndex; i++)
  12:      {
  13:          mixedPoints[i] = points[i];
  14:          mixedPoints[middleIndex + i + 1] = points[i*2];
  15:      }
  16:      mixedPoints[middleIndex] = points[middleIndex*2];// dont forget the last one
  17:      return mixedPoints;
  18:  }

 

 

Apart from the fact that this function does not follow the proposed algorithm, it does not feel good. If you have to differentiate between odd and even situations, you are likely looking at the problem from the wrong angle.

The code above showed something like the left figure:

image
It became clear that there was not an issue about odd and even altogether! The middle figure has three sub paths and has nine points. You also can look at a star shape as figure where all the points are connected and where the type of the star is being defined by the number of points are skipped.
In the case the NumberOfPoints can be divided by the NumberOfPointsSkipped you end up with multiple sub paths. In fact is you state that more generic: you always repeatedly create multiple sub paths until you have connect all you star points.

And now the code becomes elegant. The algorithm happens to follow the data structure for Path Shapes so there’s no need for building additional data structures holding temporary data.

protected%20override%20void%20Draw%28%29%0A%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20int%20step%20%3D%20Math.Min%28PointStep%2C%20%20NumberOfPoints%20/%202%29%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20//%20generate%20the%20points%20involved%0A%20%20%20%20%20%20%20%20%20%20%20%20Point%5B%5D%20points%20%3D%20new%20Point%5BNumberOfPoints%5D%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20for%20%28int%20i%3D0%3Bi%3CNumberOfPoints%3Bi++%29%0A%20%20%20%20%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20points%5Bi%5D%20%3D%20FromPolarPoint%28360.0*i/NumberOfPoints%2C%20OuterRadius%29%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20int%20numberOfpointsProcessed%20%3D%200%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20int%20startIndex%20%3D%20-1%3B//%20we%27ll%20start%20on%20the%20first%20point%20with%20index%20%3D%200%0A%0A%20%20%20%20%20%20%20%20%20%20%20%20//%20path%20is%20a%20collection%20of%20sub%20paths%20called%20PathFigures%20%20%20%20%20%20%20%20%20%20%20%0A%20%20%20%20%20%20%20%20%20%20%20%20PathFigureCollection%20pfc%20%3D%20new%20PathFigureCollection%28%29%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%0A%20%20%20%20%20%20%20%20%20%20%20%20while%20%28numberOfpointsProcessed%20%3C%20points.Length%29%0A%20%20%20%20%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20//%20create%20a%20new%20sub%20path%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20PathSegmentCollection%20segments%20%3D%20new%20PathSegmentCollection%28%29%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20startIndex++%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20int%20currentIndex%20%3D%20startIndex%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20while%20%28true%29//%20repeat%20until%20you%27re%20back%20at%20the%20start%20index%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20segments.Add%28new%20System.Windows.Media.LineSegment%28%29%20%7B%20Point%20%3D%20points%5BcurrentIndex%5D%20%7D%29%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20numberOfpointsProcessed++%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20currentIndex%20%3D%20%28currentIndex%20+%20step%29%20%25%20NumberOfPoints%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20if%20%28currentIndex%20%3D%3D%20startIndex%29%20break%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20//%20create%20a%20new%20path%20figure%20for%20this%20sub%20path%20%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20PathFigure%20pf%20%3D%20new%20PathFigure%28%29%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20Segments%20%3D%20segments%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20StartPoint%20%3D%20points%5BstartIndex%5D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20IsClosed%20%3D%20true%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20IsFilled%20%3D%20true%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pfc.Add%28pf%29%3B%20%20//add%20to%20the%20collection%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%0A%20%20%20%20%20%20%20%20%20%20%20%20//%20the%20path%20shape%20is%20a%20Framework%20element%20that%20holds%20the%20geometry%20in%20a%20PathFiguresCollection%20%0A%20%20%20%20%20%20%20%20%20%20%20%20PathGeometry%20pg%20%3D%20new%20PathGeometry%28%29%0A%20%20%20%20%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20Figures%20%3D%20pfc%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%3B%0A%0A%20%20%20%20%20%20%20%20%20%20%20%20Path%20p%20%3D%20new%20Path%28%29%0A%20%20%20%20%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20Data%20%3D%20pg%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20Stroke%20%3D%20this.Stroke%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20Fill%20%3D%20this.Fill%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%3B%0A%0A%20%20%20%20%20%20%20%20%20%20%20%20this.Children.Add%28p%29%3B%20//%20add%20the%20path%20to%20the%20UserControl%0A%0A%20%20%20%20%20%20%20%20%7D[copy code to clipboard]
   1: protected override void Draw()
   2:         {
   3:             int step = Math.Min(PointStep,  NumberOfPoints / 2);
   4:             // generate the points involved
   5:             Point[] points = new Point[NumberOfPoints];
   6:             for (int i=0;i<NumberOfPoints;i++)
   7:             {
   8:                 points[i] = FromPolarPoint(360.0*i/NumberOfPoints, OuterRadius);
   9:             }
  10:             int numberOfpointsProcessed = 0;
  11:             int startIndex = -1;// we'll start on the first point with index = 0
  12:  
  13:             // path is a collection of sub paths called PathFigures           
  14:             PathFigureCollection pfc = new PathFigureCollection();
  15:             
  16:             while (numberOfpointsProcessed < points.Length)
  17:             {
  18:                 // create a new sub path
  19:                 PathSegmentCollection segments = new PathSegmentCollection();
  20:                 startIndex++;
  21:                 int currentIndex = startIndex;
  22:                 while (true)// repeat until you're back at the start index
  23:                 {
  24:                     segments.Add(new System.Windows.Media.LineSegment() { Point = points[currentIndex] });
  25:                     numberOfpointsProcessed++;
  26:                     currentIndex = (currentIndex + step) % NumberOfPoints;
  27:                     if (currentIndex == startIndex) break;
  28:                 }
  29:                 // create a new path figure for this sub path 
  30:                 PathFigure pf = new PathFigure()
  31:                 {
  32:                     Segments = segments,
  33:                     StartPoint = points[startIndex],
  34:                     IsClosed = true,
  35:                     IsFilled = true
  36:                 };
  37:                 pfc.Add(pf);  //add to the collection                       
  38:             }
  39:             
  40:             // the path shape is a Framework element that holds the geometry in a PathFiguresCollection 
  41:             PathGeometry pg = new PathGeometry()
  42:             {
  43:                 Figures = pfc
  44:             };
  45:  
  46:             Path p = new Path()
  47:             {
  48:                 Data = pg,
  49:                 Stroke = this.Stroke,
  50:                 Fill = this.Fill
  51:             };
  52:  
  53:             this.Children.Add(p); // add the path to the UserControl
  54:  
  55:         }

The characteristic properties for the star are NumberOfPoints, the OuterRadius, the PointStep (for skipping), and Fill and Stroke. These are all defined as Dependency Properties.
Since I’m collecting a set of these shape figures (for example doughnut wedges) I also created a base class for some common properties and utility functions. Trivial stuff.

There’s one issue that I found no answer for: redrawing of the shape every time a property changes does not seem a good idea  since you’ll be setting several properties in one pass (OuterRadius, NumberOfPoints, Fill and Stroke). So how can we collect these changes and draw only when it’s necessary? In WinForms you invalidated your image and waited for the Paint event to come by. Any suggestions?

Comments