engine: interaction.tie, tie.edit and tie.delete

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
jyvikoandClaude Opus 5.5 authored and Irakli Kourtis committed 2026-10-14 15:28:02 -04:00
1 parent f9981d305e
commit 7e9366f726
6 files changed
+104 -5

No files matched your search

+8 -1
View File
@@ -414,7 +414,14 @@ pub static CATALOGUE: &[Capability] = &[
"Kinematic or distributing coupling of a set to a reference point", "Kinematic or distributing coupling of a set to a reference point",
&["model.coupling.edit", "model.coupling.delete"], &["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( ready_more(
"interaction.contact.general", "interaction.contact.general",
"General Contact", "General Contact",
+75 -2
View File
@@ -1,5 +1,5 @@
//! `interaction.contact.*`: contact definitions (pairs of surfaces, general contact) and the //! `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 //! 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` //! take the friction, penalty scale and initial-penetration choice of the model's form; `edit`
@@ -14,7 +14,7 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use fieldcraft_fem::contact::{Grid, bounds, closest_point}; 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 serde_json::{Value, json};
use super::{name, opt}; use super::{name, opt};
@@ -102,6 +102,53 @@ pub const DELETE: CommandSpec = CommandSpec {
}, },
}; };
/// A tie from the parameters.
fn tie(p: &Value) -> Result<Tie> {
Ok(Tie {
main: text(p, "main")?.to_string(),
secondary: text(p, "secondary")?.to_string(),
position_tolerance: opt::<f64>(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 { pub const FIND: CommandSpec = CommandSpec {
id: "interaction.contact.find", id: "interaction.contact.find",
label: "Find Contacts", label: "Find Contacts",
@@ -334,6 +381,32 @@ mod tests {
assert!(s.model.contacts().is_empty()); 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] #[test]
fn find_reports_and_creates_the_touching_parts() { fn find_reports_and_creates_the_touching_parts() {
let mut s = Session::default(); let mut s = Session::default();
+13
View File
@@ -31,6 +31,7 @@ pub const BOUNDARY_CONDITION_DELETE: &[Field] = existing!("Boundary condition",
pub const LOAD_DELETE: &[Field] = existing!("Load", C::Load); pub const LOAD_DELETE: &[Field] = existing!("Load", C::Load);
pub const FLUID_DOMAIN_DELETE: &[Field] = existing!("Fluid domain", C::FluidDomain); pub const FLUID_DOMAIN_DELETE: &[Field] = existing!("Fluid domain", C::FluidDomain);
pub const CONTACT_DELETE: &[Field] = existing!("Contact", C::Contact); 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 STEP_DELETE: &[Field] = existing!("Step", C::Step);
pub const SET_RESOLVE: &[Field] = existing!("Set", C::Set); pub const SET_RESOLVE: &[Field] = existing!("Set", C::Set);
@@ -614,6 +615,18 @@ pub const CONTACT_EDIT: &[Field] = &[
PENALTY, PENALTY,
PENETRATION, 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] = &[ pub const CONTACT_FIND: &[Field] = &[
positive("tolerance", "Tolerance", D::Length).hint("a tenth of the mean face size"), positive("tolerance", "Tolerance", D::Length).hint("a tenth of the mean face size"),
flag("create", "Create pairs").def("false"), flag("create", "Create pairs").def("false"),
+3
View File
@@ -74,6 +74,9 @@ pub static ALL: &[CommandSpec] = &[
contact::EDIT, contact::EDIT,
contact::DELETE, contact::DELETE,
contact::FIND, contact::FIND,
contact::TIE,
contact::TIE_EDIT,
contact::TIE_DELETE,
define::MASS_CREATE, define::MASS_CREATE,
define::MASS_EDIT, define::MASS_EDIT,
define::MASS_DELETE, define::MASS_DELETE,
+3
View File
@@ -107,6 +107,7 @@ pub enum Collection {
ReferencePoint, ReferencePoint,
Coupling, Coupling,
Contact, Contact,
Tie,
Mass, Mass,
Spring, Spring,
BoundaryCondition, BoundaryCondition,
@@ -128,6 +129,7 @@ impl Collection {
Collection::ReferencePoint => "reference point", Collection::ReferencePoint => "reference point",
Collection::Coupling => "coupling", Collection::Coupling => "coupling",
Collection::Contact => "contact", Collection::Contact => "contact",
Collection::Tie => "tie",
Collection::Mass => "point mass", Collection::Mass => "point mass",
Collection::Spring => "spring", Collection::Spring => "spring",
Collection::BoundaryCondition => "boundary condition", Collection::BoundaryCondition => "boundary condition",
@@ -151,6 +153,7 @@ impl Session {
Collection::ReferencePoint => Box::new(m.reference_points().keys().map(String::as_str)), Collection::ReferencePoint => Box::new(m.reference_points().keys().map(String::as_str)),
Collection::Coupling => Box::new(m.couplings().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::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::Mass => Box::new(m.masses().keys().map(String::as_str)),
Collection::Spring => Box::new(m.springs().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)), Collection::BoundaryCondition => Box::new(m.boundary_conditions().keys().map(String::as_str)),
+2 -2
View File
@@ -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, 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. 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 ## 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 | 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 | 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 | 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 | 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` | | Contact | Find Contacts — Find the parts of the mesh that touch, and make contact pairs of them | ready | `interaction.contact.find` |