Types/Turtle/LSystem.ps1

<#
.SYNOPSIS
    Draws a L-system pattern.
.DESCRIPTION
    Generates a pattern using a L-system.

    The initial string (Axiom) is transformed according to the rules provided for a specified number of iterations.
.LINK
    https://en.wikipedia.org/wiki/L-system
.EXAMPLE
    # Box Fractal L-System
    $Box = 'F-F-F-F'
    $Fractal = 'F-F+F+F-F'
    
    $turtle.Clear().LSystem(
            $Box,
            [Ordered]@{ F = $Fractal },
            3,
            @{
                F = { $this.Forward(10) }
                J = { $this.Jump(10) }
                '\+' = { $this.Rotate(90) }
                '-' = { $this.Rotate(-90) }
            }
    ).Pattern.Save("$pwd/BoxFractalLSystem.svg")
.EXAMPLE
    # Fractal L-System
    $Box = 'FFFF-FFFF-FFFF-FFFF'
    $Fractal = 'F-F+F+F-F'
        
    $turtle.Clear().LSystem(
            $Box,
            [Ordered]@{ F = $Fractal },
            4,
            @{
                F = { $this.Forward(10) }
                J = { $this.Jump(10) }
                '\+' = { $this.Rotate(90) }
                '-' = { $this.Rotate(-90) }
            }
    ).Symbol.Save("$pwd/FractalLSystem.svg")
.EXAMPLE
    # Arrowhead Fractal L-System
    $Box = 'FF-FF-FF'
    $Fractal = 'F-F+F+F-F'
    
    
    $turtle.Clear().LSystem(
            $Box,
            [Ordered]@{ F = $Fractal },
            4,
            @{
                F = { $this.Forward(10) }
                J = { $this.Jump(10) }
                '\+' = { $this.Rotate(90) }
                '-' = { $this.Rotate(-90) }
            }
    ).Pattern.Save("$pwd/ArrowheadFractalLSystem.svg")
.EXAMPLE
    # Tetroid LSystem
    $turtle.Clear().LSystem(
            'F',
            [Ordered]@{ F = 'F+F+F+F' +
                '+JJJJ+' +
                'F+F+F+F' +
                '++JJJJ' +
                'F+F+F+F' +
                '++JJJJ' +
                'F+F+F+F' +
                '++JJJJ' +
                '-JJJJ'
            },
            3,
            @{
                F = { $this.Forward(10) }
                J = { $this.Jump(10) }
                '\+' = { $this.Rotate(90) }
                '-' = { $this.Rotate(-90) }
            }
    ).Pattern.Save("$pwd/TetroidLSystem.svg")

.EXAMPLE
    $turtle.Clear().LSystem(
        'F',
        [Ordered]@{ F = '
F+F+F+F +JJJJ+ F+F+F+F ++ JJJJ' },
        3,
        @{
            F = { $this.Forward(10) }
            J = { $this.Jump(10) }
            '\+' = { $this.Rotate(90) }
            '-' = { $this.Rotate(-90) }
        }
    ).Pattern.Save("$pwd/LSystemCool1.svg")
.EXAMPLE
    Move-Turtle LSystem F-F-F-F ([Ordered]@{F='F-F+F+F-F'}) 3 (
        [Ordered]@{
            F = { $this.Forward(10) }
            J = { $this.Jump(10) }
            '\+' = { $this.Rotate(90) }
            '-' = { $this.Rotate(-90) }
        }
    )
    
#>

param(
# The axiom, or starting string.
[Alias('Start', 'StartString', 'Initiator')]
[string]
$Axiom,

# The rules for expanding each iteration of the axiom.
[Alias('Rules', 'ProductionRules')]
[Collections.IDictionary]
$Rule = [Ordered]@{},

# The order of magnitude (or number of iterations)
[Alias('Iterations', 'IterationCount', 'N', 'Steps', 'N','StepCount')]
[int]
$Order = 2,

# The ways each variable will be expanded.
[Collections.IDictionary]
$Variable = @{}

)

# First, let us expand our axiom
$currentState = "$Axiom"
# (at least, as long as we're supposed to)
if ($Order -ge 1) {
    $combinedPattern = "(?>$($Rule.Keys -join '|'))"
    foreach ($iteration in 1..$Order) {
        # To expand each iteration, we replace any matching characters
        $currentState = $currentState -replace $combinedPattern, {
            $match = $_
            $matchingRule = $rule["$match"]
            # a matching rule could be dynamically specified with a script block
            if ($matchingRule -is [ScriptBlock]) {
                return "$(. $matchingRule $match)"
            } else {
                # but is often statically expanded with a string.
                return $matchingRule
            }
        }    
    }        
}

# Now we know our final state
$finalState = $currentState

# and can add the appropriate data attributes.
$this.PathAttribute = [Ordered]@{
    "data-l-order" = $Order
    "data-l-axiom" = $Axiom
    "data-l-rules" = ConvertTo-Json $Rule 
    "data-l-expanded" = $finalState
}

# Next, prepare our replacements.
# The provided script block will almost always be scoped differently
# so we need to recreate it.
$localReplacement = [Ordered]@{}
foreach ($key in $variable.Keys) {
    $localReplacement[$key] =
        if ($variable[$key] -is [ScriptBlock]) {
            [ScriptBlock]::Create($variable[$key])
        } else {
            $variable[$key]
        }
}

# Now we need to find all potential matches
$MatchesAny = "(?>$($variable.Keys -join '|'))"
$allMatches = @([Regex]::Matches($finalState, $MatchesAny, 'IgnoreCase,IgnorePatternWhitespace'))
# we want to minimize rematching, so create a temporary cache.
$matchCache = @{}
:nextMatch foreach ($match in $allMatches) {
    $m = "$match"
    # If we have not mapped the match to a script,
    if (-not $matchCache[$m]) {
        # find the matching replacement.
        foreach ($key in $Variable.Keys) {
            if (-not ($match -match $key)) { continue }     
            $matchCache[$m] = $localReplacement[$key]
            break
        }    
    }
    
    # If we have a script to run
    if ($matchCache[$m] -is [ScriptBlock]) {
        # run it
        $null =  . $matchCache[$m] $match
        # and continue to the next match.
        continue nextMatch
    }
}

# return this so we can pipe and chain this method.
return $this