In my editing program, the objects that can be manipulated with the mouse are called "selectables", and they implement this interface:
public interface ISelectable { IEnumerable<IGrip> Grips { get; } void Offset(Vector v); }
So an object can be moved around the page by calling Offset, and it can have grips, which look like this:
public interface IGrip { Point Position { get; set; } }
The grips appear on the screen as little circles that you can grab and move with the mouse to change the shape of the object. Most rectangular objects have eight grips - you know how these things usually work. We could do without the Offset function, and just change the position of all the grips instead, but most object types can do a better (more efficient) job of moving themselves rigidly if they know that's what is required.
I also have a nice extension method on ISelectable called GetBounds() that loops through the grips to find the bounding rectangle. And another that does the same thing on IEnumerable<Selectables>.
Now what I want is a toolbar with some alignment buttons. When I press the Align Left button, all the selected objects should move so that their left-most point is aligned with the left-most point of the entire selection. And the same for the other three sides.
It's easy enough to write this for one side, given a property called Selected that is an IEnumerable<Selectables> containing all the objects that the user has selected (ignore the BeginUndoTransaction bit - it just ensures that all the changes we make will be undoable as a single atomic operation).
public void AlignLeft() { using (var undo = BeginUndoTransaction()) { Rect boundsTarget = Selected.GetBounds(); foreach (ISelectable s in Selected) s.Offset(new Vector( boundsTarget.X - s.GetBounds().X, 0)); undo.Commit(); } }
But if you're anything like me, you get a stomach ache if you're faced with the task of making four very similar copies of some code. We're doing basically the same thing with all four sides of the rectangle. Why do we have to write it four times?
What we need is an operator we can use on a Rect that can do the same thing to any of its sides - namely, set the position of a side without changing the size of the Rect:
public static void SetRigid(this Side e, ref Rect r, double c) { switch (e) { case Side.Left: r.X = c; break; case Side.Top: r.Y = c; break; case Side.Right: r.X = c - r.Width; break; default: r.Y = c - r.Height; break; } }
(Note that the side to operate on is specified by a parameter of type Side, introduced yesterday.)
Now our alignment function can be written like this:
public void AlignSide(Side side) { using (var undo = BeginUndoTransaction()) { Rect boundsTarget = Selected.GetBounds(); foreach (ISelectable s in Selected) { Rect boundsOld = s.GetBounds(), boundsNew = boundsOld; side.SetRigid(ref boundsNew, boundsTarget.Get(side)); s.Offset(boundsNew.TopLeft - boundsOld.TopLeft); } undo.Commit(); } }
And now that does all four directions. Just specify the side parameter.
So the moral is, don't let the lack of symmetry in classes force you to write the same code several times. Discover the operations that express the symmetry, and then write your code in terms of those operations.
No comments:
Post a Comment