forked from 1b-agent/fieldcraft-upstream
model: rename a definition and every reference to it (model.rename, #290)
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
1 parent
587cbc3b01
commit
a0ae8b2111
7 files changed
+458
-2
No files matched your search
@@ -583,7 +583,8 @@ pub static CATALOGUE: &[Capability] = &[
|
||||
soon("results.read", "Read Results", Results, "Files", "M9", "Read results in public formats"),
|
||||
soon("results.convergence", "Convergence", Results, "Files", "M9", "Mesh convergence studies"),
|
||||
// ---- Tools ----
|
||||
ready("model.info", "Model Info", Tools, "Model", "Node and element counts and the model summary"),
|
||||
// Renaming starts from a definition's item menu (Rename…); the search finds it here too.
|
||||
ready_more("model.info", "Model Info", Tools, "Model", "Node and element counts and the model summary", &["model.rename"]),
|
||||
ready("file.open", "Open Model", Tools, "File", "Open a .fieldcraft model"),
|
||||
ready("file.sample", "Samples", Tools, "File", "Open a sample model, ready to solve"),
|
||||
ready("file.save", "Save", Tools, "File", "Save the model to its .fieldcraft file"),
|
||||
|
||||
@@ -13,6 +13,9 @@ use fieldcraft_model::{
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use fieldcraft_io::{Item, ItemKind};
|
||||
use fieldcraft_model::rename::Kind as RenameKind;
|
||||
|
||||
use super::{name, opt, req};
|
||||
use crate::requirement::Requirement;
|
||||
use crate::{CommandKind, CommandSpec, EngineError, Result, Session};
|
||||
@@ -356,6 +359,60 @@ pub const STEP_ACTIVATE: CommandSpec = CommandSpec {
|
||||
run: step_activate,
|
||||
};
|
||||
|
||||
pub const RENAME: CommandSpec = CommandSpec {
|
||||
id: "model.rename",
|
||||
label: "Rename",
|
||||
params: r#"{"kind": "set" | "material" | "section" | "step" | "boundary_condition" | "load" | "reference_point" | "coupling" | "contact" | "tie" | "point_mass" | "spring" | "fluid_domain" | "datum", "from": "end", "to": "Tip"} — renames the definition and every reference to it (sets in sections, couplings, contacts, ties, discrete elements, boundary conditions, loads and explicit steps; materials in sections and fluid domains; reference points; boundary conditions and loads in the steps they are active in; steps in imperfections; fluid domains in fluid steps; datum planes in partitions). A name already taken is refused; the viewport's hidden items follow the new name"#,
|
||||
form: Some(super::forms::RENAME),
|
||||
kind: CommandKind::Model,
|
||||
requires: &[],
|
||||
run: rename,
|
||||
};
|
||||
|
||||
/// `model.rename`'s kinds, as the command takes them.
|
||||
pub const RENAME_KINDS: &[(&str, RenameKind)] = &[
|
||||
("set", RenameKind::Set),
|
||||
("material", RenameKind::Material),
|
||||
("section", RenameKind::Section),
|
||||
("step", RenameKind::Step),
|
||||
("boundary_condition", RenameKind::BoundaryCondition),
|
||||
("load", RenameKind::Load),
|
||||
("reference_point", RenameKind::ReferencePoint),
|
||||
("coupling", RenameKind::Coupling),
|
||||
("contact", RenameKind::Contact),
|
||||
("tie", RenameKind::Tie),
|
||||
("point_mass", RenameKind::PointMass),
|
||||
("spring", RenameKind::Spring),
|
||||
("fluid_domain", RenameKind::FluidDomain),
|
||||
("datum", RenameKind::Datum),
|
||||
];
|
||||
|
||||
fn rename(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let kind: &str = req(p, "kind")?;
|
||||
let from: &str = req(p, "from")?;
|
||||
let to: &str = req(p, "to")?;
|
||||
let Some((_, k)) = RENAME_KINDS.iter().find(|(id, _)| *id == kind) else {
|
||||
let kinds: Vec<&str> = RENAME_KINDS.iter().map(|(id, _)| *id).collect();
|
||||
return Err(EngineError::Params(format!("`kind` is one of {}", kinds.join(", "))));
|
||||
};
|
||||
s.model.rename(*k, from, to)?;
|
||||
// What the viewport hides follows the new name.
|
||||
let item = match k {
|
||||
RenameKind::Datum => Some(ItemKind::Datum),
|
||||
RenameKind::BoundaryCondition => Some(ItemKind::BoundaryCondition),
|
||||
RenameKind::Load => Some(ItemKind::Load),
|
||||
RenameKind::ReferencePoint => Some(ItemKind::ReferencePoint),
|
||||
RenameKind::Coupling => Some(ItemKind::Coupling),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(kind) = item
|
||||
&& s.view.hidden.remove(&Item { kind, name: from.to_string() })
|
||||
{
|
||||
s.view.hidden.insert(Item { kind, name: to.to_string() });
|
||||
}
|
||||
Ok(json!({"kind": kind, "from": from, "name": to}))
|
||||
}
|
||||
|
||||
pub const ACTIVITY: CommandSpec = CommandSpec {
|
||||
id: "model.activity",
|
||||
label: "Step Table",
|
||||
@@ -473,6 +530,27 @@ pub(crate) mod tests {
|
||||
s.run_script(&script).unwrap();
|
||||
}
|
||||
|
||||
/// A rename rewrites the references, checks clean, is undone in one step, follows the hidden
|
||||
/// state and refuses a name in use (#290).
|
||||
#[test]
|
||||
fn rename_keeps_the_model_whole_and_undoes() {
|
||||
let mut s = Session::default();
|
||||
defined_bar(&mut s);
|
||||
s.execute("view.hide", &json!({"items": [{"kind": "load", "name": "push"}]})).unwrap();
|
||||
s.execute("model.rename", &json!({"kind": "set", "from": "end", "to": "Tip"})).unwrap();
|
||||
s.execute("model.rename", &json!({"kind": "load", "from": "push", "to": "Push"})).unwrap();
|
||||
s.execute("model.rename", &json!({"kind": "step", "from": "Bend", "to": "Load"})).unwrap();
|
||||
assert!(s.model.sets().get("end").is_none() && !s.model.set_users("Tip").is_empty());
|
||||
assert!(s.model.step("Load").is_some_and(|st| st.loads.contains("Push")));
|
||||
assert!(s.view.is_hidden(fieldcraft_io::ItemKind::Load, "Push") && !s.view.is_hidden(fieldcraft_io::ItemKind::Load, "push"));
|
||||
assert_eq!(s.execute("model.check", &json!({})).unwrap()["ready"], true);
|
||||
let e = s.execute("model.rename", &json!({"kind": "set", "from": "Tip", "to": "root"})).unwrap_err().to_string();
|
||||
assert!(e.contains("already exists"), "{e}");
|
||||
assert!(s.execute("model.rename", &json!({"kind": "partition", "from": "a", "to": "b"})).is_err());
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert!(s.model.step("Bend").is_some() && s.model.step("Load").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_script_defines_a_model_that_checks_clean() {
|
||||
let mut s = Session::default();
|
||||
|
||||
@@ -834,6 +834,33 @@ pub const STEP_ACTIVATE: &[Field] = &[
|
||||
flag("active", "Active").help("On: they act in the step. Off: they are switched off from it on.").def("true"),
|
||||
];
|
||||
|
||||
pub const RENAME: &[Field] = &[
|
||||
choice(
|
||||
"kind",
|
||||
"Kind",
|
||||
&[
|
||||
("set", "Set"),
|
||||
("material", "Material"),
|
||||
("section", "Section"),
|
||||
("step", "Step"),
|
||||
("boundary_condition", "Boundary condition"),
|
||||
("load", "Load"),
|
||||
("reference_point", "Reference point"),
|
||||
("coupling", "Coupling"),
|
||||
("contact", "Contact"),
|
||||
("tie", "Tie"),
|
||||
("point_mass", "Point mass"),
|
||||
("spring", "Spring"),
|
||||
("fluid_domain", "Fluid domain"),
|
||||
("datum", "Datum"),
|
||||
],
|
||||
)
|
||||
.help("What kind of definition to rename.")
|
||||
.req(),
|
||||
text("from", "Name").help("Its current name.").req(),
|
||||
text("to", "New name").help("Every definition that refers to it is changed to the new name.").req(),
|
||||
];
|
||||
|
||||
pub const UNITS: &[Field] = &[choice(
|
||||
"system",
|
||||
"Unit system",
|
||||
|
||||
@@ -131,6 +131,7 @@ pub static ALL: &[CommandSpec] = &[
|
||||
define::STEP_EDIT,
|
||||
define::STEP_DELETE,
|
||||
define::STEP_ACTIVATE,
|
||||
define::RENAME,
|
||||
define::ACTIVITY,
|
||||
file::OPEN,
|
||||
file::SAVE,
|
||||
|
||||
@@ -44,6 +44,7 @@ pub mod materials;
|
||||
mod mesh;
|
||||
pub mod nodal;
|
||||
pub mod progress;
|
||||
pub mod rename;
|
||||
pub mod sections;
|
||||
pub mod seeds;
|
||||
pub mod sets;
|
||||
|
||||
@@ -0,0 +1,348 @@
|
||||
//! Renaming a definition and every reference to it, so a name can change while other
|
||||
//! definitions use it (deleting and recreating is refused then).
|
||||
//!
|
||||
//! What refers to what: sets are named by sections, couplings, contacts, ties, point masses and
|
||||
//! springs, boundary conditions, loads and explicit steps (history nodes, initial velocities,
|
||||
//! rigid walls, mass scaling); materials by sections and fluid domains; reference points by
|
||||
//! couplings, boundary conditions, loads, point masses and springs; boundary conditions and
|
||||
//! loads by the steps they are active in; steps by the nonlinear steps whose imperfection is one
|
||||
//! of their buckling modes; fluid domains by fluid steps; datum planes by the partitions that
|
||||
//! cut by them. Partitions are not renamed: their cut faces are named after them.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::{Error, Model, Procedure, Result, Target, check_name};
|
||||
|
||||
/// A kind of definition [`Model::rename`] renames.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Kind {
|
||||
Set,
|
||||
Material,
|
||||
Section,
|
||||
Step,
|
||||
BoundaryCondition,
|
||||
Load,
|
||||
ReferencePoint,
|
||||
Coupling,
|
||||
Contact,
|
||||
Tie,
|
||||
PointMass,
|
||||
Spring,
|
||||
FluidDomain,
|
||||
Datum,
|
||||
}
|
||||
|
||||
impl Kind {
|
||||
/// The kind in words, as errors name it.
|
||||
pub fn word(self) -> &'static str {
|
||||
match self {
|
||||
Kind::Set => "set",
|
||||
Kind::Material => "material",
|
||||
Kind::Section => "section",
|
||||
Kind::Step => "step",
|
||||
Kind::BoundaryCondition => "boundary condition",
|
||||
Kind::Load => "load",
|
||||
Kind::ReferencePoint => "reference point",
|
||||
Kind::Coupling => "coupling",
|
||||
Kind::Contact => "contact",
|
||||
Kind::Tie => "tie",
|
||||
Kind::PointMass => "point mass",
|
||||
Kind::Spring => "spring",
|
||||
Kind::FluidDomain => "fluid domain",
|
||||
Kind::Datum => "datum",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Move `map`'s entry `from` to `to`: `from` must exist and `to` must be free.
|
||||
fn rekey<T>(map: &mut BTreeMap<String, T>, what: &str, from: &str, to: &str) -> Result<()> {
|
||||
if map.contains_key(to) {
|
||||
return Err(Error::Invalid(format!("{what} `{to}` already exists")));
|
||||
}
|
||||
let value = map.remove(from).ok_or_else(|| Error::NotFound(format!("{what} `{from}`")))?;
|
||||
map.insert(to.to_string(), value);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `name` becomes `to` where it is `from`.
|
||||
fn swap(name: &mut String, from: &str, to: &str) {
|
||||
if name == from {
|
||||
*name = to.to_string();
|
||||
}
|
||||
}
|
||||
|
||||
fn swap_opt(name: &mut Option<String>, from: &str, to: &str) {
|
||||
if let Some(n) = name {
|
||||
swap(n, from, to);
|
||||
}
|
||||
}
|
||||
|
||||
fn swap_target(target: &mut Target, kind: Kind, from: &str, to: &str) {
|
||||
match (target, kind) {
|
||||
(Target::Set(s), Kind::Set) | (Target::ReferencePoint(s), Kind::ReferencePoint) => swap(s, from, to),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
impl Model {
|
||||
/// Rename definition `from` of `kind` to `to`, rewriting every reference to it. A name in use
|
||||
/// by another definition of the kind, or an invalid one, is refused and nothing changes;
|
||||
/// renaming to the same name does nothing.
|
||||
pub fn rename(&mut self, kind: Kind, from: &str, to: &str) -> Result<()> {
|
||||
let what = kind.word();
|
||||
check_name(what, to)?;
|
||||
if from == to {
|
||||
return self.exists(kind, from);
|
||||
}
|
||||
// The definition moves first: it fails before anything else changes.
|
||||
match kind {
|
||||
Kind::Set => rekey(&mut self.sets, what, from, to)?,
|
||||
Kind::Material => rekey(&mut self.materials, what, from, to)?,
|
||||
Kind::Section => rekey(&mut self.sections, what, from, to)?,
|
||||
Kind::BoundaryCondition => rekey(&mut self.boundary_conditions, what, from, to)?,
|
||||
Kind::Load => rekey(&mut self.loads, what, from, to)?,
|
||||
Kind::ReferencePoint => rekey(&mut self.reference_points, what, from, to)?,
|
||||
Kind::Coupling => rekey(&mut self.couplings, what, from, to)?,
|
||||
Kind::Contact => rekey(&mut self.contacts, what, from, to)?,
|
||||
Kind::Tie => rekey(&mut self.ties, what, from, to)?,
|
||||
Kind::PointMass => rekey(&mut self.masses, what, from, to)?,
|
||||
Kind::Spring => rekey(&mut self.springs, what, from, to)?,
|
||||
Kind::FluidDomain => rekey(&mut self.fluid_domains, what, from, to)?,
|
||||
Kind::Datum => rekey(&mut self.datums, what, from, to)?,
|
||||
Kind::Step => {
|
||||
if self.steps.iter().any(|s| s.name == to) {
|
||||
return Err(Error::Invalid(format!("step `{to}` already exists")));
|
||||
}
|
||||
let step = self.steps.iter_mut().find(|s| s.name == from).ok_or_else(|| Error::NotFound(format!("step `{from}`")))?;
|
||||
step.name = to.to_string();
|
||||
}
|
||||
}
|
||||
self.rewrite_references(kind, from, to);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn exists(&self, kind: Kind, name: &str) -> Result<()> {
|
||||
let found = match kind {
|
||||
Kind::Set => self.sets.contains_key(name),
|
||||
Kind::Material => self.materials.contains_key(name),
|
||||
Kind::Section => self.sections.contains_key(name),
|
||||
Kind::BoundaryCondition => self.boundary_conditions.contains_key(name),
|
||||
Kind::Load => self.loads.contains_key(name),
|
||||
Kind::ReferencePoint => self.reference_points.contains_key(name),
|
||||
Kind::Coupling => self.couplings.contains_key(name),
|
||||
Kind::Contact => self.contacts.contains_key(name),
|
||||
Kind::Tie => self.ties.contains_key(name),
|
||||
Kind::PointMass => self.masses.contains_key(name),
|
||||
Kind::Spring => self.springs.contains_key(name),
|
||||
Kind::FluidDomain => self.fluid_domains.contains_key(name),
|
||||
Kind::Datum => self.datums.contains_key(name),
|
||||
Kind::Step => self.steps.iter().any(|s| s.name == name),
|
||||
};
|
||||
if found { Ok(()) } else { Err(Error::NotFound(format!("{} `{name}`", kind.word()))) }
|
||||
}
|
||||
|
||||
/// Every reference to `from` of `kind` now names `to`.
|
||||
fn rewrite_references(&mut self, kind: Kind, from: &str, to: &str) {
|
||||
match kind {
|
||||
Kind::Set | Kind::ReferencePoint => {
|
||||
let set = kind == Kind::Set;
|
||||
for c in self.couplings.values_mut() {
|
||||
swap(if set { &mut c.set } else { &mut c.reference_point }, from, to);
|
||||
}
|
||||
for b in self.boundary_conditions.values_mut() {
|
||||
use crate::BoundaryCondition as B;
|
||||
let (B::Fixed { target } | B::Displacement { target, .. } | B::Symmetry { target, .. } | B::Temperature { target, .. }) = b;
|
||||
swap_target(target, kind, from, to);
|
||||
}
|
||||
for l in self.loads.values_mut() {
|
||||
use crate::Load as L;
|
||||
match l {
|
||||
L::Force { target, .. } | L::Moment { target, .. } | L::Temperature { target, .. } => swap_target(target, kind, from, to),
|
||||
L::Traction { set: s, .. }
|
||||
| L::Pressure { set: s, .. }
|
||||
| L::BodyForce { set: s, .. }
|
||||
| L::SurfaceFlux { set: s, .. }
|
||||
| L::Convection { set: s, .. }
|
||||
| L::HeatSource { set: s, .. } => {
|
||||
if set {
|
||||
swap(s, from, to);
|
||||
}
|
||||
}
|
||||
L::Gravity { set: s, .. } | L::Centrifugal { set: s, .. } => {
|
||||
if set {
|
||||
swap_opt(s, from, to);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for m in self.masses.values_mut() {
|
||||
swap_target(&mut m.target, kind, from, to);
|
||||
}
|
||||
for s in self.springs.values_mut() {
|
||||
use crate::Spring as S;
|
||||
let (S::Spring { first, second, .. } | S::Dashpot { first, second, .. }) = s;
|
||||
swap_target(first, kind, from, to);
|
||||
if let Some(t) = second {
|
||||
swap_target(t, kind, from, to);
|
||||
}
|
||||
}
|
||||
if set {
|
||||
self.rewrite_set_only(from, to);
|
||||
}
|
||||
}
|
||||
Kind::Material => {
|
||||
for s in self.sections.values_mut() {
|
||||
swap(&mut s.material, from, to);
|
||||
}
|
||||
for d in self.fluid_domains.values_mut() {
|
||||
swap(&mut d.material, from, to);
|
||||
}
|
||||
}
|
||||
Kind::BoundaryCondition | Kind::Load => {
|
||||
for step in &mut self.steps {
|
||||
let active = if kind == Kind::Load { &mut step.loads } else { &mut step.boundary_conditions };
|
||||
if active.remove(from) {
|
||||
active.insert(to.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
Kind::Step => {
|
||||
for step in &mut self.steps {
|
||||
if let Procedure::NonlinearStatic(n) = &mut step.procedure
|
||||
&& let Some(i) = &mut n.imperfection
|
||||
{
|
||||
swap(&mut i.step, from, to);
|
||||
}
|
||||
}
|
||||
}
|
||||
Kind::FluidDomain => {
|
||||
for step in &mut self.steps {
|
||||
if let Procedure::Fluid { domain, .. } = &mut step.procedure {
|
||||
swap(domain, from, to);
|
||||
}
|
||||
}
|
||||
}
|
||||
Kind::Datum => {
|
||||
for p in &mut self.partitions {
|
||||
swap(&mut p.datum, from, to);
|
||||
}
|
||||
}
|
||||
Kind::Section | Kind::Coupling | Kind::Contact | Kind::Tie | Kind::PointMass | Kind::Spring => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// The references only a set has: sections, contacts, ties and explicit steps.
|
||||
fn rewrite_set_only(&mut self, from: &str, to: &str) {
|
||||
for s in self.sections.values_mut() {
|
||||
swap(&mut s.set, from, to);
|
||||
}
|
||||
for c in self.contacts.values_mut() {
|
||||
match &mut c.surfaces {
|
||||
crate::ContactSurfaces::Pair { main, secondary } => {
|
||||
swap(main, from, to);
|
||||
swap(secondary, from, to);
|
||||
}
|
||||
crate::ContactSurfaces::General { surface } => swap_opt(surface, from, to),
|
||||
}
|
||||
}
|
||||
for t in self.ties.values_mut() {
|
||||
swap(&mut t.main, from, to);
|
||||
swap(&mut t.secondary, from, to);
|
||||
}
|
||||
for step in &mut self.steps {
|
||||
if let Procedure::ExplicitDynamic(e) = &mut step.procedure {
|
||||
swap_opt(&mut e.history_nodes, from, to);
|
||||
for v in &mut e.initial_velocities {
|
||||
swap(&mut v.set, from, to);
|
||||
}
|
||||
for w in &mut e.rigid_walls {
|
||||
swap(&mut w.set, from, to);
|
||||
}
|
||||
if let Some(m) = &mut e.mass_scaling {
|
||||
swap_opt(&mut m.set, from, to);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{BoundaryCondition, Coupling, CouplingKind, Datum, Dofs, Load, Material, Partition, ReferencePoint, Section, Set, Step, Tie};
|
||||
|
||||
/// A model whose definitions refer to each other: set `s` in a section, a coupling, a tie, a
|
||||
/// boundary condition and a load; material `m` in the section; reference point `rp` in the
|
||||
/// coupling and a load; both loads and the condition active in step `go`; datum `d` cut by.
|
||||
fn model() -> Model {
|
||||
let mut m = Model::default();
|
||||
for s in ["s", "t"] {
|
||||
m.add_set(s, Set { nodes: vec![1], ..Set::default() }).unwrap();
|
||||
}
|
||||
m.add_material("m", Material { elastic: Some(crate::Elastic::Isotropic { young: 1.0, poisson: 0.3 }), ..Material::default() }).unwrap();
|
||||
m.add_section("sec", Section { set: "s".into(), material: "m".into(), kind: crate::SectionKind::Solid {}, formulation: Default::default() })
|
||||
.unwrap();
|
||||
m.add_reference_point("rp", ReferencePoint { xyz: [0.0; 3], label: 0 }).unwrap();
|
||||
m.add_coupling("c", Coupling { reference_point: "rp".into(), set: "s".into(), kind: CouplingKind::Kinematic, dofs: Dofs::all() }).unwrap();
|
||||
m.add_tie("tie", Tie::new("s", "t")).unwrap();
|
||||
m.add_boundary_condition("fix", BoundaryCondition::Fixed { target: Target::Set("s".into()) }).unwrap();
|
||||
m.add_load("p", Load::Pressure { set: "s".into(), magnitude: 1.0 }).unwrap();
|
||||
m.add_load("f", Load::Force { target: Target::ReferencePoint("rp".into()), vector: [1.0, 0.0, 0.0], distribution: Default::default() })
|
||||
.unwrap();
|
||||
m.add_step(Step::new("go", Procedure::Static {}), None).unwrap();
|
||||
m.set_boundary_condition_active("go", "fix", true).unwrap();
|
||||
m.set_load_active("go", "p", true).unwrap();
|
||||
m.set_load_active("go", "f", true).unwrap();
|
||||
m.add_datum("d", Datum::Plane { point: [0.0; 3], normal: [0.0, 0.0, 1.0] }).unwrap();
|
||||
m.add_partition(Partition { name: "half".into(), datum: "d".into(), bodies: Vec::new() }).unwrap();
|
||||
m
|
||||
}
|
||||
|
||||
/// Whatever is renamed, every reference follows: the old name has no users left, the new
|
||||
/// one has all of them, and the model reads back from its file.
|
||||
#[test]
|
||||
fn renaming_rewrites_every_reference() {
|
||||
let mut m = model();
|
||||
let users = m.set_users("s");
|
||||
assert_eq!(users.len(), 5, "{users:?}");
|
||||
m.rename(Kind::Set, "s", "S2").unwrap();
|
||||
assert!(m.set_users("s").is_empty() && m.sets().get("s").is_none());
|
||||
assert_eq!(m.set_users("S2"), users);
|
||||
|
||||
let users = m.material_users("m");
|
||||
m.rename(Kind::Material, "m", "steel").unwrap();
|
||||
assert_eq!(m.material_users("steel"), users);
|
||||
assert_eq!(m.section("sec").map(|s| s.material.as_str()), Some("steel"));
|
||||
|
||||
let users = m.reference_point_users("rp");
|
||||
m.rename(Kind::ReferencePoint, "rp", "RP-1").unwrap();
|
||||
assert!(m.reference_point_users("rp").is_empty());
|
||||
assert_eq!(m.reference_point_users("RP-1"), users);
|
||||
|
||||
m.rename(Kind::BoundaryCondition, "fix", "clamp").unwrap();
|
||||
m.rename(Kind::Load, "f", "push").unwrap();
|
||||
m.rename(Kind::Step, "go", "Load").unwrap();
|
||||
let step = m.steps().iter().find(|s| s.name == "Load").unwrap();
|
||||
assert!(step.boundary_conditions.contains("clamp") && step.loads.contains("push") && step.loads.contains("p"));
|
||||
|
||||
m.rename(Kind::Datum, "d", "Mid").unwrap();
|
||||
assert_eq!(m.datum_users("Mid"), ["partition `half`"]);
|
||||
|
||||
let json = serde_json::to_value(&m).unwrap();
|
||||
let back: Model = serde_json::from_value(json).unwrap();
|
||||
assert_eq!(back, m);
|
||||
}
|
||||
|
||||
/// A taken, invalid or missing name changes nothing; the same name is a no-op.
|
||||
#[test]
|
||||
fn a_rename_that_cannot_be_done_changes_nothing() {
|
||||
let mut m = model();
|
||||
let before = m.clone();
|
||||
assert!(m.rename(Kind::Set, "s", "t").unwrap_err().to_string().contains("already exists"));
|
||||
assert!(m.rename(Kind::Set, "nope", "x").is_err());
|
||||
assert!(m.rename(Kind::Set, "s", "").is_err());
|
||||
assert!(m.rename(Kind::Step, "go", "go").is_ok());
|
||||
assert!(m.rename(Kind::Step, "gone", "gone").is_err());
|
||||
assert_eq!(m, before);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -156,7 +156,7 @@ the control channel and AI agents over MCP (docs/mcp.md) run the same commands.
|
||||
|
||||
| Panel | Capability | Status | Command |
|
||||
|---|---|---|---|
|
||||
| Model | Model Info — Node and element counts and the model summary | ready | `model.info` |
|
||||
| Model | Model Info — Node and element counts and the model summary | ready | `model.info`, `model.rename` |
|
||||
| File | Open Model — Open a .fieldcraft model | ready | `file.open` |
|
||||
| File | Samples — Open a sample model, ready to solve | ready | `file.sample` |
|
||||
| File | Save — Save the model to its .fieldcraft file | ready | `file.save` |
|
||||
|
||||
Reference in new issue
Block a user