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
@@ -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);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user