Difference between revisions of "W2513 Emergence & Lindenmayer Systems (Part 3)"

From Coder Merlin
 
(21 intermediate revisions by 2 users not shown)
Line 3: Line 3:
== Prerequisites ==
== Prerequisites ==
* [[W2512 Emergence & Lindenmayer Systems (Part 2)]]
* [[W2512 Emergence & Lindenmayer Systems (Part 2)]]
== Research ==


== Experiment ==
== Experiment ==


=== Getting Started ===
=== Getting Started ===
[[File:Breathe-document-new.svg|left|50px|link=|Breathe-document-new]]  Begin a '''new''' project
{{ScenesShellPrepare|LSystemScenes}}
 
 
 
Create an Igis shell project within your "project" directory.
<syntaxhighlight lang="bash">
cd ~/projects
git clone https://github.com/TheCoderMerlin/IgisShellD IgisShell-LSystems
</syntaxhighlight>
 
Enter into the Sources directory of the new project.
<syntaxhighlight lang="bash">
cd IgisShell-LSystems/Sources/IgisShellD/
</syntaxhighlight>
 
Build the project. (This may take some time.)
<syntaxhighlight lang="bash">
./make.sh
</syntaxhighlight>
 
[[File:Start button green arrow.svg|left|link=|Start button green arrow]] Run the project.
<br>
<syntaxhighlight lang="bash">
./run.sh
</syntaxhighlight>
 
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.)
 
