An L-System engine and turtle-based renderer.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

201 lines
4.3 KiB

using System;
using Cairo;
using System.Text.RegularExpressions;
using System.Resources;
using System.Collections.Generic;
namespace SBRL.SimpleTurtle
{
public class TurtleState : ICloneable {
public PointD Position;
public double Heading;
public TurtleState(PointD inPosition, double inHeading) {
Position = inPosition;
Heading = inHeading;
}
public object Clone() {
return new TurtleState(Position, Heading);
}
}
public class Turtle
{
public bool StrictMode = false;
private string commandQueue;
private readonly Stack<TurtleState> states = new Stack<TurtleState>();
public PointD Position {
get => states.Peek().Position;
protected set => states.Peek().Position = value;
}
public double Heading {
get => states.Peek().Heading;
protected set => states.Peek().Heading = value;
}
private Area bounds;
public double HeadingStep { get; set; }
public double MovementStep { get; set; }
public Turtle () {
Reset();
}
public void ApplyDefinitions(Dictionary<string, string> definitions)
{
foreach(KeyValuePair<string, string> definition in definitions)
{
switch(definition.Key.ToLower())
{
case "angle":
HeadingStep = double.Parse(definition.Value);
break;
}
}
}
public bool Commands(string commandText)
{
// Remove all whitespace
commandText = Regex.Replace(commandText, @"\s+", "");
commandText = commandText.Replace("h", "f");
string okCommands = "f+-";
foreach(char ch in commandText)
{
if(okCommands.Contains(ch.ToString()))
{
switch(ch)
{
case 'f':
Forwards();
break;
case '+':
Turn(false);
break;
case '-':
Turn(true);
break;
case '[':
Save();
break;
case ']':
Restore();
break;
default:
if (StrictMode) {
Console.WriteLine("The unexpected character '{0}' slipped through the net!", ch);
return false;
}
break;
}
}
else if(StrictMode)
{
Console.Error.WriteLine("Error: unexpected character '{0}'", ch);
return false;
}
}
commandQueue += commandText;
return true;
}
public void Draw(string filename, bool reset = true)
{
ImageSurface canvas = new ImageSurface(Format.ARGB32, (int)Math.Ceiling(bounds.Width + 10), (int)Math.Ceiling(bounds.Height + 10));
Context context = new Context(canvas);
Position = new PointD(-bounds.X + 5, -bounds.Y + 5);
Heading = 0;
context.LineWidth = 3;
context.MoveTo(Position);
foreach(char ch in commandQueue)
{
switch(ch)
{
case 'f':
PointD newPosition = new PointD(
Position.X + MovementStep * Math.Sin(Heading),
Position.Y + MovementStep * Math.Cos(Heading)
);
context.LineTo(newPosition);
Position = newPosition;
break;
case '+':
Heading += HeadingStep;
break;
case '-':
Heading -= HeadingStep;
break;
}
}
context.Stroke();
canvas.WriteToPng(string.Format(filename));
context.Dispose();
canvas.Dispose();
if(reset)
Reset();
}
public void Forwards()
{
PointD newPosition = new PointD(
Position.X + MovementStep * Math.Sin(Heading),
Position.Y + MovementStep * Math.Cos(Heading)
);
if (newPosition.X > bounds.X + bounds.Width)
bounds.Width += newPosition.X - Position.X;
if (newPosition.Y > bounds.Y + bounds.Height)
bounds.Height += newPosition.Y - Position.Y;
if (newPosition.X < bounds.X)
{
bounds.X = newPosition.X;
bounds.Width += Position.X - newPosition.X;
}
if (newPosition.Y < bounds.Y)
{
bounds.Y = newPosition.Y;
bounds.Height += Position.Y - newPosition.Y;
}
Position = newPosition;
}
public void Turn(bool anticlockwise = false)
{
if (!anticlockwise)
Heading += HeadingStep;
else
Heading -= HeadingStep;
}
public void Save()
{
states.Push(states.Peek().Clone() as TurtleState);
}
public bool Restore() {
if (states.Count <= 1)
return false;
states.Pop();
return true;
}
public void Reset()
{
states.Clear();
states.Push(new TurtleState(new PointD(0, 0), 0));
commandQueue = string.Empty;
bounds = new Area(Position.X, Position.Y, 1, 1);
HeadingStep = Math.PI / 2;
MovementStep = 25;
}
}
}