Types/Turtle/get_History.ps1

<#
.SYNOPSIS
    Gets a Turtle's history
.DESCRIPTION
    Gets an annotated history of a turtle's movements.

    This is an SVG path translated into back into human readable text and coordinates.
#>


# We will always know where we are (relative to where we started, at least)
# But knowing where we have _been_? That's going to take a bit of work.
# SVG paths can be either absolute or relative
# So to know where we have been, we need to retrace our steps.

# So let's keep our current position
$currentPosition = [Numerics.Vector2]::new(0,0)
# make a history list
$historyList = [Collections.Generic.List[PSObject]]::new()
# and create a stack to traceback our steps
$startStack = [Collections.Stack]::new()

# SVG paths proceed letter by letter,
# so we want a pattern to split at that point
$BeforeNextLetter =
    @(
        '(?='      # Lookahead for
        '['        # this set of characters:
        '\p{L}'    # Any letter
        '-'        # except
        '[E]'      # the letter 'e' (used in exponent notation).
        ']'        # Now close the character set
        ')'        # and close the lookahead.
    ) -join ''

# Get our path data
$pathData = $this.PathData
# then split it into steps and remove empty steps.
$pathSteps = $pathData -join ' ' -split $BeforeNextLetter -ne ''

# Now let's do some history:
foreach ($pathStep in $pathSteps) {
    # First, determine what letter we are dealing with
    $letter = $pathStep[0]
    # Like a lot of G-Code, SVG paths are case sensitive
    # so find out if it is uppercase by using a case sensitive comparison.
    $isUpper = "$letter".ToLower() -cne $letter
    # If it was uppercase,
    $toBy = 
        if ($isUpper) {
            'to' # the movement is absolute (to)
        } else {
            'by' # if it is lowercase, the movement is relative (by)
        }    

    # Now let's turn each step into a floating point (except the letter)
    $stepPoints = $pathStep -replace $letter -replace ',', ' ' -split '\s{1,}' -ne '' -as [float[]]
    
    # This next part is a great example of a maxim of mine:
    # Programming is tedious, not hard.

    # There are only 10 different letters in SVG syntax
    # Unfortunately, that's 10 slightly different ways to write a bit of a path.

    # They all boil down to: a letter followed by sets of N numbers

    # They almost all treat repeating numbers as additional steps

    # But we still need to handle each slightly differently.

    # So, here we go
    
    # In (mostly) alphabetical order:
    
    $historyEntry = 
    switch ($letter) {
        a {
            # `a` is for arc path
            # These take 7 steps
            # "a $RadiusX $RadiusY $Rotation $IsBigArc $IsSweep $DeltaX $DeltaY"
            for ($stepIndex = 0; $stepIndex -lt $stepPoints.Length; $stepIndex+=7) {
                $sequence = $stepPoints[$stepIndex..($stepIndex + 6)]
                $comment = "arc $toBy $sequence"                
                $delta = [Numerics.Vector2]::new.Invoke($sequence[@(-2,-1)])
                # If they are absolute coordinates,
                # subtract them to get a delta.
                if ($isUpper) { $delta -= $currentPosition }
                # Create our history entry
                [PSCustomObject]@{
                    PSTypeName='Turtle.History'
                    Letter = "$letter"
                    Start = $currentPosition
                    End = $currentPosition + $delta
                    Delta = $delta
                    Instruction = "$Letter $sequence"                    
                    Comment = $comment
                }
                # update our position and go to the next set of points.
                $currentPosition += $delta
            }
        }
        c {
            # `c` is for cubic bezier curve
            # Cubic bezier curves take 6 points:
            # "c $ControlX1 $ControlY1 $ControlX2 $ControlY2 $DeltaX $DeltaY"
            for ($stepIndex = 0; $stepIndex -lt $stepPoints.Length; $stepIndex+=6) {
                $sequence = $stepPoints[$stepIndex..($stepIndex + 5)]
                $comment = "cubic curve $toBy $sequence"
                $delta = [Numerics.Vector2]::new.Invoke($sequence[@(-2,-1)])
                # If they are absolute coordinates,
                # subtract them to get a delta.
                if ($isUpper) { $delta -= $currentPosition }
                # Create our history entry
                [PSCustomObject]@{
                    PSTypeName='Turtle.History'
                    Letter = "$letter"
                    Start = $currentPosition
                    End = $currentPosition + $delta
                    Delta = $delta
                    Instruction = "$Letter $sequence"
                    Comment = $comment
                }
                $currentPosition += $delta
            }
        }
        l {
            # `l` is for line segment
            # These take pairs of points:
            # `l $DeltaX $DeltaY`
            for ($stepIndex = 0; $stepIndex -lt $stepPoints.Length; $stepIndex+=2) {
                $sequence = $stepPoints[$stepIndex..($stepIndex + 1)]
                $comment = "line $toBy $sequence"
                $delta = [Numerics.Vector2]::new.Invoke($sequence[@(-2,-1)])
                if ($isUpper) { $delta -= $currentPosition }
                [PSCustomObject]@{
                    PSTypeName='Turtle.History'
                    Letter = "$letter"
                    Start = $currentPosition
                    End = $currentPosition + $delta
                    Delta = $delta
                    Instruction = "$Letter $sequence"
                    Comment = $comment
                }
                $currentPosition += $delta
            }
        }
        m { 
            # `m` is for move
            # Move is a pair of points
            # The first pair moves without drawing
            # "m $DeltaX $DeltaY"
            # Any more points are drawn, and effectively become
            # "l $DeltaX $DeltaY"

            # So let's go over each m in pairs.
            for ($stepIndex = 0; $stepIndex -lt $stepPoints.Length; $stepIndex+=2) {
                $sequence = $stepPoints[$stepIndex..($stepIndex + 1)]
                $comment = "line $toBy $sequence"
                $delta = [Numerics.Vector2]::new.Invoke($sequence[@(-2,-1)])                                
                if ($isUpper) { $delta -= $currentPosition }
                # If we have more than one step
                if ($stepIndex -gt 0) {                    
                    if ($letter -eq 'm') {
                        # make it an l or L, depending on the case
                        if ($isUpper) { $letter = 'L' }
                        else { $letter = 'l'}
                    }
                    $comment = "line $toBy $sequence"
                } else {
                    # If it was actually a move we also need to
                    $comment = "move $toBy $sequence"
                    # push this into our "start stack".
                    # Whenever we close paths, this is the point we are closing to.
                    $startStack.Push($currentPosition + $delta)
                }
                
                [PSCustomObject]@{
                    PSTypeName='Turtle.History'
                    Letter = "$letter"
                    Start = $currentPosition
                    End = $currentPosition + $delta
                    Delta = $delta
                    Instruction = "$Letter $sequence"
                    Comment = $comment
                }
                $currentPosition += $delta
            }
        }
        q {            
            # `q` is for quadratic bezier curve
            # It takes a control point and and an end point
            # "q $ControlX $controlY $DeltaX $DeltaY"
            for ($stepIndex = 0; $stepIndex -lt $stepPoints.Length; $stepIndex+=4) {                
                $sequence = $stepPoints[$stepIndex..($stepIndex + 3)]
                $comment = "quadratic bezier curve $toBy $sequence"
                $delta = [Numerics.Vector2]::new.Invoke($sequence[@(-2,-1)])
                if ($isUpper) { $delta -= $currentPosition }
                [PSCustomObject]@{
                    PSTypeName='Turtle.History'
                    Letter = "$letter"
                    Start = $currentPosition
                    End = $currentPosition + $delta
                    Delta = $delta
                    Instruction = "$Letter $sequence"
                    Comment = $comment
                }                
                $currentPosition += $delta
            }
        }
        s {
            # `s` is a simple or sine bezier curve
            # It takes a control point and an end point
            # "q $ControlX $controlY $DeltaX $DeltaY"
            for ($stepIndex = 0; $stepIndex -lt $stepPoints.Length; $stepIndex+=4) {
                $sequence = $stepPoints[$stepIndex..($stepIndex + 3)]
                $comment = "simple bezier curve $toBy $sequence"
                $delta = [Numerics.Vector2]::new.Invoke($sequence[@(-2,-1)])
                if ($isUpper) { $delta -= $currentPosition }
                [PSCustomObject]@{
                    PSTypeName='Turtle.History'
                    Letter = "$letter"
                    Start = $currentPosition
                    End = $currentPosition + $delta
                    Delta = $delta
                    Instruction = "$Letter $sequence"
                    Comment = $comment
                }                
                $currentPosition += $delta
            }
        }
        
        t {
            # continue simple bezier curve
            # This tries to continue the last bezier curve
            # It only has a deltaX and deltaY.
            # "t $DeltaX $DeltaY"
            # It will use the last `s` or `t` as the control point
            for ($stepIndex = 0; $stepIndex -lt $stepPoints.Length; $stepIndex+=2) {
                $sequence = $stepPoints[$stepIndex..($stepIndex + 1)]
                $comment = "continue bezier curve $toBy $sequence"
                $delta = [Numerics.Vector2]::new.Invoke($sequence[@(-2,-1)])
                if ($isUpper) { $delta -= $currentPosition }
                [PSCustomObject]@{
                    PSTypeName='Turtle.History'
                    Letter = "$letter"
                    Start = $currentPosition
                    End = $currentPosition + $delta
                    Delta = $delta
                    Instruction = "$Letter $sequence"
                    Comment = $comment
                }                
                $currentPosition += $delta
            } 
        }
        # `h` is for horizontal and `v` is for vertical
        # Each of these draw a straight line
        # "h $Distance"
        # "v $Distance"
        { $_ -in 'h', 'v' } {
            for ($stepIndex = 0; $stepIndex -lt $stepPoints.Length; $stepIndex++) {
                $sequence = $stepPoints[$stepIndex..$stepIndex]
                $comment = "$(
                    if ($letter -eq 'v') { 'vertical' } else {'horizontal'}
                ) line $toBy $sequence"

                $delta = 
                    if ($letter -eq 'v') {
                        [Numerics.Vector2]::new(0, $sequence[0])
                    } else {
                        [Numerics.Vector2]::new($sequence[0], 0)
                    }
                if ($isUpper) { $delta -= $currentPosition }
                [PSCustomObject]@{
                    PSTypeName='Turtle.History'
                    Letter = "$letter"
                    Start = $currentPosition
                    End = $currentPosition + $delta
                    Delta = $delta
                    Instruction = "$Letter $sequence"
                    Comment = $comment
                }                
                $currentPosition += $delta
            }                        
        }        
        z {
            # `z` is for close
            # This takes no parameters
            # Instead it tries to close the shape.
            # It draws a line back to the last location stack
            $closePosition = $startStack.Pop()
            $delta = $closePosition - $currentPosition
            [PSCustomObject]@{
                PSTypeName='Turtle.History'
                Letter = "$letter"
                Start = $currentPosition
                End = $currentPosition + $delta
                Delta = $delta
                Instruction = "$Letter"
                Comment = "close path"
            }
            $currentPosition += $delta
        }
    }

    $historyList.Add($historyEntry)
}

# Now that we've done some history, return our results to you.

# This should give a full history of every move this turtle has made, and exactly where it was after each step.

return $historyList