{{notice|[[File:Oxygen480-actions-help-hint.svg|frameless|30px]]|Helpful hint:  It's useful to bookmark this page in your browser.}}


=== First Steps ===
=== First Steps ===
==== Add your LSystem File ====
==== Add your LSystem File ====
Add your LSystem file from your previous project to your current project.   
Add your LSystem file from your previous project to your current project.   
{{notice|[[File:Warning icon.svg|frameless|30px]]|Warning:  Be careful to '''rename''' this file during the copy operation so that you don't overwrite the current main.swift.}}
{{Caution|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''':
 
<syntaxhighlight lang="bash">
 
cp ~/Merlin/2512\ CS-II\ Lindenmayer\ Systems/100\ Lindenmayer/C100\ LSystem-Swift/main.swift LSystem.swift
Depending upon your directory structure, execute a command similar to:
</syntaxhighlight>
{{ConsoleLine|john-williams@codermerlin:~/Experiences$|cp ~/Merlin/M2512-10\ \(01\)\ LSystems/C100\ LSystems\ \[Swift\]/main.swift ~/Experiences/LSystemScenes/Sources/ScenesShell/LSystem.swift}}


==== Remove LSystem Global Functions ====
==== Remove LSystem Global Functions ====
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.
Remove your global functions and variables from {{Pathname|LSystem.swift}}, leaving only the classes (e.g. {{SwiftClass|ProductionRule}}, {{SwiftClass|ProductionRules}}, {{SwiftClass|LSystem}}) which you've defined.  Then, ensure that you're able to successfully compile.
<syntaxhighlight lang="bash">
 
./run.sh
{{ConsoleLine|john-williams@codermerlin:~/Experiences/LSystemScenes/Sources/ScenesShell$| build}}
</syntaxhighlight>
Be sure that you're able to successfully compile before continuing.


=== Geometric Structures ===
=== Geometric Structures ===
Line 64: Line 27:


==== Getting Ready to Use the Turtle ====
==== Getting Ready to Use the Turtle ====
Edit file "main.swift":
Edit the file {{Pathname|Background.swift}}:
<syntaxhighlight lang="bash">
emacs main.swift
</syntaxhighlight>


Edit the file by finding the definition of the '''Painter''' class.  Before the '''init''' constructor, add the following:
Before the {{SwiftIdentifier|init}} constructor, add the following:
<syntaxhighlight lang="swift">
<syntaxhighlight lang="swift">
     var didPaint = false
     private var didRender = false
</syntaxhighlight>
</syntaxhighlight>


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


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:
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:
<syntaxhighlight lang="swift">
<syntaxhighlight lang="swift">
     override func render(canvas:Canvas) {
     override func render(canvas:Canvas) {
         if let canvasSize = canvas.canvasSize, !didPaint {
         if let canvasSize = canvas.canvasSize, !didRender {


             didPaint = true
             didRender = true
         }
         }
     }
     }
</syntaxhighlight>
</syntaxhighlight>


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.
The {{SwiftIdentifier|render}} method is an event handler which is invoked periodically.  The <syntaxhighlight lang='swift' inline>if let</syntaxhighlight> conditional provides us with syntactic sugar which assigns {{SwiftIdentifier|canvas.canvasSize}} to a new, local variable if (and only if) {{SwiftIdentifier|canvas.canvasSize}} is not nil ''and'' we haven't yet rendered.  Note that {{SwiftIdentifier|canvas.canvasSize}} will be nil until the browser has calculated and reported the size of the canvas.


==== Creating the Turtle ====
==== Creating the Turtle ====
Line 92: Line 52:
<syntaxhighlight lang="swift" highlight="3">
<syntaxhighlight lang="swift" highlight="3">
     override func render(canvas:Canvas) {
     override func render(canvas:Canvas) {
         if let canvasSize = canvas.canvasSize, !didPaint {
         if let canvasSize = canvas.canvasSize, !didRender {
             let turtle = Turtle(canvasSize:canvasSize)
             let turtle = Turtle(canvasSize:canvasSize)


             didPaint = true
             didRender = true
         }
         }
     }
     }
</syntaxhighlight>
</syntaxhighlight>


==== Painting the Koch Curve ====
==== Rendering the Koch Curve ====
Let's add a function to paint a Koch Curve.  Place this function somewhere within the Painter class.   
Let's add a function to render a Koch Curve.  Place this function somewhere within the {{SwiftIdentifier|Background}} class.   
<syntaxhighlight lang="swift">
<syntaxhighlight lang="swift">
   func moveTurtleForKochCurve(turtle:Turtle, generationCount:Int, steps:Int) {
   func moveTurtleForKochCurve(turtle:Turtle, generationCount:Int, steps:Int) {
Line 134: Line 94:
<syntaxhighlight lang="swift" highlight="5-6">
<syntaxhighlight lang="swift" highlight="5-6">
     override func render(canvas:Canvas) {
     override func render(canvas:Canvas) {
         if let canvasSize = canvas.canvasSize, !didPaint {
         if let canvasSize = canvas.canvasSize, !didRender {
             let turtle = Turtle(canvasSize:canvasSize)
             let turtle = Turtle(canvasSize:canvasSize)


Line 140: Line 100:
             canvas.render(turtle)
             canvas.render(turtle)


             didPaint = true
             didRender = true
         }
         }
     }
     }
</syntaxhighlight>
</syntaxhighlight>


[[File:Start button green arrow.svg|left|link=|Start button green arrow]] Run the project.  
{{RunProgram|Run the program and refresh the browser page.}}
<br>


==== Buttons to Alter Parameters ====
==== Add Button Functionality ====
We can enable the ability to alter parameters for our L-Systems by providing "buttons" in the UIWe'll start with two buttons: one to increase the generation count and one to decrease the generation countWe can create a single class, '''Button''', to implement the required functionality and create two instances of this class.
We'll be using the library {{SwiftLibrary|ScenesControls}} in order to enable us to easily add buttons to our projectTo use this library:
===== Add the Library =====
Find the current version of the libraryYou can find the libraries at /usr/local/lib/merlin.  In this case:
{{ConsoleLine|ty-cam@codermerlin:~/Experiences/LSystemScenes/Sources/ScenesShell| ls -ld /usr/local/lib/merlin/ScenesControls*}}


In a '''separate''' file named "Button.swift", add the following text:
You'll see something similar to:
<syntaxhighlight lang="swift">
{{ConsoleLines|
import Igis
drwxr-xr-x 3 root root 4096 Mar 14 11:20 /usr/local/lib/merlin/{{Red|ScenesControls-0.1.0}}
 
}}
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,
In the above case, the most recent version of {{SwiftLibrary|ScenesControls}} is '''0.1.0'''.  Now, edit the file {{Pathname|dylib.manifest}} in the root of your project, adding the required library:
        textOffset:Point, label:String, font:String, fontFillStyle:FillStyle) {
{{ConsoleLines|
        // Form the shape of the button
Igis              1.3.7<br/>
        let rect = Rect(topLeft:topLeft, size:size)
Scenes            1.1.5<br/>
        rectangle = Rectangle(rect:rect, fillMode:.fillAndStroke)
{{Red|ScenesControls    0.1.0}}
        self.buttonStrokeStyle = buttonStrokeStyle
}}
        self.buttonFillStyle = buttonFillStyle
        self.fontFillStyle = fontFillStyle


        // Form the label for the button
{{Caution|Be sure that your file terminates with a newline character. You can verify this by typing '''dylib''' in your project root and ensuring that all specified libraries are listed.}}
        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) {
Finally, import the required libraries at the top of your {{Pathname|InteractionLayer.swift}}:
        canvas.render(buttonStrokeStyle, buttonFillStyle, rectangle, fontFillStyle, text)
<syntaxhighlight lang="swift" highlight="1,3">
    }
}
</syntaxhighlight>
Before continuing, be sure that you thoroughly understand the above code.
{{notice|[[File:Emblem-question-green.svg|frameless|30px]]|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:
<syntaxhighlight lang="swift">
import Igis
import Igis
 
import Scenes
class ControlPanel {
import ScenesControls
 
    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)
    }
}
</syntaxhighlight>
</syntaxhighlight>
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:
===== Implement Ability to Set GenerationCount =====
<syntaxhighlight lang="swift" highlight="1">
In {{Pathname|Background.swift}}, add a variable to track the current generation count (above {{SwiftIdentifier|init}}):
let controlPanel = ControlPanel()
var didPaint = false
</syntaxhighlight>


 
<syntaxhighlight lang="swift" highlight=2>
Modify the '''render''' method in main.swift as follows:
     private var didRender = false
<syntaxhighlight lang="swift" highlight="5-6">
    private var generationCount = 1
     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
        }
    }
</syntaxhighlight>
</syntaxhighlight>
[[File:Start button green arrow.svg|left|link=|Start button green arrow]] Run the project.
<br>


You should see the Koch curve along with two buttons.  Click on the buttons.  What happens?  What did you expect to happen?
Next, add a function to increment the generation count:


==== Add Button Functionality ====
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:
<syntaxhighlight lang="swift">
<syntaxhighlight lang="swift">
     func hitTest(location:Point) -> Bool {
     public func incrementGenerationCount() {
         let xRange = rectangle.rect.topLeft.x ..< rectangle.rect.topLeft.x+rectangle.rect.size.width
         generationCount += 1
         let yRange = rectangle.rect.topLeft.y ..< rectangle.rect.topLeft.y+rectangle.rect.size.height
         didRender = false
        return xRange.contains(location.x) && yRange.contains(location.y)
     }
     }
</syntaxhighlight>
</syntaxhighlight>


Add the following method to the '''ControlPanel''' class:
Also, change the {{SwiftIdentifier|moveTurtleForKochCurve}} function to use the {{SwiftIdentifier|generationCount}} property.
Add the following property:
<syntaxhighlight lang="swift">
    let allButtons : [Button]
</syntaxhighlight>
 
Update the constructor to initialize the property (after the buttons have been created):
<syntaxhighlight lang="swift">
    allButtons = [increaseGenerationButton, decreaseGenerationButton]
</syntaxhighlight>


Add the following method:
===== Create Button to Activate Functionality =====
In {{Pathname|InteractionLayer.swift}}, add the following function to provide access to our {{SwiftClass|Background}} object:
<syntaxhighlight lang="swift">
<syntaxhighlight lang="swift">
    func hitTest(location:Point) -> Button? {
      func background() -> Background {
        let hitButtons = allButtons.filter {$0.hitTest(location:location)}
          guard let mainScene = scene as? MainScene else {
        return hitButtons.first
              fatalError("mainScene of type MainScene is required")
    }
          }
</syntaxhighlight>
          let backgroundLayer = mainScene.backgroundLayer
 
          let background = backgroundLayer.background
Add the following method to the '''Painter''' class:
          return background
<syntaxhighlight lang="swift">
      }
    override func onClick(location:Point) {
        if let button = controlPanel.hitTest(location:location) {
            print(button.text.text)
        }
    }
</syntaxhighlight>
</syntaxhighlight>


[[File:Start button green arrow.svg|left|link=|Start button green arrow]] Run the project.
In the same file, add a '''handler''' which will be invoked when the user clicks the mouse on the button:
<br>
 
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:
<syntaxhighlight lang="swift">
<syntaxhighlight lang="swift">
    static let increaseGenerationLabel : String = "+ Generation"
      func onIncreaseGenerationButtonClickHandler(control: Control, localLocation: Point) {
    static let decreaseGenerationLabel : String = "- Generation"
          background().incrementGenerationCount()
      }
</syntaxhighlight>
</syntaxhighlight>


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.
Finally, again in the same file, create a button and link it to our handler in {{SwiftIdentifier|init}}:
<syntaxhighlight lang="swift" highlight="3,8">
<syntaxhighlight lang="swift" highlight="4-6">
        increaseGenerationButton  = Button(topLeft:buttonTopLeft, size:buttonSize,
      init() {
                                          buttonStrokeStyle:StrokeStyle(color:Color(.darkgreen)), buttonFillStyle:FillStyle(color:Color(.lightgreen)),
          super.init(name:"Interaction")
                                          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,
          let increaseGenerationButton = Button(name: "IncreaseGenCount", labelString: "Increase Generation", topLeft: Point(x: 50, y: 50))
                                          buttonStrokeStyle:StrokeStyle(color:Color(.darkgreen)), buttonFillStyle:FillStyle(color:Color(.lightgreen)),
          increaseGenerationButton.clickHandler = onIncreaseGenerationButtonClickHandler
                                          textOffset:Point(x:2, y:20), label:ControlPanel.decreaseGenerationLabel, font:buttonFont,
          insert(entity: increaseGenerationButton, at: .front)                                                                                                                                                                                                                                                                                                  
                                          fontFillStyle:FillStyle(color:Color(.black)))
      }
</syntaxhighlight>
</syntaxhighlight>


Add a property to the '''Painter''' class to keep track of the generation count:
{{RunProgram|Run the program and refresh the browser page.}}
<syntaxhighlight lang="swift">
var generationCount = 1
</syntaxhighlight>


Modify the '''render''' method to use this property:
Click on the button.  What do you observe? Be sure that you thoroughly understand each of the above methods.
<syntaxhighlight lang="swift" highlight="7">
  override func render(canvas:Canvas) {
        if let canvasSize = canvas.canvasSize, !didPaint {
            let turtle = Turtle(canvasSize:canvasSize)


            controlPanel.paint(canvas:canvas)
{{SeeAlso|
 
[[Category:Lindenmayer system||Recursion||Production rules]]|
            moveTurtleForKochCurve(turtle:turtle, generationCount:generationCount, steps:20)
}}
            canvas.paint(turtle)
 
            didPaint = true
        }
    }
</syntaxhighlight>
 
Finally, modify the '''onClick''' method as follows:
<syntaxhighlight lang="swift">
    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)")
            }
        }
    }
</syntaxhighlight>
 
[[File:Start button green arrow.svg|left|link=|Start button green arrow]] Run the project.
<br>
Does the application behave as you expected?  Why or why not?


== Exercises ==
== Exercises ==
# Clear the canvas before painting so that the previous painting is erased
{{W2513-Exercises}}
# Add logic to limit the generation count to reasonable values
# 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
# Add buttons to control the number of steps used by the turtle for the current system being drawn
# Add buttons to cycle through at least five of your favorite colors


=== Supplemental Exercises ===
== Key Concepts ==
# 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 ==
{{#set: Has primary media type=Text }}
[[Category:Lindenmayer system]]
[[Category:Recursion]]
[[Category:Production rules]]

Latest revision as of 23:00, 13 April 2021

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

Prerequisites[edit]

Experiment[edit]

Getting Started[edit]

Create a new Scenes shell project within your Experiences directory:

ty-cam@codermerlin:~$  cd ~/Experiences

ty-cam@codermerlin:~/Experiences$  git clone https://github.com/TheCoderMerlin/ScenesShellBasic LSystemScenes


Enter the Sources/ScenesShell directory of the new project:

ty-cam@codermerlin:~/Experiences$  cd LSystemScenes/Sources/ScenesShell/


Start button green arrow
Run the program.

ty-cam@codermerlin:~/Experiences/LSystemScenes/Sources/ScenesShell$  run


Ensure that you are logged on to the wiki. Then, click on the Tools menu followed by right-clicking on IGIS and selecting the menu item Open in New Window or Open in New Tab.

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.)

Hint.pngHelpful 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.

CautionWarnIcon.png
Be careful to rename this file during the copy operation so that you don't overwrite the current main.swift.


Depending upon your directory structure, execute a command similar to:

john-williams@codermerlin:~/Experiences$ cp ~/Merlin/M2512-10\ \(01\)\ LSystems/C100\ LSystems\ \[Swift\]/main.swift ~/Experiences/LSystemScenes/Sources/ScenesShell/LSystem.swift

Remove LSystem Global Functions[edit]

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

john-williams@codermerlin:~/Experiences/LSystemScenes/Sources/ScenesShell$  build

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 the file Background.swift:

Before the init constructor, add the following:

    private var didRender = false

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

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:

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

            didRender = 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 rendered. 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, !didRender {
            let turtle = Turtle(canvasSize:canvasSize)

            didRender = true
        }
    }

Rendering the Koch Curve[edit]

Let's add a function to render a Koch Curve. Place this function somewhere within the Background 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, !didRender {
            let turtle = Turtle(canvasSize:canvasSize)

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

            didRender = true
        }
    }
Start button green arrow
Run the program and refresh the browser page.

Add Button Functionality[edit]

We'll be using the library ScenesControls in order to enable us to easily add buttons to our project. To use this library:

Add the Library[edit]

Find the current version of the library. You can find the libraries at /usr/local/lib/merlin. In this case:

ty-cam@codermerlin:~/Experiences/LSystemScenes/Sources/ScenesShell  ls -ld /usr/local/lib/merlin/ScenesControls*

You'll see something similar to:

drwxr-xr-x 3 root root 4096 Mar 14 11:20 /usr/local/lib/merlin/ScenesControls-0.1.0

In the above case, the most recent version of ScenesControls is 0.1.0. Now, edit the file dylib.manifest in the root of your project, adding the required library:

Igis 1.3.7

Scenes 1.1.5

ScenesControls 0.1.0

CautionWarnIcon.png
Be sure that your file terminates with a newline character. You can verify this by typing dylib in your project root and ensuring that all specified libraries are listed.

Finally, import the required libraries at the top of your InteractionLayer.swift:

import Igis
import Scenes
import ScenesControls
Implement Ability to Set GenerationCount[edit]

In Background.swift, add a variable to track the current generation count (above init):

    private var didRender = false
    private var generationCount = 1

Next, add a function to increment the generation count:

    public func incrementGenerationCount() {
        generationCount += 1
        didRender = false
    }

Also, change the moveTurtleForKochCurve function to use the generationCount property.

Create Button to Activate Functionality[edit]

In InteractionLayer.swift, add the following function to provide access to our Background object:

      func background() -> Background {
          guard let mainScene = scene as? MainScene else {
              fatalError("mainScene of type MainScene is required")
          }
          let backgroundLayer = mainScene.backgroundLayer
          let background = backgroundLayer.background
          return background
      }

In the same file, add a handler which will be invoked when the user clicks the mouse on the button:

      func onIncreaseGenerationButtonClickHandler(control: Control, localLocation: Point) {
          background().incrementGenerationCount()
      }

Finally, again in the same file, create a button and link it to our handler in init:

      init() {
          super.init(name:"Interaction")

          let increaseGenerationButton = Button(name: "IncreaseGenCount", labelString: "Increase Generation", topLeft: Point(x: 50, y: 50))
          increaseGenerationButton.clickHandler = onIncreaseGenerationButtonClickHandler
          insert(entity: increaseGenerationButton, at: .front)                                                                                                                                                                                                                                                                                                    
      }
Start button green arrow
Run the program and refresh the browser page.

Click on the button. What do you observe? Be sure that you thoroughly understand each of the above methods.

👀 See Also[edit]

📺 Videos[edit]

Lindenmayer Systems and The Nature of Code
Procedural Plant Generation with L-Systems

📖 Texts[edit]

W2511 Emergence & Lindenmayer Systems (Part 1)
W2512 Emergence & Lindenmayer Systems (Part 2)
W2513 Emergence & Lindenmayer Systems (Part 3)
W2514 Emergence & Lindenmayer Systems (Part 4)

📚 References[edit]


Exercises[edit]

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

Supplemental Exercises

  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]