Parameters engine: feature inputs as named model parameters (dN, editable, referenceable), parameters table with sources, values, dependencies (uses/used by) and cycles, favourites, comments, CSV/JSON export and import; more units (inch, mil, yd, km, nm) and functions (exp, ln, log, sign, pow, hypot)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
bherbruckandClaude Opus 5.5 committed 2026-10-07 23:23:50 +00:00
1 parent 6f349b79f5
commit f0cfeba59b
8 files changed
+665 -14

No files matched your search

+62 -1
View File
@@ -10,6 +10,13 @@ use crate::{EngineError, Result, Sel, Session, Snapshot};
pub static COMMANDS: &[CommandSpec] = &[
CommandSpec::new("UndoCommand", "Undo", undo).icon("undo").key("Ctrl+Z").noundo(),
CommandSpec::new("RedoCommand", "Redo", redo).icon("redo").key("Ctrl+Y").noundo(),
CommandSpec::new("parameters.list", "List Parameters", params_list).noundo().params("favorites?: bool (only favourites)"),
CommandSpec::new("parameters.favorite", "Favourite Parameter", params_favorite).params("name, favorite?: bool (default true)"),
CommandSpec::new("parameters.export", "Export Parameters", params_export)
.noundo()
.params("format?: csv|json (default csv), path?: write to a file"),
CommandSpec::new("parameters.import", "Import Parameters", params_import)
.params("text: CSV (name,unit,expression,comment) or JSON [{name, expression, unit?, comment?}] | path"),
CommandSpec::new("ChangeParameterCommand", "Change Parameters", change_param)
.at("SOLID", "MODIFY")
.icon("params")
@@ -76,7 +83,7 @@ fn change_param(s: &mut Session, p: &Value) -> Result<Value> {
};
let unit = str_(p, "unit");
let comment = str_(p, "comment");
s.doc_mut().set_param(name, &e, unit, comment)?;
s.doc_mut().change_param(name, &e, unit, comment)?;
s.refresh();
let (vals, _) = s.doc.param_values();
let v = vals.get(name).map(|v| v.v);
@@ -333,3 +340,57 @@ fn dependents(s: &mut Session, p: &Value) -> Result<Value> {
let broken: Vec<String> = newly_broken(&s.model, &after).into_iter().map(|(n, _)| n).collect();
Ok(json!({"deleted_with_it": names(&gone), "would_fail": broken}))
}
fn params_list(s: &mut Session, p: &Value) -> Result<Value> {
let only_fav = bool_(p, "favorites").unwrap_or(false);
let rows: Vec<solvecraft_doc::ParamRow> = s.doc.param_rows().into_iter().filter(|r| !only_fav || r.favorite).collect();
let cycles = solvecraft_doc::cycles(&s.doc.all_param_exprs());
Ok(json!({"parameters": rows, "cycles": cycles}))
}
fn params_favorite(s: &mut Session, p: &Value) -> Result<Value> {
let cmd = "parameters.favorite";
let name = str_(p, "name").ok_or_else(|| bad(cmd, "`name` is required"))?;
let on = bool_(p, "favorite").unwrap_or(true);
s.doc_mut().set_favorite(name, on)?;
Ok(json!({"name": name, "favorite": on}))
}
fn params_export(s: &mut Session, p: &Value) -> Result<Value> {
let cmd = "parameters.export";
let text = match str_(p, "format").unwrap_or("csv") {
"csv" => s.doc.params_csv(),
"json" => {
let rows: Vec<Value> = s
.doc
.param_rows()
.into_iter()
.filter(|r| r.source == "user")
.map(|r| json!({"name": r.name, "expression": r.expression, "unit": r.unit, "value": r.value, "comment": r.comment, "favorite": r.favorite}))
.collect();
serde_json::to_string_pretty(&rows).map_err(|e| bad(cmd, e.to_string()))?
}
other => return Err(bad(cmd, format!("unknown format `{other}`"))),
};
if let Some(path) = str_(p, "path") {
std::fs::write(path, &text).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
}
Ok(json!({ "text": text }))
}
fn params_import(s: &mut Session, p: &Value) -> Result<Value> {
let cmd = "parameters.import";
let text = match (str_(p, "text"), str_(p, "path")) {
(Some(t), _) => t.to_string(),
(None, Some(path)) => {
let meta = std::fs::metadata(path).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
if meta.len() > 16 << 20 {
return Err(bad(cmd, "file too large"));
}
std::fs::read_to_string(path).map_err(|e| EngineError::Other(format!("{path}: {e}")))?
}
_ => return Err(bad(cmd, "give `text` or `path`")),
};
let names = s.doc_mut().import_params(&text)?;
Ok(json!({ "imported": names }))
}
+1
View File
@@ -240,6 +240,7 @@ fn add_feature(s: &mut Session, p: &Value, kind: FeatureKind) -> Result<Value> {
let mut kind = kind;
super::component::to_active_frame(s, &mut kind);
let id = s.doc_mut().add_feature(kind, name)?;
s.doc_mut().name_feature_inputs(id);
let comp = s.active_component;
if comp != 0
&& let Some(f) = s.doc_mut().feature_mut(id)
+37
View File
@@ -804,3 +804,40 @@ fn components_occurrences_and_world_placement() {
let migrated = solvecraft_doc::Document::from_json(&old.to_string()).unwrap();
assert_eq!(migrated.occurrences.len(), migrated.components.len());
}
#[test]
fn feature_inputs_are_parameters() {
let mut s = Session::default();
run(&mut s, "ChangeParameterCommand", json!({"name": "w", "expression": "15 mm", "comment": "width"}));
run(&mut s, "PrimitiveBox", json!({"length": 10, "width": "w", "height": "1 in", "name": "B"}));
let l = run(&mut s, "parameters.list", json!({}));
let rows = l["parameters"].as_array().unwrap();
let feat: Vec<&Value> = rows.iter().filter(|r| r["source"] == "feature").collect();
assert_eq!(feat.len(), 3, "{l}");
let height = feat.iter().find(|r| r["input"] == "Height").unwrap();
assert!((height["value"].as_f64().unwrap() - 25.4).abs() < 1e-9);
let length_name = feat.iter().find(|r| r["input"] == "Length").unwrap()["name"].as_str().unwrap().to_string();
// A feature parameter changes the feature; other expressions can use it.
run(&mut s, "ChangeParameterCommand", json!({"name": length_name, "expression": "2 * w"}));
assert!(rel(volume(&mut s), 30.0 * 15.0 * 25.4) < 1e-9);
run(&mut s, "ChangeParameterCommand", json!({"name": "t", "expression": format!("{length_name} / 3")}));
let t = run(&mut s, "parameters.list", json!({}))["parameters"].as_array().unwrap().iter().find(|r| r["name"] == "t").unwrap()["value"]
.as_f64()
.unwrap();
assert!((t - 10.0).abs() < 1e-9);
// Cycles are refused.
assert!(s.execute("ChangeParameterCommand", &json!({"name": "w", "expression": "t"})).is_err());
// Favourites, export and import.
run(&mut s, "parameters.favorite", json!({"name": "w"}));
assert_eq!(run(&mut s, "parameters.list", json!({"favorites": true}))["parameters"].as_array().unwrap().len(), 1);
let csv = run(&mut s, "parameters.export", json!({}))["text"].as_str().unwrap().to_string();
assert!(csv.contains("w,mm,15 mm,15,width,true"), "{csv}");
run(&mut s, "parameters.import", json!({"text": "name,unit,expression,comment\nw,mm,20 mm,wider\nnew_p,mm,\"w + 1\","}));
assert!(rel(volume(&mut s), 40.0 * 20.0 * 25.4) < 1e-9);
run(&mut s, "parameters.import", json!({"text": "[{\"name\": \"w\", \"expression\": \"12\"}]"}));
assert!(rel(volume(&mut s), 24.0 * 12.0 * 25.4) < 1e-9);
assert!(s.execute("parameters.import", &json!({"text": "name,expression\nw,1/"})).is_err());
// Saved and reloaded, the names stay.
let back = solvecraft_doc::Document::from_json(&s.doc.to_json()).unwrap();
assert_eq!(back.features[0].param_names, s.doc.features[0].param_names);
}