logo-runtime 0.1.0

Implements the runtime for graphic functionality of Logo programming language
Documentation
use logo_interp::core::LogoValue;
use logo_interp::executor_state::*;
use crate::colors::{LogoColor, colors_count, get_color};
use crate::common::Pos;
use crate::state::{Delegate, PenState, State};

pub fn add_drawinglib<D: Delegate + 'static>(es: &mut EState<State<D>>) {
    es.functions.insert("show".to_string(), Function::from_proc1(show));

    es.functions.insert("cg".to_string(), Function::from_proc(cg));
    es.functions.insert("clean".to_string(), Function::from_proc(clean));
    es.functions.insert("fill".to_string(), Function::from_proc(fill));

    es.functions.insert("pu".to_string(), Function::from_proc(pu));
    es.functions.insert("pd".to_string(), Function::from_proc(pd));
    es.functions.insert("pe".to_string(), Function::from_proc(pe));

    es.functions.insert("rt".to_string(), Function::from_proc1(rt));
    es.functions.insert("right".to_string(), Function::from_proc1(rt));
    es.functions.insert("lt".to_string(), Function::from_proc1(lt));
    es.functions.insert("left".to_string(), Function::from_proc1(lt));
    es.functions.insert("fd".to_string(), Function::from_proc1(fd));
    es.functions.insert("bk".to_string(), Function::from_proc1(bk));

    es.functions.insert("heading".to_string(), Function::from_fn(heading));
    es.functions.insert("seth".to_string(), Function::from_proc1(seth));
    es.functions.insert("setheading".to_string(), Function::from_proc1(seth));
    es.functions.insert("setpos".to_string(), Function::from_proc1(setpos));
    es.functions.insert("setx".to_string(), Function::from_proc1(setx));
    es.functions.insert("sety".to_string(), Function::from_proc1(sety));
    es.functions.insert("pos".to_string(), Function::from_fn(pos));
    es.functions.insert("xcoor".to_string(), Function::from_fn(xcoor));
    es.functions.insert("ycoor".to_string(), Function::from_fn(ycoor));
    es.functions.insert("home".to_string(), Function::from_proc(home));

    es.functions.insert("pensize".to_string(), Function::from_fn(pensize));
    es.functions.insert("setpensize".to_string(), Function::from_proc1(setpensize));

    es.functions.insert("ht".to_string(), Function::from_proc(ht));
    es.functions.insert("st".to_string(), Function::from_proc(st));

    es.functions.insert("setc".to_string(), Function::from_proc1(setc));
    es.functions.insert("setcolor".to_string(), Function::from_proc1(setc));
    es.functions.insert("color".to_string(), Function::from_fn(color));
}

fn show<D: Delegate>(state: &mut EState<State<D>>, val: LogoValue) -> Result<(), String> {
    state.state.delegate.show(format!("{}", val).as_str());
    Ok(())
}

fn cg<D: Delegate>(state: &mut EState<State<D>>) -> Result<(), String> {
    let state = &mut state.state;
    state.data.turtle_pos = Pos{x: 0f64, y: 0f64};
    state.data.turtle_angle = 0f64;
    state.delegate.clear_graphics();
    Ok(())
}

fn clean<D: Delegate>(state: &mut EState<State<D>>) -> Result<(), String> {
    state.state.delegate.clear_graphics();
    Ok(())
}

fn fill<D: Delegate>(state: &mut EState<State<D>>) -> Result<(), String> {
    let state = &mut state.state;
    state.delegate.fill(state.data.turtle_pos, get_color(state.data.color_idx));
    Ok(())
}

fn pu<D: Delegate>(state: &mut EState<State<D>>) -> Result<(), String> {
    state.state.data.pen_state = PenState::Up;
    Ok(())
}

fn pd<D: Delegate>(state: &mut EState<State<D>>) -> Result<(), String> {
    state.state.data.pen_state = PenState::Down;
    Ok(())
}

fn pe<D: Delegate>(state: &mut EState<State<D>>) -> Result<(), String> {
    state.state.data.pen_state = PenState::Erase;
    Ok(())
}

