/* * PointLocator - Generalized mechanism to calculate points * * Author: Phillip Piper * Date: 18/01/2010 5:48 PM * * Change log: * 2010-02-05 JPP - Added RectangleWalker and PointWalker * 2010-01-20 JPP - Use proportions rather than just fixed corners * 2010-01-18 JPP - Initial version * * To do: * - Replace Corner with Drawing.ContentAlignment * - Add circles: ICircleLocator, PointOnCircleLocator, CircleWalker * * Copyright (C) 2009 Phillip Piper * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com. */ using System; using System.Drawing; using System.Collections.Generic; namespace BrightIdeasSoftware { /// /// Corner defines the nine commonly used locations within a rectangle. /// Technically, they are not all "corners". /// public enum Corner { None = 0, TopLeft, TopCenter, TopRight, MiddleLeft, MiddleCenter, MiddleRight, BottomLeft, BottomCenter, BottomRight } /// /// This defines whether a walk proceeds in a clockwise or anticlockwise /// direction. /// public enum WalkDirection { Clockwise, Anticlockwise } /// /// A IPointLocator calculates a point relative to a given sprite. /// public interface IPointLocator { ISprite Sprite { get; set; } Point GetPoint(); } /// /// A useful base class for point locators /// public class AbstractPointLocator : IPointLocator { #region Properties public Point Offset ; public ISprite Sprite { get { return this.sprite; } set { this.sprite = value; this.InitializeSublocators(); } } private ISprite sprite; #endregion #region Public interface /// /// Gets the point from the locator /// /// public virtual Point GetPoint() { return Point.Empty; } #endregion #region Utilities /// /// The sprite associate with this locator has changed. /// Make sure any dependent locators are updated /// protected virtual void InitializeSublocators() { } /// /// Offset a point by our Offset property /// /// The point to be offset /// protected Point OffsetBy(Point pt) { if (this.Offset == Point.Empty) return pt; Point pt2 = pt; pt2.Offset(this.Offset); return pt2; } /// /// Initialize the given locator so it refers to our sprite, /// unless it already refers to another one /// /// The locator to be initialized protected void InitializeLocator(IPointLocator locator) { if (locator != null && locator.Sprite == null) locator.Sprite = this.Sprite; } /// /// Initialize the given locator so it refers to our sprite, /// unless it already refers to another one /// /// The locator to be initialized protected void InitializeLocator(IRectangleLocator locator) { if (locator != null && locator.Sprite == null) locator.Sprite = this.Sprite; } #endregion } /// /// A FixedPointLocator simply returns the point with which it is initialized. /// public class FixedPointLocator : AbstractPointLocator { public FixedPointLocator(Point pt) { this.Point = pt; } protected Point Point ; public override Point GetPoint() { return this.OffsetBy(this.Point); } } /// /// A PointOnRectangleLocator calculate a point relative to rectangle. /// /// /// /// "Relative to a rectangle" can be indicated through a Corner, or through a fraction of /// the width/heights. Giving (0.5f, 0.5f) indicates the middle of the rectangle. /// /// /// The reference rectangle defaults to the bounds /// of the animation /// /// public class PointOnRectangleLocator : AbstractPointLocator { #region Life and death public PointOnRectangleLocator() : this(Corner.TopLeft) { } public PointOnRectangleLocator(Corner corner) : this(new AnimationBoundsLocator(), corner, Point.Empty) { } public PointOnRectangleLocator(Corner corner, Point offset) : this(new AnimationBoundsLocator(), corner, offset) { } public PointOnRectangleLocator(IRectangleLocator locator, Corner corner) : this(locator, corner, Point.Empty) { } public PointOnRectangleLocator(IRectangleLocator locator, Corner corner, Point offset) { this.RectangleLocator = locator; this.PointProportions = this.ConvertCornerToProportion(corner); this.Offset = offset; } public PointOnRectangleLocator(IRectangleLocator locator, float horizontal, float vertical) : this(locator, horizontal, vertical, Point.Empty) { } public PointOnRectangleLocator(IRectangleLocator locator, float horizontal, float vertical, Point offset) { this.RectangleLocator = locator; this.PointProportions = new SizeF(horizontal, vertical); this.Offset = offset; } #endregion #region Configuration properties protected IRectangleLocator RectangleLocator ; protected SizeF PointProportions ; #endregion #region Public interface public override Point GetPoint() { Rectangle r = this.RectangleLocator.GetRectangle(); Point pt = this.CalculateProportionalPosition(r, this.PointProportions); return this.OffsetBy(pt); } #endregion protected override void InitializeSublocators() { this.InitializeLocator(this.RectangleLocator); } #region Calculations protected Point CalculateProportionalPosition(Rectangle r, SizeF proportions) { return new Point( r.X + (int)(r.Width * proportions.Width), r.Y + (int)(r.Height * proportions.Height)); } protected SizeF ConvertCornerToProportion(Corner corner) { switch (corner) { case Corner.TopLeft: return new SizeF(0.0f, 0.0f); case Corner.TopCenter: return new SizeF(0.5f, 0.0f); case Corner.TopRight: return new SizeF(1.0f, 0.0f); case Corner.MiddleLeft: return new SizeF(0.0f, 0.5f); case Corner.MiddleCenter: return new SizeF(0.5f, 0.5f); case Corner.MiddleRight: return new SizeF(1.0f, 0.5f); case Corner.BottomLeft: return new SizeF(0.0f, 1.0f); case Corner.BottomCenter: return new SizeF(0.5f, 1.0f); case Corner.BottomRight: return new SizeF(1.0f, 1.0f); } // Should never reach here return new SizeF(0.0f, 0.0f); } #endregion } /// /// An DifferenceLocator is simply the difference between /// two point locators /// /// I can't think of a case where this would actually /// be useful. It might disappear. JPP 2010/02/05 public class DifferenceLocator : AbstractPointLocator { #region Life and death public DifferenceLocator(IPointLocator locator1, IPointLocator locator2) : this(locator1, locator2, Point.Empty) { } public DifferenceLocator(IPointLocator locator1, IPointLocator locator2, Point offset) { this.Locator1 = locator1; this.Locator2 = locator2; this.Offset = offset; } #endregion #region Configuration properties protected IPointLocator Locator1 ; protected IPointLocator Locator2 ; #endregion #region Public interface public override Point GetPoint() { Point pt1 = this.Locator1.GetPoint(); Point pt2 = this.Locator2.GetPoint(); return this.OffsetBy(new Point(pt1.X - pt2.X, pt1.Y - pt2.Y)); } #endregion protected override void InitializeSublocators() { this.InitializeLocator(this.Locator1); this.InitializeLocator(this.Locator2); } } /// /// Instances of this locator return the location that a sprite must /// move to so that one of its points (SpritePointLocator) is directly /// over another point (ReferencePointLocator). /// public class AlignedSpriteLocator : AbstractPointLocator { #region Life and death public AlignedSpriteLocator() { } public AlignedSpriteLocator(IPointLocator referencePointLocator, IPointLocator spritePointLocator) : this(referencePointLocator, spritePointLocator, Point.Empty) { } public AlignedSpriteLocator(IPointLocator referencePointLocator, IPointLocator spritePointLocator, Point offset) { this.ReferencePointLocator = referencePointLocator; this.SpritePointLocator = spritePointLocator; this.Offset = offset; } #endregion #region Configuration properties protected IPointLocator ReferencePointLocator ; protected IPointLocator SpritePointLocator ; #endregion #region Public interface public override Point GetPoint() { Point location = this.Sprite.Location; Point spritePoint = this.SpritePointLocator.GetPoint(); spritePoint.Offset(-location.X, -location.Y); Point referencePoint = this.ReferencePointLocator.GetPoint(); referencePoint.Offset(-spritePoint.X, -spritePoint.Y); return this.OffsetBy(referencePoint); } #endregion #region Initializations protected override void InitializeSublocators() { this.InitializeLocator(this.ReferencePointLocator); this.InitializeLocator(this.SpritePointLocator); } #endregion } /// /// A BaseWalker provides useful methods to classes that walk between /// a series of point. /// public class BaseWalker : AbstractPointLocator { public float WalkProgress ; /// /// Walk the given distance between the given points, starting at pt. /// Return the point where the walk ends. The walk will loop through /// the point multiple times if necessary to exhaust the distance. /// /// How far to walk? /// Where to start. /// The control points in order /// The point where the walk ends protected Point Walk(int distance, Point pt, Point[] targetPoints) { int i = 0; double remaining = distance; while (true) { double distanceToPoint = this.CalculateDistance(pt, targetPoints[i]); if (remaining <= distanceToPoint) return this.CalculateEndPoint(pt, targetPoints[i], (int)remaining); pt = targetPoints[i]; remaining -= distanceToPoint; i = (i + 1) % targetPoints.Length; } } /// /// Given a start and end points, calculate the point that is /// distance along that line. /// /// Line start point /// Line end point /// Distance from start /// The point that is 'distance' from the start protected Point CalculateEndPoint(Point pt1, Point pt2, int distance) { int dx = pt2.X - pt1.X; int dy = pt2.Y - pt1.Y; if (dx == 0) return new Point(pt1.X, pt1.Y + (distance * Math.Sign(dy))); if (dy == 0) return new Point(pt1.X + (distance * Math.Sign(dx)), pt1.Y); double inverseMagnitude = 1.0 / Math.Sqrt((dx * dx) + (dy * dy)); return new Point( pt1.X + (int)(dx * inverseMagnitude * distance), pt1.Y + (int)(dy * inverseMagnitude * distance)); } /// /// Calculate the distance between the two points /// /// /// /// protected double CalculateDistance(Point pt1, Point pt2) { int dx = pt1.X - pt2.X; int dy = pt1.Y - pt2.Y; if (dx == 0) return Math.Abs(dy); if (dy == 0) return Math.Abs(dx); return Math.Sqrt((dx * dx) + (dy * dy)); } } /// /// A PointWalker generates points as if walking between a series of control points. /// The progress through the walk is controlled by the WalkProgress property. /// public class PointWalker : BaseWalker { #region Life and death public PointWalker() { this.Locators = new List(); } public PointWalker(IEnumerable points) { List locators = new List(); foreach (Point pt in points) locators.Add(new FixedPointLocator(pt)); this.Locators = locators; } public PointWalker(IEnumerable points, Point offset) : this(points) { this.Offset = offset; } public PointWalker(IEnumerable locators) { this.Locators = locators; } public PointWalker(IEnumerable locators, Point offset) : this(locators) { this.Offset = offset; } #endregion #region Configuration properties protected IEnumerable Locators ; #endregion #region Public interface public override Point GetPoint() { Point[] points = this.GetPoints(); int totalDistance = this.CalculateTotalDistance(points); return this.OffsetBy(this.Walk((int)(totalDistance * this.WalkProgress), points[0], points)); } #endregion #region Initializations protected override void InitializeSublocators() { foreach (IPointLocator locator in this.Locators) this.InitializeLocator(locator); } #endregion #region Calculations private Point[] GetPoints() { List points = new List(); foreach (IPointLocator locator in this.Locators) points.Add(locator.GetPoint()); return points.ToArray(); } private int CalculateTotalDistance(Point[] points) { double distance = 0; for (int i = 1; i < points.Length; i++) distance += this.CalculateDistance(points[i - 1], points[i]); return (int)Math.Ceiling(distance + 1); } #endregion } /// /// Instances of this locator return points on the perimeter of /// a rectangle. /// public class RectangleWalker : BaseWalker { #region Life and death public RectangleWalker() { } public RectangleWalker(IRectangleLocator rectangleLocator) : this(rectangleLocator, WalkDirection.Clockwise) { } public RectangleWalker(IRectangleLocator rectangleLocator, WalkDirection direction) : this(rectangleLocator, direction, null) { } public RectangleWalker(IRectangleLocator rectangleLocator, WalkDirection direction, IPointLocator startPointLocator) { this.RectangleLocator = rectangleLocator; this.Direction = direction; this.StartPointLocator = startPointLocator; } #endregion #region Configuration properties public IRectangleLocator RectangleLocator ; public IPointLocator StartPointLocator ; public WalkDirection Direction ; #endregion #region Public interface public override Point GetPoint() { Rectangle r = this.RectangleLocator.GetRectangle(); int totalLength = r.Width * 2 + r.Height * 2; int distanceToWalk = (int)(totalLength * this.WalkProgress); Point pt = this.RationalizeStartPoint(r, this.StartPointLocator.GetPoint()); int segment = this.DecideSegment(r, pt); Point[] targetPoints = new Point[] {}; if (this.Direction == WalkDirection.Clockwise) { switch (segment) { case 0: targetPoints = new Point[] { r.Location, new Point(r.Right, r.Top), new Point(r.Right, r.Bottom), new Point(r.Left, r.Bottom) }; break; case 1: targetPoints = new Point[] { new Point(r.Right, r.Top), new Point(r.Right, r.Bottom), new Point(r.Left, r.Bottom), r.Location, }; break; case 2: targetPoints = new Point[] { new Point(r.Right, r.Bottom), new Point(r.Left, r.Bottom), r.Location, new Point(r.Right, r.Top) }; break; case 3: targetPoints = new Point[] { new Point(r.Left, r.Bottom), r.Location, new Point(r.Right, r.Top), new Point(r.Right, r.Bottom) }; break; } } else { switch (segment) { case 0: targetPoints = new Point[] { new Point(r.Left, r.Bottom), new Point(r.Right, r.Bottom), new Point(r.Right, r.Top), new Point(r.Left, r.Top) }; break; case 1: targetPoints = new Point[] { new Point(r.Right, r.Bottom), new Point(r.Right, r.Top), new Point(r.Left, r.Top), new Point(r.Left, r.Bottom) }; break; case 2: targetPoints = new Point[] { new Point(r.Right, r.Top), new Point(r.Left, r.Top), new Point(r.Left, r.Bottom), new Point(r.Right, r.Bottom) }; break; case 3: targetPoints = new Point[] { new Point(r.Left, r.Top), new Point(r.Left, r.Bottom), new Point(r.Right, r.Bottom), new Point(r.Right, r.Top) }; break; } } return this.OffsetBy(this.Walk(distanceToWalk, pt, targetPoints)); } protected Point RationalizeStartPoint(Rectangle r, Point point) { if (point == Point.Empty) return r.Location; return new Point( Math.Min(r.Right, Math.Max(r.Left, point.X)), Math.Min(r.Bottom, Math.Max(r.Top, point.Y))); } protected int DecideSegment(Rectangle rectangleToWalk, Point point) { // Segments (numbers are clockwise): // 0 - left // 1 - top // 2 - right // 3 - bottom if (rectangleToWalk.Location == point) { return this.Direction == WalkDirection.Clockwise ? 1 : 0; } if (point.X == rectangleToWalk.Left) return 0; if (point.Y == rectangleToWalk.Top) return 1; if (point.X == rectangleToWalk.Right) return 2; if (point.X == rectangleToWalk.Bottom) return 3; // Point not on perimeter //TODO: Do something clever here return this.Direction == WalkDirection.Clockwise ? 1 : 0; } #endregion #region Initializations protected override void InitializeSublocators() { this.InitializeLocator(this.RectangleLocator); this.InitializeLocator(this.StartPointLocator); } #endregion } }