From 121446c5c4246bb9a100bf56c9e36025496b738b Mon Sep 17 00:00:00 2001 From: Scott Richmond Date: Sun, 21 Jul 2024 19:22:42 -0400 Subject: [PATCH] first step: Ludus speaks turtle graphics, not p5 calls --- postlude.ld | 22 +---- prelude.ld | 231 +++++++++++++++++++++------------------------ src/ludus.janet | 25 ++--- src/prelude.janet | 1 + src/validate.janet | 2 +- turtle-graphics.md | 13 ++- 6 files changed, 141 insertions(+), 153 deletions(-) diff --git a/postlude.ld b/postlude.ld index 744d7ee..6bbde8b 100644 --- a/postlude.ld +++ b/postlude.ld @@ -3,23 +3,7 @@ & the goal is to output any global state held in Ludus & this does not have base loaded into it, only prelude: must be pure Ludus -if turtle_state () :visible? then render_turtle! () else nil +print! ("running postlude") -reset_turtle! () - -& let console_msgs = flush! () - -let (r, g, b, a) = unbox (bgcolor) -store! (bgcolor, colors :black) - -let draw_calls = unbox (p5_calls) -store! (p5_calls, []) - -#{ - & :result result is provided elsewhere - & :errors [] & if we get here there are no errors - & :console console_msgs - :draw concat ( - [(:background, r, g, b, a), (:stroke, 255, 255, 255, 255)] - draw_calls) -} +store! (turtle_state, turtle_init) +store! (turtle_commands, []) diff --git a/prelude.ld b/prelude.ld index e06e038..63259ad 100644 --- a/prelude.ld +++ b/prelude.ld @@ -17,7 +17,6 @@ fn mod fn neg? fn print! fn some? -fn state/call fn store! fn string fn turn/rad @@ -968,6 +967,15 @@ fn dist { ((x, y)) -> dist (x, y) } +fn heading/vector { + "Takes a turtle heading, and returns a unit vector of that heading." + (heading) -> { + & 0 is 90º/0.25T, 0.25 is 180º/0.5T, 0.5 is 270º/0.75T, 0.75 is 0º/0T + let a = add (neg (heading), 0.25) + (cos (a), sin (a)) + } +} + &&& more number functions fn random { "Returns a random something. With zero arguments, returns a random number between 0 (inclusive) and 1 (exclusive). With one argument, returns a random number between 0 and n. With two arguments, returns a random number between m and n. Alternately, given a collection (list, dict, set), it returns a random member of that collection." @@ -1088,7 +1096,7 @@ let turtle_init = #{ :position (0, 0) & let's call this the origin for now :heading 0 & this is straight up :pendown? true - :pencolor colors :white + :pencolor :white :penwidth 1 :visible? true } @@ -1096,102 +1104,91 @@ let turtle_init = #{ & turtle states: refs that get modified by calls & turtle_commands is a list of commands, expressed as tuples box turtle_commands = [] +box turtle_state = turtle_init -& and a list of turtle states -box turtle_states = [turtle_init] - -fn reset_turtle! { - "Resets the turtle to its original state." - () -> store! (turtle_states, [turtle_init]) -} - -& and a list of calls to p5--at least for now -box p5_calls = [] - -& ...and finally, a background color -& we need to store this separately because, while it can be updated later, -& it must be the first call to p5. -box bgcolor = colors :black - -fn add_call! (call) -> update! (p5_calls, append! (_, call)) +& fn reset_turtle! { +& "Resets the turtle to its original state." +& () -> store! (turtle_states, [turtle_init]) +& } fn add_command! (command) -> { update! (turtle_commands, append! (_, command)) - let prev = do turtle_states > unbox > last + let prev = unbox (turtle_state) let curr = apply_command (prev, command) - update! (turtle_states, append! (_, curr)) - let call = state/call () - if call then { add_call! (call); :ok } else :ok + store! (turtle_state, curr) + & let call = state/call () + & if call then { add_call! (call); :ok } else :ok + :ok } -fn make_line ((x1, y1), (x2, y2)) -> (:line, x1, y1, x2, y2) +& fn make_line ((x1, y1), (x2, y2)) -> (:line, x1, y1, x2, y2) -let turtle_radius = 20 +& let turtle_radius = 20 -let turtle_angle = 0.385 +& let turtle_angle = 0.385 -let turtle_color = (255, 255, 255, 150) +& let turtle_color = (255, 255, 255, 150) -fn render_turtle! () -> { - let state = do turtle_states > unbox > last - if state :visible? - then { - let (r, g, b, a) = turtle_color - add_call! ((:fill, r, g, b, a)) - let #{heading - :pencolor (pen_r, pen_g, pen_b, pen_a) - :position (x, y) - pendown? - ...} = state - let origin = mult ((0, 1), turtle_radius) - let (x1, y1) = origin - let (x2, y2) = rotate (origin, turtle_angle) - let (x3, y3) = rotate (origin, neg (turtle_angle)) - add_call! ((:push)) - add_call! ((:translate, x, y)) - add_call! ((:rotate, turn/rad (heading))) - add_call! ((:noStroke)) - add_call! ((:beginShape)) - add_call! ((:vertex, x1, y1)) - add_call! ((:vertex, x2, y2)) - add_call! ((:vertex, x3, y3)) - add_call! ((:endShape)) - & there's a happy bug here: the stroke will be the same width as the pen width. Keep this for now. Consider also showing the pen colour here? - add_call! ((:stroke, pen_r, pen_g, pen_b, pen_a)) - if pendown? then add_call! ((:line, 0, 0, x1, y1)) else nil - add_call! ((:pop)) - :ok - } - else :ok -} +& fn render_turtle! () -> { +& let state = do turtle_states > unbox > last +& if state :visible? +& then { +& let (r, g, b, a) = turtle_color +& add_call! ((:fill, r, g, b, a)) +& let #{heading +& :pencolor (pen_r, pen_g, pen_b, pen_a) +& :position (x, y) +& pendown? +& ...} = state +& let origin = mult ((0, 1), turtle_radius) +& let (x1, y1) = origin +& let (x2, y2) = rotate (origin, turtle_angle) +& let (x3, y3) = rotate (origin, neg (turtle_angle)) +& add_call! ((:push)) +& add_call! ((:translate, x, y)) +& add_call! ((:rotate, turn/rad (heading))) +& add_call! ((:noStroke)) +& add_call! ((:beginShape)) +& add_call! ((:vertex, x1, y1)) +& add_call! ((:vertex, x2, y2)) +& add_call! ((:vertex, x3, y3)) +& add_call! ((:endShape)) +& & there's a happy bug here: the stroke will be the same width as the pen width. Keep this for now. Consider also showing the pen colour here? +& add_call! ((:stroke, pen_r, pen_g, pen_b, pen_a)) +& if pendown? then add_call! ((:line, 0, 0, x1, y1)) else nil +& add_call! ((:pop)) +& :ok +& } +& else :ok +& } -fn state/call () -> { - let cmd = do turtle_commands > unbox > last > first - let states = unbox (turtle_states) - let curr = last (states) - let prev = at (states, sub (count (states), 2)) - match cmd with { - :forward -> if curr :pendown? - then make_line (prev :position, curr :position) - else nil - :back -> if curr :pendown? - then make_line (prev :position, curr :position) - else nil - :home -> if curr :pendown? - then make_line (prev :position, curr :position) - else nil - :goto -> if curr :pendown? - then make_line (prev :position, curr :position) - else nil - :penwidth -> (:strokeWeight, curr :penwidth) - :pencolor -> { - let (r, g, b, a) = curr :pencolor - (:stroke, r, g, b, a) - } - :clear -> (:background, 0, 0, 0, 255) - _ -> nil - } -} +& fn state/call () -> { +& let cmd = do turtle_commands > unbox > last > first +& let states = unbox (turtle_states) +& let curr = last (states) +& let prev = at (states, sub (count (states), 2)) +& match cmd with { +& :forward -> if curr :pendown? +& then make_line (prev :position, curr :position) +& else nil +& :back -> if curr :pendown? +& then make_line (prev :position, curr :position) +& else nil +& :home -> if curr :pendown? +& then make_line (prev :position, curr :position) +& else nil +& :goto -> if curr :pendown? +& then make_line (prev :position, curr :position) +& else nil +& :penwidth -> (:strokeWeight, curr :penwidth) +& :pencolor -> { +& let (r, g, b, a) = curr :pencolor +& (:stroke, r, g, b, a) +& } +& :clear -> (:background, 0, 0, 0, 255) +& _ -> nil +& } +& } fn forward! { "Moves the turtle forward by a number of steps. Alias: fd!" @@ -1237,6 +1234,7 @@ let pd! = pendown! fn pencolor! { "Changes the turtle's pen color. Takes a single grayscale value, an rgb tuple, or an rgba tuple. Alias: pc!" + (color as :keyword) -> add_command! ((:pencolor, color)) (gray as :number) -> add_command! ((:pencolor, (gray, gray, gray, 255))) ((r as :number, g as :number, b as :number)) -> add_command! ((:pencolor, (r, g, b, 255))) ((r as :number, g as :number, b as :number, a as :number)) -> add_command! ((:pencolor, (r, g, b, a))) @@ -1253,9 +1251,10 @@ let pw! = penwidth! fn background! { "Sets the background color behind the turtle and path. Alias: bg!" - (gray as :number) -> store! (bgcolor, (gray, gray, gray, 255)) - ((r as :number, g as :number, b as :number)) -> store! (bgcolor, (r, g, b, 255)) - ((r as :number, g as :number, b as :number, a as :number)) -> store! (bgcolor, (r, g, b, a)) + (color as :keyword) -> add_command! ((:background, :color)) + (gray as :number) -> add_command! ((:background, gray, gray, gray, 255)) + ((r as :number, g as :number, b as :number)) -> add_command! ((:background, r, g, b, 255)) + ((r as :number, g as :number, b as :number, a as :number)) -> add_command! ((:background, r, g, b, a)) } let bg! = background! @@ -1291,12 +1290,18 @@ fn hideturtle! { () -> add_command! ((:hide)) } -fn heading/vector { - "Takes a turtle heading, and returns a unit vector of that heading." - (heading) -> { - & 0 is 90º/0.25T, 0.25 is 180º/0.5T, 0.5 is 270º/0.75T, 0.75 is 0º/0T - let a = add (heading, 0.25) - (cos (a), sin (a)) +fn loadstate! { + "Sets the turtle state to a previously saved state. Returns the state." + (state) -> { + let #{:position (x, y), heading, pendown?, pencolor, penwidth, visible?} = state + let command = if tuple? (pencolor) + then { + let (r, g, b, a) = pencolor + (:loadstate, x, y, heading, visible?, pendown?, penwidth, r, g, b, a) + } + else (:loadstate, x, y, heading, visible?, pendown?, penwidth, pencolor) + add_command! (command) + state } } @@ -1329,49 +1334,37 @@ fn apply_command { (:penwidth, pixels) -> assoc (state, :penwidth, pixels) (:pencolor, color) -> assoc (state, :pencolor, color) (:setheading, heading) -> assoc (state, :heading, heading) + (:loadstate, x, y, heading, visible?, pendown?, penwidth, pencolor) -> #{:position (x, y), heading, visible?, pendown?, penwidth, pencolor} + (:loadstate, x, y, heading, visible?, pendown?, penwidth, r, g, b, a) -> #{:position (x, y), heading, visible?, pendown?, penwidth, :pencolor (r, g, b, a)} (:show) -> assoc (state, :visible?, true) (:hide) -> assoc (state, :visible?, false) } } -fn turtle_state { - "Returns the turtle's current state." - () -> do turtle_states > unbox > last -} - -fn load_turtle_state! { - "Sets the turtle state to a previously saved state. Returns the state." - (state) -> { - update! (turtle_states, append! (_, state)) - let call = state/call () - if call then { add_call! (call); :ok } else :ok - } -} - & position () -> (x, y) fn position { "Returns the turtle's current position." - () -> turtle_state () :position + () -> do turtle_state > unbox > :position } fn heading { "Returns the turtle's current heading." - () -> turtle_state () :heading + () -> do turtle_state > unbox > :heading } fn pendown? { "Returns the turtle's pen state: true if the pen is down." - () -> turtle_state () :pendown? + () -> do turtle_state > unbox > :pendown? } fn pencolor { - "Returns the turtle's pen color as an (r, g, b, a) tuple." - () -> turtle_state () :pencolor + "Returns the turtle's pen color as an (r, g, b, a) tuple or keyword." + () -> do turtle_state > unbox > :pencolor } fn penwidth { "Returns the turtle's pen width in pixels." - () -> turtle_state () :penwidth + () -> do turtle_state > unbox > :penwidth } box state = nil @@ -1392,7 +1385,6 @@ pkg Prelude { background! & turtles between? & math bg! & turtles - bgcolor & turtles bk! & turtles bool & bool bool? & bool @@ -1454,7 +1446,7 @@ pkg Prelude { left! & turtles list & lists list? & lists - load_turtle_state! & turtles + loadstate! & turtles lt! & turtles lt? & math lte? & math @@ -1476,7 +1468,6 @@ pkg Prelude { omit & set or & bool ordered? & lists tuples strings - p5_calls & turtles pc! & turtles pd! & turtles pencolor & turtles @@ -1497,9 +1488,7 @@ pkg Prelude { random & math dicts lists tuples sets random_int & math range & math lists - render_turtle! & turtles report! & environment - reset_turtle! & turtles rest & lists tuples right! & turtles round & math @@ -1533,8 +1522,8 @@ pkg Prelude { turn/deg & math turn/rad & math turtle_commands & turtles + turtle_init & turtles turtle_state & turtles - turtle_states & turtles type & values unbox & boxes unwrap! & results diff --git a/src/ludus.janet b/src/ludus.janet index dbdce7f..06d0eaa 100644 --- a/src/ludus.janet +++ b/src/ludus.janet @@ -15,11 +15,13 @@ (when (= :error prelude/pkg) (error "could not load prelude")) (def ctx @{:^parent prelude/ctx}) (def errors @[]) - (def draw @[]) (var result @"") (def console @"") (setdyn :out console) - (def out @{:errors errors :draw draw :result result :console console}) + (def out @{:errors errors :result result + :io @{ + :stdout @{:proto [:text-stream "0.1.0"] :data console} + :turtle @{:proto [:turtle-graphics "0.1.0"] :data @[]}}}) (def scanned (s/scan source)) (when (any? (scanned :errors)) (each err (scanned :errors) @@ -42,22 +44,23 @@ (break (-> out j/encode string)))) (setdyn :out stdout) (set (out :result) (b/show result)) - (var post @{}) + (set (((out :io) :turtle) :data) (get-in prelude/pkg [:turtle_commands :^value])) (try - (set post (i/interpret prelude/post/ast ctx)) + (i/interpret prelude/post/ast ctx) ([err] (e/runtime-error err))) - (set (out :draw) (post :draw)) - # out - (-> out j/encode string) - ) + (-> out j/encode string)) (comment # (do # (def start (os/clock)) (def source ` -box foo = :bar -store! (foo, :baz) -unbox (foo) +fd! (100) +rt! (0.25) +fd! (100) +lt! (0.25) +fd! (100) +setheading! (0.75) +unbox (turtle_state) `) (def out (-> source ludus diff --git a/src/prelude.janet b/src/prelude.janet index 702c74c..45ebdb7 100644 --- a/src/prelude.janet +++ b/src/prelude.janet @@ -39,3 +39,4 @@ (def validation-errors (post-validated :errors)) (when (any? validation-errors) (each err validation-errors (e/validation-error err)) (break :error)) (post-parsed :ast))) + diff --git a/src/validate.janet b/src/validate.janet index 1fea991..88c3501 100644 --- a/src/validate.janet +++ b/src/validate.janet @@ -765,7 +765,7 @@ Deferred until a later iteration of Ludus: (defn- cleanup [validator] (def declared (get-in validator [:status :declared] {})) (when (any? declared) - (each declaration declared + (each declaration (keys declared) (array/push (validator :errors) {:node declaration :msg "declared fn, but not defined"}))) validator) diff --git a/turtle-graphics.md b/turtle-graphics.md index 39edff0..90e801b 100644 --- a/turtle-graphics.md +++ b/turtle-graphics.md @@ -41,7 +41,7 @@ Turtle graphics describe the movements and drawing behaviours of screen, robot, - Shows the turtle. * `hide`, no arguments - Hides the turtle. -* `loadstate`, x: number, y: number, heading: number, pendown: boolean, width: number, color: string OR r: number, g: number, b: number, a: number +* `loadstate`, x: number, y: number, heading: number, visible: boolean, pendown: boolean, width: number, color: string OR r: number, g: number, b: number, a: number - Loads a turtle state. * `clear`, no arguments - Erases any paths drawn and sets the background color to the default. @@ -71,11 +71,22 @@ Colors should also be specifiable with strings corresponding to CSS basic colors Ludus should have access to turtle states. This is important for push/pop situations that we use for L-systems. There are two ways to do this: Ludus does its own bookkeeping for turtle states, or it has a way to get the state from a turtle. + The latter has the value of being instantaneous, and gives us an _expected_ state of the turtle after the commands are all processed. In particular, this will be necessary for the recursive L-systems that require pushing and popping turtle state. The latter has the drawback of potentially allowing the turtle state and expected turtle state to fall out of synch. + The former has the value of always giving us the correct, actual state of the turtle. It has the drawback of requiring such state reporting to be asynchronous, and perhaps wildly asynchronous, as things like moving robots and plotters will take quite some time to actually draw what Ludus tells it to. (Being able to wait until `eq? (expected, actual)` to do anything else may well be extremely useful.) + That suggests, then, that both forms of turtle state are desirable and necessary. Thus: turtles should communicate states (and thus there ought to be a protocol for communicating state back to Ludus) and Ludus should always do the bookkeeping of calculating the expected state. + +**Turtles use Cartesian, rather than screen, coordinates.** +The starting position of the turtle is `(0, 0)`, which is the origin, and _centred_ in the field of view. +Increasing the x-coordinate moves the turtle to the right; increasing the y-coordinate moves the turtle _up_. + +**Turtles use compass headings, not mathematical angles.** +Turtles start pointing vertially, at heading `0`. +Turning right _increases_ the heading; pointing due "east" is `0.25`; south `0.5`, and west, `0.75`.