fn rt<D: Delegate>(state: &mut EState<State<D>>, val: f64) -> Result<(), String> {
    state.state.data.turtle_angle += val;
    Ok(())
}

fn lt<D: Delegate>(state: &mut EState<State<D>>, val: f64) -> Result<(), String> {
    state.state.data.turtle_angle -= val;
    Ok(())
}

fn fd<D: Delegate>(state: &mut EState<State<D>>, val: f64) -> Result<(), String> {
    let old_pos = state.state.data.turtle_pos;
    let angle = state.state.data.turtle_angle;
    let delta_x = angle.to_radians().sin() * val;
    let delta_y = angle.to_radians().cos() * val;
    let new_pos = Pos{x: old_pos.x + delta_x, y: old_pos.y + delta_y};
    move_turtle(&mut state.state, new_pos);
    Ok(())
}

fn bk<D: Delegate>(state: &mut EState<State<D>>, val: f64) -> Result<(), String> {
    fd(state, -val)
}

fn heading<D: Delegate>(state: &mut EState<State<D>>) -> Result<f64, String> {
    Ok(state.state.data.turtle_angle)
}

fn seth<D: Delegate>(state: &mut EState<State<D>>, h: f64) -> Result<(), String> {
    state.state.data.turtle_angle = h;
    Ok(())
}

fn pos<D: Delegate>(state: &mut EState<State<D>>) -> Result<Vec<f64>, String> {
    Ok(vec![state.state.data.turtle_pos.x, state.state.data.turtle_pos.y])
}

fn setpos<D: Delegate>(state: &mut EState<State<D>>, pos: Vec<f64>) -> Result<(), String> {
    if pos.len() != 2 {
        Err("Setpos takes exactly 2 coordinates".to_string())
    }
    else {
        move_turtle(&mut state.state, Pos{ x: pos[0], y: pos[1] });
        Ok(())
    }
}

fn xcoor<D: Delegate>(state: &mut EState<State<D>>) -> Result<f64, String> {
    Ok(state.state.data.turtle_pos.x)
}

fn ycoor<D: Delegate>(state: &mut EState<State<D>>) -> Result<f64, String> {
    Ok(state.state.data.turtle_pos.y)
}

fn setx<D: Delegate>(state: &mut EState<State<D>>, x: f64) -> Result<(), String> {
    let y = state.state.data.turtle_pos.y;
    move_turtle(&mut state.state, Pos{x, y});
    Ok(())
}

fn sety<D: Delegate>(state: &mut EState<State<D>>, y: f64) -> Result<(), String> {
    let x = state.state.data.turtle_pos.x;
    move_turtle(&mut state.state, Pos{x, y});
    Ok(())
}

fn home<D: Delegate>(state: &mut EState<State<D>>) -> Result<(), String> {
    move_turtle(&mut state.state, Pos{ x: 0f64, y: 0f64 });
    state.state.data.turtle_angle = 0.0;
    Ok(())
}

fn setpensize<D: Delegate>(state: &mut EState<State<D>>, pen_size: f64) -> Result<(), String> {
    state.state.data.pen_size = pen_size;
    Ok(())
}

fn pensize<D: Delegate>(state: &mut EState<State<D>>) -> Result<f64, String> {
    Ok(state.state.data.pen_size)
}

fn ht<D: Delegate>(state: &mut EState<State<D>>) -> Result<(), String> {
    state.state.data.turtle_visible = false;
    Ok(())
}

fn st<D: Delegate>(state: &mut EState<State<D>>) -> Result<(), String> {
    state.state.data.turtle_visible = true;
    Ok(())
}

fn setc<D: Delegate>(state: &mut EState<State<D>>, color: i32) -> Result<(), String> {
    if color < 0 || color >= colors_count() {
        return Err(format!("Invalid color number {}", color));
    }
    state.state.data.color_idx = color;
    Ok(())
}

fn color<D: Delegate>(state: &mut EState<State<D>>) -> Result<i32, String> {
    Ok(state.state.data.color_idx)
}

