diff --git a/crates/engine/src/catalogue.rs b/crates/engine/src/catalogue.rs index de74bbe..186efe0 100644 --- a/crates/engine/src/catalogue.rs +++ b/crates/engine/src/catalogue.rs @@ -414,7 +414,14 @@ pub static CATALOGUE: &[Capability] = &[ "Kinematic or distributing coupling of a set to a reference point", &["model.coupling.edit", "model.coupling.delete"], ), - soon("interaction.tie", "Tie", Interactions, "Constraints", "M7", "Tie non-matching meshes together"), + ready_more( + "interaction.tie", + "Tie", + Interactions, + "Constraints", + "Tie non-matching meshes together (dual mortar constraints)", + &["interaction.tie.edit", "interaction.tie.delete"], + ), ready_more( "interaction.contact.general", "General Contact", diff --git a/crates/engine/src/cmd/contact.rs b/crates/engine/src/cmd/contact.rs index ddf29ba..1086be7 100644 --- a/crates/engine/src/cmd/contact.rs +++ b/crates/engine/src/cmd/contact.rs @@ -1,5 +1,5 @@ //! `interaction.contact.*`: contact definitions (pairs of surfaces, general contact) and the -//! search for surfaces that touch. +//! search for surfaces that touch; `interaction.tie*`: ties between non-matching meshes. //! //! A contact's surfaces are sets of element faces ([`fieldcraft_model::contact`]). The commands //! take the friction, penalty scale and initial-penetration choice of the model's form; `edit` @@ -14,7 +14,7 @@ use std::collections::BTreeMap; use fieldcraft_fem::contact::{Grid, bounds, closest_point}; -use fieldcraft_model::{Contact, ContactSurfaces, ElementFace, Friction, InitialPenetration, Model, Set}; +use fieldcraft_model::{Contact, ContactSurfaces, ElementFace, Friction, InitialPenetration, Model, Set, Tie}; use serde_json::{Value, json}; use super::{name, opt}; @@ -102,6 +102,53 @@ pub const DELETE: CommandSpec = CommandSpec { }, }; +/// A tie from the parameters. +fn tie(p: &Value) -> Result { + Ok(Tie { + main: text(p, "main")?.to_string(), + secondary: text(p, "secondary")?.to_string(), + position_tolerance: opt::(p, "position_tolerance")?, + }) +} + +pub const TIE: CommandSpec = CommandSpec { + id: "interaction.tie", + label: "Tie", + params: r#"{"name": "glue", "main": "plate" (a set of element faces), "secondary": "rib" (another), "position_tolerance": 0.1 (optional: how far a secondary node may lie from the main surface and still be tied; default half the main element's thickness)} — bonds the secondary surface to the main one across non-matching meshes: each secondary node follows the main surface under it (dual mortar constraints in the implicit analyses; a constant stress crosses the tie exactly); fails if the name is taken"#, + form: Some(super::forms::TIE), + kind: CommandKind::Model, + run: |s: &mut Session, p: &Value| { + let n = name(p)?; + s.model.add_tie(n, tie(p)?)?; + Ok(json!({"name": n})) + }, +}; + +pub const TIE_EDIT: CommandSpec = CommandSpec { + id: "interaction.tie.edit", + label: "Edit Tie", + params: r#"{"name": "glue", "main": "plate", "secondary": "rib", "position_tolerance": 0.1} — replaces the tie of that name (see interaction.tie)"#, + form: Some(super::forms::TIE_EDIT), + kind: CommandKind::Model, + run: |s: &mut Session, p: &Value| { + let n = name(p)?; + s.model.replace_tie(n, tie(p)?)?; + Ok(json!({"name": n})) + }, +}; + +pub const TIE_DELETE: CommandSpec = CommandSpec { + id: "interaction.tie.delete", + label: "Delete Tie", + params: r#"{"name": "glue"}"#, + form: Some(super::forms::TIE_DELETE), + kind: CommandKind::Model, + run: |s: &mut Session, p: &Value| { + s.model.remove_tie(name(p)?)?; + Ok(Value::Null) + }, +}; + pub const FIND: CommandSpec = CommandSpec { id: "interaction.contact.find", label: "Find Contacts", @@ -334,6 +381,32 @@ mod tests { assert!(s.model.contacts().is_empty()); } + #[test] + fn ties_are_created_edited_and_deleted() { + let mut s = Session::default(); + stacked(&mut s); + s.execute("model.set.create", &json!({"name": "a", "faces": [{"element": 5, "face": 2}]})).unwrap(); + s.execute("model.set.create", &json!({"name": "b", "faces": [{"element": 9, "face": 1}]})).unwrap(); + s.execute("interaction.tie", &json!({"name": "glue", "main": "a", "secondary": "b"})).unwrap(); + assert_eq!(s.model.tie("glue"), Some(&Tie::new("a", "b"))); + for bad in [ + json!({"name": "glue", "main": "a", "secondary": "b"}), + json!({"name": "x", "main": "a", "secondary": "a"}), + json!({"name": "x", "main": "a", "secondary": "nope"}), + json!({"name": "x", "main": "a", "secondary": "b", "position_tolerance": -1}), + json!({"name": "x", "main": "a", "secondary": "b", "position_tolerance": "far"}), + json!({"name": "x", "main": 3, "secondary": "b"}), + ] { + assert!(s.execute("interaction.tie", &bad).is_err(), "{bad}"); + } + s.execute("interaction.tie.edit", &json!({"name": "glue", "main": "b", "secondary": "a", "position_tolerance": 0.01})).unwrap(); + assert_eq!(s.model.tie("glue").and_then(|t| t.position_tolerance), Some(0.01)); + assert!(s.execute("interaction.tie.edit", &json!({"name": "nope", "main": "a", "secondary": "b"})).is_err()); + assert!(s.execute("model.set.delete", &json!({"name": "a"})).unwrap_err().to_string().contains("tie `glue`")); + s.execute("interaction.tie.delete", &json!({"name": "glue"})).unwrap(); + assert!(s.model.ties().is_empty() && s.execute("interaction.tie.delete", &json!({"name": "glue"})).is_err()); + } + #[test] fn find_reports_and_creates_the_touching_parts() { let mut s = Session::default(); diff --git a/crates/engine/src/cmd/forms.rs b/crates/engine/src/cmd/forms.rs index 13b2404..704c67f 100644 --- a/crates/engine/src/cmd/forms.rs +++ b/crates/engine/src/cmd/forms.rs @@ -31,6 +31,7 @@ pub const BOUNDARY_CONDITION_DELETE: &[Field] = existing!("Boundary condition", pub const LOAD_DELETE: &[Field] = existing!("Load", C::Load); pub const FLUID_DOMAIN_DELETE: &[Field] = existing!("Fluid domain", C::FluidDomain); pub const CONTACT_DELETE: &[Field] = existing!("Contact", C::Contact); +pub const TIE_DELETE: &[Field] = existing!("Tie", C::Tie); pub const STEP_DELETE: &[Field] = existing!("Step", C::Step); pub const SET_RESOLVE: &[Field] = existing!("Set", C::Set); @@ -614,6 +615,18 @@ pub const CONTACT_EDIT: &[Field] = &[ PENALTY, PENETRATION, ]; +pub const TIE: &[Field] = &[ + NAME, + reference("main", "Main surface", C::Set).req(), + reference("secondary", "Secondary surface", C::Set).req(), + positive("position_tolerance", "Position tolerance", D::Length).hint("half the main element's thickness"), +]; +pub const TIE_EDIT: &[Field] = &[ + reference("name", "Tie", C::Tie).req(), + reference("main", "Main surface", C::Set).req(), + reference("secondary", "Secondary surface", C::Set).req(), + positive("position_tolerance", "Position tolerance", D::Length).hint("half the main element's thickness"), +]; pub const CONTACT_FIND: &[Field] = &[ positive("tolerance", "Tolerance", D::Length).hint("a tenth of the mean face size"), flag("create", "Create pairs").def("false"), diff --git a/crates/engine/src/cmd/mod.rs b/crates/engine/src/cmd/mod.rs index 2d104e9..94a136a 100644 --- a/crates/engine/src/cmd/mod.rs +++ b/crates/engine/src/cmd/mod.rs @@ -74,6 +74,9 @@ pub static ALL: &[CommandSpec] = &[ contact::EDIT, contact::DELETE, contact::FIND, + contact::TIE, + contact::TIE_EDIT, + contact::TIE_DELETE, define::MASS_CREATE, define::MASS_EDIT, define::MASS_DELETE, diff --git a/crates/engine/src/schema.rs b/crates/engine/src/schema.rs index b0d3245..5fa157c 100644 --- a/crates/engine/src/schema.rs +++ b/crates/engine/src/schema.rs @@ -107,6 +107,7 @@ pub enum Collection { ReferencePoint, Coupling, Contact, + Tie, Mass, Spring, BoundaryCondition, @@ -128,6 +129,7 @@ impl Collection { Collection::ReferencePoint => "reference point", Collection::Coupling => "coupling", Collection::Contact => "contact", + Collection::Tie => "tie", Collection::Mass => "point mass", Collection::Spring => "spring", Collection::BoundaryCondition => "boundary condition", @@ -151,6 +153,7 @@ impl Session { Collection::ReferencePoint => Box::new(m.reference_points().keys().map(String::as_str)), Collection::Coupling => Box::new(m.couplings().keys().map(String::as_str)), Collection::Contact => Box::new(m.contacts().keys().map(String::as_str)), + Collection::Tie => Box::new(m.ties().keys().map(String::as_str)), Collection::Mass => Box::new(m.masses().keys().map(String::as_str)), Collection::Spring => Box::new(m.springs().keys().map(String::as_str)), Collection::BoundaryCondition => Box::new(m.boundary_conditions().keys().map(String::as_str)), diff --git a/docs/features.md b/docs/features.md index 3f28f88..55958fa 100644 --- a/docs/features.md +++ b/docs/features.md @@ -6,7 +6,7 @@ Every entry is a toolbar button. **Ready** entries run their command; **coming s shown greyed with the roadmap milestone that brings them (ROADMAP.md). Scripts, the command line, the control channel and AI agents over MCP (docs/mcp.md) run the same commands. -59 ready, 45 coming soon. +60 ready, 44 coming soon. ## Geometry @@ -102,7 +102,7 @@ the control channel and AI agents over MCP (docs/mcp.md) run the same commands. |---|---|---|---| | Constraints | Reference Point — A reference point for couplings, loads and boundary conditions | ready | `model.reference_point.create`, `model.reference_point.edit`, `model.reference_point.delete` | | Constraints | Coupling — Kinematic or distributing coupling of a set to a reference point | ready | `model.coupling.create`, `model.coupling.edit`, `model.coupling.delete` | -| Constraints | Tie — Tie non-matching meshes together | coming soon (M7) | | +| Constraints | Tie — Tie non-matching meshes together (dual mortar constraints) | ready | `interaction.tie`, `interaction.tie.edit`, `interaction.tie.delete` | | Contact | General Contact — Contact between everything that may touch, or self contact of a surface, with friction (explicit dynamics) | ready | `interaction.contact.general`, `interaction.contact.edit`, `interaction.contact.delete` | | Contact | Contact Pair — Contact between two surfaces: with friction in explicit dynamics, mortar contact with friction in nonlinear static steps | ready | `interaction.contact.pair`, `interaction.contact.edit`, `interaction.contact.delete` | | Contact | Find Contacts — Find the parts of the mesh that touch, and make contact pairs of them | ready | `interaction.contact.find` |