W2513 Emergence & Lindenmayer Systems (Part 3)

From Coder Merlin
Within these castle walls be forged Mavens of Computer Science ...
— Merlin, The Coder
Serpinski Lsystem

Prerequisites[edit]

Research[edit]

Experiment[edit]

Getting Started[edit]

Breathe-document-new

Begin a new project


Create an Igis shell project within your "project" directory.

cd ~/projects
git clone https://github.com/TheCoderMerlin/IgisShellD IgisShell-LSystems

Enter into the Sources directory of the new project.

cd IgisShell-LSystems/Sources/IgisShellD/

Build the project. (This may take some time.)

./make.sh
Start button green arrow

Run the project.


./run.sh

Open a browser (or use a new tab on an already-open browser). Go to the URL: http://www.codermerlin.com/users/user-name/dyn/index.html

NOTE: You MUST change user-name to your actual user name. For example, http://www.codermerlin.com/users/john-williams/dyn/index.html

You'll know you're successful if you see the title bar change to "Coder Merlin: IGIS". (The browser window will be blank because we haven't added any graphics yet.)

Oxygen480-actions-help-hint.svg Helpful hint: It's useful to bookmark this page in your browser.

First Steps[edit]

Add your LSystem File[edit]

Add your LSystem file from your previous project to your current project.

Warning icon.svg Warning: Be careful to rename this file during the copy operation so that you don't overwrite the current main.swift.

Assuming that your previous project is in the directory ~/Merlin/2512 CS-II Lindenmayer Systems/100 Lindenmayer/C100 LSystem-Swift:

cp ~/Merlin/2512\ CS-II\ Lindenmayer\ Systems/100\ Lindenmayer/C100\ LSystem-Swift/main.swift LSystem.swift

Remove LSystem Global Functions[edit]

Remove your global functions and variables from LSystem.swift, leaving only the classes (e.g. class ProductionRule, class LSystem) which you've defined. Then, ensure that you're able to successfully compile.

./run.sh

Be sure that you're able to successfully compile before continuing.

Geometric Structures[edit]

Recall from your previous lab that Lindenmayer Systems are defined by G = (V, ω, P) along with a mechanism to translate the generated strings into geometric structures. This lab will focus on this mechanism.

Getting Ready to Use the Turtle[edit]

Edit file "main.swift":

emacs main.swift

Edit the file by finding the definition of the Painter class. Before the init constructor, add the following:

    var didPaint = false

This will enable us to keep track of whether or not we need to paint.

In order to use the turtle, we need to ensure that we know the size of the canvas in our render method. Add the following text below the setup method:

    override func render(canvas:Canvas) {
        if let canvasSize = canvas.canvasSize, !didPaint {

            didPaint = true
        }
    }

The render method is an event handler which is invoked periodically. The if let conditional provides us with syntactic sugar which assigns canvas.canvasSize to a new, local variable if (and only if) canvas.canvasSize is not nil and we haven't yet painted. Note that canvas.canvasSize will be nil until the browser has calculated and reported the size of the canvas.

Creating the Turtle[edit]

Add a statement to create a new turtle. Remember that the turtle will be created in its home position, at the center of the screen, and facing north.

    override func render(canvas:Canvas) {
        if let canvasSize = canvas.canvasSize, !didPaint {
            let turtle = Turtle(canvasSize:canvasSize)

            didPaint = true
        }
    }

Painting the Koch Curve[edit]

Let's add a function to paint a Koch Curve. Place this function somewhere within the Painter class.

   func moveTurtleForKochCurve(turtle:Turtle, generationCount:Int, steps:Int) {
        // Create the LSystem
        let alphabet = Set<Character>(["F", "+", "-"])
        let axiom = "F"
        let productionRules = [ProductionRule(predecessor:"F", successor:"F+F-F-F+F")]

        let lSystem = LSystem(alphabet:alphabet, axiom:axiom, productionRules:productionRules)
        let production = lSystem.produce(generationCount:generationCount)

        // Start in a good direction
        turtle.right(degrees:90)

        // Map the LSystem to turtle graphics
        for letter in production {
            switch (letter) {
            case "F":
                turtle.forward(steps:steps)
            case "+":
                turtle.left(degrees:90)
            case "-":
                turtle.right(degrees:90)
            default:
                fatalError("Unexepected letter '\(letter)' in production.")
            }
        }
    }

We've defined the function but haven't invoked it from anywhere. Let's do that now from the render method. Modify the method so that it appears as:

    override func render(canvas:Canvas) {
        if let canvasSize = canvas.canvasSize, !didPaint {
            let turtle = Turtle(canvasSize:canvasSize)

            moveTurtleForKochCurve(turtle:turtle, generationCount:3, steps:20)
            canvas.render(turtle)

            didPaint = true
        }
    }
Start button green arrow

Run the project.


Buttons to Alter Parameters[edit]

We can enable the ability to alter parameters for our L-Systems by providing "buttons" in the UI. We'll start with two buttons: one to increase the generation count and one to decrease the generation count. We can create a single class, Button, to implement the required functionality and create two instances of this class.

In a separate file named "Button.swift", add the following text:

import Igis

class Button {

    public let rectangle : Rectangle
    public let text : Text
    public let buttonStrokeStyle : StrokeStyle
    public let buttonFillStyle : FillStyle
    public let fontFillStyle : FillStyle

    init(topLeft:Point, size:Size, buttonStrokeStyle:StrokeStyle, buttonFillStyle:FillStyle,
         textOffset:Point, label:String, font:String, fontFillStyle:FillStyle) {
        // Form the shape of the button
        let rect = Rect(topLeft:topLeft, size:size)
        rectangle = Rectangle(rect:rect, fillMode:.fillAndStroke)
        self.buttonStrokeStyle = buttonStrokeStyle
        self.buttonFillStyle = buttonFillStyle
        self.fontFillStyle = fontFillStyle

        // Form the label for the button
        let textLocation = Point(x:topLeft.x+textOffset.x, y:topLeft.y+textOffset.y)
        text = Text(location:textLocation, text:label)
        text.font = font
    }

    func paint(canvas:Canvas) {
        canvas.render(buttonStrokeStyle, buttonFillStyle, rectangle, fontFillStyle, text)
    }
}

Before continuing, be sure that you thoroughly understand the above code.

Emblem-question-green.svg Question: In the paint method, does the ordering of the items to be rendered matter?

In order to group the buttons together and provide additional functionality, let's create a container class called ControlPanel.

In a separate file named "ControlPanel.swift", add the following text:

import Igis

class ControlPanel {

    let increaseGenerationButton : Button
    let decreaseGenerationButton : Button

    init() {
        let buttonTopLeft = Point(x:10, y:10)
        let buttonSize = Size(width:130, height:25)
        let buttonMargin = 20
        let buttonFont = "16pt Arial"

        increaseGenerationButton  = Button(topLeft:buttonTopLeft, size:buttonSize,
                                           buttonStrokeStyle:StrokeStyle(color:Color(.darkgreen)), buttonFillStyle:FillStyle(color:Color(.lightgreen)),
                                           textOffset:Point(x:2, y:20), label:"+ Generation", font:buttonFont,
                                           fontFillStyle:FillStyle(color:Color(.black)))

        decreaseGenerationButton  = Button(topLeft:Point(x:buttonTopLeft.x+buttonSize.width+buttonMargin, y:buttonTopLeft.y), size:buttonSize,
                                           buttonStrokeStyle:StrokeStyle(color:Color(.darkgreen)), buttonFillStyle:FillStyle(color:Color(.lightgreen)),
                                           textOffset:Point(x:2, y:20), label:"- Generation", font:buttonFont,
                                           fontFillStyle:FillStyle(color:Color(.black)))
    }

    func paint(canvas:Canvas) {
        increaseGenerationButton.paint(canvas:canvas)
        decreaseGenerationButton.paint(canvas:canvas)
    }
}

Note how we judiciously make use of constants to layout our buttons.

Finally, let's create the control panel and paint our controls. Add a property to the Painter class, immediately above the didPaint property:

let controlPanel = ControlPanel()
var didPaint = false


Modify the render method in main.swift as follows:

    override func render(canvas:Canvas) {
        if let canvasSize = canvas.canvasSize, !didPaint {
            let turtle = Turtle(canvasSize:canvasSize)

            controlPanel.paint(canvas:canvas)

            moveTurtleForKochCurve(turtle:turtle, generationCount:3, steps:20)
            canvas.render(turtle)

            didPaint = true
        }
    }
Start button green arrow

Run the project.


You should see the Koch curve along with two buttons. Click on the buttons. What happens? What did you expect to happen?

Add Button Functionality[edit]

The process of determining whether a user-controlled cursor, such as a mouse or a touch-point, intersects an on-screen, graphical object is termed hit-testing. We'll use hit-testing to determine if our buttons have been "pressed". Because our buttons are rectangular, the algorithm to determine a test is straight-forward.

Add the following method to the Button class:

    func hitTest(location:Point) -> Bool {
        let xRange = rectangle.rect.topLeft.x ..< rectangle.rect.topLeft.x+rectangle.rect.size.width
        let yRange = rectangle.rect.topLeft.y ..< rectangle.rect.topLeft.y+rectangle.rect.size.height
        return xRange.contains(location.x) && yRange.contains(location.y)
    }

Add the following method to the ControlPanel class: Add the following property:

    let allButtons : [Button]

Update the constructor to initialize the property (after the buttons have been created):

    allButtons = [increaseGenerationButton, decreaseGenerationButton]

Add the following method:

    func hitTest(location:Point) -> Button? {
        let hitButtons = allButtons.filter {$0.hitTest(location:location)}
        return hitButtons.first
    }

Add the following method to the Painter class:

    override func onClick(location:Point) {
        if let button = controlPanel.hitTest(location:location) {
            print(button.text.text)
        }
    }
Start button green arrow

Run the project.


Click on each of the buttons while watching the console output. What do you observe? Be sure that you thoroughly understand each of the above methods.

Let's refactor the ControlPanel a bit so that we're using static constants for the button labels. We'll use a static constant because the constant will be the same for all instances. Using a constant will allow us to make comparisons to the label to determine which button was pressed and respond appropriately without the risk of misspelling the label. Update the ControlPanel class as follows:

First, add the static constants:

    static let increaseGenerationLabel : String = "+ Generation" 
    static let decreaseGenerationLabel : String = "- Generation"

Then, use these constants when creating the buttons. Note that we need to reference the enclosing object's name (in this case ControlPanel) to inform the compiler that we're referencing static constants rather than instance constants.

        increaseGenerationButton  = Button(topLeft:buttonTopLeft, size:buttonSize,
                                           buttonStrokeStyle:StrokeStyle(color:Color(.darkgreen)), buttonFillStyle:FillStyle(color:Color(.lightgreen)),
                                           textOffset:Point(x:2, y:20), label:ControlPanel.increaseGenerationLabel, font:buttonFont,
                                           fontFillStyle:FillStyle(color:Color(.black)))

        decreaseGenerationButton  = Button(topLeft:Point(x:buttonTopLeft.x+buttonSize.width+buttonMargin, y:buttonTopLeft.y), size:buttonSize,
                                           buttonStrokeStyle:StrokeStyle(color:Color(.darkgreen)), buttonFillStyle:FillStyle(color:Color(.lightgreen)),
                                           textOffset:Point(x:2, y:20), label:ControlPanel.decreaseGenerationLabel, font:buttonFont,
                                           fontFillStyle:FillStyle(color:Color(.black)))

Add a property to the Painter class to keep track of the generation count:

var generationCount = 1

Modify the render method to use this property:

   override func render(canvas:Canvas) {
        if let canvasSize = canvas.canvasSize, !didPaint {
            let turtle = Turtle(canvasSize:canvasSize)

            controlPanel.paint(canvas:canvas)

            moveTurtleForKochCurve(turtle:turtle, generationCount:generationCount, steps:20)
            canvas.paint(turtle)

            didPaint = true
        }
    }

Finally, modify the onClick method as follows:

    override func onClick(location:Point) {
        if let button = controlPanel.hitTest(location:location) {
            let label = button.text.text
            switch (label) {
            case ControlPanel.increaseGenerationLabel:
                generationCount += 1
                didPaint = false
            case ControlPanel.decreaseGenerationLabel:
                generationCount -= 1
                didPaint = false
            default:
                fatalError("Unexpected button label: \(label)")
            }
        }
    }
Start button green arrow

Run the project.


Does the application behave as you expected? Why or why not?

Exercises[edit]

  1. Clear the canvas before painting so that the previous painting is erased
  2. Add logic to limit the generation count to reasonable values
  3. Add at least two additional L-Systems (that do not require saving/restoring the Turtle's state) and buttons to cycle through the current system being drawn
  4. Add buttons to control the number of steps used by the turtle for the current system being drawn
  5. Add buttons to cycle through at least five of your favorite colors

Supplemental Exercises[edit]

  1. Reposition the initial turtle position to a logical location based on the L-System presented. (For example, if the system grows up and to the right, a reasonable starting position would be the bottom-left of the canvas.)

Key Concepts[edit]