fn move_turtle<D: Delegate>(state: &mut State<D>, pos: Pos) {
    let old_pos = state.data.turtle_pos;
    let w2 = state.data.canvas_width as f64 / 2f64;
    let h2 = state.data.canvas_height as f64 / 2f64;
    if pos.y > old_pos.y + f64::EPSILON {
        let xp = intersect_horizontal(old_pos, pos, h2, -w2, w2);
        if xp.is_some() {
            draw_line(state, old_pos, Pos{x: xp.unwrap(), y: h2});
            state.data.turtle_pos = Pos{x: xp.unwrap(), y: -h2};
            move_turtle(state, Pos{x: pos.x, y: pos.y - state.data.canvas_height as f64});
            return;
        }
    }
    if pos.y + f64::EPSILON < old_pos.y {
        let xp = intersect_horizontal(old_pos, pos, -h2, -w2, w2);
        if xp.is_some() {
            draw_line(state, old_pos, Pos{x: xp.unwrap(), y: -h2});
            state.data.turtle_pos = Pos{x: xp.unwrap(), y: h2};
            move_turtle(state, Pos{x: pos.x, y: pos.y + state.data.canvas_height as f64});
            return;
        }
    }
    if pos.x > old_pos.x + f64::EPSILON {
        let yp = intersect_vertical(old_pos, pos, w2, -h2, h2);
        if yp.is_some() {
            draw_line(state, old_pos, Pos{x: w2, y: yp.unwrap()});
            state.data.turtle_pos = Pos{x: -w2, y: yp.unwrap()};
            move_turtle(state, Pos{x: pos.x - state.data.canvas_width as f64, y: pos.y});
            return;
        }
    }
    if pos.x + f64::EPSILON < old_pos.x {
        let yp = intersect_vertical(old_pos, pos, -w2, -h2, h2);
        if yp.is_some() {
            draw_line(state, old_pos, Pos{x: -w2, y: yp.unwrap()});
            state.data.turtle_pos = Pos{x: w2, y: yp.unwrap()};
            move_turtle(state, Pos{x: pos.x + state.data.canvas_width as f64, y: pos.y});
            return;
        }
    }
    state.data.turtle_pos = pos;
    draw_line(state, old_pos, pos);
}

fn draw_line<D: Delegate>(state: &mut State<D>, p1: Pos, p2: Pos) {
    let mut color = get_color(state.data.color_idx);
    if state.data.pen_state == PenState::Erase {
        color = LogoColor{r: 255, g: 255, b: 255};
    }
    if state.data.pen_state != PenState::Up {
        state.delegate.draw_line(p1, p2, state.data.pen_size, color);
    }
}

fn intersect_horizontal(p1: Pos, p2: Pos, y: f64, x1: f64, x2: f64) -> Option<f64> {
    if p1.y.min(p2.y) > y || p1.y.max(p2.y) < y {
        return None;
    }
    let xp = p1.x - (p1.y - y) / (p1.y - p2.y) * (p1.x - p2.x);
    if xp >= x1 && xp <= x2 {
        Some(xp)
    }
    else {
        None
    }
}

fn intersect_vertical(p1: Pos, p2: Pos, x: f64, y1: f64, y2: f64) -> Option<f64> {
    if p1.x.min(p2.x) > x || p1.x.max(p2.x) < x {
        return None;
    }
    let yp = p1.y - (p1.x - x) / (p1.x - p2.x) * (p1.y - p2.y);
    if yp >= y1 && yp <= y2 {
        Some(yp)
    }
    else {
        None
    }
}

#[test]
fn test_move_turtle() {
    use crate::state::NoOpDelegate;
    use approx::assert_relative_eq;

    let mut state = EState::new(State::new(800, 450, NoOpDelegate{}));
    fd(&mut state, 50.0).unwrap();
    assert_relative_eq!(state.state.data.turtle_pos.y, 50.0, epsilon = 0.00001);
    fd(&mut state, 400.0).unwrap();
    assert_relative_eq!(state.state.data.turtle_pos.y, 0.0, epsilon = 0.00001);
    rt(&mut state, 90.0).unwrap();
    fd(&mut state, 500.0).unwrap();
    assert_relative_eq!(state.state.data.turtle_pos.x, -300.0, epsilon = 0.00001);
}