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.
202 lines
6.8 KiB
202 lines
6.8 KiB
import lsystempkg/raylib, lsystempkg/raygui
|
|
import math, os
|
|
import algorithm, heapqueue, random, options, sequtils, sugar, tables, system
|
|
|
|
# Seed RNG with current time
|
|
randomize()
|
|
|
|
type Term = enum LeafTerm, LineTerm, GoLeft, GoRight, PushTerm, PopTerm
|
|
type Terms = seq[Term]
|
|
|
|
iterator rewrite(terms: Terms) : Terms =
|
|
var currentTerms: Terms = terms
|
|
var newTerms: Terms
|
|
|
|
while true:
|
|
# Reset this each iteration to gather new expansions
|
|
newTerms = @[]
|
|
for term in currentTerms:
|
|
case term:
|
|
of LeafTerm: newTerms &= @[LineTerm, LineTerm, PushTerm, GoRight, LeafTerm, PopTerm, GoLeft, LeafTerm]
|
|
else: newTerms &= @[term]
|
|
currentTerms = newTerms
|
|
yield currentTerms
|
|
|
|
type StackControl = enum Push, Pop
|
|
|
|
# An intruction along with a change in angle and magnitude (i.e. a vector)
|
|
type DrawInstruction = object
|
|
angle_change: float64
|
|
magnitude: float64
|
|
|
|
type
|
|
InstructionKind = enum pkDraw, pkStack
|
|
Instruction = object
|
|
case kind: InstructionKind
|
|
of pkDraw: drawInstruction: DrawInstruction
|
|
of pkStack: stackInstruction: StackControl
|
|
|
|
proc `$` (i: Instruction): string =
|
|
case i.kind:
|
|
of pkDraw: return "angle_change = " & $i.drawInstruction.angle_change & ", magnitude = " & $i.drawInstruction.magnitude
|
|
of pkStack: return "direction = " & $i.stackInstruction
|
|
|
|
iterator axiomToInstructions(maxIterations: int) : Instruction =
|
|
let axiom = @[LeafTerm]
|
|
var n = 0
|
|
var termsToConvert: Terms
|
|
for terms in rewrite(axiom):
|
|
n += 1
|
|
if n == maxIterations:
|
|
termsToConvert = terms
|
|
break
|
|
|
|
var magnitudes: seq[float64] = @[18.float64, 34.float64, 50.float64]
|
|
let magnitude_weights = [3, 6, 1]
|
|
let magnitude_cdf = math.cumsummed(magnitude_weights)
|
|
|
|
let angles: seq[float64] = @[10 * 1.618, 20 * 1.618, 30 * 1.618]
|
|
let angle_weights = [3, 5, 2]
|
|
let angle_cdf = math.cumsummed(angle_weights)
|
|
|
|
var angle_delta: float64
|
|
var magnitude: float64
|
|
# every time you encounter a push divide, and a pop multiply
|
|
# type Term = enum LeafTerm, LineTerm, GoLeft, GoRight, PushTerm, PopTerm
|
|
|
|
# axiom
|
|
yield Instruction(kind: pkDraw, drawInstruction: DrawInstruction(angle_change: 180, magnitude: 180))
|
|
for term in termsToConvert:
|
|
angle_delta = sample(angles, angle_cdf)
|
|
magnitude = sample(magnitudes, magnitude_cdf)
|
|
case term:
|
|
of LeafTerm:
|
|
magnitude = magnitude / 1.5
|
|
yield Instruction(kind: pkDraw, drawInstruction: DrawInstruction(angle_change: angle_delta, magnitude: magnitude))
|
|
yield Instruction(kind: pkDraw, drawInstruction: DrawInstruction(angle_change: 0, magnitude: -magnitude)) # hack
|
|
yield Instruction(kind: pkDraw, drawInstruction: DrawInstruction(angle_change: -(angle_delta*2), magnitude: magnitude))
|
|
of LineTerm:
|
|
yield Instruction(kind: pkDraw, drawInstruction: DrawInstruction(angle_change: 0, magnitude: magnitude)) # don't change direction
|
|
of GoLeft:
|
|
yield Instruction(kind: pkDraw, drawInstruction: DrawInstruction(angle_change: angle_delta, magnitude: magnitude)) # change direction to left 45 degrees
|
|
of GoRight:
|
|
yield Instruction(kind: pkDraw, drawInstruction: DrawInstruction(angle_change: -angle_delta, magnitude: magnitude)) # change direction to right 45 degrees
|
|
of PushTerm:
|
|
yield Instruction(kind: pkStack, stackInstruction: Push)
|
|
of PopTerm:
|
|
magnitude = magnitude * 1.5
|
|
yield Instruction(kind: pkStack, stackInstruction: Pop)
|
|
|
|
# A Position along with its angle
|
|
type Position = object
|
|
x: float64
|
|
y: float64
|
|
angle: float64
|
|
|
|
proc `$` (p: Position): string =
|
|
return "x = " & $p.x & ", " & "y = " & $p.y & ", " & "angle = " & $p.angle
|
|
|
|
# Line (along with the angle relative to origin
|
|
type DrawLine = object
|
|
start_pos: Vector2
|
|
end_pos: Vector2
|
|
angle: float64
|
|
|
|
proc `$` (d: DrawLine): string =
|
|
return "start_pos = " & $d.start_pos & ", " & "end_pos = " & $d.end_pos
|
|
|
|
proc calculateNextLine(inst: DrawInstruction, pos: Position) : DrawLine =
|
|
# Change the angle
|
|
let new_angle = inst.angle_change + pos.angle
|
|
|
|
# Use the same magnitude as before
|
|
let magnitude = inst.magnitude
|
|
|
|
# Convert from polar coordinates to cartesian
|
|
let new_x = -(magnitude * cos(degToRad(new_angle)))
|
|
let new_y = magnitude * sin(degToRad(new_angle))
|
|
|
|
result.start_pos = Vector2(x: pos.x, y: pos.y)
|
|
|
|
# Ending position is relative to the starting position, so add the coordinates
|
|
result.end_pos = Vector2(x: result.start_pos.x+new_x, y: result.start_pos.y+new_y)
|
|
|
|
result.angle = new_angle
|
|
|
|
proc executeProgram(instructions: seq[Instruction], positions: seq[Position], current_pos: Position) : seq[DrawLine] =
|
|
# each instruction will be followed by a stack control instruction
|
|
if instructions.len <= 0:
|
|
echo "Returning"
|
|
return @[]
|
|
|
|
let inst = instructions[0]
|
|
|
|
var nextLine: DrawLine
|
|
|
|
case inst.kind:
|
|
of pkStack:
|
|
if inst.stackInstruction == Push:
|
|
return executeProgram(instructions[1..^1], current_pos & positions, current_pos)
|
|
elif inst.stackInstruction == Pop:
|
|
let newCurrent = positions[0]
|
|
return executeProgram(instructions[1..^1], positions[1..^1], newCurrent)
|
|
else:
|
|
return
|
|
of pkDraw:
|
|
nextLine = calculateNextLine(inst.drawInstruction, current_pos)
|
|
let new_position = Position(x: nextLine.end_pos.x,
|
|
y: nextLine.end_pos.y,
|
|
angle: nextLine.angle)
|
|
# leave the stack alone, set the current position however
|
|
return @[nextLine] & executeProgram(instructions[1..^1], positions, new_position)
|
|
|
|
proc guiLoop*(instructions: seq[Instruction]) =
|
|
# TODO get from xlib
|
|
var screenWidth: int = 100
|
|
var screenHeight: int = 100
|
|
|
|
SetConfigFlags(ord(ConfigFlags.FLAG_WINDOW_UNDECORATED))
|
|
|
|
InitWindow(screenWidth, screenHeight, "L-Systems")
|
|
|
|
let monitor = GetCurrentMonitor()
|
|
screenWidth = (monitor.GetMonitorWidth()).int
|
|
screenHeight = (monitor.GetMonitorHeight()).int
|
|
|
|
SetWindowSize(screenWidth, screenHeight)
|
|
SetWindowTitle("L-Systems")
|
|
MaximizeWindow()
|
|
|
|
#GuiLoadStyle("styles/terminal/terminal.rgs")
|
|
|
|
var mousePos = Vector2(x: 0, y: 0)
|
|
var windowPos = Vector2(x: screenWidth.float64, y: screenHeight.float64)
|
|
var panOffset = mousePos
|
|
|
|
var dragWindow = false
|
|
var exitWindow = false
|
|
|
|
var restartButton = false
|
|
|
|
SetTargetFPS(60)
|
|
|
|
# "axiom"
|
|
let startingPosition = Position(x: screenWidth/2, y: screenHeight.float64-100, angle: 90)
|
|
let drawLines = executeProgram(instructions, @[startingPosition], startingPosition)
|
|
|
|
while not exitWindow and not WindowShouldClose():
|
|
BeginDrawing()
|
|
|
|
screenWidth = (monitor.GetMonitorWidth() / 2).int
|
|
screenHeight = (monitor.GetMonitorHeight() / 2).int
|
|
# This must come before anything else!
|
|
ClearBackground(BLACK)
|
|
for line in drawLines:
|
|
DrawLineEx(line.start_pos, line.end_pos, 3, WHITE)
|
|
|
|
EndDrawing()
|
|
CloseWindow()
|
|
|
|
when isMainModule:
|
|
#guiLoop()
|
|
guiLoop(toSeq(axiomToInstructions(7)))
|
|
|