use std::collections::HashMap;
use std::f64::consts::PI;
use crate::model::{AdjAngle, AdjCoord, GeomGuide};
#[derive(Clone, Copy, Debug)]
pub struct GuideContext {
pub w: f64,
pub h: f64,
}
impl GuideContext {
pub fn new(w: f64, h: f64) -> Self {
Self { w, h }
}
}
pub type GuideValues = HashMap<String, f64>;
pub fn evaluate_guides(guides: &[GeomGuide], ctx: GuideContext) -> GuideValues {
let mut values = GuideValues::new();
for guide in guides {
if let Some(v) = evaluate_formula(&guide.formula, &values, ctx) {
values.insert(guide.name.clone(), v);
}
}
values
}
pub fn resolve_adj_coord(c: &AdjCoord, values: &GuideValues, ctx: GuideContext) -> f64 {
match c {
AdjCoord::Lit(n) => *n as f64,
AdjCoord::Guide(name) => values
.get(name.as_str())
.copied()
.or_else(|| named_constant(name, ctx))
.unwrap_or(0.0),
}
}
pub fn resolve_adj_angle(a: &AdjAngle, values: &GuideValues, ctx: GuideContext) -> f64 {
resolve_adj_coord(a, values, ctx)
}
fn evaluate_formula(formula: &str, values: &GuideValues, ctx: GuideContext) -> Option<f64> {
let mut tokens = formula.split_whitespace();
let op = tokens.next()?;
let a = tokens.next();
let b = tokens.next();
let c = tokens.next();
let v = |t: &str| -> f64 { resolve_token(t, values, ctx) };
Some(match op {
"*/" => {
let (a, b, c) = (v(a?), v(b?), v(c?));
if c == 0.0 {
0.0
} else {
(a * b) / c
}
}
"+-" => v(a?) + v(b?) - v(c?),
"+/" => {
let (a, b, c) = (v(a?), v(b?), v(c?));
if c == 0.0 {
0.0
} else {
(a + b) / c
}
}
"?:" => {
if v(a?) > 0.0 {
v(b?)
} else {
v(c?)
}
}
"cat2" => {
let (a, b, c) = (v(a?), v(b?), v(c?));
a * (c.atan2(b)).cos()
}
"mod" => {
let (a, b, c) = (v(a?), v(b?), v(c?));
(a * a + b * b + c * c).sqrt()
}
"pin" => {
let (a, b, c) = (v(a?), v(b?), v(c?));
b.max(a).min(c)
}
"sat2" => {
let (a, b, c) = (v(a?), v(b?), v(c?));
a * (c.atan2(b)).sin()
}
"at2" => {
let (a, b) = (v(a?), v(b?));
b.atan2(a) * 180.0 / PI * 60_000.0
}
"cos" => {
let (a, b) = (v(a?), v(b?));
a * (b * PI / 180.0 / 60_000.0).cos()
}
"max" => v(a?).max(v(b?)),
"min" => v(a?).min(v(b?)),
"sin" => {
let (a, b) = (v(a?), v(b?));
a * (b * PI / 180.0 / 60_000.0).sin()
}
"tan" => {
let (a, b) = (v(a?), v(b?));
a * (b * PI / 180.0 / 60_000.0).tan()
}
"abs" => v(a?).abs(),
"sqrt" => v(a?).sqrt(),
"val" => v(a?),
_ => return None,
})
}
fn resolve_token(token: &str, values: &GuideValues, ctx: GuideContext) -> f64 {
if let Ok(n) = token.parse::<i64>() {
return n as f64;
}
if let Ok(n) = token.parse::<f64>() {
return n;
}
if let Some(v) = named_constant(token, ctx) {
return v;
}
values.get(token).copied().unwrap_or(0.0)
}
fn named_constant(name: &str, ctx: GuideContext) -> Option<f64> {
Some(match name {
"w" => ctx.w,
"h" => ctx.h,
"ss" => ctx.w.min(ctx.h),
"ls" => ctx.w.max(ctx.h),
"hc" => ctx.w / 2.0,
"vc" => ctx.h / 2.0,
"t" | "l" => 0.0,
"b" => ctx.h,
"r" => ctx.w,
"wd2" => ctx.w / 2.0,
"wd4" => ctx.w / 4.0,
"wd8" => ctx.w / 8.0,
"wd16" => ctx.w / 16.0,
"wd32" => ctx.w / 32.0,
"hd2" => ctx.h / 2.0,
"hd4" => ctx.h / 4.0,
"hd8" => ctx.h / 8.0,
"hd16" => ctx.h / 16.0,
"hd32" => ctx.h / 32.0,
"cd2" => 360.0 / 2.0 * 60_000.0,
"cd4" => 360.0 / 4.0 * 60_000.0,
"cd8" => 360.0 / 8.0 * 60_000.0,
"3cd4" => 3.0 * 360.0 / 4.0 * 60_000.0,
"3cd8" => 3.0 * 360.0 / 8.0 * 60_000.0,
"5cd8" => 5.0 * 360.0 / 8.0 * 60_000.0,
"7cd8" => 7.0 * 360.0 / 8.0 * 60_000.0,
_ => return None,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn ctx() -> GuideContext {
GuideContext::new(100.0, 50.0)
}
fn g(name: &str, formula: &str) -> GeomGuide {
GeomGuide {
name: name.into(),
formula: formula.into(),
}
}
#[test]
fn val_literal() {
let vs = evaluate_guides(&[g("a", "val 42")], ctx());
assert_eq!(vs["a"], 42.0);
}
#[test]
fn val_named_constant_w() {
let vs = evaluate_guides(&[g("x", "val w")], ctx());
assert_eq!(vs["x"], 100.0);
}
#[test]
fn val_named_constant_hc_vc() {
let vs = evaluate_guides(&[g("cx", "val hc"), g("cy", "val vc")], ctx());
assert_eq!(vs["cx"], 50.0);
assert_eq!(vs["cy"], 25.0);
}
#[test]
fn val_ss_ls() {
let vs = evaluate_guides(&[g("s", "val ss"), g("l", "val ls")], ctx());
assert_eq!(vs["s"], 50.0);
assert_eq!(vs["l"], 100.0);
}
#[test]
fn muldiv() {
let vs = evaluate_guides(&[g("r", "*/ 10 3 2")], ctx());
assert_eq!(vs["r"], 15.0);
}
#[test]
fn muldiv_by_zero_returns_zero() {
let vs = evaluate_guides(&[g("r", "*/ 10 3 0")], ctx());
assert_eq!(vs["r"], 0.0);
}
#[test]
fn plus_minus() {
let vs = evaluate_guides(&[g("r", "+- 10 5 3")], ctx());
assert_eq!(vs["r"], 12.0);
}
#[test]
fn plus_div() {
let vs = evaluate_guides(&[g("r", "+/ 10 6 4")], ctx());
assert_eq!(vs["r"], 4.0);
}
#[test]
fn ternary_selects_by_sign() {
let vs = evaluate_guides(&[g("a", "?: 1 42 99"), g("b", "?: -1 42 99")], ctx());
assert_eq!(vs["a"], 42.0);
assert_eq!(vs["b"], 99.0);
}
#[test]
fn pin_clamp() {
let vs = evaluate_guides(
&[
g("a", "pin 0 5 10"),
g("b", "pin 0 -1 10"),
g("c", "pin 0 20 10"),
],
ctx(),
);
assert_eq!(vs["a"], 5.0);
assert_eq!(vs["b"], 0.0);
assert_eq!(vs["c"], 10.0);
}
#[test]
fn min_max() {
let vs = evaluate_guides(&[g("mn", "min 3 7"), g("mx", "max 3 7")], ctx());
assert_eq!(vs["mn"], 3.0);
assert_eq!(vs["mx"], 7.0);
}
#[test]
fn abs_and_sqrt() {
let vs = evaluate_guides(&[g("a", "abs -9"), g("s", "sqrt 16")], ctx());
assert_eq!(vs["a"], 9.0);
assert_eq!(vs["s"], 4.0);
}
#[test]
fn mod_is_vector_magnitude() {
let vs = evaluate_guides(&[g("m", "mod 3 4 12")], ctx());
assert!((vs["m"] - 13.0).abs() < 1e-9);
}
#[test]
fn trig_cos_sin_zero_angle() {
let vs = evaluate_guides(&[g("c", "cos 100 0"), g("s", "sin 100 0")], ctx());
assert!((vs["c"] - 100.0).abs() < 1e-9);
assert!(vs["s"].abs() < 1e-9);
}
#[test]
fn trig_cos_90deg() {
let vs = evaluate_guides(
&[g("c", "cos 100 5400000"), g("s", "sin 100 5400000")],
ctx(),
);
assert!(vs["c"].abs() < 1e-6);
assert!((vs["s"] - 100.0).abs() < 1e-6);
}
#[test]
fn at2_returns_angle_in_60k_deg() {
let vs = evaluate_guides(&[g("a", "at2 0 100")], ctx());
assert!((vs["a"] - 5_400_000.0).abs() < 1e-3);
}
#[test]
fn cat2_sat2_polar_projection() {
let vs = evaluate_guides(
&[g("cx", "cat2 100 0 100"), g("cy", "sat2 100 0 100")],
ctx(),
);
assert!(vs["cx"].abs() < 1e-6);
assert!((vs["cy"] - 100.0).abs() < 1e-6);
}
#[test]
fn guide_references_preceding_guide() {
let vs = evaluate_guides(
&[g("half_w", "*/ w 1 2"), g("quarter_w", "*/ half_w 1 2")],
ctx(),
);
assert_eq!(vs["half_w"], 50.0);
assert_eq!(vs["quarter_w"], 25.0);
}
#[test]
fn forward_reference_resolves_to_zero() {
let vs = evaluate_guides(&[g("first", "val later"), g("later", "val 10")], ctx());
assert_eq!(vs["first"], 0.0);
assert_eq!(vs["later"], 10.0);
}
#[test]
fn malformed_formula_is_skipped() {
let vs = evaluate_guides(&[g("a", "bogus_op 1 2")], ctx());
assert!(!vs.contains_key("a"));
}
#[test]
fn resolve_adj_coord_lit_vs_guide() {
let mut values = GuideValues::new();
values.insert("x".into(), 42.0);
assert_eq!(resolve_adj_coord(&AdjCoord::Lit(7), &values, ctx()), 7.0);
assert_eq!(
resolve_adj_coord(&AdjCoord::Guide("x".into()), &values, ctx()),
42.0
);
assert_eq!(
resolve_adj_coord(&AdjCoord::Guide("w".into()), &values, ctx()),
100.0
);
assert_eq!(
resolve_adj_coord(&AdjCoord::Guide("nope".into()), &values, ctx()),
0.0
);
}
#[test]
fn angle_constants_cd4_is_90deg() {
let vs = evaluate_guides(&[g("a", "val cd4")], ctx());
assert_eq!(vs["a"], 5_400_000.0);
}
}