use super::*;
impl EngineState {
pub fn expressions_json(&self) -> String {
self.history.expressions()
}
pub fn set_expressions(&mut self, expressions: &str) -> String {
self.history.set_expressions(expressions);
self.rerun_history()
}
pub fn configurator_json(&self) -> String {
self.history.configurator().to_string()
}
pub fn expression_variables_json(&self) -> String {
let vars = parse_expression_variables(&self.history.expressions());
serde_json::to_string(&vars).unwrap_or_else(|_| "[]".to_string())
}
}
fn parse_expression_variables(source: &str) -> Vec<serde_json::Value> {
let mut cleaned = String::with_capacity(source.len());
for line in source.lines() {
let code = match line.find("//") {
Some(idx) => &line[..idx],
None => line,
};
cleaned.push_str(code);
cleaned.push('\n');
}
let mut out = Vec::new();
for stmt in cleaned.split(';') {
let stmt = stmt.trim();
if stmt.is_empty() {
continue;
}
let Some(eq) = stmt.find('=') else {
continue;
};
let name = stmt[..eq].trim();
let expr = stmt[eq + 1..].trim();
if name.is_empty() || expr.is_empty() || !is_identifier(name) {
continue;
}
out.push(serde_json::json!({ "name": name, "expr": expr }));
}
out
}
fn is_identifier(s: &str) -> bool {
let mut chars = s.chars();
match chars.next() {
Some(c) if c.is_ascii_alphabetic() || c == '_' || c == '$' => {}
_ => return false,
}
chars.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$')
}
#[cfg(test)]
mod expressions_tests {
use super::*;
fn extent_x(engine: &EngineState, name: &str) -> f64 {
let s = engine.scene.solid(name).expect("solid present");
s.bbox.max[0] - s.bbox.min[0]
}
fn var_cube_request(expressions: &str) -> String {
serde_json::json!({
"expressions": expressions,
"configurator": {},
"features": [{
"type": "P.CU",
"inputParams": {
"id": "Var",
"sizeX": "boxW", "sizeY": 20.0, "sizeZ": 20.0,
"transform": {
"position": [0.0, 0.0, 0.0],
"rotationEuler": [0.0, 0.0, 0.0],
"scale": [1.0, 1.0, 1.0]
},
"boolean": { "targets": [], "operation": "NONE" }
},
"persistentData": {}
}]
})
.to_string()
}
#[test]
fn expressions_drive_feature_param_and_resize_live() {
let mut engine = EngineState::new();
engine.set_history_json(&var_cube_request("boxW = 30;")).unwrap();
let x0 = extent_x(&engine, "Var");
assert!((x0 - 30.0).abs() < 1e-6, "sizeX resolved from boxW=30: {x0}");
engine.set_expressions("boxW = 50;");
let x1 = extent_x(&engine, "Var");
assert!(x1 > x0 + 10.0, "raising boxW resized the cube: {x1} > {x0}");
assert!((x1 - 50.0).abs() < 1e-6, "sizeX now resolves to 50: {x1}");
assert_eq!(engine.expressions_json(), "boxW = 50;");
let vars: serde_json::Value =
serde_json::from_str(&engine.expression_variables_json()).unwrap();
assert_eq!(vars[0]["name"], "boxW");
assert_eq!(vars[0]["expr"], "50");
}
#[test]
fn parse_variables_handles_comments_and_computed_rhs() {
let vars = parse_expression_variables(
"boxW = 30; // width\nboxH = boxW / 2;\n; \n7 = bad;",
);
assert_eq!(vars.len(), 2);
assert_eq!(vars[0]["name"], "boxW");
assert_eq!(vars[0]["expr"], "30");
assert_eq!(vars[1]["name"], "boxH");
assert_eq!(vars[1]["expr"], "boxW / 2");
}
}