Configurations: a configuration table of rows (parameter expressions and feature suppressions per row) that switch the design (config.activate: undoable, all or nothing), kept through parameter renames and feature deletes; FusionStartDesignConfigModeCmd, FusionShowDesignConfigPanelCmd (with whether the design still matches the active row), config.column, config.row
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
1 parent
10a67ef259
commit
b78cb886b8
7 files changed
+385
No files matched your search
@@ -52,6 +52,7 @@ headline counts in-scope tabs only and lists the deferred ones separately.
|
||||
| M10 | Sheet metal: flange (base, edge, contour), hem, unfold/refold, convert, flat pattern + DXF, rules: done (oracle 67-76). PLASTIC enclosure subset: Boss, Rib, Web, Lip/Groove, Snap Fit, Rest, Plastic Rules (library, edit, assign; rule draft and clearance as feature defaults): done, no Fusion oracle (licence), tested against hand-computed geometry. UI: SHEET METAL and PLASTIC tabs in Fusion's layout with dialogs (live preview, Edit Feature, pre-selection) for Flange, Hem, Unfold/Refold, Convert, Sheet Metal Rules, Create Flat Pattern (flat view with bend lines, bend table, DXF export), Boss, Lip/Groove, Snap Fit, Rest, Manage and Assign Plastic Rules; Bend (Fold) has no engine command yet | in progress | 36 |
|
||||
| M11 | 2D drawings | deferred | — |
|
||||
| M12 | Release: installers, signing, docs, performance | planned | 16 |
|
||||
| M14 | Configurations: a table of rows (parameter expressions and feature suppressions per row) switched with config.activate (undoable, all or nothing), kept through parameter renames and feature deletes (FusionStartDesignConfigModeCmd, FusionShowDesignConfigPanelCmd, config.column/row/activate). Left: configuration rules, per-row materials and appearances | in progress | 4 |
|
||||
| M13 | Never lose work: atomic saves (temporary file, flush, rename; a crash at any moment leaves the old file or the new one) with the replaced version kept as `<file>.bak`; autosave of unsaved changes to the recovery folder every 5 minutes (preference) and after big operations, only when something changed; "Recover unsaved design?" on the next launch (`doc.recovery_list`, `doc.recover`, `doc.recovery_discard`); a kill-mid-save test (process killed while saving a 0.5 MB design 25 times). File format: `"format": "solvecraft/2"`, older formats upgraded step by step (`doc::format`), newer ones refused with a message, out-of-range numbers and broken placements refused; a corpus of designs saved by M0, components-era, joints-era and current builds (apps/solvecraft-cli/tests/corpus) that must open and evaluate as saved; damaged and hostile variants (truncation, bit flips, hostile values, removed keys, repeated features, deep nesting) never panic | done | 6 |
|
||||
|
||||
## Known limitations
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
//! Configurations: a table whose rows are variants of the design. Each column is a parameter
|
||||
//! (its expression per row) or a feature (suppressed or not per row); activating a row sets
|
||||
//! those values in the design, which then rebuilds as usual.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{DocError, Document, Result};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Column {
|
||||
/// A parameter's expression.
|
||||
Param(String),
|
||||
/// A feature's suppression (by feature id).
|
||||
Suppress(u64),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct ConfigRow {
|
||||
pub name: String,
|
||||
/// One cell per column: an expression, or "true"/"false" for suppression.
|
||||
pub cells: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
pub struct ConfigTable {
|
||||
pub columns: Vec<Column>,
|
||||
pub rows: Vec<ConfigRow>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub active: Option<String>,
|
||||
}
|
||||
|
||||
pub const MAX_COLUMNS: usize = 500;
|
||||
pub const MAX_ROWS: usize = 2000;
|
||||
|
||||
impl ConfigTable {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.columns.is_empty() && self.rows.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl Document {
|
||||
/// A column's current value in the design.
|
||||
pub fn config_current(&self, c: &Column) -> Option<String> {
|
||||
match c {
|
||||
Column::Param(n) => self.all_param_exprs().into_iter().find(|(m, _, _)| m == n).map(|(_, e, _)| e),
|
||||
Column::Suppress(id) => self.feature(*id).map(|f| f.suppressed.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// A column's display key: `param:<name>` or `suppress:<feature name>`.
|
||||
pub fn config_key(&self, c: &Column) -> String {
|
||||
match c {
|
||||
Column::Param(n) => format!("param:{n}"),
|
||||
Column::Suppress(id) => format!("suppress:{}", self.feature(*id).map(|f| f.name.clone()).unwrap_or_else(|| id.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the design to a row's values (all or nothing).
|
||||
pub fn apply_configuration(&mut self, row: &str) -> Result<()> {
|
||||
let r = self.configs.rows.iter().find(|r| r.name == row).cloned().ok_or_else(|| DocError::Unknown(format!("configuration `{row}`")))?;
|
||||
let mut next = self.clone();
|
||||
for (c, v) in self.configs.columns.iter().zip(&r.cells) {
|
||||
match c {
|
||||
Column::Param(n) => {
|
||||
if next.config_current(c).as_deref() != Some(v.as_str()) {
|
||||
next.change_param(n, v, None, None).map_err(|e| DocError::Invalid(format!("configuration `{row}`, parameter `{n}`: {e}")))?;
|
||||
}
|
||||
}
|
||||
Column::Suppress(id) => {
|
||||
let on = match v.trim() {
|
||||
"true" | "1" | "yes" => true,
|
||||
"false" | "0" | "no" => false,
|
||||
o => return Err(DocError::Invalid(format!("configuration `{row}`: suppression must be true or false, not `{o}`"))),
|
||||
};
|
||||
let f = next.feature_mut(*id).ok_or_else(|| DocError::Unknown(format!("configuration `{row}`: feature {id} is gone")))?;
|
||||
f.suppressed = on;
|
||||
}
|
||||
}
|
||||
}
|
||||
next.configs.active = Some(row.to_string());
|
||||
*self = next;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The active row's values all hold in the design.
|
||||
pub fn configuration_matches(&self) -> bool {
|
||||
let Some(r) = self.configs.active.as_ref().and_then(|a| self.configs.rows.iter().find(|r| r.name == *a)) else { return false };
|
||||
self.configs.columns.iter().zip(&r.cells).all(|(c, v)| match c {
|
||||
Column::Suppress(_) => self.config_current(c).as_deref() == Some(v.trim()),
|
||||
Column::Param(_) => self.config_current(c).is_some_and(|x| x.replace(' ', "") == v.replace(' ', "")),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn rows_switch_parameters() {
|
||||
let mut d = Document::new("A");
|
||||
d.set_param("w", "10 mm", None, None).unwrap();
|
||||
d.configs.columns.push(Column::Param("w".into()));
|
||||
d.configs.rows.push(ConfigRow { name: "Small".into(), cells: vec!["10 mm".into()] });
|
||||
d.configs.rows.push(ConfigRow { name: "Large".into(), cells: vec!["40 mm".into()] });
|
||||
d.apply_configuration("Large").unwrap();
|
||||
assert_eq!(d.config_current(&Column::Param("w".into())).as_deref(), Some("40 mm"));
|
||||
assert!(d.configuration_matches());
|
||||
d.set_param("w", "12 mm", None, None).unwrap();
|
||||
assert!(!d.configuration_matches());
|
||||
d.configs.rows.push(ConfigRow { name: "Bad".into(), cells: vec!["1/".into()] });
|
||||
let before = d.clone();
|
||||
assert!(d.apply_configuration("Bad").is_err());
|
||||
assert_eq!(d, before, "all or nothing");
|
||||
}
|
||||
}
|
||||
@@ -1057,6 +1057,9 @@ pub struct Document {
|
||||
/// Plastic rules.
|
||||
#[serde(default, skip_serializing_if = "crate::plastic::PlasticSettings::is_empty")]
|
||||
pub plastic: crate::plastic::PlasticSettings,
|
||||
/// Configurations (rows of parameter values and suppressions).
|
||||
#[serde(default, skip_serializing_if = "crate::config::ConfigTable::is_empty")]
|
||||
pub configs: crate::config::ConfigTable,
|
||||
/// Favourite parameters (by name).
|
||||
#[serde(default, skip_serializing_if = "std::collections::BTreeSet::is_empty")]
|
||||
pub favorites: std::collections::BTreeSet<String>,
|
||||
@@ -1140,6 +1143,7 @@ impl Document {
|
||||
assembly: Default::default(),
|
||||
sheet: Default::default(),
|
||||
plastic: Default::default(),
|
||||
configs: Default::default(),
|
||||
body_components: Default::default(),
|
||||
materials: Default::default(),
|
||||
appearances: Default::default(),
|
||||
@@ -1296,6 +1300,16 @@ impl Document {
|
||||
}
|
||||
}
|
||||
self.features.retain(|f| !gone.contains(&f.id));
|
||||
// Configuration columns of deleted features go with them.
|
||||
let keep: Vec<bool> = self.configs.columns.iter().map(|c| !matches!(c, crate::config::Column::Suppress(id) if gone.contains(id))).collect();
|
||||
if keep.contains(&false) {
|
||||
let mut k = keep.iter();
|
||||
self.configs.columns.retain(|_| k.next().copied().unwrap_or(true));
|
||||
for r in &mut self.configs.rows {
|
||||
let mut k = keep.iter();
|
||||
r.cells.retain(|_| k.next().copied().unwrap_or(true));
|
||||
}
|
||||
}
|
||||
if let Some(m) = self.marker
|
||||
&& m > idx
|
||||
{
|
||||
|
||||
@@ -9,6 +9,7 @@ pub mod appearance;
|
||||
mod assembly;
|
||||
pub mod canvas;
|
||||
mod clipboard;
|
||||
pub mod config;
|
||||
mod document;
|
||||
mod eval;
|
||||
pub mod expr;
|
||||
|
||||
@@ -650,6 +650,17 @@ impl Document {
|
||||
if self.favorites.remove(old) {
|
||||
self.favorites.insert(new.to_string());
|
||||
}
|
||||
for c in &mut self.configs.columns {
|
||||
if *c == crate::config::Column::Param(old.to_string()) {
|
||||
*c = crate::config::Column::Param(new.to_string());
|
||||
}
|
||||
}
|
||||
// Configuration cells refer to parameters too.
|
||||
for r in &mut self.configs.rows {
|
||||
for cell in &mut r.cells {
|
||||
*cell = expr::rename_reference(cell, old, new);
|
||||
}
|
||||
}
|
||||
if let Some(c) = self.param_comments.remove(old) {
|
||||
self.param_comments.insert(new.to_string(), c);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
//! Configurations: the design's configuration table (rows of parameter values and feature
|
||||
//! suppressions) and switching between its rows.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use solvecraft_doc::config::{Column, ConfigRow, MAX_COLUMNS, MAX_ROWS};
|
||||
|
||||
use super::CommandSpec;
|
||||
use crate::params::{bad, bool_, str_, string_list};
|
||||
use crate::{Result, Session};
|
||||
|
||||
pub static COMMANDS: &[CommandSpec] = &[
|
||||
CommandSpec::new("FusionStartDesignConfigModeCmd", "Configure", configure).at("SOLID", "CONFIGURE").icon("params").params(
|
||||
"params?: [parameter names]; features?: [feature names] (columns to add; with no rows yet, a \"Default\" row takes the current values)",
|
||||
),
|
||||
CommandSpec::new("FusionShowDesignConfigPanelCmd", "Display Configuration Table", show)
|
||||
.at("SOLID", "CONFIGURE")
|
||||
.icon("params")
|
||||
.noundo()
|
||||
.params("→ columns (param:<name> or suppress:<feature>), rows, the active row and whether the design still matches it"),
|
||||
CommandSpec::new("config.column", "Configuration Column", column).params("param? | feature? (name); delete?: bool"),
|
||||
CommandSpec::new("config.row", "Configuration Row", row).params(
|
||||
"name; from? (row to copy; default the current values); values?: {param name or feature name: expression or true/false}; rename?; delete?: bool",
|
||||
),
|
||||
CommandSpec::new("config.activate", "Activate Configuration", activate).params("row: name — sets its parameter values and suppressions"),
|
||||
];
|
||||
|
||||
fn column_of(s: &Session, p: &Value, cmd: &str) -> Result<Column> {
|
||||
if let Some(n) = str_(p, "param") {
|
||||
if !s.doc.all_param_exprs().iter().any(|(m, _, _)| m == n) {
|
||||
return Err(bad(cmd, format!("no parameter `{n}`")));
|
||||
}
|
||||
return Ok(Column::Param(n.to_string()));
|
||||
}
|
||||
let f = str_(p, "feature").ok_or_else(|| bad(cmd, "give `param` or `feature`"))?;
|
||||
let id = s.doc.find_feature(f).map(|x| x.id).ok_or_else(|| bad(cmd, format!("no feature `{f}`")))?;
|
||||
Ok(Column::Suppress(id))
|
||||
}
|
||||
|
||||
/// Add a column, filling existing rows with the design's current value.
|
||||
fn add_column(s: &mut Session, c: Column, cmd: &str) -> Result<()> {
|
||||
if s.doc.configs.columns.contains(&c) {
|
||||
return Ok(());
|
||||
}
|
||||
if s.doc.configs.columns.len() >= MAX_COLUMNS {
|
||||
return Err(bad(cmd, "too many columns"));
|
||||
}
|
||||
let cur = s.doc.config_current(&c).unwrap_or_default();
|
||||
let t = &mut s.doc_mut().configs;
|
||||
t.columns.push(c);
|
||||
for r in &mut t.rows {
|
||||
r.cells.push(cur.clone());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn configure(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "FusionStartDesignConfigModeCmd";
|
||||
for n in string_list(p, "params") {
|
||||
let c = column_of(s, &json!({ "param": n }), cmd)?;
|
||||
add_column(s, c, cmd)?;
|
||||
}
|
||||
for n in string_list(p, "features") {
|
||||
let c = column_of(s, &json!({ "feature": n }), cmd)?;
|
||||
add_column(s, c, cmd)?;
|
||||
}
|
||||
if s.doc.configs.rows.is_empty() {
|
||||
let cells = s.doc.configs.columns.iter().map(|c| s.doc.config_current(c).unwrap_or_default()).collect();
|
||||
let t = &mut s.doc_mut().configs;
|
||||
t.rows.push(ConfigRow { name: "Default".into(), cells });
|
||||
t.active = Some("Default".into());
|
||||
}
|
||||
show(s, p)
|
||||
}
|
||||
|
||||
fn show(s: &mut Session, _p: &Value) -> Result<Value> {
|
||||
let t = &s.doc.configs;
|
||||
let keys: Vec<String> = t.columns.iter().map(|c| s.doc.config_key(c)).collect();
|
||||
let rows: Vec<Value> = t
|
||||
.rows
|
||||
.iter()
|
||||
.map(|r| json!({"name": r.name, "values": keys.iter().zip(&r.cells).map(|(k, v)| (k.clone(), json!(v))).collect::<serde_json::Map<_, _>>()}))
|
||||
.collect();
|
||||
Ok(json!({"columns": keys, "rows": rows, "active": t.active, "matches": s.doc.configuration_matches()}))
|
||||
}
|
||||
|
||||
fn column(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "config.column";
|
||||
let c = column_of(s, p, cmd)?;
|
||||
if bool_(p, "delete").unwrap_or(false) {
|
||||
let t = &mut s.doc_mut().configs;
|
||||
let i = t.columns.iter().position(|x| *x == c).ok_or_else(|| bad(cmd, "not a column of the table"))?;
|
||||
t.columns.remove(i);
|
||||
for r in &mut t.rows {
|
||||
if i < r.cells.len() {
|
||||
r.cells.remove(i);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
add_column(s, c, cmd)?;
|
||||
}
|
||||
show(s, p)
|
||||
}
|
||||
|
||||
fn row(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "config.row";
|
||||
let name = str_(p, "name").map(str::trim).filter(|n| !n.is_empty() && n.len() <= 128).ok_or_else(|| bad(cmd, "`name` is required"))?.to_string();
|
||||
let idx = s.doc.configs.rows.iter().position(|r| r.name == name);
|
||||
if bool_(p, "delete").unwrap_or(false) {
|
||||
let i = idx.ok_or_else(|| bad(cmd, format!("no row `{name}`")))?;
|
||||
let t = &mut s.doc_mut().configs;
|
||||
t.rows.remove(i);
|
||||
if t.active.as_deref() == Some(name.as_str()) {
|
||||
t.active = None;
|
||||
}
|
||||
return show(s, p);
|
||||
}
|
||||
let mut cells: Vec<String> = match (idx, str_(p, "from")) {
|
||||
(_, Some(f)) => s.doc.configs.rows.iter().find(|r| r.name == f).map(|r| r.cells.clone()).ok_or_else(|| bad(cmd, format!("no row `{f}`")))?,
|
||||
(Some(i), None) => s.doc.configs.rows.get(i).map(|r| r.cells.clone()).unwrap_or_default(),
|
||||
(None, None) => s.doc.configs.columns.iter().map(|c| s.doc.config_current(c).unwrap_or_default()).collect(),
|
||||
};
|
||||
if let Some(vals) = p.get("values") {
|
||||
let vals = vals.as_object().ok_or_else(|| bad(cmd, "`values` must map columns to values"))?;
|
||||
for (k, v) in vals {
|
||||
let text = match v {
|
||||
Value::String(t) => t.clone(),
|
||||
Value::Bool(b) => b.to_string(),
|
||||
Value::Number(n) => n.to_string(),
|
||||
_ => return Err(bad(cmd, format!("value for `{k}` must be an expression or true/false"))),
|
||||
};
|
||||
if text.len() > 4096 {
|
||||
return Err(bad(cmd, "value too long"));
|
||||
}
|
||||
let key = k.strip_prefix("param:").or_else(|| k.strip_prefix("suppress:")).unwrap_or(k);
|
||||
let col = s
|
||||
.doc
|
||||
.configs
|
||||
.columns
|
||||
.iter()
|
||||
.position(|c| match c {
|
||||
Column::Param(n) => n == key,
|
||||
Column::Suppress(id) => s.doc.feature(*id).is_some_and(|f| f.name == key),
|
||||
})
|
||||
.ok_or_else(|| bad(cmd, format!("`{k}` is not a column (config.column adds one)")))?;
|
||||
if let Some(cell) = cells.get_mut(col) {
|
||||
*cell = text;
|
||||
}
|
||||
}
|
||||
}
|
||||
let rename = str_(p, "rename").map(str::trim).filter(|n| !n.is_empty() && n.len() <= 128).map(str::to_string);
|
||||
let t = &mut s.doc_mut().configs;
|
||||
match idx {
|
||||
Some(i) => {
|
||||
if let Some(r) = t.rows.get_mut(i) {
|
||||
r.cells = cells;
|
||||
if let Some(n) = rename {
|
||||
if t.active.as_deref() == Some(r.name.as_str()) {
|
||||
t.active = Some(n.clone());
|
||||
}
|
||||
r.name = n;
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
if t.rows.len() >= MAX_ROWS {
|
||||
return Err(bad(cmd, "too many rows"));
|
||||
}
|
||||
t.rows.push(ConfigRow { name, cells });
|
||||
}
|
||||
}
|
||||
show(s, p)
|
||||
}
|
||||
|
||||
fn activate(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "config.activate";
|
||||
let r = str_(p, "row").ok_or_else(|| bad(cmd, "`row` is required"))?.to_string();
|
||||
s.doc_mut().apply_configuration(&r).map_err(|e| bad(cmd, e.to_string()))?;
|
||||
s.refresh();
|
||||
let errors: Vec<String> = s.model.results.iter().filter_map(|x| x.error.clone()).collect();
|
||||
let mut out = show(s, p)?;
|
||||
out["errors"] = json!(errors);
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::Session;
|
||||
|
||||
fn run(s: &mut Session, id: &str, p: Value) -> Value {
|
||||
match s.execute(id, &p) {
|
||||
Ok(v) => v,
|
||||
Err(e) => panic!("{id} {p}: {e}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn volume(s: &mut Session) -> f64 {
|
||||
run(s, "MeasureCommand", json!({}))["total"]["volume_mm3"].as_f64().unwrap_or(f64::NAN)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn configurations_switch_sizes_and_suppressions() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "ChangeParameterCommand", json!({"name": "len", "expression": "40 mm"}));
|
||||
run(&mut s, "PrimitiveBox", json!({"length": "len", "width": 20, "height": 10}));
|
||||
run(&mut s, "FusionFilletEdgesCommand", json!({"edges": [[0, 0, 5]], "radius": 3}));
|
||||
let fillet = s.doc.features.last().map(|f| f.name.clone()).unwrap_or_default();
|
||||
let t = run(&mut s, "FusionStartDesignConfigModeCmd", json!({"params": ["len"], "features": [fillet]}));
|
||||
assert_eq!(t["rows"][0]["name"], "Default");
|
||||
run(&mut s, "config.row", json!({"name": "Long", "values": {"len": "80 mm"}}));
|
||||
run(&mut s, "config.row", json!({"name": "Sharp", "from": "Long", "values": {fillet.clone(): true}}));
|
||||
let filleted = 8000.0 - (9.0 - std::f64::consts::PI * 9.0 / 4.0) * 10.0;
|
||||
assert!((volume(&mut s) - filleted).abs() < 0.5);
|
||||
run(&mut s, "config.activate", json!({"row": "Long"}));
|
||||
assert!((volume(&mut s) - (16000.0 - (9.0 - std::f64::consts::PI * 9.0 / 4.0) * 10.0)).abs() < 0.5, "{}", volume(&mut s));
|
||||
run(&mut s, "config.activate", json!({"row": "Sharp"}));
|
||||
assert!((volume(&mut s) - 16000.0).abs() < 1e-6);
|
||||
let t = run(&mut s, "FusionShowDesignConfigPanelCmd", json!({}));
|
||||
assert_eq!((t["active"].as_str(), t["matches"].as_bool()), (Some("Sharp"), Some(true)), "{t}");
|
||||
// Undo goes back to the previous configuration's values.
|
||||
run(&mut s, "UndoCommand", json!({}));
|
||||
assert!(volume(&mut s) > 15000.0 && volume(&mut s) < 16000.0);
|
||||
// Renaming the parameter keeps the column; editing it by hand breaks the match.
|
||||
run(&mut s, "parameters.rename", json!({"name": "len", "new_name": "length"}));
|
||||
let t = run(&mut s, "FusionShowDesignConfigPanelCmd", json!({}));
|
||||
assert_eq!(t["columns"][0], "param:length", "{t}");
|
||||
run(&mut s, "ChangeParameterCommand", json!({"name": "length", "expression": "50 mm"}));
|
||||
assert_eq!(run(&mut s, "FusionShowDesignConfigPanelCmd", json!({}))["matches"], false);
|
||||
// Bad input.
|
||||
assert!(s.execute("config.row", &json!({"name": "X", "values": {"nope": 1}})).is_err());
|
||||
assert!(s.execute("config.activate", &json!({"row": "Missing"})).is_err());
|
||||
run(&mut s, "config.row", json!({"name": "Broken", "values": {"length": "1/"}}));
|
||||
assert!(s.execute("config.activate", &json!({"row": "Broken"})).is_err());
|
||||
run(&mut s, "config.row", json!({"name": "Broken", "delete": true}));
|
||||
run(&mut s, "config.column", json!({"feature": fillet, "delete": true}));
|
||||
assert_eq!(run(&mut s, "FusionShowDesignConfigPanelCmd", json!({}))["columns"].as_array().map(Vec::len), Some(1));
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ mod face;
|
||||
mod features;
|
||||
mod features_more;
|
||||
pub use features::auto_operation;
|
||||
mod config;
|
||||
mod file;
|
||||
mod inspect;
|
||||
pub(crate) mod joints;
|
||||
@@ -144,6 +145,7 @@ pub fn command_specs() -> Vec<&'static CommandSpec> {
|
||||
v.extend(component::COMMANDS.iter());
|
||||
v.extend(joints::COMMANDS.iter());
|
||||
v.extend(motion::COMMANDS.iter());
|
||||
v.extend(config::COMMANDS.iter());
|
||||
v.extend(browser::COMMANDS.iter());
|
||||
v.extend(delete::COMMANDS.iter());
|
||||
v.extend(edit::COMMANDS.iter());
|
||||
|
||||
Reference in new issue
Block a user