// File: Lindenmayer.cs // Created by: Beau Skinner // Creation date: Nov 19, 2003 using System; using System.IO; using System.Collections; namespace Lindenmayer { /// /// Encapsulates the set of transformation rules for a Lindenmayer grammar. /// public class Rules { #region Inner Classes /// /// An enumerator class for the Rules class. /// public class RuleEnumerator { #region Constructors /// /// Constructor. Sets index one left of first rule. /// /// Rules object to enumerate. public RuleEnumerator(Rules rules) { m_rules = rules; m_idx = -1; } #endregion #region Properties /// /// Accesses the rule in the set corresponding to the current index. /// public DictionaryEntry Current { get { return (DictionaryEntry)m_rules.m_rules[m_idx]; } } #endregion #region Public Members /// /// Moves index to next rule. /// /// False if and only if there are no more rules. public bool MoveNext() { m_idx++; return(m_idx < m_rules.m_rules.Count); } #endregion #region Fields int m_idx; Rules m_rules; #endregion } #endregion #region Constructors /// /// Constructor. Creates a blank set of rules. /// public Rules() { m_rules = new ArrayList(); } #endregion #region Public Members /// /// Adds a rule to the stored set. /// /// Left side of rule. /// Right side of rule. public void Add(string key, string val) { m_rules.Add(new DictionaryEntry(key, val)); } /// /// Returns an enumerator for this Rules object. /// /// Enumerator for this Rules object. public RuleEnumerator GetEnumerator() { return new RuleEnumerator(this); } #endregion #region Fields private ArrayList m_rules; #endregion } /// /// Generates PostScript files that are images of consecutive iterations of /// a Lindenmayer fractal described by a given Lindenmayer grammar. /// public class Lindenmayer { #region Inner Classes /// /// Encapsulates a Point as an x and y coordinate. /// private class Point { #region Constructors /// /// Constructor. Sets Point to (0,0). /// public Point() { m_x = m_y = 0; } /// /// Constructor. Sets Point to given coordinates. /// /// x coordinate. /// y coordinate. public Point(double x, double y) { m_x = x; m_y = y; } #endregion #region Properties /// /// Accesses the x coordinate of the Point. /// public double x { get { return m_x; } set { m_x = value; } } /// /// Accesses the y coordinate of the Point. /// public double y { get { return m_y; } set { m_y = value; } } #endregion #region Fields double m_x, m_y; #endregion } #endregion #region Constructors /// /// Constructor. Sets fractal name and grammar according to given values. /// /// Name of fractal (used in naming the PostScript files). /// Initial axiom. /// Initial heading. /// Rotation unit. /// Set of transformation rules. public Lindenmayer(string name, string seed, double bearing, double angle, Rules rules) { m_name = name; m_movement = seed; m_initBearing = bearing % 360; m_angle = angle % 360; m_rules = rules; // Tracks the number of iterations that have been formed. Used in naming the PostScript files. m_iteration = 1; // Filename for temporary output. m_tempFile = "linden.tmp"; // Width and height of standard PostScript page in pixels. m_pageWidth = 72*8.5; m_pageHeight = 72*11; Init(); } #endregion #region Properties /// /// Accesses the name of the fractal. /// public string Name { get { return m_name; } } #endregion #region Public Members /// /// Applies the transformation rules to the current axiom to produce the axiom for the next /// iteration. /// public void Iterate() { // Loop through each rule and replace occurrences of the left side in the current axiom // with the right side. Parts of the axiom that are between a slash and backslash are // not modified, as those are the parts that have already been replaced by another // rule. foreach (DictionaryEntry rule in m_rules) { string key = rule.Key.ToString(); string val = rule.Value.ToString(); int fSlash = m_movement.IndexOf('/'); int bSlash = -1; while (fSlash >= 0) { Replace(key, val, bSlash+1, fSlash-1); bSlash = m_movement.IndexOf('\\', fSlash+1); fSlash = m_movement.IndexOf('/', bSlash+1); } Replace(key, val, bSlash+1, m_movement.Length-1); } // Remove any slashes and backslashes from the axiom. m_movement = m_movement.Replace("/", "").Replace("\\", ""); m_iteration++; } /// /// Outputs the current axiom to a PostScript file as a fractal image. /// public void Output() { Init(); OpenTempOutput(); Start(); // moveto the current turtle position to start the image. PSGoTo(false); // Keep track of the total number of linetos and movetos. int length = 1; // Loop through each character in the current axiom string. foreach (char c in m_movement) { switch (c) { // F or G means move forward with pen down. case 'F': case 'G': Move(true); length++; break; // f or g means move forward with pen up. case 'f': case 'g': Move(false); length++; break; // + means rotate one unit counter-clockwise. case '+': Turn(1); break; // - means rotate one unit clockwise, // or rotate -1 units counter-clockwise. case '-': Turn(-1); break; // [ means store the current position. case '[': SavePosition(); break; // ] means return to the most recently remembered position. case ']': RestorePosition(); length++; break; default: break; } // If the path has exceeded the threshold length, end it and // start a new one. if (length % m_threshold == 0) { Refresh(); length++; } } End(); CloseOutput(); // If any movements were made, output them in a PostScript file. if (length > 1) { OpenOutput(); WriteBoundaries(); CopyOutput(); CloseOutput(); } } #endregion #region Private Members /// /// Inititalizes values to prepare for outputting current axiom as a PostSript image. /// private void Init() { m_bearing = m_initBearing; m_storedPositions = new Stack(); m_storedBearings = new Stack(); m_position = new Point(); m_min = new Point(); m_max = new Point(); // Maximum path length (movetos and linetos). m_threshold = 1000; } /// /// Replaces occurrences of a given key in a given substring of the current axiom /// with a given value. /// /// /// The value is surrounded by a slash on the left and a backslash on the right, to /// indicate that part of the axiom has already been replaced. /// /// Key to replace. /// Value to replace key with. /// Start index of substring in which to replace occurrences of the key. /// End index of substring in which to replace occurrences of the key. private void Replace(string key, string val, int start, int end) { m_movement = m_movement.Substring(0, start) + m_movement.Substring(start, end-start+1).Replace(key, "/" + val + "\\") + m_movement.Substring(end+1); } /// /// Opens the temporary file for output. If a file of the same name exists, it is erased. /// private void OpenTempOutput() { m_output = new StreamWriter(m_tempFile); } /// /// Opens and initializes PostScript file for output. If a file of the same name exists, it is erased. /// private void OpenOutput() { m_output = new StreamWriter(m_name + m_iteration + ".ps"); m_output.WriteLine("%!"); m_output.WriteLine(); } /// /// Copies the contents of the temporary file to the end of the PostScript file and erases /// the temporary file. /// private void CopyOutput() { m_input = new StreamReader(m_tempFile); string line; while ((line = m_input.ReadLine()) != null) { m_output.WriteLine(line); } m_input.Close(); File.Delete(m_tempFile); } /// /// Closes the PostScript file. /// private void CloseOutput() { m_output.Close(); } /// /// Starts a new PostScript path. /// private void Start() { m_output.WriteLine("newpath"); } /// /// Ends a PostScript path. /// private void End() { m_output.WriteLine("stroke"); m_output.WriteLine("closepath"); } /// /// Ends the current PostScript path and begins a new one at the same /// point that the previous path left off. /// private void Refresh() { End(); m_output.Flush(); Start(); // To start the new path, moveto the current turtle position. PSGoTo(false); } /// /// Writes scale, setlinewidth, and translate commands to the PostScript file /// according to the boundaries of the fractal. /// private void WriteBoundaries() { // According to the maximum x and y coordinates the turtle went to, calculate // the width and height of the fractal. These values are increased slightly // to allow a small space between the fractal and the page boundaries. double fractalWidth = (m_max.x - m_min.x) * 1.01; double fractalHeight = (m_max.y - m_min.y) * 1.01; // We scale the maximum amount that will keep the fractal from being too wide // or too tall. double xscale = m_pageWidth/fractalWidth; double yscale = m_pageHeight/fractalHeight; double scale = Math.Min(xscale, yscale); // We must translate the centre of the fractal to the centre of the page. // To do this, we subtract the current location of the fractal's centre from // the coordinates of the centre of the page. double xcentre = m_pageWidth/2/scale - (m_min.x + m_max.x)/2; double ycentre = m_pageHeight/2/scale - (m_min.y + m_max.y)/2; // Output the appropriate commands to the PostScript file. m_output.WriteLine("{0} dup scale", scale); m_output.WriteLine("1 {0} div setlinewidth", scale); m_output.WriteLine("{0} {1} translate", xcentre, ycentre); m_output.WriteLine(); } /// /// Moves the turtle forward one unit and outputs to the PostScript file. /// /// True will cause a line to be drawn. False will simply move the turtle. private void Move(bool draw) { UnitForward(); PSGoTo(draw); } /// /// Rotates the turtle counter-clockwise by some multiple of the rotation unit. /// /// Multiple of rotation unit by which the turtle is to be rotated. private void Turn(int mult) { m_bearing = (m_bearing + mult*m_angle) % 360; } /// /// Add the current turtle position to the top of the Stack of stored positions. /// Add the current turtle heading to the top of the Stack of stored headings. /// private void SavePosition() { Point tempPosition = new Point(m_position.x, m_position.y); m_storedPositions.Push(tempPosition); m_storedBearings.Push(m_bearing); } /// /// Remove the first item on the Stack of stored positions and move the turtle to that point. /// Remove the first item on the Stack of stored headings and rotate the turtle to that heading. /// private void RestorePosition() { m_position = (Point)m_storedPositions.Pop(); m_bearing = (double)m_storedBearings.Pop(); PSGoTo(false); } /// /// Outputs a lineto or moveto command to the PostScript file, moving to the current turtle position. /// /// True will output a lineto command. False will output a moveto command. private void PSGoTo(bool draw) { string command; if (draw) { command = "lineto"; } else { command = "moveto"; } m_output.WriteLine("{0} {1} {2}", m_position.x, m_position.y, command); UpdateBoundaries(); } /// /// Updates the fractal boundaries according to the boundaries found so far and the current turtle position. /// private void UpdateBoundaries() { // If the x coordinate is less than the smallest found so far, update the left boundary. m_min.x = Math.Min(m_min.x, m_position.x); // If the y coordinate is less than the smallest found so far, update the bottom boundary. m_min.y = Math.Min(m_min.y, m_position.y); // If the x coordinate is more than the largest found so far, update the right boundary. m_max.x = Math.Max(m_max.x, m_position.x); // If the y coordinate is more than the largest found so far, update the top boundary. m_max.y = Math.Max(m_max.y, m_position.y); } /// /// Moves the turtle to a point one unit forward in the direction of the current heading. /// private void UnitForward() { // Calculate the change in each coordinate required to move the turtle one unit forward // in the direction of its current heading. double dy = Math.Sin(ToRadians(m_bearing)); double dx = Math.Cos(ToRadians(m_bearing)); // Add these to the current position to move the turtle forward one unit. m_position.x += dx; m_position.y += dy; } /// /// Converts a given value in degrees into radians. /// /// Degrees to convert. /// Given degrees converted to radians. private double ToRadians(double degrees) { return degrees * Math.PI / 180; } #endregion #region Fields private string m_movement, m_name, m_tempFile; private double m_bearing, m_initBearing; private double m_angle; private Rules m_rules; private int m_iteration; private Stack m_storedPositions, m_storedBearings; private Point m_position, m_min, m_max; private StreamWriter m_output; private StreamReader m_input; private int m_threshold; private double m_pageWidth, m_pageHeight; #endregion } }