forked from 1b-agent/fieldcraft-upstream
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:
1 parent
f9981d305e
commit
7e9366f726
6 files changed
+104
-5
No files matched your search
@@ -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",
|
||||
|
||||
@@ -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<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 {
|
||||
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();
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)),
|
||||
|
||||
+2
-2
@@ -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` |
|
||||
|
||||
Reference in new issue
Block a user