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 <[email protected]> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
1 parent
6f349b79f5
commit
f0cfeba59b
8 files changed
+665
-14
No files matched your search
@@ -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 }))
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
Reference in new issue
Block a user