use serde::Deserialize; use std::collections::HashMap; use std::sync::OnceLock; #[derive(Debug, Clone, Deserialize)] pub struct ParamDef { pub default: String, #[allow(dead_code)] pub desc: String, } #[derive(Debug, Clone, Deserialize)] pub struct ComponentDef { #[serde(rename = "type")] pub ctype: String, #[serde(default)] pub pts: Vec<[String; 2]>, #[serde(default)] pub cx: String, #[serde(default)] pub cy: String, #[serde(default)] pub rx: String, #[serde(default)] pub ry: String, #[serde(default)] pub r: String, #[serde(default)] pub n: usize, #[serde(default)] pub fill: String, #[serde(default)] pub closed: bool, } #[derive(Debug, Clone, Deserialize)] pub struct ShapeTemplateDef { pub name: String, #[allow(dead_code)] pub size_mm: u32, pub description: String, #[serde(default)] pub parameters: HashMap, pub components: Vec, } pub struct TemplateComponent { pub pts: Vec<[f32; 2]>, pub closed: bool, pub fill: bool, pub fill_color: String, } static TEMPLATES: OnceLock> = OnceLock::new(); fn init_templates() -> Vec { let json = include_str!("../../hcie-ai/templates_db.json"); serde_json::from_str(json).unwrap_or_default() } pub fn get_template_def(name: &str) -> Option { TEMPLATES.get_or_init(init_templates).iter().find(|t| t.name == name).cloned() } pub fn all_names() -> Vec { TEMPLATES.get_or_init(init_templates).iter().map(|t| t.name.clone()).collect() } pub fn search_templates(query: &str) -> Vec { let q_lower = query.to_lowercase(); TEMPLATES.get_or_init(init_templates) .iter() .filter(|t| t.name.to_lowercase().contains(&q_lower) || t.description.to_lowercase().contains(&q_lower)) .cloned() .collect() } pub fn eval_template(def: &ShapeTemplateDef, provided_params: &HashMap) -> Vec { let mut num_vars = HashMap::new(); let mut str_vars = HashMap::new(); for (k, v) in &def.parameters { let val_str = provided_params.get(k).unwrap_or(&v.default); if let Ok(num) = val_str.parse::() { num_vars.insert(k.clone(), num); } else { str_vars.insert(k.clone(), val_str.clone()); } } let eval = |expr: &str| -> f32 { if let Ok(num) = expr.parse::() { return num; } math_eval(expr, &num_vars).unwrap_or(0.0) }; let subst = |expr: &str| -> String { let mut res = expr.to_string(); for (k, v) in &str_vars { res = res.replace(&format!("{{{}}}", k), v); } res }; let mut out = Vec::new(); for c in &def.components { let fill_str = subst(&c.fill); let fill = fill_str != "none" && !fill_str.is_empty(); let mut pts = Vec::new(); match c.ctype.as_str() { "path" => { for pt in &c.pts { pts.push([eval(&pt[0]), eval(&pt[1])]); } } "circle" => { let cx = eval(&c.cx); let cy = eval(&c.cy); let rx_val = if !c.r.is_empty() { eval(&c.r) } else { eval(&c.rx) }; let ry_val = if !c.r.is_empty() { eval(&c.r) } else { eval(&c.ry) }; pts = circle_points(cx, cy, rx_val, ry_val, c.n.max(8)); } "star" => { let outer = eval(&c.rx); let inner = eval(&c.ry); pts = star_points(c.n.max(5), outer, inner); } _ => {} } if !pts.is_empty() { out.push(TemplateComponent { pts, closed: c.closed, fill, fill_color: fill_str }); } } out } fn math_eval(expr: &str, vars: &HashMap) -> Option { let tokens = tokenize(expr); let mut pos = 0; parse_expr(&tokens, &mut pos, vars) } fn tokenize(expr: &str) -> Vec { let mut tokens = Vec::new(); let mut current = String::new(); for c in expr.chars() { if c.is_whitespace() { continue; } if "+-*/()".contains(c) { if !current.is_empty() { tokens.push(current.clone()); current.clear(); } tokens.push(c.to_string()); } else { current.push(c); } } if !current.is_empty() { tokens.push(current); } tokens } fn parse_expr(tokens: &[String], pos: &mut usize, vars: &HashMap) -> Option { let mut left = parse_term(tokens, pos, vars)?; while *pos < tokens.len() { let op = &tokens[*pos]; if op == "+" { *pos += 1; left += parse_term(tokens, pos, vars)?; } else if op == "-" { *pos += 1; left -= parse_term(tokens, pos, vars)?; } else { break; } } Some(left) } fn parse_term(tokens: &[String], pos: &mut usize, vars: &HashMap) -> Option { let mut left = parse_factor(tokens, pos, vars)?; while *pos < tokens.len() { let op = &tokens[*pos]; if op == "*" { *pos += 1; left *= parse_factor(tokens, pos, vars)?; } else if op == "/" { *pos += 1; left /= parse_factor(tokens, pos, vars)?; } else { break; } } Some(left) } fn parse_factor(tokens: &[String], pos: &mut usize, vars: &HashMap) -> Option { if *pos >= tokens.len() { return None; } let t = &tokens[*pos]; if t == "(" { *pos += 1; let val = parse_expr(tokens, pos, vars)?; if *pos < tokens.len() && tokens[*pos] == ")" { *pos += 1; } return Some(val); } if let Ok(n) = t.parse::() { *pos += 1; return Some(n); } if let Some(v) = vars.get(t) { *pos += 1; return Some(*v); } None } fn circle_points(cx: f32, cy: f32, rx: f32, ry: f32, n: usize) -> Vec<[f32; 2]> { let mut pts = Vec::with_capacity(n); for i in 0..n { let a = i as f32 / n as f32 * std::f32::consts::TAU; pts.push([cx + a.cos() * rx, cy + a.sin() * ry]); } pts } fn star_points(n: usize, outer: f32, inner: f32) -> Vec<[f32; 2]> { let mut pts = Vec::with_capacity(n * 2); for i in 0..n { let a1 = i as f32 / n as f32 * std::f32::consts::TAU - std::f32::consts::FRAC_PI_2; let a2 = (i as f32 + 0.5) / n as f32 * std::f32::consts::TAU - std::f32::consts::FRAC_PI_2; pts.push([a1.cos() * outer, a1.sin() * outer]); pts.push([a2.cos() * inner, a2.sin() * inner]); } pts }