forked from 1b-agent/fieldcraft-upstream
Rename the project to FieldCraft
Crates fieldcraft-*, binaries fieldcraft and fieldcraft-cli, the .fieldcraft file (format tag fieldcraft/1, manifest fieldcraft.json), FIELDCRAFT_* environment variables, the MCP server name, egui widget ids (fc_*), the app icon document FieldCraft.icon and its generated family, docs and licences. No compatibility shim: no files of the earlier name exist. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
This commit is contained in:
1 parent
9bc4e1b182
commit
d5bd74570f
268 files changed
+1666
-1666
No files matched your search
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "modelcraft-decks"
|
||||
description = "ModelCraft's solver decks: Abaqus input, Nastran bulk data and LS-DYNA keyword writers (and later readers)"
|
||||
name = "fieldcraft-decks"
|
||||
description = "FieldCraft's solver decks: Abaqus input, Nastran bulk data and LS-DYNA keyword writers (and later readers)"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
@@ -9,7 +9,7 @@ license.workspace = true
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
modelcraft-model = { workspace = true }
|
||||
fieldcraft-model = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
+10
-10
@@ -6,7 +6,7 @@
|
||||
//! # Layout
|
||||
//!
|
||||
//! The deck is *flat*: nodes, elements, sets, materials, sections and couplings sit at model level,
|
||||
//! with no parts, assembly or instances. ModelCraft has one mesh with model-wide labels and no
|
||||
//! with no parts, assembly or instances. FieldCraft has one mesh with model-wide labels and no
|
||||
//! instance transforms, so a part/assembly wrapper would only prefix every set name with an
|
||||
//! instance name (`Part-1-1.root`) and bring the 80-character limit on the joined name closer. When
|
||||
//! the model gains several parts or instances the writer switches to the part/assembly form.
|
||||
@@ -53,7 +53,7 @@
|
||||
//!
|
||||
//! # Element types
|
||||
//!
|
||||
//! | ModelCraft | Solid section | Shell section | Beam | Rod |
|
||||
//! | FieldCraft | Solid section | Shell section | Beam | Rod |
|
||||
//! |---|---|---|---|---|
|
||||
//! | tet4 | C3D4 (H) | | | |
|
||||
//! | tet10 | C3D10 (H) | | | |
|
||||
@@ -76,7 +76,7 @@
|
||||
//! differs by direction and a spring along a skew fixed vector are reported (they need a local
|
||||
//! orientation, not written yet).
|
||||
//!
|
||||
//! | ModelCraft | Plane stress | Plane strain | Axisymmetric |
|
||||
//! | FieldCraft | Plane stress | Plane strain | Axisymmetric |
|
||||
//! |---|---|---|---|
|
||||
//! | tri3 | CPS3 | CPE3 | CAX3 |
|
||||
//! | tri6 | CPS6 | CPE6 | CAX6 |
|
||||
@@ -115,7 +115,7 @@
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::fmt::Write as _;
|
||||
|
||||
use modelcraft_model::{
|
||||
use fieldcraft_model::{
|
||||
BeamProfile, BoundaryCondition, CouplingKind, Elastic, Element, ElementKind, Hardening, Integration, Load, Model, OutputVariable, Plastic,
|
||||
Procedure, Resolved, Section, SectionKind, Step, Symmetry, Target,
|
||||
};
|
||||
@@ -330,10 +330,10 @@ fn discrete_groups<'r>(model: &Model, r: &'r Resolved) -> Result<Vec<DiscreteGro
|
||||
let mut next = model.elements().keys().next_back().map_or(1, |l| l.saturating_add(1));
|
||||
let mut label = || -> Result<u32> {
|
||||
let l = next;
|
||||
if l > modelcraft_model::MAX_LABEL {
|
||||
if l > fieldcraft_model::MAX_LABEL {
|
||||
return Err(Error::Limit(format!(
|
||||
"no element label left below {} for point masses, springs and dashpots",
|
||||
modelcraft_model::MAX_LABEL as u64 + 1
|
||||
fieldcraft_model::MAX_LABEL as u64 + 1
|
||||
)));
|
||||
}
|
||||
next += 1;
|
||||
@@ -371,7 +371,7 @@ fn discrete_groups<'r>(model: &Model, r: &'r Resolved) -> Result<Vec<DiscreteGro
|
||||
}
|
||||
for s in &r.springs {
|
||||
let what = s.kind();
|
||||
let line = matches!(s.direction, modelcraft_model::ResolvedDirection::Line(_));
|
||||
let line = matches!(s.direction, fieldcraft_model::ResolvedDirection::Line(_));
|
||||
let (element_type, dofs, nodes) = match (s.second, crate::axis_dof(s.direction), line) {
|
||||
(Some(b), _, true) => (if s.dashpot { "DASHPOTA" } else { "SPRINGA" }, String::new(), vec![s.first, b]),
|
||||
(Some(b), Some(d), false) => (if s.dashpot { "DASHPOT2" } else { "SPRING2" }, format!("{d}, {d}"), vec![s.first, b]),
|
||||
@@ -556,7 +556,7 @@ impl<'a> StepLoads<'a> {
|
||||
/// `everything` is every element and the section sets that hold them (gravity on the whole
|
||||
/// model).
|
||||
fn new(model: &'a Model, r: &'a Resolved, step: &'a Step, everything: &'a (Vec<&'a str>, Vec<u32>)) -> StepLoads<'a> {
|
||||
let axisymmetric = r.extent() == Some(modelcraft_model::Extent::Revolved);
|
||||
let axisymmetric = r.extent() == Some(fieldcraft_model::Extent::Revolved);
|
||||
let mut s = StepLoads { concentrated: crate::concentrated_loads(model, r, &step.loads), axisymmetric, ..StepLoads::default() };
|
||||
let (mut gravity, mut body) = (Vec::new(), Vec::new());
|
||||
for name in &step.loads {
|
||||
@@ -637,8 +637,8 @@ impl Deck {
|
||||
None => "units not specified".to_string(),
|
||||
};
|
||||
self.line("*HEADING");
|
||||
self.line(&format!("ModelCraft model, {units}"));
|
||||
self.comment("Written by ModelCraft. The numbers are in one consistent unit system and are not converted.");
|
||||
self.line(&format!("FieldCraft model, {units}"));
|
||||
self.comment("Written by FieldCraft. The numbers are in one consistent unit system and are not converted.");
|
||||
}
|
||||
|
||||
fn nodes(&mut self, model: &Model) -> Result<()> {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Solver decks: the solver-neutral model written as Abaqus input, Nastran bulk data and LS-DYNA
|
||||
//! keyword files. Each format is its own module (`abaqus`, `nastran`, `lsdyna`), written from the
|
||||
//! vendor's public reference manual in our own words; examples and expected files are our own.
|
||||
//! Writers read [`modelcraft_model::Model`] and its `resolve()` result, never another format.
|
||||
//! Writers read [`fieldcraft_model::Model`] and its `resolve()` result, never another format.
|
||||
//!
|
||||
//! Writers produce text; saving it is the caller's business (the engine's `deck.write`), so the
|
||||
//! crate stays free of the file system. [`FORMATS`] lists every writer by the name `deck.write`
|
||||
@@ -15,8 +15,8 @@ pub mod nastran;
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use modelcraft_model::nodal::edge_pressure;
|
||||
use modelcraft_model::{Diagnostic, Load, Model, Resolved, ResolvedDirection};
|
||||
use fieldcraft_model::nodal::edge_pressure;
|
||||
use fieldcraft_model::{Diagnostic, Load, Model, Resolved, ResolvedDirection};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
|
||||
pub enum Error {
|
||||
@@ -25,7 +25,7 @@ pub enum Error {
|
||||
Unsupported { format: &'static str, what: String },
|
||||
/// The model is not ready to write (its check failed).
|
||||
#[error(transparent)]
|
||||
Model(#[from] modelcraft_model::Error),
|
||||
Model(#[from] fieldcraft_model::Error),
|
||||
#[error("limit reached: {0}")]
|
||||
Limit(String),
|
||||
}
|
||||
@@ -70,11 +70,11 @@ impl Format {
|
||||
/// stay (the check warns about the ones no step uses).
|
||||
pub fn with_steps(model: &Model, names: &[String]) -> Result<Model> {
|
||||
if names.is_empty() {
|
||||
return Err(Error::Model(modelcraft_model::Error::Invalid("choose at least one step to write".into())));
|
||||
return Err(Error::Model(fieldcraft_model::Error::Invalid("choose at least one step to write".into())));
|
||||
}
|
||||
if let Some(n) = names.iter().find(|n| model.step(n).is_none()) {
|
||||
let shown: String = n.chars().take(modelcraft_model::MAX_NAME_LEN).collect();
|
||||
return Err(Error::Model(modelcraft_model::Error::NotFound(format!("step `{shown}`"))));
|
||||
let shown: String = n.chars().take(fieldcraft_model::MAX_NAME_LEN).collect();
|
||||
return Err(Error::Model(fieldcraft_model::Error::NotFound(format!("step `{shown}`"))));
|
||||
}
|
||||
let mut out = model.clone();
|
||||
let dropped: Vec<String> = model.steps().iter().map(|s| s.name.clone()).filter(|s| !names.contains(s)).collect();
|
||||
@@ -84,7 +84,7 @@ pub fn with_steps(model: &Model, names: &[String]) -> Result<Model> {
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// A fluid step: solved by ModelCraft's lattice Boltzmann solver, never written to a deck.
|
||||
/// A fluid step: solved by FieldCraft's lattice Boltzmann solver, never written to a deck.
|
||||
pub(crate) fn fluid_step(format: &'static str, step: &str) -> Error {
|
||||
Error::Unsupported { format, what: format!("fluid step `{step}` (solve it with solve.fluid; write the structural steps only)") }
|
||||
}
|
||||
@@ -155,7 +155,7 @@ pub fn concentrated_loads<'a>(model: &Model, r: &Resolved, active: impl IntoIter
|
||||
|
||||
/// The consistent nodal forces (degrees of freedom 1 and 2) of the active pressures on a 2D
|
||||
/// model's element edges, for formats that write them as concentrated loads: the model's edge
|
||||
/// integration ([`modelcraft_model::nodal::edge_pressure`]), a positive pressure pushing
|
||||
/// integration ([`fieldcraft_model::nodal::edge_pressure`]), a positive pressure pushing
|
||||
/// into the element, summed per node and direction; zero sums are left out.
|
||||
pub fn edge_pressure_loads<'a>(model: &Model, r: &Resolved, active: impl IntoIterator<Item = &'a str>) -> Result<BTreeMap<(u32, u8), f64>> {
|
||||
let mut out: BTreeMap<(u32, u8), f64> = BTreeMap::new();
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
//!
|
||||
//! Every mapping is checked by the sign of the element Jacobian in the tests.
|
||||
|
||||
use modelcraft_model::{ElementKind, Formulation, Integration, SectionKind};
|
||||
use fieldcraft_model::{ElementKind, Formulation, Integration, SectionKind};
|
||||
|
||||
/// How an element is written: its keyword and its node list in LS-DYNA order.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
|
||||
@@ -61,7 +61,7 @@ mod elements;
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use modelcraft_model::{
|
||||
use fieldcraft_model::{
|
||||
BoundaryCondition, CouplingKind, Diagnostic, Dofs, Elastic, Element, ElementKind, Hardening, Load, Material, Model, OutputVariable, Plastic,
|
||||
Procedure, Resolved, ResolvedDirection, Section, SectionKind, Step, Target,
|
||||
};
|
||||
@@ -102,7 +102,7 @@ const MAX_SHELL_POINTS: u32 = 10;
|
||||
|
||||
/// A resolved model that breaks its own invariants (unreachable after `resolve`).
|
||||
fn broken(what: String) -> Error {
|
||||
Error::Model(modelcraft_model::Error::Invalid(format!("inconsistent resolved model: {what}")))
|
||||
Error::Model(fieldcraft_model::Error::Invalid(format!("inconsistent resolved model: {what}")))
|
||||
}
|
||||
|
||||
/// One `*PART`: a section's elements of one kind.
|
||||
@@ -159,7 +159,7 @@ fn write_with(model: &Model, r: &Resolved) -> Result<Written> {
|
||||
let cases = if steps.len() > 1 { steps.len() } else { 0 };
|
||||
|
||||
d.line("*KEYWORD long=y");
|
||||
d.comment("Written by ModelCraft: implicit linear analysis, long format (20-column fields).");
|
||||
d.comment("Written by FieldCraft: implicit linear analysis, long format (20-column fields).");
|
||||
d.comment(&format!("Units: {} (consistent; nothing is converted).", model.units().label()));
|
||||
if cases > 0 {
|
||||
d.comment(&format!("{cases} steps are written as *CASE analyses: run LS-DYNA with CASE on its command line."));
|
||||
@@ -676,7 +676,7 @@ fn discrete(d: &mut Deck, model: &Model, r: &Resolved, plan: &Plan) -> Result<()
|
||||
let limit = |what: &str| Error::Limit(format!("no {what} id left for point masses, springs and dashpots"));
|
||||
let mut eid = model.elements().keys().next_back().copied().unwrap_or(0);
|
||||
let mut next_eid = || -> Result<u32> {
|
||||
eid = eid.checked_add(1).filter(|e| *e <= modelcraft_model::MAX_LABEL).ok_or_else(|| limit("element"))?;
|
||||
eid = eid.checked_add(1).filter(|e| *e <= fieldcraft_model::MAX_LABEL).ok_or_else(|| limit("element"))?;
|
||||
Ok(eid)
|
||||
};
|
||||
d.comment("Point masses");
|
||||
@@ -754,7 +754,7 @@ fn discrete(d: &mut Deck, model: &Model, r: &Resolved, plan: &Plan) -> Result<()
|
||||
while taken.contains(&node) {
|
||||
node = node.checked_add(1).ok_or_else(|| limit("node"))?;
|
||||
}
|
||||
if node > modelcraft_model::MAX_LABEL {
|
||||
if node > fieldcraft_model::MAX_LABEL {
|
||||
return Err(limit("node"));
|
||||
}
|
||||
ground.push((node, model.label_xyz(s.first).ok_or_else(|| broken(format!("node {}", s.first)))?));
|
||||
|
||||
@@ -52,7 +52,7 @@ mod card;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::fmt::Write as _;
|
||||
|
||||
use modelcraft_model::{
|
||||
use fieldcraft_model::{
|
||||
BeamProfile, Coupling, CouplingKind, Dofs, Elastic, ElementKind, Formulation, Hardening, Integration, Load, MAX_LABEL, Model, OutputVariable,
|
||||
Plastic, Procedure, Resolved, SectionKind, Step,
|
||||
};
|
||||
@@ -688,9 +688,9 @@ impl<'m> Deck<'m> {
|
||||
|
||||
fn emit(&self) -> Result<String> {
|
||||
let mut out = String::new();
|
||||
out.push_str("$ Nastran bulk data written by ModelCraft\n");
|
||||
out.push_str("$ Nastran bulk data written by FieldCraft\n");
|
||||
let _ = writeln!(out, "$ units: {}", self.model.units().label());
|
||||
let _ = writeln!(out, "SOL {}\nCEND\nTITLE = ModelCraft model\nECHO = NONE", self.sol);
|
||||
let _ = writeln!(out, "SOL {}\nCEND\nTITLE = FieldCraft model\nECHO = NONE", self.sol);
|
||||
for (i, sub) in self.subcases.iter().enumerate() {
|
||||
let _ = writeln!(out, "SUBCASE {}", i + 1);
|
||||
let title: String = format!(" SUBTITLE = {}", sub.step.name).chars().take(72).collect();
|
||||
@@ -945,7 +945,7 @@ impl<'m> Deck<'m> {
|
||||
}
|
||||
|
||||
/// A `PLOAD4` for one element face, positive pushing into the element.
|
||||
fn pressure(&self, sid: u32, f: modelcraft_model::ElementFace, magnitude: f64) -> Option<Card> {
|
||||
fn pressure(&self, sid: u32, f: fieldcraft_model::ElementFace, magnitude: f64) -> Option<Card> {
|
||||
let el = self.model.element(f.element)?;
|
||||
let card = Card::new("PLOAD4").int(sid).int(f.element);
|
||||
if el.kind.dimension() == 2 {
|
||||
@@ -1010,7 +1010,7 @@ fn plasticity(out: &mut String, name: &str, mid: u32, young: f64, plastic: &Plas
|
||||
write_named(out, &format!("stress-strain curve of {name}"), tid, Card::new("TABLES1").int(tid).end_line().reals(curve).text("ENDT"))
|
||||
}
|
||||
(Plastic::Table { .. }, None) | (Plastic::JohnsonCook { .. }, _) => {
|
||||
Err(Error::Model(modelcraft_model::Error::Invalid(format!("material `{name}`: its plasticity was not planned for MATS1"))))
|
||||
Err(Error::Model(fieldcraft_model::Error::Invalid(format!("material `{name}`: its plasticity was not planned for MATS1"))))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use modelcraft_decks::{Error, abaqus};
|
||||
use modelcraft_model::{
|
||||
use fieldcraft_decks::{Error, abaqus};
|
||||
use fieldcraft_model::{
|
||||
BeamProfile, BoundaryCondition, Coupling, CouplingKind, Distribution, Dofs, Elastic, ElementFace, ElementKind, Formulation, Integration, Load,
|
||||
Material, Model, OutputVariable, Procedure, ReferencePoint, Section, SectionKind, Set, Step, Symmetry, Target, UnitSystem,
|
||||
};
|
||||
@@ -185,12 +185,12 @@ fn read(deck: &str) -> Parsed {
|
||||
|
||||
fn golden(name: &str, deck: &str) {
|
||||
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/abaqus").join(format!("{name}.inp"));
|
||||
if std::env::var_os("MODELCRAFT_BLESS").is_some() {
|
||||
if std::env::var_os("FIELDCRAFT_BLESS").is_some() {
|
||||
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
|
||||
std::fs::write(&path, deck).unwrap();
|
||||
}
|
||||
let expected = std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("{}: {e} (run with MODELCRAFT_BLESS=1 to write it)", path.display()));
|
||||
assert!(expected == deck, "{name}.inp differs from the written deck (MODELCRAFT_BLESS=1 rewrites it):\n{deck}");
|
||||
let expected = std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("{}: {e} (run with FIELDCRAFT_BLESS=1 to write it)", path.display()));
|
||||
assert!(expected == deck, "{name}.inp differs from the written deck (FIELDCRAFT_BLESS=1 rewrites it):\n{deck}");
|
||||
}
|
||||
|
||||
fn assert_lines_fit(deck: &str) {
|
||||
@@ -513,16 +513,16 @@ fn numbers_read_back_exactly() {
|
||||
#[test]
|
||||
fn largest_labels_longest_numbers_and_names_fit_a_line() {
|
||||
let mut m = Model::default();
|
||||
let base = modelcraft_model::MAX_LABEL - 30;
|
||||
let base = fieldcraft_model::MAX_LABEL - 30;
|
||||
let ugly = -1.234_567_890_123_456_7e-300;
|
||||
let mut xyz = one_element(ElementKind::Hex20).nodes().values().map(|n| n.xyz).collect::<Vec<_>>();
|
||||
xyz[0] = [ugly, ugly, ugly];
|
||||
for (i, p) in xyz.iter().enumerate() {
|
||||
m.set_node(base + i as u32, *p).unwrap();
|
||||
}
|
||||
m.set_element(modelcraft_model::MAX_LABEL, ElementKind::Hex20, (0..20).map(|i| base + i).collect()).unwrap();
|
||||
let long = format!("A{}", "x".repeat(modelcraft_model::MAX_NAME_LEN - 1));
|
||||
m.add_set(&long, Set { elements: vec![modelcraft_model::MAX_LABEL], nodes: (0..20).map(|i| base + i).collect(), ..Set::default() }).unwrap();
|
||||
m.set_element(fieldcraft_model::MAX_LABEL, ElementKind::Hex20, (0..20).map(|i| base + i).collect()).unwrap();
|
||||
let long = format!("A{}", "x".repeat(fieldcraft_model::MAX_NAME_LEN - 1));
|
||||
m.add_set(&long, Set { elements: vec![fieldcraft_model::MAX_LABEL], nodes: (0..20).map(|i| base + i).collect(), ..Set::default() }).unwrap();
|
||||
m.add_material(
|
||||
&long,
|
||||
Material { elastic: Some(Elastic::Isotropic { young: ugly.abs(), poisson: 0.3 }), density: Some(1.0), ..Material::default() },
|
||||
@@ -537,7 +537,7 @@ fn largest_labels_longest_numbers_and_names_fit_a_line() {
|
||||
let deck = abaqus::write(&m).unwrap();
|
||||
assert_lines_fit(&deck);
|
||||
let p = read(&deck);
|
||||
assert_eq!(p.elements[&modelcraft_model::MAX_LABEL].1.len(), 20, "a hex20 continues onto a second line");
|
||||
assert_eq!(p.elements[&fieldcraft_model::MAX_LABEL].1.len(), 20, "a hex20 continues onto a second line");
|
||||
assert_eq!(p.nodes[&base], [ugly; 3]);
|
||||
}
|
||||
|
||||
@@ -704,7 +704,7 @@ fn a_model_that_is_not_ready_is_refused() {
|
||||
}
|
||||
|
||||
/// The cantilever with its steel given each plasticity in turn.
|
||||
fn plastic_cantilever(p: modelcraft_model::Plastic) -> Model {
|
||||
fn plastic_cantilever(p: fieldcraft_model::Plastic) -> Model {
|
||||
let mut m = cantilever();
|
||||
m.replace_material("Steel", Material { plastic: Some(p), ..steel() }).unwrap();
|
||||
m
|
||||
@@ -714,7 +714,7 @@ fn plastic_cantilever(p: modelcraft_model::Plastic) -> Model {
|
||||
/// erosion, failure models of Abaqus/Explicit, are reported.
|
||||
#[test]
|
||||
fn plasticity_is_written_and_failure_reported() {
|
||||
use modelcraft_model::{Damage, Erosion, Hardening, HardeningPoint, Plastic};
|
||||
use fieldcraft_model::{Damage, Erosion, Hardening, HardeningPoint, Plastic};
|
||||
let section = |deck: &str| deck.split("*MATERIAL, NAME=Steel").nth(1).and_then(|s| s.split("*SOLID SECTION").next()).unwrap().to_string();
|
||||
let deck =
|
||||
abaqus::write(&plastic_cantilever(Plastic::Bilinear { yield_stress: 250.0, hardening_modulus: 1000.0, hardening: Hardening::Isotropic }))
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
*HEADING
|
||||
ModelCraft model, units mm-t-s-N-MPa
|
||||
** Written by ModelCraft. The numbers are in one consistent unit system and are not converted.
|
||||
FieldCraft model, units mm-t-s-N-MPa
|
||||
** Written by FieldCraft. The numbers are in one consistent unit system and are not converted.
|
||||
** Nodes
|
||||
*NODE
|
||||
1, 0.0, 0.0, 0.0
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
*HEADING
|
||||
ModelCraft model, units mm-t-s-N-MPa
|
||||
** Written by ModelCraft. The numbers are in one consistent unit system and are not converted.
|
||||
FieldCraft model, units mm-t-s-N-MPa
|
||||
** Written by FieldCraft. The numbers are in one consistent unit system and are not converted.
|
||||
** Nodes
|
||||
*NODE
|
||||
1, 0.0, 0.0, 0.0
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
*HEADING
|
||||
ModelCraft model, units mm-t-s-N-MPa
|
||||
** Written by ModelCraft. The numbers are in one consistent unit system and are not converted.
|
||||
FieldCraft model, units mm-t-s-N-MPa
|
||||
** Written by FieldCraft. The numbers are in one consistent unit system and are not converted.
|
||||
** Nodes
|
||||
*NODE
|
||||
1, 0.0, 0.0, 0.0
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
//! Trusses (rod3), point masses, rotary inertia, springs and dashpots in every deck format:
|
||||
//! what each writes, and what each reports as unsupported.
|
||||
|
||||
use modelcraft_decks::{Error, abaqus, lsdyna, nastran};
|
||||
use modelcraft_model::{
|
||||
use fieldcraft_decks::{Error, abaqus, lsdyna, nastran};
|
||||
use fieldcraft_model::{
|
||||
BoundaryCondition, Direction, Elastic, ElementKind, Formulation, Mass, Material, Model, PointMass, Procedure, ReferencePoint, Section,
|
||||
SectionKind, Set, Spring, Step, Target, UnitSystem,
|
||||
};
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
$ Nastran bulk data written by ModelCraft
|
||||
$ Nastran bulk data written by FieldCraft
|
||||
$ units: mm-t-s-N-MPa
|
||||
SOL 101
|
||||
CEND
|
||||
TITLE = ModelCraft model
|
||||
TITLE = FieldCraft model
|
||||
ECHO = NONE
|
||||
SUBCASE 1
|
||||
SUBTITLE = Bend
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
$ Nastran bulk data written by ModelCraft
|
||||
$ Nastran bulk data written by FieldCraft
|
||||
$ units: unspecified
|
||||
SOL 103
|
||||
CEND
|
||||
TITLE = ModelCraft model
|
||||
TITLE = FieldCraft model
|
||||
ECHO = NONE
|
||||
SUBCASE 1
|
||||
SUBTITLE = Modes
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
$ Nastran bulk data written by ModelCraft
|
||||
$ Nastran bulk data written by FieldCraft
|
||||
$ units: mm-t-s-N-MPa
|
||||
SOL 101
|
||||
CEND
|
||||
TITLE = ModelCraft model
|
||||
TITLE = FieldCraft model
|
||||
ECHO = NONE
|
||||
SUBCASE 1
|
||||
SUBTITLE = Sag
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
//! LS-DYNA deck writer: golden files of our own small models, node-order mappings checked by the
|
||||
//! sign of the element Jacobian, a re-read of our own output, and what the format cannot take.
|
||||
//!
|
||||
//! Golden files live in `tests/lsdyna/`; `MODELCRAFT_BLESS=1 cargo test -p modelcraft-decks`
|
||||
//! Golden files live in `tests/lsdyna/`; `FIELDCRAFT_BLESS=1 cargo test -p fieldcraft-decks`
|
||||
//! rewrites them after an intended change (review the diff).
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use modelcraft_decks::{Error, lsdyna};
|
||||
use modelcraft_model::*;
|
||||
use fieldcraft_decks::{Error, lsdyna};
|
||||
use fieldcraft_model::*;
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Our own models.
|
||||
@@ -200,11 +200,11 @@ fn golden(name: &str, model: &Model) -> lsdyna::Written {
|
||||
let again = lsdyna::write(model).unwrap();
|
||||
assert_eq!(written, again, "deterministic");
|
||||
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/lsdyna").join(format!("{name}.k"));
|
||||
if std::env::var_os("MODELCRAFT_BLESS").is_some() {
|
||||
if std::env::var_os("FIELDCRAFT_BLESS").is_some() {
|
||||
std::fs::write(&path, &written.text).unwrap();
|
||||
}
|
||||
let expected = std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("{}: {e} (run with MODELCRAFT_BLESS=1)", path.display()));
|
||||
assert!(written.text == expected, "{name}.k differs from the golden file (MODELCRAFT_BLESS=1 rewrites it)");
|
||||
let expected = std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("{}: {e} (run with FIELDCRAFT_BLESS=1)", path.display()));
|
||||
assert!(written.text == expected, "{name}.k differs from the golden file (FIELDCRAFT_BLESS=1 rewrites it)");
|
||||
assert!(written.text.is_ascii());
|
||||
for line in written.text.lines() {
|
||||
assert!(line.len() <= 200 && line == line.trim_end(), "{line:?}");
|
||||
@@ -688,7 +688,7 @@ fn hostile_numbers_stay_within_their_fields() {
|
||||
fn explicit_steps_are_refused_by_every_format() {
|
||||
let mut m = cantilever();
|
||||
m.add_step(step("Impact", Procedure::ExplicitDynamic(ExplicitDynamic::new(1e-3)), &["clamp"], &[]), None).unwrap();
|
||||
for f in modelcraft_decks::FORMATS {
|
||||
for f in fieldcraft_decks::FORMATS {
|
||||
let e = f.write(&m).unwrap_err();
|
||||
assert!(matches!(e, Error::Unsupported { .. }), "{}: {e}", f.name);
|
||||
assert!(e.to_string().contains("step `Impact`: explicit dynamic"), "{}: {e}", f.name);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
*KEYWORD long=y
|
||||
$ Written by ModelCraft: implicit linear analysis, long format (20-column fields).
|
||||
$ Written by FieldCraft: implicit linear analysis, long format (20-column fields).
|
||||
$ Units: mm-t-s-N-MPa (consistent; nothing is converted).
|
||||
*NODE
|
||||
$ nid x y z
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
*KEYWORD long=y
|
||||
$ Written by ModelCraft: implicit linear analysis, long format (20-column fields).
|
||||
$ Written by FieldCraft: implicit linear analysis, long format (20-column fields).
|
||||
$ Units: mm-t-s-N-MPa (consistent; nothing is converted).
|
||||
$ 2 steps are written as *CASE analyses: run LS-DYNA with CASE on its command line.
|
||||
*NODE
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
*KEYWORD long=y
|
||||
$ Written by ModelCraft: implicit linear analysis, long format (20-column fields).
|
||||
$ Written by FieldCraft: implicit linear analysis, long format (20-column fields).
|
||||
$ Units: mm-t-s-N-MPa (consistent; nothing is converted).
|
||||
*NODE
|
||||
$ nid x y z
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
*KEYWORD long=y
|
||||
$ Written by ModelCraft: implicit linear analysis, long format (20-column fields).
|
||||
$ Written by FieldCraft: implicit linear analysis, long format (20-column fields).
|
||||
$ Units: m-kg-s-N-Pa (consistent; nothing is converted).
|
||||
*NODE
|
||||
$ nid x y z
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
//! output (grids, elements, constraint sets, case control), node order checked against Nastran's
|
||||
//! own element conventions by Jacobian sign and mid-side positions, and what is unsupported.
|
||||
//!
|
||||
//! Golden files are rewritten with `MODELCRAFT_BLESS=1 cargo test -p modelcraft-decks`.
|
||||
//! Golden files are rewritten with `FIELDCRAFT_BLESS=1 cargo test -p fieldcraft-decks`.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use modelcraft_decks::nastran;
|
||||
use modelcraft_decks::{Error, with_steps};
|
||||
use modelcraft_model::{
|
||||
use fieldcraft_decks::nastran;
|
||||
use fieldcraft_decks::{Error, with_steps};
|
||||
use fieldcraft_model::{
|
||||
BeamProfile, BoundaryCondition, Coupling, CouplingKind, Distribution, Dofs, Elastic, ElementFace, ElementKind, Formulation, Integration, Load,
|
||||
Material, Model, Procedure, ReferencePoint, Section, SectionKind, Set, Step, Target, UnitSystem,
|
||||
};
|
||||
@@ -266,12 +266,12 @@ fn modes() -> Model {
|
||||
|
||||
fn golden(name: &str, deck: &str) {
|
||||
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/golden/nastran").join(format!("{name}.bdf"));
|
||||
if std::env::var_os("MODELCRAFT_BLESS").is_some() {
|
||||
if std::env::var_os("FIELDCRAFT_BLESS").is_some() {
|
||||
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
|
||||
std::fs::write(&path, deck).unwrap();
|
||||
}
|
||||
let want = std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("{}: {e} (bless with MODELCRAFT_BLESS=1)", path.display()));
|
||||
assert_eq!(deck, want, "{name}: the deck changed (bless with MODELCRAFT_BLESS=1 after checking it)");
|
||||
let want = std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("{}: {e} (bless with FIELDCRAFT_BLESS=1)", path.display()));
|
||||
assert_eq!(deck, want, "{name}: the deck changed (bless with FIELDCRAFT_BLESS=1 after checking it)");
|
||||
}
|
||||
|
||||
fn unsupported(m: &Model) -> String {
|
||||
@@ -640,7 +640,7 @@ fn steps_are_chosen_and_mixed_solutions_are_refused() {
|
||||
// A modes step holds the clamp but carries no loads.
|
||||
assert!(r.cards("FORCE").next().is_none());
|
||||
assert_eq!(read(&nastran::write(&with_steps(&m, &only(&["Bend"])).unwrap()).unwrap()).sol, 101);
|
||||
let nastran = modelcraft_decks::format("nastran").unwrap();
|
||||
let nastran = fieldcraft_decks::format("nastran").unwrap();
|
||||
assert_eq!(nastran.write_steps(&m, Some(&only(&["Bend"]))).unwrap().text, nastran::write(&with_steps(&m, &only(&["Bend"])).unwrap()).unwrap());
|
||||
|
||||
for bad in [only(&[]), only(&["Nope"]), only(&[&"x".repeat(100_000)]), only(&["", "\u{0}"])] {
|
||||
@@ -740,7 +740,7 @@ fn conflicting_values_and_ids_at_the_top_of_the_range() {
|
||||
/// damage and erosion are reported.
|
||||
#[test]
|
||||
fn plasticity_is_written_as_mats1() {
|
||||
use modelcraft_model::{Erosion, Hardening, HardeningPoint, Plastic};
|
||||
use fieldcraft_model::{Erosion, Hardening, HardeningPoint, Plastic};
|
||||
let with = |p: Plastic| {
|
||||
let mut m = cantilever();
|
||||
m.replace_material("Steel", Material { plastic: Some(p), ..steel() }).unwrap();
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
//! 2D models (plane stress, plane strain, axisymmetric) in every deck format: element names,
|
||||
//! section data, edge surfaces and pressures, centrifugal loads, and what a format cannot write.
|
||||
|
||||
use modelcraft_decks::{Error, abaqus, edge_pressure_loads, lsdyna, nastran};
|
||||
use modelcraft_model::{
|
||||
use fieldcraft_decks::{Error, abaqus, edge_pressure_loads, lsdyna, nastran};
|
||||
use fieldcraft_model::{
|
||||
BoundaryCondition, Elastic, ElementEdge, ElementKind, Formulation, Integration, Load, Material, Model, Procedure, Section, SectionKind, Set,
|
||||
Step, Target, UnitSystem,
|
||||
};
|
||||
|
||||
+13
-13
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "modelcraft-engine"
|
||||
description = "ModelCraft's command engine: every user-visible action as a command with a JSON form"
|
||||
name = "fieldcraft-engine"
|
||||
description = "FieldCraft's command engine: every user-visible action as a command with a JSON form"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
@@ -9,17 +9,17 @@ license.workspace = true
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
modelcraft-model = { workspace = true }
|
||||
modelcraft-geometry = { workspace = true }
|
||||
modelcraft-mesh = { workspace = true }
|
||||
modelcraft-io = { workspace = true }
|
||||
modelcraft-decks = { workspace = true }
|
||||
modelcraft-solver = { workspace = true }
|
||||
modelcraft-explicit = { workspace = true }
|
||||
modelcraft-fem = { workspace = true }
|
||||
modelcraft-lbm = { workspace = true }
|
||||
modelcraft-parallel = { workspace = true }
|
||||
modelcraft-render = { workspace = true }
|
||||
fieldcraft-model = { workspace = true }
|
||||
fieldcraft-geometry = { workspace = true }
|
||||
fieldcraft-mesh = { workspace = true }
|
||||
fieldcraft-io = { workspace = true }
|
||||
fieldcraft-decks = { workspace = true }
|
||||
fieldcraft-solver = { workspace = true }
|
||||
fieldcraft-explicit = { workspace = true }
|
||||
fieldcraft-fem = { workspace = true }
|
||||
fieldcraft-lbm = { workspace = true }
|
||||
fieldcraft-parallel = { workspace = true }
|
||||
fieldcraft-render = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
@@ -2,16 +2,16 @@
|
||||
//!
|
||||
//! The desktop app keeps an [`Autosaver`] and calls [`Autosaver::save`] on a timer: while the
|
||||
//! session has unsaved changes not yet autosaved, the whole model is written to the app's entry in
|
||||
//! the recovery folder ([`modelcraft_io::recovery`]); once nothing is unsaved the entry goes. After
|
||||
//! the recovery folder ([`fieldcraft_io::recovery`]); once nothing is unsaved the entry goes. After
|
||||
//! a crash the next launch lists the entry (`file.recovery.list`) and opens it (`file.recover`).
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use modelcraft_io::recovery::Slot;
|
||||
use fieldcraft_io::recovery::Slot;
|
||||
|
||||
use crate::{Result, Session};
|
||||
|
||||
pub use modelcraft_io::recovery::default_dir;
|
||||
pub use fieldcraft_io::recovery::default_dir;
|
||||
|
||||
/// One app's autosaver.
|
||||
#[derive(Debug)]
|
||||
@@ -67,7 +67,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn autosaves_unsaved_work_and_recovers_it_after_a_crash() {
|
||||
let dir = std::env::temp_dir().join(format!("modelcraft-autosave-{}", std::process::id()));
|
||||
let dir = std::env::temp_dir().join(format!("fieldcraft-autosave-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
let mut s = Session::default();
|
||||
let mut a = Autosaver::new(&dir).unwrap();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! The capability catalogue: everything ModelCraft does or will do, in one table.
|
||||
//! The capability catalogue: everything FieldCraft does or will do, in one table.
|
||||
//!
|
||||
//! Each [`Capability`] is a toolbar entry: its tab and panel, its label and one-line tip, and
|
||||
//! either the command of the same id it runs ([`Status::Ready`]) or the milestone that brings it
|
||||
@@ -138,7 +138,7 @@ pub enum Status {
|
||||
Ready,
|
||||
/// Coming in this roadmap milestone (`M2`…).
|
||||
Planned { milestone: &'static str },
|
||||
/// Runs `deck.write` with this `format` once `modelcraft_decks::FORMATS` has a writer for it;
|
||||
/// Runs `deck.write` with this `format` once `fieldcraft_decks::FORMATS` has a writer for it;
|
||||
/// until then coming in `milestone`.
|
||||
Deck { format: &'static str, milestone: &'static str },
|
||||
}
|
||||
@@ -163,7 +163,7 @@ impl Capability {
|
||||
match self.status {
|
||||
Status::Ready => Some(self.id),
|
||||
Status::Planned { .. } => None,
|
||||
Status::Deck { format, .. } => modelcraft_decks::format(format).map(|_| crate::cmd::deck::WRITE.id),
|
||||
Status::Deck { format, .. } => fieldcraft_decks::format(format).map(|_| crate::cmd::deck::WRITE.id),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -431,13 +431,13 @@ pub static CATALOGUE: &[Capability] = &[
|
||||
deck("deck.write.abaqus", "Abaqus Input", "abaqus", "Write an Abaqus input file (.inp)"),
|
||||
deck("deck.write.nastran", "Nastran Deck", "nastran", "Write Nastran bulk data (SOL 101, 103)"),
|
||||
deck("deck.write.lsdyna", "LS-DYNA Deck", "lsdyna", "Write an LS-DYNA keyword file"),
|
||||
soon("deck.read", "Read Deck", Solve, "Decks", "M9", "Read a deck, keeping keywords ModelCraft does not know"),
|
||||
soon("deck.read", "Read Deck", Solve, "Decks", "M9", "Read a deck, keeping keywords FieldCraft does not know"),
|
||||
ready_more(
|
||||
"solve.static",
|
||||
"Static",
|
||||
Solve,
|
||||
"Implicit",
|
||||
"Linear static (or coupled temperature–displacement) solve of a step with ModelCraft's own solver; heat transfer in the drop-down",
|
||||
"Linear static (or coupled temperature–displacement) solve of a step with FieldCraft's own solver; heat transfer in the drop-down",
|
||||
&["solve.heat"],
|
||||
),
|
||||
ready("solve.frequency", "Frequency", Solve, "Implicit", "Natural frequencies and mode shapes of a frequency step"),
|
||||
@@ -482,10 +482,10 @@ pub static CATALOGUE: &[Capability] = &[
|
||||
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"),
|
||||
ready("file.open", "Open Model", Tools, "File", "Open a .modelcraft model"),
|
||||
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 .modelcraft file"),
|
||||
ready("file.save_as", "Save As", Tools, "File", "Save the model to a new .modelcraft file"),
|
||||
ready("file.save", "Save", Tools, "File", "Save the model to its .fieldcraft file"),
|
||||
ready("file.save_as", "Save As", Tools, "File", "Save the model to a new .fieldcraft file"),
|
||||
ready_more(
|
||||
"file.recover",
|
||||
"Recover",
|
||||
@@ -527,7 +527,7 @@ pub fn features_page() -> String {
|
||||
let mut md = String::from(
|
||||
"# Features\n\n\
|
||||
Generated from the capability catalogue (`crates/engine/src/catalogue.rs`) by\n\
|
||||
`cargo run -p modelcraft-cli -- features > docs/features.md`; a test fails when it is out of date.\n\
|
||||
`cargo run -p fieldcraft-cli -- features > docs/features.md`; a test fails when it is out of date.\n\
|
||||
Every entry is a toolbar button. **Ready** entries run their command; **coming soon** entries are\n\
|
||||
shown greyed with the roadmap milestone that brings them (ROADMAP.md). Scripts, the command line,\n\
|
||||
the control channel and AI agents over MCP (docs/mcp.md) run the same commands.\n\n",
|
||||
@@ -592,16 +592,16 @@ mod tests {
|
||||
}
|
||||
|
||||
/// Deck entries follow the writers: one entry per format, ready exactly when its writer is in
|
||||
/// `modelcraft_decks::FORMATS`, running `deck.write` with its format.
|
||||
/// `fieldcraft_decks::FORMATS`, running `deck.write` with its format.
|
||||
#[test]
|
||||
fn deck_entries_follow_the_writers() {
|
||||
let decks: Vec<(&Capability, &str)> =
|
||||
CATALOGUE.iter().filter_map(|c| if let Status::Deck { format, .. } = c.status { Some((c, format)) } else { None }).collect();
|
||||
for f in modelcraft_decks::FORMATS {
|
||||
for f in fieldcraft_decks::FORMATS {
|
||||
assert!(decks.iter().any(|(_, name)| *name == f.name), "deck format `{}` has no catalogue entry", f.name);
|
||||
}
|
||||
for (c, format) in decks {
|
||||
let written = modelcraft_decks::format(format).is_some();
|
||||
let written = fieldcraft_decks::format(format).is_some();
|
||||
assert_eq!(c.command(), written.then_some("deck.write"), "{}", c.id);
|
||||
assert_eq!(c.milestone().is_none(), written, "{}", c.id);
|
||||
assert_eq!(c.preset(), serde_json::json!({"format": format}));
|
||||
@@ -640,6 +640,6 @@ mod tests {
|
||||
fn features_page_is_current() {
|
||||
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../docs/features.md");
|
||||
let committed = std::fs::read_to_string(&path).unwrap_or_default().replace("\r\n", "\n");
|
||||
assert!(committed == features_page(), "docs/features.md is out of date: run `cargo run -p modelcraft-cli -- features > docs/features.md`");
|
||||
assert!(committed == features_page(), "docs/features.md is out of date: run `cargo run -p fieldcraft-cli -- features > docs/features.md`");
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
//! `mesh.block`: a structured mesh of one six-sided block (solid elements) or four-sided planar
|
||||
//! patch (2D elements) from its corners and curved edges ([`modelcraft_mesh::block`]), joined to
|
||||
//! patch (2D elements) from its corners and curved edges ([`fieldcraft_mesh::block`]), joined to
|
||||
//! the mesh already there and tagged as a body (a patch: a face of the body) with named faces,
|
||||
//! edges and vertices, so sets by geometry select it.
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use modelcraft_mesh::block::{Block, BlockMesh, Curve, Patch, block, patch};
|
||||
use modelcraft_mesh::join::{check_conforming, weld};
|
||||
use modelcraft_model::{ElementKind, Entity, GeometryRef, Model};
|
||||
use fieldcraft_mesh::block::{Block, BlockMesh, Curve, Patch, block, patch};
|
||||
use fieldcraft_mesh::join::{check_conforming, weld};
|
||||
use fieldcraft_model::{ElementKind, Entity, GeometryRef, Model};
|
||||
use serde::Deserialize;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
@@ -178,7 +178,7 @@ fn block_mesh(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
s.model.tag_element(label, body_tag)?;
|
||||
}
|
||||
for (refs, pieces, tag) in [
|
||||
(&face_refs, &mesh.faces, Model::tag_element_face as fn(&mut Model, u32, u8, u32) -> modelcraft_model::Result<()>),
|
||||
(&face_refs, &mesh.faces, Model::tag_element_face as fn(&mut Model, u32, u8, u32) -> fieldcraft_model::Result<()>),
|
||||
(&edge_refs, &mesh.edges, Model::tag_element_edge),
|
||||
] {
|
||||
for (k, r) in refs {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use modelcraft_io::roots::Access;
|
||||
use fieldcraft_io::roots::Access;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::req;
|
||||
@@ -11,22 +11,22 @@ use crate::{CommandKind, CommandSpec, EngineError, Result, Session};
|
||||
pub const WRITE: CommandSpec = CommandSpec {
|
||||
id: "deck.write",
|
||||
label: "Write Solver Deck",
|
||||
params: r#"{"format": "abaqus|nastran|lsdyna", "path": "part.inp", "steps": ["Load"]} — writes the model as a solver deck (formats: modelcraft_decks::FORMATS); `steps` (optional) writes only those steps, as a Nastran run needs (static or frequency steps, not both); LS-DYNA writes several steps as *CASE analyses (run it with CASE); fails naming every error of the model check and everything the format cannot express"#,
|
||||
params: r#"{"format": "abaqus|nastran|lsdyna", "path": "part.inp", "steps": ["Load"]} — writes the model as a solver deck (formats: fieldcraft_decks::FORMATS); `steps` (optional) writes only those steps, as a Nastran run needs (static or frequency steps, not both); LS-DYNA writes several steps as *CASE analyses (run it with CASE); fails naming every error of the model check and everything the format cannot express"#,
|
||||
kind: CommandKind::Session,
|
||||
run: write,
|
||||
};
|
||||
|
||||
fn write(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let name: &str = req(p, "format")?;
|
||||
let format = modelcraft_decks::format(name).ok_or_else(|| {
|
||||
let format = fieldcraft_decks::format(name).ok_or_else(|| {
|
||||
let shown: String = name.chars().take(32).collect();
|
||||
let known: Vec<&str> = modelcraft_decks::FORMATS.iter().map(|f| f.name).collect();
|
||||
let known: Vec<&str> = fieldcraft_decks::FORMATS.iter().map(|f| f.name).collect();
|
||||
EngineError::Params(format!("unknown deck format `{shown}`; known: {}", known.join(", ")))
|
||||
})?;
|
||||
let path = super::file_param(s, p, "path", Access::Write)?;
|
||||
let steps: Option<Vec<String>> = super::opt(p, "steps")?;
|
||||
let deck = format.write_steps(&s.model, steps.as_deref())?;
|
||||
modelcraft_io::fs::write_atomic(Path::new(&path), deck.text.as_bytes(), false)?;
|
||||
fieldcraft_io::fs::write_atomic(Path::new(&path), deck.text.as_bytes(), false)?;
|
||||
Ok(json!({"format": format.name, "path": path, "bytes": deck.text.len(), "warnings": deck.warnings}))
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn writes_the_defined_model_and_refuses_what_it_cannot() {
|
||||
let dir = std::env::temp_dir().join(format!("modelcraft-deck-write-{}", std::process::id()));
|
||||
let dir = std::env::temp_dir().join(format!("fieldcraft-deck-write-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let path = dir.join("bar.inp");
|
||||
let path_str = path.to_string_lossy().into_owned();
|
||||
@@ -46,7 +46,7 @@ mod tests {
|
||||
let journal = s.journal.len();
|
||||
let out = s.execute("deck.write", &json!({"format": "abaqus", "path": path_str})).unwrap();
|
||||
let written = std::fs::read_to_string(&path).unwrap();
|
||||
assert_eq!(written, modelcraft_decks::abaqus::write(&s.model).unwrap());
|
||||
assert_eq!(written, fieldcraft_decks::abaqus::write(&s.model).unwrap());
|
||||
assert_eq!(out["bytes"], written.len());
|
||||
assert_eq!(out["warnings"].as_array().map(Vec::len), Some(1), "`press` is in no step: {out}");
|
||||
assert!(written.contains("*ELEMENT, TYPE=C3D8I"), "{written}");
|
||||
@@ -81,14 +81,14 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn writes_lsdyna_keyword_decks() {
|
||||
let dir = std::env::temp_dir().join(format!("modelcraft-deck-lsdyna-{}", std::process::id()));
|
||||
let dir = std::env::temp_dir().join(format!("fieldcraft-deck-lsdyna-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let path = dir.join("bar.k");
|
||||
let mut s = Session::default();
|
||||
crate::cmd::define::tests::defined_bar(&mut s);
|
||||
let out = s.execute("deck.write", &json!({"format": "lsdyna", "path": path.to_string_lossy()})).unwrap();
|
||||
let written = std::fs::read_to_string(&path).unwrap();
|
||||
assert_eq!(written, modelcraft_decks::lsdyna::write(&s.model).unwrap().text);
|
||||
assert_eq!(written, fieldcraft_decks::lsdyna::write(&s.model).unwrap().text);
|
||||
assert_eq!(out["format"], "lsdyna");
|
||||
assert!(written.starts_with("*KEYWORD long=y\n") && written.contains("*CASE_BEGIN_2"), "two steps, two cases");
|
||||
// One chosen step: a single analysis, no cases.
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
//! points, couplings, point masses, springs and dashpots, boundary conditions, loads, fluid
|
||||
//! domains and steps, and the step activity table.
|
||||
//!
|
||||
//! Each definition's parameters are its JSON form in the model (the same form a `.modelcraft`
|
||||
//! Each definition's parameters are its JSON form in the model (the same form a `.fieldcraft`
|
||||
//! file stores), plus its `name`. `create` fails when the name is taken, `edit` replaces an
|
||||
//! existing definition, `delete` refuses while another definition refers to it.
|
||||
|
||||
use modelcraft_model::{
|
||||
use fieldcraft_model::{
|
||||
BoundaryCondition, Coupling, FaceCondition, FluidBoundary, FluidDomain, Load, Material, Model, PointMass, ReferencePoint, Section, Set, Spring,
|
||||
Step,
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Files: open and save `.modelcraft` models, and recover autosaved ones.
|
||||
//! Files: open and save `.fieldcraft` models, and recover autosaved ones.
|
||||
//!
|
||||
//! A saved model refers to its design and imported meshes by path and content hash; opening it
|
||||
//! opens them again. Paths inside the model file's folder are stored relative to it, so a folder
|
||||
@@ -9,12 +9,12 @@
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
use modelcraft_geometry::tagged_mesh;
|
||||
use modelcraft_geometry::{Design, MAX_DESIGN_BYTES, open_design};
|
||||
use modelcraft_io::{Geometry, ModelFile, SourceFile};
|
||||
use fieldcraft_geometry::tagged_mesh;
|
||||
use fieldcraft_geometry::{Design, MAX_DESIGN_BYTES, open_design};
|
||||
use fieldcraft_io::{Geometry, ModelFile, SourceFile};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use modelcraft_io::roots::{Access, Roots};
|
||||
use fieldcraft_io::roots::{Access, Roots};
|
||||
|
||||
use super::{file_param, req};
|
||||
use crate::{CommandKind, CommandSpec, EngineError, Result, Session, next_revision};
|
||||
@@ -22,7 +22,7 @@ use crate::{CommandKind, CommandSpec, EngineError, Result, Session, next_revisio
|
||||
pub const OPEN: CommandSpec = CommandSpec {
|
||||
id: "file.open",
|
||||
label: "Open Model",
|
||||
params: r#"{"path": "bracket.modelcraft"} — opens the model, its mesh and its journal, and opens its design and imported meshes again from their files (one that is missing or changed is listed in `warnings`; the model still opens); unsaved changes of the current model are discarded and the undo history starts empty"#,
|
||||
params: r#"{"path": "bracket.fieldcraft"} — opens the model, its mesh and its journal, and opens its design and imported meshes again from their files (one that is missing or changed is listed in `warnings`; the model still opens); unsaved changes of the current model are discarded and the undo history starts empty"#,
|
||||
kind: CommandKind::Session,
|
||||
run: open,
|
||||
};
|
||||
@@ -30,7 +30,7 @@ pub const OPEN: CommandSpec = CommandSpec {
|
||||
pub const SAVE: CommandSpec = CommandSpec {
|
||||
id: "file.save",
|
||||
label: "Save Model",
|
||||
params: r#"{"path"?: "bracket.modelcraft"} — saves to `path`, else to the file the model was opened from or last saved to; written atomically, the previous version kept as `<file>.bak`"#,
|
||||
params: r#"{"path"?: "bracket.fieldcraft"} — saves to `path`, else to the file the model was opened from or last saved to; written atomically, the previous version kept as `<file>.bak`"#,
|
||||
kind: CommandKind::Session,
|
||||
run: save,
|
||||
};
|
||||
@@ -38,7 +38,7 @@ pub const SAVE: CommandSpec = CommandSpec {
|
||||
pub const SAVE_AS: CommandSpec = CommandSpec {
|
||||
id: "file.save_as",
|
||||
label: "Save Model As",
|
||||
params: r#"{"path": "bracket.modelcraft"} — saves to a new file, which becomes the model's file"#,
|
||||
params: r#"{"path": "bracket.fieldcraft"} — saves to a new file, which becomes the model's file"#,
|
||||
kind: CommandKind::Session,
|
||||
run: save_as,
|
||||
};
|
||||
@@ -46,7 +46,7 @@ pub const SAVE_AS: CommandSpec = CommandSpec {
|
||||
pub const RECOVERY_LIST: CommandSpec = CommandSpec {
|
||||
id: "file.recovery.list",
|
||||
label: "Recoverable Models",
|
||||
params: r#"{"dir"?: "…"} — models autosaved by apps that crashed or closed with unsaved changes, newest first (default folder: MODELCRAFT_RECOVERY_DIR or the per-user data folder)"#,
|
||||
params: r#"{"dir"?: "…"} — models autosaved by apps that crashed or closed with unsaved changes, newest first (default folder: FIELDCRAFT_RECOVERY_DIR or the per-user data folder)"#,
|
||||
kind: CommandKind::Session,
|
||||
run: recovery_list,
|
||||
};
|
||||
@@ -70,7 +70,7 @@ pub const RECOVERY_DISCARD: CommandSpec = CommandSpec {
|
||||
pub const SAMPLE: CommandSpec = CommandSpec {
|
||||
id: "file.sample",
|
||||
label: "Open Sample",
|
||||
params: r#"{"name": "parts|cantilever|impact"} — opens a sample model (modelcraft_engine::sample) in place of the current one, unsaved changes discarded; `cantilever` and `impact` are ready to solve (solve.run)"#,
|
||||
params: r#"{"name": "parts|cantilever|impact"} — opens a sample model (fieldcraft_engine::sample) in place of the current one, unsaved changes discarded; `cantilever` and `impact` are ready to solve (solve.run)"#,
|
||||
kind: CommandKind::Session,
|
||||
run: open_sample,
|
||||
};
|
||||
@@ -120,7 +120,7 @@ fn reopen<T>(
|
||||
base: Option<&Path>,
|
||||
roots: &Roots,
|
||||
max: usize,
|
||||
open: impl FnOnce(&[u8]) -> modelcraft_geometry::Result<T>,
|
||||
open: impl FnOnce(&[u8]) -> fieldcraft_geometry::Result<T>,
|
||||
warnings: &mut Vec<String>,
|
||||
) -> (Option<T>, SourceFile) {
|
||||
let path = resolved_path(stored.path(), base);
|
||||
@@ -132,7 +132,7 @@ fn reopen<T>(
|
||||
return (None, kept);
|
||||
}
|
||||
};
|
||||
let bytes = match modelcraft_geometry::read_file(&checked, max) {
|
||||
let bytes = match fieldcraft_geometry::read_file(&checked, max) {
|
||||
Ok(b) => b,
|
||||
Err(e) => {
|
||||
warnings.push(format!("{what} `{path}` was not opened ({e}); the model opens without it"));
|
||||
@@ -154,15 +154,15 @@ fn reopen<T>(
|
||||
}
|
||||
|
||||
impl Session {
|
||||
/// The bytes of the `.modelcraft` file holding this session, with geometry paths inside
|
||||
/// The bytes of the `.fieldcraft` file holding this session, with geometry paths inside
|
||||
/// `base` (the folder the file goes to) stored relative to it.
|
||||
pub fn file_bytes(&self, base: Option<&Path>) -> Result<Vec<u8>> {
|
||||
let stored = |s: &SourceFile| s.with_path(stored_path(s.path(), base));
|
||||
let geometry = Geometry {
|
||||
design: self.sources.design.as_ref().map(stored).transpose()?,
|
||||
meshes: self.sources.meshes.iter().map(stored).collect::<modelcraft_io::Result<_>>()?,
|
||||
meshes: self.sources.meshes.iter().map(stored).collect::<fieldcraft_io::Result<_>>()?,
|
||||
};
|
||||
Ok(modelcraft_io::write(&self.model, &geometry, &self.journal)?)
|
||||
Ok(fieldcraft_io::write(&self.model, &geometry, &self.journal)?)
|
||||
}
|
||||
|
||||
/// A session holding what a file holds, its geometry opened again from the paths it names
|
||||
@@ -211,10 +211,10 @@ fn summary(s: &Session, warnings: &[String]) -> Value {
|
||||
})
|
||||
}
|
||||
|
||||
/// Replace the session with a `.modelcraft` file's content; `path` is its own file (geometry
|
||||
/// Replace the session with a `.fieldcraft` file's content; `path` is its own file (geometry
|
||||
/// paths are relative to its folder), `saved` whether that file holds this content.
|
||||
fn load(s: &mut Session, bytes: &[u8], path: Option<String>, saved: bool) -> Result<Value> {
|
||||
let file = modelcraft_io::read(bytes)?;
|
||||
let file = fieldcraft_io::read(bytes)?;
|
||||
let (mut opened, warnings) = Session::from_file(file, path.as_deref().and_then(|p| Path::new(p).parent()), &s.file_access);
|
||||
opened.path = path;
|
||||
if saved {
|
||||
@@ -226,8 +226,8 @@ fn load(s: &mut Session, bytes: &[u8], path: Option<String>, saved: bool) -> Res
|
||||
|
||||
fn open(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let path = absolute(&file_param(s, p, "path", Access::Read)?);
|
||||
let max = usize::try_from(modelcraft_io::MAX_FILE_BYTES).unwrap_or(usize::MAX);
|
||||
let bytes = modelcraft_geometry::read_file(&path, max)?;
|
||||
let max = usize::try_from(fieldcraft_io::MAX_FILE_BYTES).unwrap_or(usize::MAX);
|
||||
let bytes = fieldcraft_geometry::read_file(&path, max)?;
|
||||
load(s, &bytes, Some(path.clone()), true).map_err(|e| EngineError::Params(format!("{path}: {e}")))
|
||||
}
|
||||
|
||||
@@ -238,7 +238,7 @@ fn save_to(s: &mut Session, path: &str) -> Result<Value> {
|
||||
return Err(EngineError::Params(format!("`{target}` exists and is not a regular file")));
|
||||
}
|
||||
let bytes = s.file_bytes(Path::new(&target).parent())?;
|
||||
modelcraft_io::fs::write_atomic(Path::new(&target), &bytes, true)?;
|
||||
fieldcraft_io::fs::write_atomic(Path::new(&target), &bytes, true)?;
|
||||
s.mark_saved(target.clone());
|
||||
Ok(json!({"path": target, "bytes": bytes.len()}))
|
||||
}
|
||||
@@ -265,19 +265,19 @@ fn recovery_dir(s: &Session, p: &Value, access: Access) -> Result<PathBuf> {
|
||||
match p.get("dir") {
|
||||
Some(_) => Ok(PathBuf::from(file_param(s, p, "dir", access)?)),
|
||||
None => {
|
||||
crate::autosave::default_dir().ok_or_else(|| EngineError::Params("no recovery folder: set MODELCRAFT_RECOVERY_DIR or give `dir`".into()))
|
||||
crate::autosave::default_dir().ok_or_else(|| EngineError::Params("no recovery folder: set FIELDCRAFT_RECOVERY_DIR or give `dir`".into()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn recovery_list(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let entries = modelcraft_io::recovery::orphans(&recovery_dir(s, p, Access::Read)?);
|
||||
let entries = fieldcraft_io::recovery::orphans(&recovery_dir(s, p, Access::Read)?);
|
||||
serde_json::to_value(entries).map_err(|e| EngineError::Params(e.to_string()))
|
||||
}
|
||||
|
||||
fn recover(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let dir = recovery_dir(s, p, Access::Read)?;
|
||||
let (bytes, entry) = modelcraft_io::recovery::load(&dir, req::<&str>(p, "id")?)?;
|
||||
let (bytes, entry) = fieldcraft_io::recovery::load(&dir, req::<&str>(p, "id")?)?;
|
||||
// Recovered work is unsaved until it is saved. Autosaves keep geometry paths absolute.
|
||||
load(s, &bytes, entry.path, false)
|
||||
}
|
||||
@@ -285,26 +285,26 @@ fn recover(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
fn recovery_discard(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let dir = recovery_dir(s, p, Access::Write)?;
|
||||
if p.get("all").and_then(Value::as_bool) == Some(true) {
|
||||
let ids: Vec<String> = modelcraft_io::recovery::orphans(&dir).into_iter().map(|e| e.id).collect();
|
||||
let ids: Vec<String> = fieldcraft_io::recovery::orphans(&dir).into_iter().map(|e| e.id).collect();
|
||||
for id in &ids {
|
||||
modelcraft_io::recovery::remove(&dir, id)?;
|
||||
fieldcraft_io::recovery::remove(&dir, id)?;
|
||||
}
|
||||
return Ok(json!({"discarded": ids}));
|
||||
}
|
||||
let id = req::<&str>(p, "id")?;
|
||||
modelcraft_io::recovery::remove(&dir, id)?;
|
||||
fieldcraft_io::recovery::remove(&dir, id)?;
|
||||
Ok(json!({"discarded": [id]}))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use modelcraft_geometry::sample::{Plate, plate_design};
|
||||
use fieldcraft_geometry::sample::{Plate, plate_design};
|
||||
use serde_json::json;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn dir(tag: &str) -> PathBuf {
|
||||
let d = std::env::temp_dir().join(format!("modelcraft-file-{tag}-{}", std::process::id()));
|
||||
let d = std::env::temp_dir().join(format!("fieldcraft-file-{tag}-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&d);
|
||||
std::fs::create_dir_all(&d).unwrap();
|
||||
d
|
||||
@@ -327,17 +327,17 @@ mod tests {
|
||||
s.execute("geometry.open", &json!({"path": design})).unwrap();
|
||||
defined(&mut s);
|
||||
assert!(s.execute("file.save", &json!({})).is_err(), "no file yet, and no dialog");
|
||||
let path = d.join("bracket.modelcraft");
|
||||
let path = d.join("bracket.fieldcraft");
|
||||
s.execute("file.save_as", &json!({"path": path})).unwrap();
|
||||
assert!(!s.is_dirty());
|
||||
let bytes = std::fs::read(&path).unwrap();
|
||||
let file = modelcraft_io::read(&bytes).unwrap();
|
||||
let file = fieldcraft_io::read(&bytes).unwrap();
|
||||
assert_eq!(file.geometry.design.as_ref().unwrap().path(), "plate.solidcraft", "stored next to the model, relative");
|
||||
|
||||
// Saving again keeps the previous version and gives the same bytes.
|
||||
s.execute("file.save", &json!({})).unwrap();
|
||||
assert_eq!(std::fs::read(&path).unwrap(), bytes);
|
||||
assert_eq!(std::fs::read(modelcraft_io::fs::backup_path(&path)).unwrap(), bytes);
|
||||
assert_eq!(std::fs::read(fieldcraft_io::fs::backup_path(&path)).unwrap(), bytes);
|
||||
|
||||
let mut t = Session::default();
|
||||
t.execute("mesh.node.add", &json!({"label": 9, "xyz": [0, 0, 0]})).unwrap();
|
||||
@@ -383,17 +383,17 @@ mod tests {
|
||||
};
|
||||
refused(s.execute("geometry.open", &json!({"path": outside.join("plate.solidcraft")})));
|
||||
refused(s.execute("geometry.import.mesh", &json!({"path": outside.join("x.3mf")})));
|
||||
refused(s.execute("file.open", &json!({"path": outside.join("x.modelcraft")})));
|
||||
refused(s.execute("file.open", &json!({"path": outside.join("x.fieldcraft")})));
|
||||
refused(s.execute("file.recovery.list", &json!({"dir": outside})));
|
||||
refused(s.execute("file.recovery.discard", &json!({"all": true, "dir": input})));
|
||||
s.execute("geometry.open", &json!({"path": input.join("plate.solidcraft")})).unwrap();
|
||||
defined(&mut s);
|
||||
// Writes go to the write root only: not the read root, not elsewhere.
|
||||
refused(s.execute("file.save_as", &json!({"path": input.join("m.modelcraft")})));
|
||||
refused(s.execute("file.save_as", &json!({"path": outside.join("m.modelcraft")})));
|
||||
refused(s.execute("file.save_as", &json!({"path": output.join("../outside/m.modelcraft")})));
|
||||
refused(s.execute("file.save_as", &json!({"path": input.join("m.fieldcraft")})));
|
||||
refused(s.execute("file.save_as", &json!({"path": outside.join("m.fieldcraft")})));
|
||||
refused(s.execute("file.save_as", &json!({"path": output.join("../outside/m.fieldcraft")})));
|
||||
refused(s.execute("deck.write", &json!({"format": "abaqus", "path": outside.join("m.inp")})));
|
||||
let saved = output.join("m.modelcraft");
|
||||
let saved = output.join("m.fieldcraft");
|
||||
s.execute("file.save_as", &json!({"path": saved})).unwrap();
|
||||
assert!(saved.is_file());
|
||||
assert_eq!(std::fs::read_dir(&outside).unwrap().count(), 1, "nothing written outside");
|
||||
@@ -402,8 +402,8 @@ mod tests {
|
||||
// opens without it.
|
||||
let mut other = Session::default();
|
||||
other.execute("geometry.open", &json!({"path": outside.join("plate.solidcraft")})).unwrap();
|
||||
other.execute("file.save_as", &json!({"path": input.join("other.modelcraft")})).unwrap();
|
||||
let r = s.execute("file.open", &json!({"path": input.join("other.modelcraft")})).unwrap();
|
||||
other.execute("file.save_as", &json!({"path": input.join("other.fieldcraft")})).unwrap();
|
||||
let r = s.execute("file.open", &json!({"path": input.join("other.fieldcraft")})).unwrap();
|
||||
assert!(r["warnings"][0].as_str().unwrap().contains("automation path rejected"), "{r}");
|
||||
assert!(s.design.is_none());
|
||||
assert!(s.file_access.is_confined());
|
||||
@@ -414,7 +414,7 @@ mod tests {
|
||||
#[test]
|
||||
fn hostile_file_params_and_files_change_nothing() {
|
||||
let d = dir("hostile");
|
||||
let junk = d.join("junk.modelcraft");
|
||||
let junk = d.join("junk.fieldcraft");
|
||||
std::fs::write(&junk, b"PK\x03\x04 not a model").unwrap();
|
||||
let mut s = Session::default();
|
||||
defined(&mut s);
|
||||
@@ -422,12 +422,12 @@ mod tests {
|
||||
for (id, p) in [
|
||||
("file.open", json!({"path": junk})),
|
||||
("file.open", json!({"path": d})),
|
||||
("file.open", json!({"path": d.join("missing.modelcraft")})),
|
||||
("file.open", json!({"path": d.join("missing.fieldcraft")})),
|
||||
("file.open", json!({"path": "\u{0}"})),
|
||||
("file.open", json!({"path": 5})),
|
||||
("file.save", json!({"path": ""})),
|
||||
("file.save", json!({"path": d})),
|
||||
("file.save_as", json!({"path": d.join("no/such/dir/a.modelcraft")})),
|
||||
("file.save_as", json!({"path": d.join("no/such/dir/a.fieldcraft")})),
|
||||
("file.save_as", json!({"path": "x".repeat(5000)})),
|
||||
("file.save_as", json!({})),
|
||||
("file.recover", json!({"id": "../../etc/passwd", "dir": d})),
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use modelcraft_geometry::tagged_mesh;
|
||||
use modelcraft_geometry::{MAX_DESIGN_BYTES, Shape, SurfaceMesh, open_design};
|
||||
use modelcraft_io::SourceFile;
|
||||
use modelcraft_io::roots::Access;
|
||||
use fieldcraft_geometry::tagged_mesh;
|
||||
use fieldcraft_geometry::{MAX_DESIGN_BYTES, Shape, SurfaceMesh, open_design};
|
||||
use fieldcraft_io::SourceFile;
|
||||
use fieldcraft_io::roots::Access;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::{CommandKind, CommandSpec, EngineError, Result, Session};
|
||||
@@ -42,7 +42,7 @@ pub const IMPORT_MESH: CommandSpec = CommandSpec {
|
||||
|
||||
fn open(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let path = super::file_param(s, p, "path", Access::Read)?;
|
||||
let bytes = modelcraft_geometry::read_file(&path, MAX_DESIGN_BYTES)?;
|
||||
let bytes = fieldcraft_geometry::read_file(&path, MAX_DESIGN_BYTES)?;
|
||||
let source = SourceFile::of(super::file::absolute(&path), &bytes)?;
|
||||
let design = open_design(&bytes)?;
|
||||
let out = json!({
|
||||
@@ -98,10 +98,10 @@ fn tessellate(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
fn import_mesh(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let path = super::file_param(s, p, "path", Access::Read)?;
|
||||
if s.sources.meshes.len() >= modelcraft_io::MAX_MESH_SOURCES {
|
||||
return Err(EngineError::Params(format!("a model imports at most {} meshes", modelcraft_io::MAX_MESH_SOURCES)));
|
||||
if s.sources.meshes.len() >= fieldcraft_io::MAX_MESH_SOURCES {
|
||||
return Err(EngineError::Params(format!("a model imports at most {} meshes", fieldcraft_io::MAX_MESH_SOURCES)));
|
||||
}
|
||||
let bytes = modelcraft_geometry::read_file(&path, tagged_mesh::MAX_FILE_BYTES)?;
|
||||
let bytes = fieldcraft_geometry::read_file(&path, tagged_mesh::MAX_FILE_BYTES)?;
|
||||
let source = SourceFile::of(super::file::absolute(&path), &bytes)?;
|
||||
let surfaces = tagged_mesh::read_3mf(&bytes)?;
|
||||
let first = s.surfaces.len();
|
||||
@@ -130,7 +130,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn import_adds_tagged_surfaces_and_failures_change_nothing() {
|
||||
let dir = std::env::temp_dir().join(format!("modelcraft-import-mesh-{}", std::process::id()));
|
||||
let dir = std::env::temp_dir().join(format!("fieldcraft-import-mesh-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let model = format!(
|
||||
r#"<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02" xmlns:ft="{}"><resources>
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use modelcraft_geometry::{FaceSurface, Shape, SurfaceMesh};
|
||||
use modelcraft_mesh::hex::{HexMesh, HexMethod, HexOptions, Surface, hex_mesh};
|
||||
use modelcraft_mesh::size::UniformSize;
|
||||
use modelcraft_mesh::surface::BoundarySurface;
|
||||
use modelcraft_model::{GeometryRef, Model};
|
||||
use fieldcraft_geometry::{FaceSurface, Shape, SurfaceMesh};
|
||||
use fieldcraft_mesh::hex::{HexMesh, HexMethod, HexOptions, Surface, hex_mesh};
|
||||
use fieldcraft_mesh::size::UniformSize;
|
||||
use fieldcraft_mesh::surface::BoundarySurface;
|
||||
use fieldcraft_model::{GeometryRef, Model};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::{CommandKind, CommandSpec, EngineError, Result, Session};
|
||||
@@ -175,7 +175,7 @@ fn generate(s: &mut Session, p: &Value, default: HexMethod, allowed: &[HexMethod
|
||||
.map_err(|e| EngineError::Params(format!("body `{}`: {e}", b.name)))?;
|
||||
let chord = if b.surfaces.is_empty() { 0.0 } else { number(p, "tolerance")?.unwrap_or(DEFAULT_RELATIVE_TOLERANCE * b.diagonal) };
|
||||
let options =
|
||||
HexOptions { method, quadratic, chordal_tolerance: chord, max_elements: modelcraft_model::MAX_ELEMENTS / 8, ..HexOptions::default() };
|
||||
HexOptions { method, quadratic, chordal_tolerance: chord, max_elements: fieldcraft_model::MAX_ELEMENTS / 8, ..HexOptions::default() };
|
||||
let mesh = hex_mesh(&surface, &b.surfaces, &UniformSize(h), &options).map_err(|e| EngineError::Params(format!("body `{}`: {e}", b.name)))?;
|
||||
meshes.push((b, mesh));
|
||||
}
|
||||
@@ -242,7 +242,7 @@ fn tag_faces(model: &mut Model, b: &Input<'_>, m: &HexMesh, first_element: u32)
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use modelcraft_geometry::sample::Plate;
|
||||
use fieldcraft_geometry::sample::Plate;
|
||||
|
||||
fn opened() -> (crate::tests::TempDir, Session) {
|
||||
let (dir, path) = crate::tests::sample_design_file(&Plate::default());
|
||||
@@ -261,7 +261,7 @@ mod tests {
|
||||
let n = body["elements"].as_u64().unwrap() as usize;
|
||||
assert!(n > 100, "{r}");
|
||||
assert!(body["quality"]["min_scaled_jacobian"].as_f64().unwrap() > 0.3, "{r}");
|
||||
assert_eq!(s.model.element_counts(), vec![(modelcraft_model::ElementKind::Hex8, n)]);
|
||||
assert_eq!(s.model.element_counts(), vec![(fieldcraft_model::ElementKind::Hex8, n)]);
|
||||
// Volume: the plate minus the hole, to the tessellation's accuracy.
|
||||
let v = 40.0 * 30.0 * 10.0 - std::f64::consts::PI * 25.0 * 10.0;
|
||||
assert!((body["volume"].as_f64().unwrap() - v).abs() < 0.01 * v, "{r}");
|
||||
@@ -280,7 +280,7 @@ mod tests {
|
||||
// Hex20 with mid-edge nodes on the hole.
|
||||
let r2 = s.execute("mesh.hex.generate", &json!({"order": 2, "size": 2.5})).unwrap();
|
||||
assert_eq!(r2["bodies"][0]["kind"], "hex20");
|
||||
assert_eq!(s.model.element_counts(), vec![(modelcraft_model::ElementKind::Hex20, n)]);
|
||||
assert_eq!(s.model.element_counts(), vec![(fieldcraft_model::ElementKind::Hex20, n)]);
|
||||
let v2 = r2["bodies"][0]["volume"].as_f64().unwrap();
|
||||
assert!((v2 - v).abs() < (body["volume"].as_f64().unwrap() - v).abs() + 1e-6 * v, "hex20 {v2} vs hex8 {} vs {v}", body["volume"]);
|
||||
// The set still resolves after the remesh.
|
||||
@@ -288,7 +288,7 @@ mod tests {
|
||||
assert!(set["faces"]["count"].as_u64().unwrap() > 0);
|
||||
// Undo returns the hex8 mesh.
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(s.model.element_counts(), vec![(modelcraft_model::ElementKind::Hex8, n)]);
|
||||
assert_eq!(s.model.element_counts(), vec![(fieldcraft_model::ElementKind::Hex8, n)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use std::borrow::Cow;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use modelcraft_geometry::SurfaceMesh;
|
||||
use modelcraft_mesh::cdt::{CdtOptions, tetrahedralize};
|
||||
use modelcraft_mesh::delaunay::Triangulation;
|
||||
use modelcraft_mesh::quality::{self, QualityReport};
|
||||
use modelcraft_mesh::surface::{BoundarySurface, Defect, FeatureKind, SurfaceOptions};
|
||||
use modelcraft_model::{ElementKind, GeometryRef, MAX_ELEMENTS};
|
||||
use fieldcraft_geometry::SurfaceMesh;
|
||||
use fieldcraft_mesh::cdt::{CdtOptions, tetrahedralize};
|
||||
use fieldcraft_mesh::delaunay::Triangulation;
|
||||
use fieldcraft_mesh::quality::{self, QualityReport};
|
||||
use fieldcraft_mesh::surface::{BoundarySurface, Defect, FeatureKind, SurfaceOptions};
|
||||
use fieldcraft_model::{ElementKind, GeometryRef, MAX_ELEMENTS};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::{label, labels, listed, optional_number, points, xyz};
|
||||
@@ -196,8 +196,8 @@ fn polygon(v: &Value, what: &str) -> Result<Vec<[f64; 2]>> {
|
||||
}
|
||||
|
||||
fn planar(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
use modelcraft_mesh::planar::{Curve, face_boundary, mesh_region};
|
||||
use modelcraft_model::Entity;
|
||||
use fieldcraft_mesh::planar::{Curve, face_boundary, mesh_region};
|
||||
use fieldcraft_model::Entity;
|
||||
|
||||
let size = p
|
||||
.get("size")
|
||||
@@ -212,7 +212,7 @@ fn planar(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
.filter(|k| k.dimension() == 2)
|
||||
.ok_or_else(|| EngineError::Params("`kind` must be quad4, quad8, tri3 or tri6".into()))?,
|
||||
};
|
||||
let opts = modelcraft_mesh::quad::QuadOptions {
|
||||
let opts = fieldcraft_mesh::quad::QuadOptions {
|
||||
max_elements: optional_count(p, "max_elements", 1_000_000, 1..=PLANAR_MAX_ELEMENTS)?,
|
||||
..Default::default()
|
||||
};
|
||||
@@ -415,7 +415,7 @@ fn surface_check(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let options = surface_options(p, &mesh)?;
|
||||
let surface = match BoundarySurface::new(&mesh.positions, &mesh.triangles, &mesh.tri_face, &options) {
|
||||
Ok(surface) => surface,
|
||||
Err(modelcraft_mesh::Error::Surface(d)) => {
|
||||
Err(fieldcraft_mesh::Error::Surface(d)) => {
|
||||
return Ok(json!({"valid": false, "defects": {"count": d.total, "listed": d.listed.iter().map(defect_json).collect::<Vec<_>>()}}));
|
||||
}
|
||||
Err(e) => return Err(e.into()),
|
||||
@@ -511,8 +511,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn a_tessellated_block_is_a_valid_boundary() {
|
||||
use modelcraft_mesh::surface::{BoundarySurface, FeatureKind, SurfaceOptions};
|
||||
let mesh = modelcraft_geometry::block([0.0; 3], [2.0, 3.0, 4.0]).unwrap().tessellate(0.01).unwrap();
|
||||
use fieldcraft_mesh::surface::{BoundarySurface, FeatureKind, SurfaceOptions};
|
||||
let mesh = fieldcraft_geometry::block([0.0; 3], [2.0, 3.0, 4.0]).unwrap().tessellate(0.01).unwrap();
|
||||
let options = SurfaceOptions { weld_distance: mesh.weld_distance, ..SurfaceOptions::default() };
|
||||
let s = BoundarySurface::new(&mesh.positions, &mesh.triangles, &mesh.tri_face, &options).unwrap();
|
||||
assert!(s.points().len() < mesh.positions.len(), "the tessellation is a soup that welding joins");
|
||||
@@ -535,9 +535,9 @@ mod tests {
|
||||
fn surface_check_on_an_open_design_and_on_imported_surfaces() {
|
||||
use std::sync::Arc;
|
||||
|
||||
use modelcraft_geometry::sample::{Plate, plate_design};
|
||||
use modelcraft_geometry::tagged_mesh::TaggedSurface;
|
||||
use modelcraft_geometry::{SurfaceMesh, open_design};
|
||||
use fieldcraft_geometry::sample::{Plate, plate_design};
|
||||
use fieldcraft_geometry::tagged_mesh::TaggedSurface;
|
||||
use fieldcraft_geometry::{SurfaceMesh, open_design};
|
||||
|
||||
let plate = Plate::default();
|
||||
let mut s = Session { design: Some(Arc::new(open_design(&plate_design(&plate).unwrap()).unwrap())), ..Session::default() };
|
||||
@@ -590,8 +590,8 @@ mod tests {
|
||||
fn tet_generate_meshes_a_body_with_tags_and_appends() {
|
||||
use std::sync::Arc;
|
||||
|
||||
use modelcraft_geometry::open_design;
|
||||
use modelcraft_geometry::sample::{Plate, plate_design};
|
||||
use fieldcraft_geometry::open_design;
|
||||
use fieldcraft_geometry::sample::{Plate, plate_design};
|
||||
|
||||
let plate = Plate::default();
|
||||
let mut s = Session { design: Some(Arc::new(open_design(&plate_design(&plate).unwrap()).unwrap())), ..Session::default() };
|
||||
@@ -599,7 +599,7 @@ mod tests {
|
||||
let elements = r["elements"].as_u64().unwrap() as usize;
|
||||
assert!(elements > 0, "{r}");
|
||||
assert_eq!(s.model.elements().len(), elements);
|
||||
assert!(s.model.elements().values().all(|e| e.kind == modelcraft_model::ElementKind::Tet4));
|
||||
assert!(s.model.elements().values().all(|e| e.kind == fieldcraft_model::ElementKind::Tet4));
|
||||
let (volume, boundary) = (r["volume"].as_f64().unwrap(), r["boundary_volume"].as_f64().unwrap());
|
||||
assert!((volume - boundary).abs() <= 1e-9 * boundary, "{r}");
|
||||
// Every element is on the body; boundary faces carry the 8 face names.
|
||||
@@ -641,8 +641,8 @@ mod tests {
|
||||
fn tet_generate_honours_the_session_caps() {
|
||||
use std::sync::Arc;
|
||||
|
||||
use modelcraft_geometry::open_design;
|
||||
use modelcraft_geometry::sample::{Plate, plate_design};
|
||||
use fieldcraft_geometry::open_design;
|
||||
use fieldcraft_geometry::sample::{Plate, plate_design};
|
||||
|
||||
use crate::{EngineError, Limits};
|
||||
|
||||
@@ -650,7 +650,7 @@ mod tests {
|
||||
let mut s = Session { design: Some(design), limits: Limits { max_tets: 10, ..Limits::default() }, ..Session::default() };
|
||||
let plate = json!({"body": "Body1", "tolerance": 0.5});
|
||||
let refused = s.execute("mesh.tet.generate", &plate).unwrap_err();
|
||||
assert!(matches!(refused, EngineError::Mesh(modelcraft_mesh::Error::Limit(_))), "{refused}");
|
||||
assert!(matches!(refused, EngineError::Mesh(fieldcraft_mesh::Error::Limit(_))), "{refused}");
|
||||
assert!(s.model.elements().is_empty());
|
||||
let r = s.execute("mesh.tet.generate", &json!({"body": "Body1", "tolerance": 0.5, "max_tets": 1_000_000})).unwrap();
|
||||
assert!(r["elements"].as_u64().unwrap() > 0, "{r}");
|
||||
@@ -660,8 +660,8 @@ mod tests {
|
||||
fn tet_generate_on_an_imported_surface_and_an_open_one() {
|
||||
use std::sync::Arc;
|
||||
|
||||
use modelcraft_geometry::SurfaceMesh;
|
||||
use modelcraft_geometry::tagged_mesh::TaggedSurface;
|
||||
use fieldcraft_geometry::SurfaceMesh;
|
||||
use fieldcraft_geometry::tagged_mesh::TaggedSurface;
|
||||
|
||||
let p = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
|
||||
let t = vec![[0, 2, 1], [0, 1, 3], [1, 2, 3], [0, 3, 2]];
|
||||
@@ -692,7 +692,7 @@ mod tests {
|
||||
assert_eq!(r["kind"], "quad8");
|
||||
let elements = r["elements"].as_u64().unwrap() as usize;
|
||||
assert!(elements > 20, "{r}");
|
||||
assert!(s.model.elements().values().all(|e| e.kind == modelcraft_model::ElementKind::Quad8));
|
||||
assert!(s.model.elements().values().all(|e| e.kind == fieldcraft_model::ElementKind::Quad8));
|
||||
assert!(s.model.nodes().values().all(|n| n.xyz[2] == 0.0));
|
||||
// Sets on the tagged edges carry the boundary condition and the load.
|
||||
for (cmd, params) in [
|
||||
@@ -736,8 +736,8 @@ mod tests {
|
||||
fn planar_meshes_a_flat_face_of_a_design_with_edges_named_by_their_neighbours() {
|
||||
use std::sync::Arc;
|
||||
|
||||
use modelcraft_geometry::open_design;
|
||||
use modelcraft_geometry::sample::{Plate, plate_design};
|
||||
use fieldcraft_geometry::open_design;
|
||||
use fieldcraft_geometry::sample::{Plate, plate_design};
|
||||
|
||||
let design = open_design(&plate_design(&Plate::default()).unwrap()).unwrap();
|
||||
let names: Vec<String> = design.bodies[0].faces().iter().map(|f| f.name.clone()).collect();
|
||||
|
||||
@@ -13,11 +13,11 @@ mod results;
|
||||
pub(crate) mod solve;
|
||||
mod tags;
|
||||
|
||||
use modelcraft_model::MAX_LABEL;
|
||||
use fieldcraft_model::MAX_LABEL;
|
||||
use serde::Deserialize;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use modelcraft_io::roots::Access;
|
||||
use fieldcraft_io::roots::Access;
|
||||
|
||||
use crate::{CommandSpec, EngineError, Result, Session};
|
||||
|
||||
@@ -120,7 +120,7 @@ pub(crate) fn label(p: &Value, key: &str) -> Result<u32> {
|
||||
|
||||
/// The first node and element labels after the model's largest, for `nodes` new nodes and
|
||||
/// `elements` new elements, all within [`MAX_LABEL`].
|
||||
pub(crate) fn fresh_labels(model: &modelcraft_model::Model, nodes: usize, elements: usize) -> Result<(u32, u32)> {
|
||||
pub(crate) fn fresh_labels(model: &fieldcraft_model::Model, nodes: usize, elements: usize) -> Result<(u32, u32)> {
|
||||
let first = |last: Option<&u32>, count: usize| {
|
||||
let first = last.map_or(Some(1), |l| l.checked_add(1))?;
|
||||
(u64::from(first) + count as u64 <= u64::from(MAX_LABEL) + 1).then_some(first)
|
||||
@@ -168,7 +168,7 @@ pub(crate) fn labels(p: &Value, key: &str, max_len: usize) -> Result<Vec<u32>> {
|
||||
fn path_param<'a>(p: &'a Value, key: &str) -> Result<&'a str> {
|
||||
p.get(key)
|
||||
.and_then(Value::as_str)
|
||||
.filter(|s| !s.trim().is_empty() && s.len() <= modelcraft_io::MAX_PATH_LEN && !s.contains('\0'))
|
||||
.filter(|s| !s.trim().is_empty() && s.len() <= fieldcraft_io::MAX_PATH_LEN && !s.contains('\0'))
|
||||
.ok_or_else(|| EngineError::Params(format!("`{key}` must be a file path")))
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use modelcraft_model::{Severity, UnitSystem};
|
||||
use fieldcraft_model::{Severity, UnitSystem};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::{listed, name};
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
//! `solve.*`: run the model's steps with the implicit, the explicit or the lattice Boltzmann
|
||||
//! solver, keep the results in the session, and read them back.
|
||||
|
||||
use modelcraft_explicit::{ExplicitResult, solve_explicit};
|
||||
use modelcraft_lbm::{FluidResult, Obstacle};
|
||||
use modelcraft_mesh::surface::{BoundarySurface, SurfaceOptions};
|
||||
use modelcraft_model::FluidDomain;
|
||||
use modelcraft_parallel as parallel;
|
||||
use modelcraft_solver::{
|
||||
use fieldcraft_explicit::{ExplicitResult, solve_explicit};
|
||||
use fieldcraft_lbm::{FluidResult, Obstacle};
|
||||
use fieldcraft_mesh::surface::{BoundarySurface, SurfaceOptions};
|
||||
use fieldcraft_model::FluidDomain;
|
||||
use fieldcraft_parallel as parallel;
|
||||
use fieldcraft_solver::{
|
||||
FrequencyOptions, Limits, Location, MassKind, StaticResult, StepResult as ImplicitResult, solve_frequency, solve_heat, solve_static,
|
||||
};
|
||||
use serde_json::{Value, json};
|
||||
@@ -25,7 +25,7 @@ pub const RUN: CommandSpec = CommandSpec {
|
||||
pub const STATIC: CommandSpec = CommandSpec {
|
||||
id: "solve.static",
|
||||
label: "Solve Static Step",
|
||||
params: r#"{"step": "Load", "max_dofs": 6000000, "max_factor_entries": 400000000, "threads": 8} — solves a linear static or coupled temperature–displacement step (the only one when `step` is left out) and keeps the result in the session; the caps may only be lowered; `threads` (1 to 256, by default the session's: `MODELCRAFT_THREADS` or the cores) changes only the time, never the result"#,
|
||||
params: r#"{"step": "Load", "max_dofs": 6000000, "max_factor_entries": 400000000, "threads": 8} — solves a linear static or coupled temperature–displacement step (the only one when `step` is left out) and keeps the result in the session; the caps may only be lowered; `threads` (1 to 256, by default the session's: `FIELDCRAFT_THREADS` or the cores) changes only the time, never the result"#,
|
||||
kind: CommandKind::Model,
|
||||
run: run_static,
|
||||
};
|
||||
@@ -129,7 +129,7 @@ fn extreme(values: impl Iterator<Item = (u32, f64)>, better: fn(f64, f64) -> boo
|
||||
}
|
||||
|
||||
fn run_all(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
use modelcraft_model::Procedure;
|
||||
use fieldcraft_model::Procedure;
|
||||
type Solve = fn(&mut Session, &Value) -> Result<Value>;
|
||||
let steps: Vec<(String, &'static str, Solve)> = s
|
||||
.model
|
||||
@@ -242,7 +242,7 @@ fn run_frequency(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
|
||||
fn run_explicit(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
use modelcraft_explicit::Limits as ExplicitLimits;
|
||||
use fieldcraft_explicit::Limits as ExplicitLimits;
|
||||
let limits = ExplicitLimits {
|
||||
max_dofs: cap(p, "max_dofs", ExplicitLimits::MAX_DOFS)?,
|
||||
max_increments: cap(p, "max_increments", ExplicitLimits::MAX_INCREMENTS)?,
|
||||
@@ -340,18 +340,18 @@ fn obstacles(s: &Session, domain: &FluidDomain) -> Result<Vec<Obstacle>> {
|
||||
}
|
||||
|
||||
fn run_fluid(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let defaults = modelcraft_lbm::Limits::default();
|
||||
let limits = modelcraft_lbm::Limits {
|
||||
let defaults = fieldcraft_lbm::Limits::default();
|
||||
let limits = fieldcraft_lbm::Limits {
|
||||
max_cells: cap(p, "max_cells", defaults.max_cells)?,
|
||||
max_steps: cap(p, "max_steps", defaults.max_steps)?,
|
||||
max_cell_updates: cap(p, "max_cell_updates", defaults.max_cell_updates)?,
|
||||
max_frame_bytes: cap(p, "max_frame_bytes", defaults.max_frame_bytes)?,
|
||||
};
|
||||
let step = opt::<String>(p, "step")?;
|
||||
let (_, domain) = modelcraft_lbm::fluid_step(&s.model, step.as_deref())?;
|
||||
let (_, domain) = fieldcraft_lbm::fluid_step(&s.model, step.as_deref())?;
|
||||
let obstacles = obstacles(s, domain)?;
|
||||
let model = &s.model;
|
||||
let r = parallel::with_threads(threads(s, p)?, || modelcraft_lbm::solve_fluid(model, step.as_deref(), &obstacles, &limits))?;
|
||||
let r = parallel::with_threads(threads(s, p)?, || fieldcraft_lbm::solve_fluid(model, step.as_deref(), &obstacles, &limits))?;
|
||||
let summary = fluid_summary(&r);
|
||||
s.store_result(StepResult::Fluid(r));
|
||||
Ok(summary)
|
||||
@@ -475,7 +475,7 @@ fn probe(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
|
||||
/// The node with a result nearest to each point (lowest label on a tie), where it is and how far.
|
||||
fn nearest_nodes(model: &modelcraft_model::Model, with_result: &[u32], at: &[[f64; 3]]) -> Result<Vec<(u32, [f64; 3], f64)>> {
|
||||
fn nearest_nodes(model: &fieldcraft_model::Model, with_result: &[u32], at: &[[f64; 3]]) -> Result<Vec<(u32, [f64; 3], f64)>> {
|
||||
let candidates: Vec<(u32, [f64; 3])> = with_result.iter().filter_map(|&l| Some((l, model.node(l)?.xyz))).collect();
|
||||
at.iter()
|
||||
.map(|p| {
|
||||
@@ -604,7 +604,7 @@ fn probe_frame(r: &ExplicitResult, step: &str, nodes: &[u32], elements: &[u32],
|
||||
Ok(json!({
|
||||
"element": l,
|
||||
"stress": f.stress.get(i),
|
||||
"von_mises": f.stress.get(i).map(modelcraft_fem::von_mises),
|
||||
"von_mises": f.stress.get(i).map(fieldcraft_fem::von_mises),
|
||||
"strain": f.strain.get(i),
|
||||
"plastic_strain": f.plastic_strain.get(i),
|
||||
"damage": f.damage.get(i),
|
||||
@@ -855,8 +855,8 @@ pub(crate) mod tests {
|
||||
|
||||
#[test]
|
||||
fn a_fluid_domain_takes_its_obstacles_from_the_open_design() {
|
||||
use modelcraft_geometry::sample::{Plate, plate_design};
|
||||
let dir = std::env::temp_dir().join(format!("modelcraft-fluid-{}", std::process::id()));
|
||||
use fieldcraft_geometry::sample::{Plate, plate_design};
|
||||
let dir = std::env::temp_dir().join(format!("fieldcraft-fluid-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let path = dir.join("plate.solidcraft");
|
||||
std::fs::write(&path, plate_design(&Plate::default()).unwrap()).unwrap();
|
||||
@@ -965,7 +965,7 @@ pub(crate) mod tests {
|
||||
/// ρ = 1, clamped at x = 0; one node set per cross-section (`x0`, `x1`, …) and the tip's
|
||||
/// corner node `tip`; a frequency step `Modes` and the section `beam`.
|
||||
fn explicit_cantilever(s: &mut Session, length: f64, n: [usize; 2]) {
|
||||
use modelcraft_model::{ElementKind, Set};
|
||||
use fieldcraft_model::{ElementKind, Set};
|
||||
let [nx, ny] = n;
|
||||
let label = |i: usize, j: usize, k: usize| (1 + i + (nx + 1) * (j + (ny + 1) * k)) as u32;
|
||||
for k in 0..=ny {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Geometry tags on the mesh: which body, face, edge or vertex each mesh entity lies on. Meshers
|
||||
//! write them; these commands set them by hand (for a mesh from elsewhere) and clear them.
|
||||
|
||||
use modelcraft_model::{ElementFace, GeometryRef, MAX_ELEMENTS};
|
||||
use fieldcraft_model::{ElementFace, GeometryRef, MAX_ELEMENTS};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::{opt, req};
|
||||
|
||||
@@ -14,9 +14,9 @@
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::Arc;
|
||||
|
||||
use modelcraft_geometry::Design;
|
||||
use modelcraft_geometry::tagged_mesh::TaggedSurface;
|
||||
use modelcraft_model::Model;
|
||||
use fieldcraft_geometry::Design;
|
||||
use fieldcraft_geometry::tagged_mesh::TaggedSurface;
|
||||
use fieldcraft_model::Model;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::{EngineError, Result, Session};
|
||||
@@ -44,7 +44,7 @@ pub(crate) struct State {
|
||||
model: Model,
|
||||
design: Option<Arc<Design>>,
|
||||
surfaces: Vec<Arc<TaggedSurface>>,
|
||||
sources: modelcraft_io::Geometry,
|
||||
sources: fieldcraft_io::Geometry,
|
||||
revision: u64,
|
||||
}
|
||||
|
||||
|
||||
+27
-27
@@ -6,8 +6,8 @@
|
||||
//! the geometry are restored and an [`EngineError`] comes back.
|
||||
//!
|
||||
//! Commands that change the model are journalled and can be undone and redone
|
||||
//! (`edit.undo`, `edit.redo`; [`history`]). The session is saved to and opened from `.modelcraft`
|
||||
//! files (`file.save`, `file.open`; [`modelcraft_io`]) and autosaved for recovery after a crash
|
||||
//! (`edit.undo`, `edit.redo`; [`history`]). The session is saved to and opened from `.fieldcraft`
|
||||
//! files (`file.save`, `file.open`; [`fieldcraft_io`]) and autosaved for recovery after a crash
|
||||
//! ([`autosave`]).
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||
#![forbid(unsafe_code)]
|
||||
@@ -23,10 +23,10 @@ pub mod view;
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use modelcraft_explicit::ExplicitResult;
|
||||
use modelcraft_geometry::Design;
|
||||
use modelcraft_geometry::tagged_mesh::TaggedSurface;
|
||||
use modelcraft_model::Model;
|
||||
use fieldcraft_explicit::ExplicitResult;
|
||||
use fieldcraft_geometry::Design;
|
||||
use fieldcraft_geometry::tagged_mesh::TaggedSurface;
|
||||
use fieldcraft_model::Model;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::history::History;
|
||||
@@ -38,21 +38,21 @@ pub enum EngineError {
|
||||
#[error("{0}")]
|
||||
Params(String),
|
||||
#[error(transparent)]
|
||||
Model(#[from] modelcraft_model::Error),
|
||||
Model(#[from] fieldcraft_model::Error),
|
||||
#[error(transparent)]
|
||||
Geometry(#[from] modelcraft_geometry::Error),
|
||||
Geometry(#[from] fieldcraft_geometry::Error),
|
||||
#[error(transparent)]
|
||||
Mesh(#[from] modelcraft_mesh::Error),
|
||||
Mesh(#[from] fieldcraft_mesh::Error),
|
||||
#[error(transparent)]
|
||||
File(#[from] modelcraft_io::Error),
|
||||
File(#[from] fieldcraft_io::Error),
|
||||
#[error(transparent)]
|
||||
Deck(#[from] modelcraft_decks::Error),
|
||||
Deck(#[from] fieldcraft_decks::Error),
|
||||
#[error(transparent)]
|
||||
Solver(#[from] modelcraft_solver::Error),
|
||||
Solver(#[from] fieldcraft_solver::Error),
|
||||
#[error(transparent)]
|
||||
Explicit(#[from] modelcraft_explicit::Error),
|
||||
Explicit(#[from] fieldcraft_explicit::Error),
|
||||
#[error(transparent)]
|
||||
Fluid(#[from] modelcraft_lbm::Error),
|
||||
Fluid(#[from] fieldcraft_lbm::Error),
|
||||
/// A command panicked; the model is unchanged.
|
||||
#[error("internal error in {command}: {message}")]
|
||||
Internal { command: String, message: String },
|
||||
@@ -63,9 +63,9 @@ pub type Result<T> = std::result::Result<T, EngineError>;
|
||||
/// The result of a solved step, from the implicit, the explicit or the lattice Boltzmann solver.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum StepResult {
|
||||
Implicit(modelcraft_solver::StepResult),
|
||||
Implicit(fieldcraft_solver::StepResult),
|
||||
Explicit(ExplicitResult),
|
||||
Fluid(modelcraft_lbm::FluidResult),
|
||||
Fluid(fieldcraft_lbm::FluidResult),
|
||||
}
|
||||
|
||||
impl StepResult {
|
||||
@@ -104,7 +104,7 @@ pub struct CommandSpec {
|
||||
/// Our own dotted, lower-case id: `<area>.<thing>[.<variant>]`, such as `mesh.node.add`.
|
||||
pub id: &'static str,
|
||||
pub label: &'static str,
|
||||
/// The JSON parameters, documented for scripts, `modelcraft-cli commands` and agents.
|
||||
/// The JSON parameters, documented for scripts, `fieldcraft-cli commands` and agents.
|
||||
pub params: &'static str,
|
||||
pub kind: CommandKind,
|
||||
pub run: fn(&mut Session, &Value) -> Result<Value>,
|
||||
@@ -136,15 +136,15 @@ pub struct Limits {
|
||||
/// `mesh.tet.generate`'s `max_tets`.
|
||||
pub max_tets: usize,
|
||||
/// Threads the solvers use (the `solve.*` commands' `threads`): by default
|
||||
/// `MODELCRAFT_THREADS` or the available cores, 1 to [`modelcraft_parallel::MAX_THREADS`].
|
||||
/// `FIELDCRAFT_THREADS` or the available cores, 1 to [`fieldcraft_parallel::MAX_THREADS`].
|
||||
/// Results do not depend on it.
|
||||
pub threads: usize,
|
||||
}
|
||||
|
||||
impl Default for Limits {
|
||||
fn default() -> Self {
|
||||
let cdt = modelcraft_mesh::cdt::CdtOptions::default();
|
||||
Limits { max_steiner: cdt.max_steiner, max_tets: cdt.max_tets, threads: modelcraft_parallel::default_threads() }
|
||||
let cdt = fieldcraft_mesh::cdt::CdtOptions::default();
|
||||
Limits { max_steiner: cdt.max_steiner, max_tets: cdt.max_tets, threads: fieldcraft_parallel::default_threads() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,12 +158,12 @@ pub struct Session {
|
||||
/// Each is shared, so the per-command snapshot is cheap.
|
||||
pub surfaces: Vec<Arc<TaggedSurface>>,
|
||||
/// The files the design and the surfaces were read from, with the hash of what was read:
|
||||
/// what a `.modelcraft` file records to open them again.
|
||||
pub sources: modelcraft_io::Geometry,
|
||||
/// what a `.fieldcraft` file records to open them again.
|
||||
pub sources: fieldcraft_io::Geometry,
|
||||
/// Commands that changed the model, in order (a script that rebuilds it). Undo takes the
|
||||
/// last one off; redo puts it back.
|
||||
pub journal: Vec<(String, Value)>,
|
||||
/// The `.modelcraft` file the session was opened from or last saved to.
|
||||
/// The `.fieldcraft` file the session was opened from or last saved to.
|
||||
pub path: Option<String>,
|
||||
history: History,
|
||||
/// Identifies the current state: it changes with every edit, undo and redo, and comes back
|
||||
@@ -181,7 +181,7 @@ pub struct Session {
|
||||
pub limits: Limits,
|
||||
/// The folders commands may read and write files in (an automation client's roots,
|
||||
/// docs/mcp.md); unconfined by default. A file replacing the session keeps them.
|
||||
pub file_access: modelcraft_io::roots::Roots,
|
||||
pub file_access: fieldcraft_io::roots::Roots,
|
||||
}
|
||||
|
||||
impl Session {
|
||||
@@ -265,7 +265,7 @@ impl Session {
|
||||
}
|
||||
|
||||
/// The path to use for `access` to the file at `path`, checked against [`Session::file_access`].
|
||||
pub fn checked_path(&self, path: &str, access: modelcraft_io::roots::Access) -> Result<String> {
|
||||
pub fn checked_path(&self, path: &str, access: fieldcraft_io::roots::Access) -> Result<String> {
|
||||
let checked = self.file_access.check(std::path::Path::new(path), access)?;
|
||||
match checked.to_str() {
|
||||
Some(p) => Ok(p.to_string()),
|
||||
@@ -283,7 +283,7 @@ impl Session {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use modelcraft_geometry::sample::{Plate, plate_design};
|
||||
use fieldcraft_geometry::sample::{Plate, plate_design};
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
@@ -541,7 +541,7 @@ mod tests {
|
||||
fn new() -> Self {
|
||||
static N: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
|
||||
let n = N.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
let dir = std::env::temp_dir().join(format!("modelcraft-engine-{}-{n}", std::process::id()));
|
||||
let dir = std::env::temp_dir().join(format!("fieldcraft-engine-{}-{n}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
TempDir(dir)
|
||||
}
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
//! journalled nor undone, and it does not change the session revision. A result kept from an
|
||||
//! earlier revision is still shown, marked stale ([`Session::latest_result`]).
|
||||
|
||||
use modelcraft_explicit::{ExplicitResult, Frame};
|
||||
use modelcraft_model::Model;
|
||||
use modelcraft_solver::{FrequencyResult, HeatResult, StaticResult};
|
||||
use fieldcraft_explicit::{ExplicitResult, Frame};
|
||||
use fieldcraft_model::Model;
|
||||
use fieldcraft_solver::{FrequencyResult, HeatResult, StaticResult};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
@@ -21,16 +21,16 @@ pub enum Kind<'a> {
|
||||
Frequency(&'a FrequencyResult),
|
||||
Explicit(&'a ExplicitResult),
|
||||
/// Per cell, not per node: no nodal fields to display yet.
|
||||
Fluid(&'a modelcraft_lbm::FluidResult),
|
||||
Fluid(&'a fieldcraft_lbm::FluidResult),
|
||||
Heat(&'a HeatResult),
|
||||
}
|
||||
|
||||
impl StepResult {
|
||||
pub fn kind(&self) -> Kind<'_> {
|
||||
match self {
|
||||
StepResult::Implicit(modelcraft_solver::StepResult::Static(r)) => Kind::Static(r),
|
||||
StepResult::Implicit(modelcraft_solver::StepResult::Frequency(r)) => Kind::Frequency(r),
|
||||
StepResult::Implicit(modelcraft_solver::StepResult::Heat(r)) => Kind::Heat(r),
|
||||
StepResult::Implicit(fieldcraft_solver::StepResult::Static(r)) => Kind::Static(r),
|
||||
StepResult::Implicit(fieldcraft_solver::StepResult::Frequency(r)) => Kind::Frequency(r),
|
||||
StepResult::Implicit(fieldcraft_solver::StepResult::Heat(r)) => Kind::Heat(r),
|
||||
StepResult::Explicit(r) => Kind::Explicit(r),
|
||||
StepResult::Fluid(r) => Kind::Fluid(r),
|
||||
}
|
||||
@@ -306,7 +306,7 @@ pub fn field_values(model: &Model, r: &StepResult, field: Field, pick: Pick) ->
|
||||
}
|
||||
};
|
||||
let each = |f: &dyn Fn(&[f64; 6]) -> f64| -> Result<Vec<f64>> { Ok(stresses()?.iter().map(f).collect()) };
|
||||
let principal = |k: usize| each(&|t| modelcraft_fem::principal(t).get(k).copied().unwrap_or(f64::NAN));
|
||||
let principal = |k: usize| each(&|t| fieldcraft_fem::principal(t).get(k).copied().unwrap_or(f64::NAN));
|
||||
match field {
|
||||
Field::Displacement => component(|v| magnitude(*v)),
|
||||
Field::DisplacementX => component(|v| v[0]),
|
||||
@@ -328,7 +328,7 @@ pub fn field_values(model: &Model, r: &StepResult, field: Field, pick: Pick) ->
|
||||
Kind::Heat(h) => Ok(h.flux.iter().map(|v| magnitude(*v)).collect()),
|
||||
_ => Err(unavailable()),
|
||||
},
|
||||
Field::VonMises => each(&modelcraft_fem::von_mises),
|
||||
Field::VonMises => each(&fieldcraft_fem::von_mises),
|
||||
Field::MaxPrincipal => principal(0),
|
||||
Field::MidPrincipal => principal(1),
|
||||
Field::MinPrincipal => principal(2),
|
||||
|
||||
@@ -235,7 +235,7 @@ mod tests {
|
||||
assert_eq!(info["nodes"], 9 * 5 * 3 + 4 * 4 * 4);
|
||||
// Tet volumes are positive and add up to the cube's.
|
||||
let mut volume = 0.0;
|
||||
for e in s.model.elements().values().filter(|e| e.kind == modelcraft_model::ElementKind::Tet4) {
|
||||
for e in s.model.elements().values().filter(|e| e.kind == fieldcraft_model::ElementKind::Tet4) {
|
||||
let p: Vec<[f64; 3]> = e.nodes.iter().map(|n| s.model.node(*n).unwrap().xyz).collect();
|
||||
let v = signed_volume(&p) / 6.0;
|
||||
assert!(v > 0.0);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//! Headless pictures of a session: the model, its geometry and the results display drawn with
|
||||
//! the CPU rasterizer of [`modelcraft_render`], with no window or GPU (the MCP server's
|
||||
//! the CPU rasterizer of [`fieldcraft_render`], with no window or GPU (the MCP server's
|
||||
//! screenshots without an app, docs/mcp.md).
|
||||
//!
|
||||
//! The picture follows the desktop viewport's conventions: geometry draws as faces with their
|
||||
@@ -8,9 +8,9 @@
|
||||
//! deformed shape (automatic or chosen scale) with its contour bands. Animation is not drawn: a
|
||||
//! picture shows the full deformation (a mode at its positive shape).
|
||||
|
||||
use modelcraft_model::ElementKind;
|
||||
use modelcraft_render::contour::{Bands, band_color, split_triangle};
|
||||
use modelcraft_render::{Aabb3, Camera, DisplayMesh, Rgb, Scene, SceneLine, SceneTri, StandardView, Vec3};
|
||||
use fieldcraft_model::ElementKind;
|
||||
use fieldcraft_render::contour::{Bands, band_color, split_triangle};
|
||||
use fieldcraft_render::{Aabb3, Camera, DisplayMesh, Rgb, Scene, SceneLine, SceneTri, StandardView, Vec3};
|
||||
|
||||
use crate::results::{displacements, field_values, magnitude, range};
|
||||
use crate::{EngineError, Result, Session};
|
||||
@@ -181,7 +181,7 @@ pub fn render_png(s: &Session, view: StandardView, w: usize, h: usize, p: &Palet
|
||||
cam.set_view(view);
|
||||
cam.fit(&bounds);
|
||||
scene.radius = if bounds.is_empty() { 1.0 } else { (cam.target.dist(bounds.center()) + bounds.diagonal() * 0.5).max(1e-3) };
|
||||
let png = modelcraft_render::render_png(&scene, &cam, w, h).map_err(|e| EngineError::Params(format!("render: {e}")))?;
|
||||
let png = fieldcraft_render::render_png(&scene, &cam, w, h).map_err(|e| EngineError::Params(format!("render: {e}")))?;
|
||||
Ok((png, shown))
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "modelcraft-explicit"
|
||||
description = "ModelCraft's explicit dynamics solver: central-difference time integration with lumped mass, one-point hexahedra with hourglass control, nodal-pressure tetrahedra, bulk viscosity and energy accounting"
|
||||
name = "fieldcraft-explicit"
|
||||
description = "FieldCraft's explicit dynamics solver: central-difference time integration with lumped mass, one-point hexahedra with hourglass control, nodal-pressure tetrahedra, bulk viscosity and energy accounting"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
@@ -9,8 +9,8 @@ license.workspace = true
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
modelcraft-model = { workspace = true }
|
||||
modelcraft-fem = { workspace = true }
|
||||
modelcraft-parallel = { workspace = true }
|
||||
fieldcraft-model = { workspace = true }
|
||||
fieldcraft-fem = { workspace = true }
|
||||
fieldcraft-parallel = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
@@ -3,7 +3,7 @@
|
||||
//! - **Trusses** (rod2, rod3), elastic, with lumped (HRZ) mass. With nonlinear geometry a rod2
|
||||
//! is corotational: its strain is (l − L)/L from its current length l and its force acts along
|
||||
//! its current axis (Belytschko et al. 2014, §2.4 and §4.5); a rod3 then is not supported yet.
|
||||
//! Without it the internal force is K u with the kernel's stiffness ([`modelcraft_fem::truss`]).
|
||||
//! Without it the internal force is K u with the kernel's stiffness ([`fieldcraft_fem::truss`]).
|
||||
//! The stress is the uniaxial tensor along the axis (the mean of the integration points'), and
|
||||
//! the critical increment comes from the kernel's frequency bound (exact for rod2: L/c).
|
||||
//! - **Point masses** add to the lumped mass of their node, per direction.
|
||||
@@ -21,10 +21,10 @@
|
||||
//! §6.3), which is 2/D for a dashpot alone. Rotary inertia and rotational springs need
|
||||
//! rotational degrees of freedom, which this solver does not have yet.
|
||||
|
||||
use modelcraft_fem::DofKind;
|
||||
use modelcraft_fem::dense::Matrix;
|
||||
use modelcraft_fem::truss::{Truss, uniaxial};
|
||||
use modelcraft_model::{ElementKind, Resolved, ResolvedDirection};
|
||||
use fieldcraft_fem::DofKind;
|
||||
use fieldcraft_fem::dense::Matrix;
|
||||
use fieldcraft_fem::truss::{Truss, uniaxial};
|
||||
use fieldcraft_model::{ElementKind, Resolved, ResolvedDirection};
|
||||
|
||||
use crate::system::NODE_DOFS;
|
||||
use crate::{Error, Result};
|
||||
@@ -92,7 +92,7 @@ impl Rod {
|
||||
|
||||
/// The element's degrees of freedom, node by node.
|
||||
pub fn dofs(&self) -> impl Iterator<Item = usize> + '_ {
|
||||
self.nodes.iter().flat_map(|n| modelcraft_fem::truss::DOFS.node.iter().filter_map(DofKind::direction).map(|d| dof(*n, d)))
|
||||
self.nodes.iter().flat_map(|n| fieldcraft_fem::truss::DOFS.node.iter().filter_map(DofKind::direction).map(|d| dof(*n, d)))
|
||||
}
|
||||
|
||||
/// Add its internal force into `force` and return the element's mean stress and strain
|
||||
|
||||
@@ -14,9 +14,9 @@
|
||||
//! strain and the stiffness-form hourglass forces) is rotated with the increment's rotation —
|
||||
//! the Cayley transform of the spin increment (Jaumann rate, Hughes & Winget 1980) or the
|
||||
//! increment of the polar rotation (Green–Naghdi rate, D. P. Flanagan, L. M. Taylor, CMAME
|
||||
//! 62(3):305–320, 1987), see [`modelcraft_fem::rotation`] — then the constitutive increment is
|
||||
//! 62(3):305–320, 1987), see [`fieldcraft_fem::rotation`] — then the constitutive increment is
|
||||
//! added: hypoelastic, or J2 radial return with Johnson–Cook damage
|
||||
//! ([`modelcraft_fem::plasticity`]). Orthotropic elasticity is updated in the polar frame (its
|
||||
//! ([`fieldcraft_fem::plasticity`]). Orthotropic elasticity is updated in the polar frame (its
|
||||
//! axes turn with the material) whichever rate the step names.
|
||||
//!
|
||||
//! **One-point hexahedron** (D. P. Flanagan, T. Belytschko, IJNME 17(5):679–706, 1981): the
|
||||
@@ -44,7 +44,7 @@
|
||||
//! Δt = L / (Q + √(Q² + c²)), Q = C_l c + C_q L |ε̇ᵥ| (Wilkins 1980).
|
||||
//!
|
||||
//! **Critical increment.** Each element's stable length L = 2c/ω_max comes from the bound on its
|
||||
//! highest frequency in its current shape ([`modelcraft_fem::uniform`], with the element's lumped
|
||||
//! highest frequency in its current shape ([`fieldcraft_fem::uniform`], with the element's lumped
|
||||
//! masses, plus the hourglass stiffness when it is used), so without bulk viscosity its critical
|
||||
//! increment is 2/ω_max (Belytschko et al. 2014, §6.3). The elastic moduli bound the tangent
|
||||
//! moduli of plastic flow, so the bound holds for the inelastic materials too.
|
||||
@@ -54,11 +54,11 @@
|
||||
//! reaches its material's erosion plastic strain is removed: it carries no stress from then on,
|
||||
//! its nodes keep their mass and momentum, and its internal energy at that moment is reported.
|
||||
|
||||
use modelcraft_fem::material::stress;
|
||||
use modelcraft_fem::plasticity::{Increment, PointState};
|
||||
use modelcraft_fem::rotation::{self, IDENTITY, Mat3, Polar};
|
||||
use modelcraft_fem::uniform::{Hex8, Tet4, UniformStrain};
|
||||
use modelcraft_model::{BulkViscosity, HourglassKind, StressRate};
|
||||
use fieldcraft_fem::material::stress;
|
||||
use fieldcraft_fem::plasticity::{Increment, PointState};
|
||||
use fieldcraft_fem::rotation::{self, IDENTITY, Mat3, Polar};
|
||||
use fieldcraft_fem::uniform::{Hex8, Tet4, UniformStrain};
|
||||
use fieldcraft_model::{BulkViscosity, HourglassKind, StressRate};
|
||||
|
||||
use crate::system::{Material, NODE_DOFS, System};
|
||||
use crate::{Error, Result};
|
||||
@@ -330,7 +330,7 @@ fn current(mat: &Material, rest: f64, volume: f64) -> (f64, f64) {
|
||||
}
|
||||
|
||||
/// The bound ω² ≤ k λ_max(Σ b bᵀ) / (V m_min) on an element's highest frequency
|
||||
/// ([`modelcraft_fem::uniform`]), with the lumped masses of the undeformed element and
|
||||
/// ([`fieldcraft_fem::uniform`]), with the lumped masses of the undeformed element and
|
||||
/// `gradient_norm` = λ_max(Σ b bᵀ).
|
||||
pub fn frequency_bound<const N: usize>(k: &UniformStrain<N>, gradient_norm: f64, modulus: f64, mass: &[f64; N]) -> f64 {
|
||||
let lightest = mass.iter().copied().fold(f64::INFINITY, f64::min);
|
||||
@@ -353,7 +353,7 @@ pub fn hex_scales(
|
||||
mass: &[f64; 8],
|
||||
mat: &Material,
|
||||
density: f64,
|
||||
hourglass: modelcraft_model::Hourglass,
|
||||
hourglass: fieldcraft_model::Hourglass,
|
||||
) -> HexScales {
|
||||
let norm = k.gradient_norm();
|
||||
// h: the edge of the cube with the element's volume and gradient norm.
|
||||
@@ -439,7 +439,7 @@ fn run<T: Send>(
|
||||
) -> Result<()> {
|
||||
let mut all = pieces::<T>(states, count, &index)?;
|
||||
for round in all.chunks_mut(ROUND) {
|
||||
modelcraft_parallel::for_each_mut(round, |_, piece| {
|
||||
fieldcraft_parallel::for_each_mut(round, |_, piece| {
|
||||
let Piece { items, base, states, out } = piece;
|
||||
*out = items
|
||||
.clone()
|
||||
@@ -476,7 +476,7 @@ fn hexahedron(sys: &System, h: &crate::system::Hex, m: &Motion, es: &mut Element
|
||||
if es.failed.is_some() {
|
||||
return Ok(Outcome::Skipped);
|
||||
}
|
||||
let hourglass = modelcraft_model::Hourglass { kind: sys.hourglass, coefficient: sys.hourglass_coefficient };
|
||||
let hourglass = fieldcraft_model::Hourglass { kind: sys.hourglass, coefficient: sys.hourglass_coefficient };
|
||||
let e = Nodal::gather(sys, &h.nodes, m);
|
||||
let x_now = e.positions(0.0);
|
||||
let (mid, now) = if sys.nonlinear {
|
||||
@@ -607,7 +607,7 @@ pub fn internal_forces(sys: &System, m: &Motion, state: &mut State, ws: &mut Wor
|
||||
// Pass 1: each tetrahedron's increment, and each node's mean volumetric increment.
|
||||
let increments = {
|
||||
let st: &State = state;
|
||||
modelcraft_parallel::map_grained(tets.len(), BLOCK, |k| match tets.get(k) {
|
||||
fieldcraft_parallel::map_grained(tets.len(), BLOCK, |k| match tets.get(k) {
|
||||
Some(t) => tetrahedron_increment(sys, t, m, st),
|
||||
None => Ok(None),
|
||||
})
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
//! The run starts from u = 0 and the initial velocities with the half increment
|
||||
//! v^{½} = v^0 + ½Δt^{½} a^0.
|
||||
|
||||
use modelcraft_model::{ExplicitDynamic, Model, OutputVariable, Procedure, Step};
|
||||
use fieldcraft_model::{ExplicitDynamic, Model, OutputVariable, Procedure, Step};
|
||||
|
||||
use crate::elements::{Motion, State, Workspace, internal_forces};
|
||||
use crate::result::{ControllingElement, Energies, ErodedElement, ExplicitResult, Frame, History};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! ModelCraft's explicit dynamics solver: transient response of a model's explicit dynamic step
|
||||
//! FieldCraft's explicit dynamics solver: transient response of a model's explicit dynamic step
|
||||
//! by central-difference time integration.
|
||||
//!
|
||||
//! The method is the explicit central-difference scheme of T. Belytschko, W. K. Liu, B. Moran,
|
||||
@@ -24,7 +24,7 @@
|
||||
//! docs/solvers/literature.md, Parts F and J). Without it the gradients stay those of the
|
||||
//! undeformed mesh (small displacements).
|
||||
//!
|
||||
//! Elements are updated on threads ([`modelcraft_parallel`]), each owning its state, and their
|
||||
//! Elements are updated on threads ([`fieldcraft_parallel`]), each owning its state, and their
|
||||
//! nodal forces are added in label order as a sequential loop adds them, with no parallel
|
||||
//! reductions, so the same model gives the same numbers bit for bit on any number of threads;
|
||||
//! sizes, increments and output are capped by [`Limits`].
|
||||
@@ -46,14 +46,14 @@ pub use result::{ControllingElement, Energies, ErodedElement, ExplicitResult, Fr
|
||||
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error(transparent)]
|
||||
Model(#[from] modelcraft_model::Error),
|
||||
Model(#[from] fieldcraft_model::Error),
|
||||
#[error("invalid input: {0}")]
|
||||
Invalid(String),
|
||||
#[error("not supported yet: {0}")]
|
||||
NotSupported(String),
|
||||
/// An element kernel failed (inverted geometry, …).
|
||||
#[error("element {label}: {source}")]
|
||||
Element { label: u32, source: modelcraft_fem::Error },
|
||||
Element { label: u32, source: fieldcraft_fem::Error },
|
||||
#[error("limit reached: {0}")]
|
||||
Limit(String),
|
||||
/// The solution stopped being finite.
|
||||
@@ -68,7 +68,7 @@ pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
impl Error {
|
||||
/// A kernel error, naming the element.
|
||||
pub fn element(label: u32, source: modelcraft_fem::Error) -> Error {
|
||||
pub fn element(label: u32, source: fieldcraft_fem::Error) -> Error {
|
||||
Error::Element { label, source }
|
||||
}
|
||||
}
|
||||
@@ -86,7 +86,7 @@ pub struct Limits {
|
||||
|
||||
impl Limits {
|
||||
pub const MAX_DOFS: usize = 6_000_000;
|
||||
pub const MAX_INCREMENTS: u64 = modelcraft_model::steps::MAX_INCREMENTS;
|
||||
pub const MAX_INCREMENTS: u64 = fieldcraft_model::steps::MAX_INCREMENTS;
|
||||
pub const MAX_OUTPUT_VALUES: usize = 50_000_000;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
//! ([`crate::discrete`]), prescribed velocities, rigid walls, initial velocities and the load
|
||||
//! vector of an explicit dynamic step.
|
||||
|
||||
use modelcraft_fem::material::Elasticity;
|
||||
use modelcraft_fem::plasticity::J2;
|
||||
use modelcraft_fem::uniform::{Hex8, Tet4, modulus_bound};
|
||||
use modelcraft_fem::{DofKind, DofLayout, DofSet, face};
|
||||
use modelcraft_model::{
|
||||
use fieldcraft_fem::material::Elasticity;
|
||||
use fieldcraft_fem::plasticity::J2;
|
||||
use fieldcraft_fem::uniform::{Hex8, Tet4, modulus_bound};
|
||||
use fieldcraft_fem::{DofKind, DofLayout, DofSet, face};
|
||||
use fieldcraft_model::{
|
||||
Elastic, ElementKind, ExplicitDynamic, HourglassKind, Integration, Load, Model, Resolved, SectionKind, Step, StressRate, Target,
|
||||
};
|
||||
|
||||
@@ -30,7 +30,7 @@ pub struct Material {
|
||||
/// Dilatational wave speed √(M/ρ), with M the largest normal stiffness (λ + 2μ when
|
||||
/// isotropic).
|
||||
pub wave_speed: f64,
|
||||
/// The bound of [`modelcraft_fem::uniform::modulus_bound`].
|
||||
/// The bound of [`fieldcraft_fem::uniform::modulus_bound`].
|
||||
pub modulus: f64,
|
||||
/// The largest normal stiffness M (λ + 2μ when isotropic): c = √(M/ρ).
|
||||
pub stiffest: f64,
|
||||
@@ -143,7 +143,7 @@ fn material(model: &Model, name: &str) -> Result<Material> {
|
||||
.density
|
||||
.filter(|d| *d > 0.0 && d.is_finite())
|
||||
.ok_or_else(|| invalid(format!("material `{name}` has no density, and explicit dynamics needs one")))?;
|
||||
let d = modelcraft_fem::elasticity(elastic).map_err(|e| invalid(format!("material `{name}`: {e}")))?;
|
||||
let d = fieldcraft_fem::elasticity(elastic).map_err(|e| invalid(format!("material `{name}`: {e}")))?;
|
||||
let stiffest = d[0][0].max(d[1][1]).max(d[2][2]);
|
||||
// The model keeps plasticity, damage and erosion to isotropic materials.
|
||||
let j2 = match *elastic {
|
||||
@@ -171,7 +171,7 @@ fn material(model: &Model, name: &str) -> Result<Material> {
|
||||
}
|
||||
|
||||
/// The section's elements of a kind the explicit solver has, or why not.
|
||||
fn check_formulation(kind: ElementKind, section_name: &str, f: &modelcraft_model::Formulation) -> Result<()> {
|
||||
fn check_formulation(kind: ElementKind, section_name: &str, f: &fieldcraft_model::Formulation) -> Result<()> {
|
||||
let refuse = |why: &str| Err(Error::NotSupported(format!("section `{section_name}`: {why}")));
|
||||
if f.hybrid {
|
||||
return refuse("hybrid elements in explicit dynamics");
|
||||
@@ -327,7 +327,7 @@ impl System {
|
||||
)));
|
||||
}
|
||||
let young =
|
||||
modelcraft_fem::truss::young_modulus(source.elastic.as_ref().ok_or_else(|| Error::Internal("truss material".into()))?)
|
||||
fieldcraft_fem::truss::young_modulus(source.elastic.as_ref().ok_or_else(|| Error::Internal("truss material".into()))?)
|
||||
.map_err(|e| invalid(format!("material `{}` of element {label}: {e}", mat.name)))?;
|
||||
let x: Vec<[f64; 3]> = idx.iter().map(|i| sys.xyz.get(*i).copied().unwrap_or_default()).collect();
|
||||
let rod = Rod::new(label, index, kind, idx.clone(), &x, mi, crate::discrete::RodSection { young, density: rho, area })?;
|
||||
@@ -518,7 +518,7 @@ impl System {
|
||||
self.each_element(chosen, |nodes, weights, rho| {
|
||||
for (node, w) in nodes.iter().zip(weights) {
|
||||
let x = xyz.get(*node).copied().unwrap_or_default();
|
||||
let a = modelcraft_model::centrifugal_acceleration(*angular_velocity, *point, *axis, x);
|
||||
let a = fieldcraft_model::centrifugal_acceleration(*angular_velocity, *point, *axis, x);
|
||||
add_at(&mut f, *node, &a.map(|c| rho * w * c));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -13,10 +13,10 @@
|
||||
//!
|
||||
//! Run with `--nocapture` to see the errors.
|
||||
|
||||
use modelcraft_explicit::elements::{Motion, State, Workspace, internal_forces};
|
||||
use modelcraft_explicit::system::System;
|
||||
use modelcraft_explicit::{ExplicitResult, Failure, Limits, solve_explicit};
|
||||
use modelcraft_model::*;
|
||||
use fieldcraft_explicit::elements::{Motion, State, Workspace, internal_forces};
|
||||
use fieldcraft_explicit::system::System;
|
||||
use fieldcraft_explicit::{ExplicitResult, Failure, Limits, solve_explicit};
|
||||
use fieldcraft_model::*;
|
||||
|
||||
/// A block [0, size] of `n` cells: one hex8 per cell, or six tet4 (Kuhn's split).
|
||||
fn block(n: [usize; 3], size: [f64; 3], kind: ElementKind) -> Model {
|
||||
@@ -179,7 +179,7 @@ impl Driver {
|
||||
self.try_go(to, dt).unwrap()
|
||||
}
|
||||
|
||||
fn try_go(&mut self, to: impl Fn([f64; 3]) -> [f64; 3], dt: f64) -> modelcraft_explicit::Result<[f64; 6]> {
|
||||
fn try_go(&mut self, to: impl Fn([f64; 3]) -> [f64; 3], dt: f64) -> fieldcraft_explicit::Result<[f64; 6]> {
|
||||
let mut du = vec![0.0; self.u.len()];
|
||||
for (i, x0) in self.sys.xyz.iter().enumerate() {
|
||||
let x = to(*x0);
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
|
||||
use std::f64::consts::PI;
|
||||
|
||||
use modelcraft_explicit::{ExplicitResult, Limits, solve_explicit};
|
||||
use modelcraft_fem::truss::axial;
|
||||
use modelcraft_model::*;
|
||||
use fieldcraft_explicit::{ExplicitResult, Limits, solve_explicit};
|
||||
use fieldcraft_fem::truss::axial;
|
||||
use fieldcraft_model::*;
|
||||
|
||||
fn model(step: ExplicitDynamic) -> Model {
|
||||
let mut m = Model::default();
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
//! a free bar by α ΔT L without stress; a bar held at both ends carries σ = −E α ΔT, and with
|
||||
//! perfectly plastic J2 the stress stops at the yield stress.
|
||||
|
||||
use modelcraft_explicit::{Limits, solve_explicit};
|
||||
use modelcraft_model::*;
|
||||
use fieldcraft_explicit::{Limits, solve_explicit};
|
||||
use fieldcraft_model::*;
|
||||
|
||||
const YOUNG: f64 = 1.0e4;
|
||||
const ALPHA: f64 = 1.0e-4;
|
||||
@@ -115,7 +115,7 @@ fn a_clamped_bar_carries_minus_e_alpha_dt_up_to_yield() {
|
||||
let r = solve_explicit(&bar(kind, true, Some(plastic)), None, &Limits::default()).unwrap();
|
||||
let last = r.frames.last().unwrap();
|
||||
for (s, ep) in last.stress.iter().zip(&last.plastic_strain) {
|
||||
let mises = modelcraft_fem::von_mises(s);
|
||||
let mises = fieldcraft_fem::von_mises(s);
|
||||
assert!((mises - 0.5 * sigma.abs()).abs() < 0.02 * sigma.abs(), "{kind:?}: {s:?}");
|
||||
assert!(*ep > 0.0, "{kind:?}: plastic strain {ep}");
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
//! The bar has Poisson's ratio 0, so a uniaxial stress wave travels at the rod speed c = √(E/ρ)
|
||||
//! without lateral inertia effects (d'Alembert's solution of the one-dimensional wave equation).
|
||||
|
||||
use modelcraft_explicit::{ExplicitResult, Limits, solve_explicit};
|
||||
use modelcraft_model::*;
|
||||
use fieldcraft_explicit::{ExplicitResult, Limits, solve_explicit};
|
||||
use fieldcraft_model::*;
|
||||
|
||||
const YOUNG: f64 = 1.0e4;
|
||||
const DENSITY: f64 = 1.0;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "modelcraft-fem"
|
||||
description = "ModelCraft's finite element kernels: shape functions, quadrature, strain-displacement matrices, element stiffness and mass, elastic constitutive matrices"
|
||||
name = "fieldcraft-fem"
|
||||
description = "FieldCraft's finite element kernels: shape functions, quadrature, strain-displacement matrices, element stiffness and mass, elastic constitutive matrices"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
@@ -9,5 +9,5 @@ license.workspace = true
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
modelcraft-model = { workspace = true }
|
||||
fieldcraft-model = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
@@ -28,7 +28,7 @@
|
||||
//!
|
||||
//! Node orders are corners first, then mid-side nodes in the model's edge order (the model is the
|
||||
//! one place they are written down), then face and body nodes, the Abaqus/VTK convention of
|
||||
//! [`modelcraft_model::ElementKind`]. The functions are the textbook ones (O. C. Zienkiewicz,
|
||||
//! [`fieldcraft_model::ElementKind`]. The functions are the textbook ones (O. C. Zienkiewicz,
|
||||
//! R. L. Taylor, J. Z. Zhu, *The Finite Element Method: Its Basis and Fundamentals*, 7th ed.,
|
||||
//! 2013, ch. 6; T. J. R. Hughes, *The Finite Element Method*, Dover 2000, ch. 3).
|
||||
//!
|
||||
@@ -49,7 +49,7 @@
|
||||
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use modelcraft_model::ElementKind;
|
||||
use fieldcraft_model::ElementKind;
|
||||
|
||||
use crate::dense::positive_inverse;
|
||||
use crate::jet::Jet;
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
//! webs or the two flanges of a box) (W. D. Pilkey, *Analysis and Design of Elastic Beams*,
|
||||
//! Wiley 2002, ch. 6).
|
||||
|
||||
use modelcraft_model::{BeamProfile, Elastic};
|
||||
use fieldcraft_model::{BeamProfile, Elastic};
|
||||
|
||||
use crate::dense::Matrix;
|
||||
use crate::dof::{DofSet, ElementDofs};
|
||||
@@ -211,7 +211,7 @@ fn line_axis(xyz: &[[f64; 3]]) -> Result<(Vec3, f64)> {
|
||||
let [a, b] = xyz else {
|
||||
return Err(Error::Invalid(format!("a two-node line element has 2 nodes, got {}", xyz.len())));
|
||||
};
|
||||
modelcraft_model::check_coordinates(xyz.as_flattened()).map_err(|e| Error::Invalid(e.to_string()))?;
|
||||
fieldcraft_model::check_coordinates(xyz.as_flattened()).map_err(|e| Error::Invalid(e.to_string()))?;
|
||||
let d = sub(*b, *a);
|
||||
let length = norm(d);
|
||||
let ex = unit(d).ok_or_else(|| Error::Degenerate("a line element has zero length".into()))?;
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
//! discontinuous finite element functions using a least squares method*, IJNME 8(3):461–480,
|
||||
//! 1974); mid-side nodes take the mean of their edge's corners.
|
||||
|
||||
use modelcraft_model::ElementKind;
|
||||
use fieldcraft_model::ElementKind;
|
||||
|
||||
use crate::basis::Basis;
|
||||
use crate::dense::{Cholesky, Matrix, inverse3, positive_inverse};
|
||||
@@ -163,10 +163,10 @@ impl Formulation {
|
||||
}
|
||||
|
||||
/// The formulation a model section asks for, if this element type has it.
|
||||
pub fn from_section(kind: ElementKind, f: &modelcraft_model::Formulation) -> Result<Formulation> {
|
||||
pub fn from_section(kind: ElementKind, f: &fieldcraft_model::Formulation) -> Result<Formulation> {
|
||||
let ty = element_type(kind)?;
|
||||
if f.hybrid {
|
||||
if f.incompatible_modes || f.integration == modelcraft_model::Integration::Reduced {
|
||||
if f.incompatible_modes || f.integration == fieldcraft_model::Integration::Reduced {
|
||||
return Err(Error::NotSupported("hybrid elements with incompatible modes or reduced integration".into()));
|
||||
}
|
||||
return crate::hybrid::pressures(kind).map(|pressures| Formulation::Hybrid { pressures }).ok_or_else(|| {
|
||||
@@ -184,9 +184,9 @@ impl Formulation {
|
||||
};
|
||||
}
|
||||
match f.integration {
|
||||
modelcraft_model::Integration::Full => Ok(Formulation::Full),
|
||||
modelcraft_model::Integration::Reduced if ty.reduced.is_some() => Ok(Formulation::Reduced),
|
||||
modelcraft_model::Integration::Reduced => Err(Error::NotSupported(format!(
|
||||
fieldcraft_model::Integration::Full => Ok(Formulation::Full),
|
||||
fieldcraft_model::Integration::Reduced if ty.reduced.is_some() => Ok(Formulation::Reduced),
|
||||
fieldcraft_model::Integration::Reduced => Err(Error::NotSupported(format!(
|
||||
"reduced integration of {} elements (hex8 needs hourglass control; use full integration or incompatible modes)",
|
||||
kind.name()
|
||||
))),
|
||||
@@ -254,7 +254,7 @@ impl<'a> Solid<'a> {
|
||||
if xyz.len() != ty.nodes {
|
||||
return Err(Error::Invalid(format!("a {} element has {} nodes, got {}", kind.name(), ty.nodes, xyz.len())));
|
||||
}
|
||||
modelcraft_model::check_coordinates(xyz.as_flattened()).map_err(|e| Error::Invalid(e.to_string()))?;
|
||||
fieldcraft_model::check_coordinates(xyz.as_flattened()).map_err(|e| Error::Invalid(e.to_string()))?;
|
||||
let ok = match formulation {
|
||||
Formulation::Full => true,
|
||||
Formulation::Reduced => ty.reduced.is_some(),
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
//!
|
||||
//! A pressure p on a face gives node i the force fᵢ = −p ∫ Nᵢ n dA, where n dA =
|
||||
//! (∂x/∂ξ × ∂x/∂η) dξ dη on the face's own, possibly curved, geometry; faces are numbered so this
|
||||
//! normal points out of the element ([`modelcraft_model::ElementKind::faces`]), and a positive
|
||||
//! normal points out of the element ([`fieldcraft_model::ElementKind::faces`]), and a positive
|
||||
//! pressure pushes in. This is the consistent load vector of the virtual work of the surface load
|
||||
//! (Zienkiewicz, Taylor & Zhu 2013, §6.6), integrated with the model's face rules (exact for a
|
||||
//! uniform pressure on a flat face).
|
||||
@@ -10,10 +10,10 @@
|
||||
use crate::{Error, Result};
|
||||
|
||||
/// ∫ Nᵢ n dA for every node of a face (node coordinates in the face's node order): the model's
|
||||
/// face integration ([`modelcraft_model::nodal::face_normal_weights`]), so pressures and the
|
||||
/// face integration ([`fieldcraft_model::nodal::face_normal_weights`]), so pressures and the
|
||||
/// tractions the model distributes use the same rule.
|
||||
pub fn normal_weights(xyz: &[[f64; 3]]) -> Result<Vec<[f64; 3]>> {
|
||||
modelcraft_model::nodal::face_normal_weights(xyz).map_err(|e| Error::Invalid(e.to_string()))
|
||||
fieldcraft_model::nodal::face_normal_weights(xyz).map_err(|e| Error::Invalid(e.to_string()))
|
||||
}
|
||||
|
||||
/// Consistent nodal forces of a uniform pressure (positive pushes into the element).
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
//! Faces are integrated on their own (possibly curved) geometry with the model's face rule
|
||||
//! (the one pressures use), exact for N Nᵀ on flat faces.
|
||||
|
||||
use modelcraft_model::ElementKind;
|
||||
use fieldcraft_model::ElementKind;
|
||||
|
||||
use crate::basis::Basis;
|
||||
use crate::dense::Matrix;
|
||||
@@ -47,7 +47,7 @@ impl<'a> Conduction<'a> {
|
||||
if xyz.len() != ty.nodes {
|
||||
return Err(Error::Invalid(format!("a {} element has {} nodes, got {}", kind.name(), ty.nodes, xyz.len())));
|
||||
}
|
||||
modelcraft_model::check_coordinates(xyz.as_flattened()).map_err(|e| Error::Invalid(e.to_string()))?;
|
||||
fieldcraft_model::check_coordinates(xyz.as_flattened()).map_err(|e| Error::Invalid(e.to_string()))?;
|
||||
Ok(Conduction { ty, basis: Basis::of(kind), xyz })
|
||||
}
|
||||
|
||||
@@ -119,11 +119,11 @@ impl<'a> Conduction<'a> {
|
||||
}
|
||||
|
||||
/// Visit the points of a face (node coordinates in the face's node order): shape values and
|
||||
/// the area weight, with the model's face rule ([`modelcraft_model::nodal::integrate_face`], the
|
||||
/// the area weight, with the model's face rule ([`fieldcraft_model::nodal::integrate_face`], the
|
||||
/// one pressures and tractions use).
|
||||
fn each_face_point(xyz: &[[f64; 3]], mut visit: impl FnMut(&[f64], f64)) -> Result<()> {
|
||||
let mut collapsed = false;
|
||||
modelcraft_model::nodal::integrate_face(xyz, |n, nda| {
|
||||
fieldcraft_model::nodal::integrate_face(xyz, |n, nda| {
|
||||
let area = (nda[0] * nda[0] + nda[1] * nda[1] + nda[2] * nda[2]).sqrt();
|
||||
collapsed |= !(area > 0.0 && area.is_finite());
|
||||
visit(n, area);
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
//! symmetric indefinite saddle point that the Cholesky factorization cannot solve; it waits for
|
||||
//! an indefinite (LDLᵀ) factorization.
|
||||
|
||||
use modelcraft_model::ElementKind;
|
||||
use fieldcraft_model::ElementKind;
|
||||
|
||||
use crate::dense::{Cholesky, Matrix};
|
||||
use crate::element::{Formulation, Solid, ThermalStrain, accumulate_symmetric, b_block};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Finite element kernels shared by ModelCraft's structural solvers.
|
||||
//! Finite element kernels shared by FieldCraft's structural solvers.
|
||||
//!
|
||||
//! Pure functions over element geometry and material constants, with no solver loop and no
|
||||
//! global state, so the implicit solver (`modelcraft-solver`) and the explicit solver that follows
|
||||
//! global state, so the implicit solver (`fieldcraft-solver`) and the explicit solver that follows
|
||||
//! it use the same element technology:
|
||||
//! - [`dof`]: degree-of-freedom kinds, what elements declare, and the global numbering
|
||||
//! ([`DofLayout`]) every solver assembles on;
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
//! S₁₁ = 1/E₁, S₁₂ = −ν₁₂/E₁, S₁₃ = −ν₁₃/E₁, S₂₃ = −ν₂₃/E₂ (ν_ji/E_j = ν_ij/E_i), and
|
||||
//! 1/G on the shear diagonal.
|
||||
|
||||
use modelcraft_model::Elastic;
|
||||
use fieldcraft_model::Elastic;
|
||||
|
||||
use crate::dense::inverse3;
|
||||
use crate::{Error, Result};
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
//! inside the element (r > 0) for every element with some node off the axis; the solver holds
|
||||
//! the radial displacement of nodes on the axis at zero, as symmetry requires.
|
||||
|
||||
use modelcraft_model::{ElementKind, Extent, SectionKind};
|
||||
use fieldcraft_model::{ElementKind, Extent, SectionKind};
|
||||
|
||||
use crate::basis::Basis;
|
||||
use crate::dense::{Cholesky, Matrix};
|
||||
@@ -166,7 +166,7 @@ pub fn plane_type(kind: ElementKind) -> Result<&'static ElementType> {
|
||||
}
|
||||
|
||||
/// The formulation a 2D section asks for, if this element type has it.
|
||||
pub fn formulation(kind: ElementKind, f: &modelcraft_model::Formulation) -> Result<Formulation> {
|
||||
pub fn formulation(kind: ElementKind, f: &fieldcraft_model::Formulation) -> Result<Formulation> {
|
||||
let ty = plane_type(kind)?;
|
||||
if f.hybrid {
|
||||
return Err(Error::NotSupported("hybrid (mixed displacement–pressure) 2D elements".into()));
|
||||
@@ -179,9 +179,9 @@ pub fn formulation(kind: ElementKind, f: &modelcraft_model::Formulation) -> Resu
|
||||
};
|
||||
}
|
||||
match f.integration {
|
||||
modelcraft_model::Integration::Full => Ok(Formulation::Full),
|
||||
modelcraft_model::Integration::Reduced if ty.reduced.is_some() => Ok(Formulation::Reduced),
|
||||
modelcraft_model::Integration::Reduced => Err(Error::NotSupported(format!(
|
||||
fieldcraft_model::Integration::Full => Ok(Formulation::Full),
|
||||
fieldcraft_model::Integration::Reduced if ty.reduced.is_some() => Ok(Formulation::Reduced),
|
||||
fieldcraft_model::Integration::Reduced => Err(Error::NotSupported(format!(
|
||||
"reduced integration of 2D {} elements (quad4 needs hourglass control; use full integration or incompatible modes)",
|
||||
kind.name()
|
||||
))),
|
||||
@@ -254,7 +254,7 @@ impl<'a> Plane<'a> {
|
||||
if xyz.len() != ty.nodes {
|
||||
return Err(Error::Invalid(format!("a {} element has {} nodes, got {}", kind.name(), ty.nodes, xyz.len())));
|
||||
}
|
||||
modelcraft_model::check_coordinates(xyz.as_flattened()).map_err(|e| Error::Invalid(e.to_string()))?;
|
||||
fieldcraft_model::check_coordinates(xyz.as_flattened()).map_err(|e| Error::Invalid(e.to_string()))?;
|
||||
if xyz.iter().any(|p| p[2] != 0.0) {
|
||||
return Err(Error::Invalid(format!("a 2D {} element must lie in the XY plane (z = 0)", kind.name())));
|
||||
}
|
||||
@@ -760,7 +760,7 @@ mod tests {
|
||||
let f = e.body_force_field(|x| [x[0], 0.0, 0.0]).unwrap();
|
||||
assert!((f.iter().map(|v| v[0]).sum::<f64>() - 2.0 * pi * 7.0 / 3.0).abs() < 1e-12);
|
||||
// Pressure on the outer edge (x = 2) of the ring pushes inward with p · 2π · 2 · 1.
|
||||
let fp = modelcraft_model::nodal::edge_pressure(&[sq[1], sq[2]], Theory::Axisymmetric.extent(), 1.0).unwrap();
|
||||
let fp = fieldcraft_model::nodal::edge_pressure(&[sq[1], sq[2]], Theory::Axisymmetric.extent(), 1.0).unwrap();
|
||||
assert!((fp.iter().map(|v| v[0]).sum::<f64>() + 4.0 * pi).abs() < 1e-12, "{fp:?}");
|
||||
}
|
||||
|
||||
@@ -783,6 +783,6 @@ mod tests {
|
||||
let on_axis = sq.map(|p| [p[0] - 1.0, p[1], 0.0]);
|
||||
let e = Plane::new(ElementKind::Quad4, &on_axis, Formulation::IncompatibleModes, Theory::Axisymmetric).unwrap();
|
||||
assert!(e.stiffness(&d).is_ok());
|
||||
assert!(formulation(ElementKind::Quad4, &modelcraft_model::Formulation { hybrid: true, ..Default::default() }).is_err());
|
||||
assert!(formulation(ElementKind::Quad4, &fieldcraft_model::Formulation { hybrid: true, ..Default::default() }).is_err());
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@
|
||||
//! Stress 6-vectors hold tensor components (xx, yy, zz, xy, xz, yz), strain increments
|
||||
//! engineering shears.
|
||||
|
||||
use modelcraft_model::{Damage, Elastic, Hardening, Plastic};
|
||||
use fieldcraft_model::{Damage, Elastic, Hardening, Plastic};
|
||||
|
||||
use crate::material::von_mises;
|
||||
use crate::{Error, Result};
|
||||
@@ -328,7 +328,7 @@ impl J2 {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use modelcraft_model::HardeningPoint;
|
||||
use fieldcraft_model::HardeningPoint;
|
||||
|
||||
const E: f64 = 200_000.0;
|
||||
const NU: f64 = 0.3;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Shape functions and their derivatives in natural coordinates.
|
||||
//!
|
||||
//! Node order is the model's (Abaqus/VTK, [`modelcraft_model::ElementKind`]):
|
||||
//! Node order is the model's (Abaqus/VTK, [`fieldcraft_model::ElementKind`]):
|
||||
//! - tetrahedra on the reference element with corners (0,0,0), (1,0,0), (0,1,0), (0,0,1) and the
|
||||
//! volume coordinates L₁ = 1 − ξ − η − ζ, L₂ = ξ, L₃ = η, L₄ = ζ; tet10 mid-side nodes on edges
|
||||
//! 1-2, 2-3, 3-1, 1-4, 2-4, 3-4 with N = 4 Lₐ L_b, corners N = L(2L − 1);
|
||||
@@ -9,7 +9,7 @@
|
||||
//! N = ⅛(1 + ξξᵢ)(1 + ηηᵢ)(1 + ζζᵢ)(ξξᵢ + ηηᵢ + ζζᵢ − 2), a mid-side node with ξᵢ = 0
|
||||
//! N = ¼(1 − ξ²)(1 + ηηᵢ)(1 + ζζᵢ), and likewise), mid-side nodes 9–12 on the bottom edges,
|
||||
//! 13–16 on the top edges, 17–20 on the vertical edges (mid-side positions come from
|
||||
//! [`modelcraft_model::ElementKind::edges`], the one place the node order is written down);
|
||||
//! [`fieldcraft_model::ElementKind::edges`], the one place the node order is written down);
|
||||
//! - the six-node wedge (pentahedron) as the product of the linear triangle {ξ, η ≥ 0,
|
||||
//! ξ + η ≤ 1} (area coordinates L₁ = 1 − ξ − η, L₂ = ξ, L₃ = η) and the linear interval
|
||||
//! ζ ∈ [−1, 1]: nodes 1–3 at ζ = −1 counter-clockwise seen from above, nodes 4–6 above them,
|
||||
@@ -19,7 +19,7 @@
|
||||
//! Element Method: Its Basis and Fundamentals*, 7th ed., Butterworth-Heinemann 2013, ch. 6;
|
||||
//! T. J. R. Hughes, *The Finite Element Method*, Dover 2000, ch. 3).
|
||||
|
||||
use modelcraft_model::ElementKind;
|
||||
use fieldcraft_model::ElementKind;
|
||||
|
||||
/// Most nodes an element family has (hex27, [`crate::basis::Basis::Hex27`]).
|
||||
pub const MAX_NODES: usize = 27;
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
//! the nodes (triangle, constant membrane and bending fields). Transverse shear stresses are
|
||||
//! reported at the mid-surface as the peak of the parabolic distribution, 3/2 Q/t.
|
||||
|
||||
use modelcraft_model::{Elastic, ElementKind};
|
||||
use fieldcraft_model::{Elastic, ElementKind};
|
||||
|
||||
use crate::dense::Matrix;
|
||||
use crate::dof::{DofSet, ElementDofs};
|
||||
@@ -213,7 +213,7 @@ impl Shell {
|
||||
if xyz.len() != nodes {
|
||||
return Err(Error::Invalid(format!("a {} element has {nodes} nodes, got {}", kind.name(), xyz.len())));
|
||||
}
|
||||
modelcraft_model::check_coordinates(xyz.as_flattened()).map_err(|e| Error::Invalid(e.to_string()))?;
|
||||
fieldcraft_model::check_coordinates(xyz.as_flattened()).map_err(|e| Error::Invalid(e.to_string()))?;
|
||||
let degenerate = || Error::Degenerate(format!("a {} shell element is degenerate (zero area, inverted or not convex)", kind.name()));
|
||||
let p = |i: usize| xyz.get(i).copied().unwrap_or_default();
|
||||
let (normal, along) = if nodes == 3 {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Element-level checks: rigid-body modes, constant-strain reproduction (the single-element form
|
||||
//! of the patch test), mass and load totals, extrapolation.
|
||||
|
||||
use modelcraft_model::ElementKind;
|
||||
use fieldcraft_model::ElementKind;
|
||||
|
||||
use crate::dense::{Cholesky, Matrix};
|
||||
use crate::element::{Formulation, Solid};
|
||||
@@ -203,7 +203,7 @@ fn bad_input_is_an_error() {
|
||||
|
||||
#[test]
|
||||
fn section_formulations() {
|
||||
use modelcraft_model::{Formulation as F, Integration};
|
||||
use fieldcraft_model::{Formulation as F, Integration};
|
||||
let full = F::default();
|
||||
let reduced = F { integration: Integration::Reduced, ..F::default() };
|
||||
let im = F { incompatible_modes: true, ..F::default() };
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
//! stiffness and mass of a straight member, and the usual full integration of a curved one.
|
||||
//! Material: the Young's modulus of an isotropic material; the truss knows no other constant.
|
||||
|
||||
use modelcraft_model::{Elastic, ElementKind};
|
||||
use fieldcraft_model::{Elastic, ElementKind};
|
||||
|
||||
use crate::basis::Basis;
|
||||
use crate::dense::Matrix;
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
//! element frequency satisfies ω² ≤ k λ_max(Σ b bᵀ) / (V min m_I). The bound is exact for a
|
||||
//! cube (its dilatation mode) and conservative for distorted elements.
|
||||
|
||||
use modelcraft_model::Elastic;
|
||||
use fieldcraft_model::Elastic;
|
||||
|
||||
use crate::dense::inverse3;
|
||||
use crate::material::Elasticity;
|
||||
@@ -58,7 +58,7 @@ fn degenerate(what: &str) -> Error {
|
||||
impl UniformStrain<8> {
|
||||
/// The uniform-strain hexahedron of corners `xyz` (the model's hex8 node order).
|
||||
pub fn hex8(xyz: &[[f64; 3]; 8]) -> Result<Hex8> {
|
||||
modelcraft_model::check_coordinates(xyz.as_flattened()).map_err(|e| Error::Invalid(e.to_string()))?;
|
||||
fieldcraft_model::check_coordinates(xyz.as_flattened()).map_err(|e| Error::Invalid(e.to_string()))?;
|
||||
let mut b = [[0.0; 3]; 8];
|
||||
let mut nodal_volume = [0.0; 8];
|
||||
let mut volume = 0.0;
|
||||
@@ -171,7 +171,7 @@ impl UniformStrain<8> {
|
||||
impl UniformStrain<4> {
|
||||
/// The linear tetrahedron of corners `xyz` (the model's tet4 node order).
|
||||
pub fn tet4(xyz: &[[f64; 3]; 4]) -> Result<Tet4> {
|
||||
modelcraft_model::check_coordinates(xyz.as_flattened()).map_err(|e| Error::Invalid(e.to_string()))?;
|
||||
fieldcraft_model::check_coordinates(xyz.as_flattened()).map_err(|e| Error::Invalid(e.to_string()))?;
|
||||
let [x0, x1, x2, x3] = *xyz;
|
||||
let e = |x: [f64; 3]| [x[0] - x0[0], x[1] - x0[1], x[2] - x0[2]];
|
||||
// J[a][c] = ∂x_c/∂ξ_a: the edges from corner 1.
|
||||
@@ -330,7 +330,7 @@ mod tests {
|
||||
}
|
||||
assert!((h.nodal_volume.iter().sum::<f64>() - h.volume).abs() < 1e-14);
|
||||
// The volume of the distorted hexahedron agrees with the fem element's.
|
||||
let solid = crate::Solid::new(modelcraft_model::ElementKind::Hex8, &hex, crate::Formulation::Full).unwrap();
|
||||
let solid = crate::Solid::new(fieldcraft_model::ElementKind::Hex8, &hex, crate::Formulation::Full).unwrap();
|
||||
assert!((solid.volume().unwrap() - h.volume).abs() < 1e-14);
|
||||
let mut inverted = hex;
|
||||
inverted.swap(0, 1);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "modelcraft-geometry"
|
||||
description = "ModelCraft's geometry boundary: CAD import, measures and tessellation, implemented with SolidCraft's B-rep kernel"
|
||||
name = "fieldcraft-geometry"
|
||||
description = "FieldCraft's geometry boundary: CAD import, measures and tessellation, implemented with SolidCraft's B-rep kernel"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
@@ -9,7 +9,7 @@ license.workspace = true
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
modelcraft-model = { workspace = true }
|
||||
fieldcraft-model = { workspace = true }
|
||||
solidcraft-geom = { workspace = true }
|
||||
solidcraft-kernel = { workspace = true }
|
||||
solidcraft-doc = { workspace = true }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! The geometry boundary.
|
||||
//!
|
||||
//! Meshing in ModelCraft starts from a triangulated boundary surface. This crate produces it:
|
||||
//! Meshing in FieldCraft starts from a triangulated boundary surface. This crate produces it:
|
||||
//! B-rep bodies come from SolidCraft's kernel (`solidcraft-kernel`, a pinned git dependency;
|
||||
//! plan/adr/0001-libraries.md) and leave it as a [`SurfaceMesh`] from SolidCraft's tessellator,
|
||||
//! with each triangle tagged by the B-rep face it lies on (for feature edges and for loads and
|
||||
|
||||
@@ -30,7 +30,7 @@ const MAX_BUILD_ITEMS: usize = 10_000;
|
||||
const MAX_XML_DEPTH: usize = 256;
|
||||
/// Largest coordinate magnitude accepted, in the file and after units and transforms: the FE
|
||||
/// model's own limit, so every imported vertex can become a node.
|
||||
use modelcraft_model::MAX_COORD;
|
||||
use fieldcraft_model::MAX_COORD;
|
||||
/// A transform whose determinant is below this fraction of its largest row length cubed
|
||||
/// flattens the object (relative, so a uniform scale is never refused).
|
||||
const MIN_RELATIVE_DET: f64 = 1e-12;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "modelcraft-io"
|
||||
description = "ModelCraft's files: the .modelcraft model file, atomic saves and autosave recovery"
|
||||
name = "fieldcraft-io"
|
||||
description = "FieldCraft's files: the .fieldcraft model file, atomic saves and autosave recovery"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
@@ -9,7 +9,7 @@ license.workspace = true
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
modelcraft-model = { workspace = true }
|
||||
fieldcraft-model = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
+4
-4
@@ -142,7 +142,7 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
fn dir(tag: &str) -> PathBuf {
|
||||
let d = std::env::temp_dir().join(format!("modelcraft-fs-{tag}-{}", std::process::id()));
|
||||
let d = std::env::temp_dir().join(format!("fieldcraft-fs-{tag}-{}", std::process::id()));
|
||||
let _ = fs::remove_dir_all(&d);
|
||||
fs::create_dir_all(&d).unwrap();
|
||||
d
|
||||
@@ -151,7 +151,7 @@ mod tests {
|
||||
#[test]
|
||||
fn replaces_and_keeps_a_backup() {
|
||||
let d = dir("bak");
|
||||
let p = d.join("a.modelcraft");
|
||||
let p = d.join("a.fieldcraft");
|
||||
write_atomic(&p, b"one", true).unwrap();
|
||||
assert!(!backup_path(&p).exists(), "nothing to back up the first time");
|
||||
write_atomic(&p, b"two", true).unwrap();
|
||||
@@ -167,7 +167,7 @@ mod tests {
|
||||
#[test]
|
||||
fn a_save_stopped_part_way_keeps_the_old_file() {
|
||||
let d = dir("stop");
|
||||
let p = d.join("a.modelcraft");
|
||||
let p = d.join("a.fieldcraft");
|
||||
write_atomic(&p, b"old", true).unwrap();
|
||||
for stop in [Step::TempWritten, Step::BackupWritten, Step::Replaced] {
|
||||
write_steps(&p, b"new content", true, stop).unwrap();
|
||||
@@ -189,7 +189,7 @@ mod tests {
|
||||
#[test]
|
||||
fn a_failed_write_leaves_the_target() {
|
||||
let d = dir("fail");
|
||||
let p = d.join("missing-dir").join("a.modelcraft");
|
||||
let p = d.join("missing-dir").join("a.fieldcraft");
|
||||
assert!(write_atomic(&p, b"x", true).is_err());
|
||||
assert!(!p.exists());
|
||||
let _ = fs::remove_dir_all(&d);
|
||||
|
||||
+21
-21
@@ -1,9 +1,9 @@
|
||||
//! ModelCraft's files.
|
||||
//! FieldCraft's files.
|
||||
//!
|
||||
//! A `.modelcraft` file ([`write`], [`read`]; docs/formats/modelcraft.md) is a ZIP package of four
|
||||
//! A `.fieldcraft` file ([`write`], [`read`]; docs/formats/fieldcraft.md) is a ZIP package of four
|
||||
//! parts, stored uncompressed so each can be read with any unzip tool:
|
||||
//!
|
||||
//! - `modelcraft.json`: the manifest. The format tag (`modelcraft/1`), the program that wrote
|
||||
//! - `fieldcraft.json`: the manifest. The format tag (`fieldcraft/1`), the program that wrote
|
||||
//! it, the geometry the model is built on (the SolidCraft design and the imported 3MF meshes,
|
||||
//! each a path and the SHA-256 of its content, opened again when the model is), and the size
|
||||
//! and SHA-256 of every other part.
|
||||
@@ -30,16 +30,16 @@ pub mod roots;
|
||||
use std::collections::BTreeMap;
|
||||
use std::io::{Cursor, Read, Write};
|
||||
|
||||
use modelcraft_model::Model;
|
||||
use fieldcraft_model::Model;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
/// The format tag of the files this version writes and reads.
|
||||
pub const FORMAT: &str = "modelcraft/1";
|
||||
pub const FORMAT: &str = "fieldcraft/1";
|
||||
|
||||
/// The file name extension.
|
||||
pub const EXTENSION: &str = "modelcraft";
|
||||
pub const EXTENSION: &str = "fieldcraft";
|
||||
|
||||
/// Largest file read.
|
||||
pub const MAX_FILE_BYTES: u64 = 8 << 30;
|
||||
@@ -57,17 +57,17 @@ pub const MAX_MESH_SOURCES: usize = 10_000;
|
||||
pub const MAX_PATH_LEN: usize = 4096;
|
||||
const MAX_COMMAND_LEN: usize = 256;
|
||||
|
||||
const MANIFEST: &str = "modelcraft.json";
|
||||
const MANIFEST: &str = "fieldcraft.json";
|
||||
const MODEL: &str = "model.json";
|
||||
const MESH: &str = "mesh.bin";
|
||||
const JOURNAL: &str = "journal.json";
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
|
||||
pub enum Error {
|
||||
/// Not a `.modelcraft` file, or a damaged or invalid one.
|
||||
/// Not a `.fieldcraft` file, or a damaged or invalid one.
|
||||
#[error("{0}")]
|
||||
Invalid(String),
|
||||
/// A `.modelcraft` file of a format version this program does not read.
|
||||
/// A `.fieldcraft` file of a format version this program does not read.
|
||||
#[error("{0}")]
|
||||
Version(String),
|
||||
#[error("limit reached: {0}")]
|
||||
@@ -87,9 +87,9 @@ fn invalid<T>(message: impl Into<String>) -> Result<T> {
|
||||
}
|
||||
|
||||
/// A model error met while reading `part`.
|
||||
pub(crate) fn model_error(part: &str, e: modelcraft_model::Error) -> Error {
|
||||
pub(crate) fn model_error(part: &str, e: fieldcraft_model::Error) -> Error {
|
||||
match e {
|
||||
modelcraft_model::Error::Limit(m) => Error::Limit(format!("{part}: {m}")),
|
||||
fieldcraft_model::Error::Limit(m) => Error::Limit(format!("{part}: {m}")),
|
||||
e => Error::Invalid(format!("{part}: {e}")),
|
||||
}
|
||||
}
|
||||
@@ -101,7 +101,7 @@ pub fn sha256_hex(bytes: &[u8]) -> String {
|
||||
|
||||
/// A file the model is built on: its path and the SHA-256 of the content that was opened.
|
||||
///
|
||||
/// A relative path is relative to the folder of the `.modelcraft` file that refers to it.
|
||||
/// A relative path is relative to the folder of the `.fieldcraft` file that refers to it.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(try_from = "SourceFileRepr", into = "SourceFileRepr")]
|
||||
pub struct SourceFile {
|
||||
@@ -178,7 +178,7 @@ pub struct Geometry {
|
||||
/// One journal entry: a command id and its parameters.
|
||||
pub type JournalEntry = (String, Value);
|
||||
|
||||
/// What a `.modelcraft` file holds.
|
||||
/// What a `.fieldcraft` file holds.
|
||||
#[derive(Debug, Clone, Default, PartialEq)]
|
||||
pub struct ModelFile {
|
||||
pub model: Model,
|
||||
@@ -233,7 +233,7 @@ fn journal_text(journal: &[JournalEntry]) -> Result<Vec<u8>> {
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// The bytes of a `.modelcraft` file holding `model`, built on `geometry` by the commands in
|
||||
/// The bytes of a `.fieldcraft` file holding `model`, built on `geometry` by the commands in
|
||||
/// `journal`. The same arguments always give the same bytes.
|
||||
pub fn write(model: &Model, geometry: &Geometry, journal: &[JournalEntry]) -> Result<Vec<u8>> {
|
||||
if geometry.meshes.len() > MAX_MESH_SOURCES {
|
||||
@@ -246,7 +246,7 @@ pub fn write(model: &Model, geometry: &Geometry, journal: &[JournalEntry]) -> Re
|
||||
let parts = [(MODEL, model_json), (MESH, mesh), (JOURNAL, journal)];
|
||||
let manifest = Manifest {
|
||||
format: FORMAT.into(),
|
||||
generator: format!("ModelCraft {}", env!("CARGO_PKG_VERSION")),
|
||||
generator: format!("FieldCraft {}", env!("CARGO_PKG_VERSION")),
|
||||
geometry: geometry.clone(),
|
||||
parts: parts.iter().map(|(name, bytes)| (name.to_string(), Part { bytes: bytes.len() as u64, sha256: sha256_hex(bytes) })).collect(),
|
||||
};
|
||||
@@ -285,15 +285,15 @@ fn read_part(zip: &mut zip::ZipArchive<Cursor<&[u8]>>, name: &str, cap: u64, fil
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
/// Read a `.modelcraft` file.
|
||||
/// Read a `.fieldcraft` file.
|
||||
pub fn read(bytes: &[u8]) -> Result<ModelFile> {
|
||||
if bytes.len() as u64 > MAX_FILE_BYTES {
|
||||
return Err(Error::Limit(format!("the file is {} MB (at most {} MB)", bytes.len() >> 20, MAX_FILE_BYTES >> 20)));
|
||||
}
|
||||
if !bytes.starts_with(b"PK\x03\x04") {
|
||||
return invalid("not a ModelCraft file (not a ZIP package)");
|
||||
return invalid("not a FieldCraft file (not a ZIP package)");
|
||||
}
|
||||
let mut zip = zip::ZipArchive::new(Cursor::new(bytes)).map_err(|e| Error::Invalid(format!("not a ModelCraft file: {e}")))?;
|
||||
let mut zip = zip::ZipArchive::new(Cursor::new(bytes)).map_err(|e| Error::Invalid(format!("not a FieldCraft file: {e}")))?;
|
||||
let names = [MANIFEST, MODEL, MESH, JOURNAL];
|
||||
let mut seen = Vec::with_capacity(zip.len());
|
||||
for name in zip.file_names() {
|
||||
@@ -310,13 +310,13 @@ pub fn read(bytes: &[u8]) -> Result<ModelFile> {
|
||||
let manifest: Value = serde_json::from_slice(&manifest_bytes).map_err(|e| json_error(MANIFEST, e))?;
|
||||
match manifest.get("format").and_then(Value::as_str) {
|
||||
Some(FORMAT) => {}
|
||||
Some(f) if f.starts_with("modelcraft/") => {
|
||||
Some(f) if f.starts_with("fieldcraft/") => {
|
||||
return Err(Error::Version(format!(
|
||||
"the file is format `{}`; this version of ModelCraft reads `{FORMAT}`",
|
||||
"the file is format `{}`; this version of FieldCraft reads `{FORMAT}`",
|
||||
f.chars().take(40).collect::<String>()
|
||||
)));
|
||||
}
|
||||
_ => return invalid(format!("not a ModelCraft file: `{MANIFEST}` has no `{FORMAT}` format tag")),
|
||||
_ => return invalid(format!("not a FieldCraft file: `{MANIFEST}` has no `{FORMAT}` format tag")),
|
||||
}
|
||||
let manifest: Manifest = serde_json::from_value(manifest).map_err(|e| json_error(MANIFEST, e))?;
|
||||
if manifest.geometry.meshes.len() > MAX_MESH_SOURCES {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! The binary mesh part of a `.modelcraft` file (`mesh.bin`): nodes, elements and the geometry
|
||||
//! The binary mesh part of a `.fieldcraft` file (`mesh.bin`): nodes, elements and the geometry
|
||||
//! tags that say how the mesh lies on the geometry.
|
||||
//!
|
||||
//! Little-endian throughout:
|
||||
@@ -21,8 +21,8 @@
|
||||
//! in the manifest; decoding checks every count against the bytes present before reading, and
|
||||
//! every value through the model's own checked methods.
|
||||
|
||||
use modelcraft_model::geometry::MAX_ENTITIES;
|
||||
use modelcraft_model::{ElementKind, Entity, GeometryRef, MAX_ELEMENTS, MAX_NODES, Model};
|
||||
use fieldcraft_model::geometry::MAX_ENTITIES;
|
||||
use fieldcraft_model::{ElementKind, Entity, GeometryRef, MAX_ELEMENTS, MAX_NODES, Model};
|
||||
|
||||
use crate::{Error, MAX_FILE_BYTES, Result, invalid, model_error};
|
||||
|
||||
@@ -221,7 +221,7 @@ pub fn decode_into(model: &mut Model, bytes: &[u8]) -> Result<()> {
|
||||
}
|
||||
let mut r = Reader { bytes, pos: 0 };
|
||||
if r.take(MAGIC.len()).ok() != Some(MAGIC.as_slice()) {
|
||||
return invalid(format!("{PART} is not a ModelCraft mesh (no {} header)", String::from_utf8_lossy(MAGIC)));
|
||||
return invalid(format!("{PART} is not a FieldCraft mesh (no {} header)", String::from_utf8_lossy(MAGIC)));
|
||||
}
|
||||
let nodes = count(&mut r, "nodes", MAX_NODES)?;
|
||||
let elements = count(&mut r, "elements", MAX_ELEMENTS)?;
|
||||
|
||||
+12
-12
@@ -1,6 +1,6 @@
|
||||
//! Autosave storage and crash recovery.
|
||||
//!
|
||||
//! A running app keeps one entry in the recovery folder: `<id>.modelcraft` (the model as of the
|
||||
//! A running app keeps one entry in the recovery folder: `<id>.fieldcraft` (the model as of the
|
||||
//! last autosave), `<id>.json` (its [`Entry`]: the model's own file, when it has one, and when it
|
||||
//! was autosaved) and `<id>.lock`, which the app holds locked while it runs. An entry whose lock
|
||||
//! nobody holds belongs to an app that crashed or closed with unsaved changes: [`orphans`] lists
|
||||
@@ -17,19 +17,19 @@ use serde::{Deserialize, Serialize};
|
||||
use crate::fs::{read_capped, write_atomic};
|
||||
use crate::{EXTENSION, Error, MAX_FILE_BYTES, Result};
|
||||
|
||||
/// The recovery folder: `MODELCRAFT_RECOVERY_DIR`, else `ModelCraft/recovery` (`modelcraft/` on
|
||||
/// The recovery folder: `FIELDCRAFT_RECOVERY_DIR`, else `FieldCraft/recovery` (`fieldcraft/` on
|
||||
/// Linux) in the platform's per-user data folder.
|
||||
pub fn default_dir() -> Option<PathBuf> {
|
||||
let env = |k: &str| std::env::var_os(k).filter(|d| !d.is_empty()).map(PathBuf::from);
|
||||
if let Some(d) = env("MODELCRAFT_RECOVERY_DIR") {
|
||||
if let Some(d) = env("FIELDCRAFT_RECOVERY_DIR") {
|
||||
return Some(d);
|
||||
}
|
||||
let (root, name) = if cfg!(windows) {
|
||||
(env("LOCALAPPDATA")?, "ModelCraft")
|
||||
(env("LOCALAPPDATA")?, "FieldCraft")
|
||||
} else if cfg!(target_os = "macos") {
|
||||
(env("HOME")?.join("Library/Application Support"), "ModelCraft")
|
||||
(env("HOME")?.join("Library/Application Support"), "FieldCraft")
|
||||
} else {
|
||||
(env("XDG_DATA_HOME").or_else(|| env("HOME").map(|h| h.join(".local/share")))?, "modelcraft")
|
||||
(env("XDG_DATA_HOME").or_else(|| env("HOME").map(|h| h.join(".local/share")))?, "fieldcraft")
|
||||
};
|
||||
Some(root.join(name).join("recovery"))
|
||||
}
|
||||
@@ -210,7 +210,7 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
fn dir(tag: &str) -> PathBuf {
|
||||
let d = std::env::temp_dir().join(format!("modelcraft-recovery-{tag}-{}", std::process::id()));
|
||||
let d = std::env::temp_dir().join(format!("fieldcraft-recovery-{tag}-{}", std::process::id()));
|
||||
let _ = fs::remove_dir_all(&d);
|
||||
d
|
||||
}
|
||||
@@ -219,7 +219,7 @@ mod tests {
|
||||
fn a_crashed_apps_autosave_is_offered_and_a_running_ones_is_not() {
|
||||
let d = dir("crash");
|
||||
let mut a = Slot::new(&d).unwrap();
|
||||
a.write(b"model bytes", Some("/m/part.modelcraft"), 8, 1).unwrap();
|
||||
a.write(b"model bytes", Some("/m/part.fieldcraft"), 8, 1).unwrap();
|
||||
assert!(orphans(&d).is_empty(), "held by a running app");
|
||||
assert!(remove(&d, a.id()).is_err(), "a running app's entry stays");
|
||||
let id = a.id().to_string();
|
||||
@@ -229,7 +229,7 @@ mod tests {
|
||||
assert_eq!(found.iter().map(|e| e.id.as_str()).collect::<Vec<_>>(), [id.as_str()]);
|
||||
let (bytes, entry) = load(&d, &id).unwrap();
|
||||
assert_eq!(bytes, b"model bytes");
|
||||
assert_eq!((entry.path.as_deref(), entry.nodes, entry.elements), (Some("/m/part.modelcraft"), 8, 1));
|
||||
assert_eq!((entry.path.as_deref(), entry.nodes, entry.elements), (Some("/m/part.fieldcraft"), 8, 1));
|
||||
// A new app gets its own entry and still sees the crashed one.
|
||||
let b = Slot::new(&d).unwrap();
|
||||
assert_ne!(b.id(), id);
|
||||
@@ -264,11 +264,11 @@ mod tests {
|
||||
}
|
||||
// An entry naming another id, junk JSON and a huge description are not listed.
|
||||
fs::write(d.join("1-1-1.json"), br#"{"id": "2-2-2", "path": null, "saved_at": 1, "nodes": 0, "elements": 0}"#).unwrap();
|
||||
fs::write(d.join("1-1-1.modelcraft"), b"x").unwrap();
|
||||
fs::write(d.join("2-2-2.modelcraft"), b"x").unwrap();
|
||||
fs::write(d.join("1-1-1.fieldcraft"), b"x").unwrap();
|
||||
fs::write(d.join("2-2-2.fieldcraft"), b"x").unwrap();
|
||||
fs::write(d.join("3-3-3.json"), b"{not json").unwrap();
|
||||
fs::write(d.join("4-4-4.json"), vec![b' '; (MAX_ENTRY_BYTES + 1) as usize]).unwrap();
|
||||
fs::write(d.join("4-4-4.modelcraft"), b"x").unwrap();
|
||||
fs::write(d.join("4-4-4.fieldcraft"), b"x").unwrap();
|
||||
assert!(orphans(&d).is_empty());
|
||||
assert!(load(&d, "1-1-1").is_err());
|
||||
let _ = fs::remove_dir_all(&d);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Folders an automation client may read from and write to.
|
||||
//!
|
||||
//! An agent driving ModelCraft (the MCP server, docs/mcp.md) can be kept to the folders it was
|
||||
//! An agent driving FieldCraft (the MCP server, docs/mcp.md) can be kept to the folders it was
|
||||
//! given: every file a command reads must lie inside the read root, every file it writes inside
|
||||
//! the write root. [`Roots::check`] decides, before the file is touched:
|
||||
//!
|
||||
@@ -153,7 +153,7 @@ mod tests {
|
||||
struct Dir(PathBuf);
|
||||
impl Dir {
|
||||
fn new(tag: &str) -> Dir {
|
||||
let d = std::env::temp_dir().join(format!("modelcraft-roots-{tag}-{}", std::process::id()));
|
||||
let d = std::env::temp_dir().join(format!("fieldcraft-roots-{tag}-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&d);
|
||||
std::fs::create_dir_all(&d).unwrap();
|
||||
Dir(std::fs::canonicalize(&d).unwrap())
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
//! `.modelcraft` files: round trips, stable bytes and hostile files.
|
||||
//! `.fieldcraft` files: round trips, stable bytes and hostile files.
|
||||
|
||||
use std::io::{Cursor, Read, Write};
|
||||
|
||||
use modelcraft_io::{Error, Geometry, ModelFile, SourceFile, mesh, read, write};
|
||||
use modelcraft_model::{ElementKind, Entity, GeometryRef, Model, UnitSystem};
|
||||
use fieldcraft_io::{Error, Geometry, ModelFile, SourceFile, mesh, read, write};
|
||||
use fieldcraft_model::{ElementKind, Entity, GeometryRef, Model, UnitSystem};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
/// A hex and a tet sharing a face, tagged on geometry, with a few definitions.
|
||||
@@ -71,7 +71,7 @@ fn package(parts: &[(String, Vec<u8>)]) -> Vec<u8> {
|
||||
fn with_part(bytes: &[u8], name: &str, new: Vec<u8>) -> Vec<u8> {
|
||||
let mut ps = parts(bytes);
|
||||
let mut manifest: Value = serde_json::from_slice(&ps[0].1).unwrap();
|
||||
manifest["parts"][name] = json!({"bytes": new.len(), "sha256": modelcraft_io::sha256_hex(&new)});
|
||||
manifest["parts"][name] = json!({"bytes": new.len(), "sha256": fieldcraft_io::sha256_hex(&new)});
|
||||
ps[0].1 = serde_json::to_vec(&manifest).unwrap();
|
||||
for p in &mut ps {
|
||||
if p.0 == name {
|
||||
@@ -100,10 +100,10 @@ fn round_trip_keeps_everything_and_bytes_are_stable() {
|
||||
|
||||
// The parts, in order, all stored as plain bytes.
|
||||
let names: Vec<String> = parts(&bytes).into_iter().map(|(n, _)| n).collect();
|
||||
assert_eq!(names, ["modelcraft.json", "model.json", "mesh.bin", "journal.json"]);
|
||||
assert_eq!(names, ["fieldcraft.json", "model.json", "mesh.bin", "journal.json"]);
|
||||
let ps = parts(&bytes);
|
||||
let manifest: Value = serde_json::from_slice(&ps[0].1).unwrap();
|
||||
assert_eq!(manifest["format"], "modelcraft/1");
|
||||
assert_eq!(manifest["format"], "fieldcraft/1");
|
||||
assert_eq!(manifest["geometry"]["design"]["path"], "parts/plate.solidcraft");
|
||||
let model_text = String::from_utf8(ps[1].1.clone()).unwrap();
|
||||
assert!(model_text.contains("\"Steel\"") && !model_text.contains("\"nodes\""), "{model_text}");
|
||||
@@ -148,12 +148,12 @@ fn hostile_files_are_refused_cleanly() {
|
||||
}
|
||||
|
||||
// Not a model file at all.
|
||||
for junk in [&b""[..], b"PK", b"PK\x03\x04garbage", b"{\"format\": \"modelcraft/1\"}", &[0u8; 4096]] {
|
||||
for junk in [&b""[..], b"PK", b"PK\x03\x04garbage", b"{\"format\": \"fieldcraft/1\"}", &[0u8; 4096]] {
|
||||
assert!(matches!(read(junk), Err(Error::Invalid(_))), "{junk:?}");
|
||||
}
|
||||
|
||||
// Wrong and future versions.
|
||||
let v2 = with_manifest(&good, |m| m["format"] = json!("modelcraft/2"));
|
||||
let v2 = with_manifest(&good, |m| m["format"] = json!("fieldcraft/2"));
|
||||
assert!(matches!(read(&v2), Err(Error::Version(_))));
|
||||
for f in [json!("solidcraft/16"), json!(null), json!(1)] {
|
||||
let b = with_manifest(&good, |m| m["format"] = f.clone());
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "modelcraft-lbm"
|
||||
description = "ModelCraft's lattice Boltzmann flow solver: D3Q19 two-relaxation-time collision, bounce-back walls, Zou–He inlets and outlets, exact voxelisation of face-tagged surfaces, momentum-exchange wall forces"
|
||||
name = "fieldcraft-lbm"
|
||||
description = "FieldCraft's lattice Boltzmann flow solver: D3Q19 two-relaxation-time collision, bounce-back walls, Zou–He inlets and outlets, exact voxelisation of face-tagged surfaces, momentum-exchange wall forces"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
@@ -9,7 +9,7 @@ license.workspace = true
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
modelcraft-model = { workspace = true }
|
||||
modelcraft-mesh = { workspace = true }
|
||||
modelcraft-parallel = { workspace = true }
|
||||
fieldcraft-model = { workspace = true }
|
||||
fieldcraft-mesh = { workspace = true }
|
||||
fieldcraft-parallel = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
@@ -36,9 +36,9 @@
|
||||
//! only its own populations; the wall forces are summed in link order. There are no parallel
|
||||
//! reductions, so the result is the same bit for bit on any number of threads and platform.
|
||||
|
||||
use modelcraft_model::{BoxFace, FaceCondition, Profile};
|
||||
use fieldcraft_model::{BoxFace, FaceCondition, Profile};
|
||||
|
||||
use modelcraft_parallel as parallel;
|
||||
use fieldcraft_parallel as parallel;
|
||||
|
||||
use crate::lattice::{C, Q, W, c, equilibrium, moving_wall, opposite};
|
||||
use crate::setup::{MAX_LATTICE_SPEED, Obstacle, Setup};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! ModelCraft's lattice Boltzmann flow solver (CPU).
|
||||
//! FieldCraft's lattice Boltzmann flow solver (CPU).
|
||||
//!
|
||||
//! It solves a fluid step of the model ([`modelcraft_model::Procedure::Fluid`]) in its fluid
|
||||
//! It solves a fluid step of the model ([`fieldcraft_model::Procedure::Fluid`]) in its fluid
|
||||
//! domain: weakly compressible, isothermal, Newtonian flow on a uniform grid of cubic cells that
|
||||
//! fills the domain's box, less the cells inside the obstacle bodies.
|
||||
//!
|
||||
@@ -33,14 +33,14 @@
|
||||
//! - P. Lallemand, L.-S. Luo, *Theory of the lattice Boltzmann method: dispersion, dissipation,
|
||||
//! isotropy, Galilean invariance, and stability*, PRE 61:6546, 2000 (stability limits).
|
||||
//! - H. Edelsbrunner, E. P. Mücke, *Simulation of Simplicity*, ACM TOG 9(1):66–104, 1990;
|
||||
//! J. R. Shewchuk 1997 (exact predicates, through `modelcraft_mesh::predicates`).
|
||||
//! J. R. Shewchuk 1997 (exact predicates, through `fieldcraft_mesh::predicates`).
|
||||
//!
|
||||
//! No code of any lattice Boltzmann package was read; everything is written from these papers and
|
||||
//! the book.
|
||||
//!
|
||||
//! # Determinism
|
||||
//! Double precision and fixed loop orders. Collision and streaming run on threads by fixed blocks
|
||||
//! of cells ([`modelcraft_parallel`]), each cell's update independent of the others; the wall
|
||||
//! of cells ([`fieldcraft_parallel`]), each cell's update independent of the others; the wall
|
||||
//! forces and every other sum run in a fixed order on one thread. The same model gives the same
|
||||
//! result bit for bit on any number of threads and on every platform, wasm included.
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||
@@ -71,9 +71,9 @@ pub enum Error {
|
||||
#[error("unstable: {0}")]
|
||||
Unstable(String),
|
||||
#[error(transparent)]
|
||||
Model(#[from] modelcraft_model::Error),
|
||||
Model(#[from] fieldcraft_model::Error),
|
||||
#[error(transparent)]
|
||||
Mesh(#[from] modelcraft_mesh::Error),
|
||||
Mesh(#[from] fieldcraft_mesh::Error),
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
@@ -25,8 +25,8 @@
|
||||
//! While running, a lattice speed above [`MAX_LATTICE_SPEED`] or a non-finite or non-positive
|
||||
//! density stops the run with [`crate::Error::Unstable`].
|
||||
|
||||
use modelcraft_mesh::surface::BoundarySurface;
|
||||
use modelcraft_model::{BoxFace, FaceCondition, FluidDomain, Model, Procedure, SurfaceCondition};
|
||||
use fieldcraft_mesh::surface::BoundarySurface;
|
||||
use fieldcraft_model::{BoxFace, FaceCondition, FluidDomain, Model, Procedure, SurfaceCondition};
|
||||
|
||||
use crate::voxel::Grid;
|
||||
use crate::{Error, Limits, Result};
|
||||
@@ -101,7 +101,7 @@ fn norm(v: [f64; 3]) -> f64 {
|
||||
}
|
||||
|
||||
/// The fluid step named `step` (or the only fluid step when `step` is `None`) and its domain.
|
||||
pub fn fluid_step<'m>(model: &'m Model, step: Option<&str>) -> Result<(&'m modelcraft_model::Step, &'m FluidDomain)> {
|
||||
pub fn fluid_step<'m>(model: &'m Model, step: Option<&str>) -> Result<(&'m fieldcraft_model::Step, &'m FluidDomain)> {
|
||||
let s = model.select_step(step, |p| p.fluid_domain().is_some(), "fluid")?;
|
||||
let name = s.procedure.fluid_domain().unwrap_or_default();
|
||||
let domain = model.fluid_domain(name).ok_or_else(|| Error::NotFound(format!("fluid domain `{name}`")))?;
|
||||
@@ -258,7 +258,7 @@ impl Setup {
|
||||
for sb in domain.surfaces.iter().filter(|sb| &sb.face.body == body) {
|
||||
let mut hit = false;
|
||||
for (tag, name) in names.iter().enumerate() {
|
||||
if sb.face.selects(&modelcraft_model::GeometryRef::face(body.as_str(), name.as_str())?) {
|
||||
if sb.face.selects(&fieldcraft_model::GeometryRef::face(body.as_str(), name.as_str())?) {
|
||||
hit = true;
|
||||
if let (Some(slot), SurfaceCondition::MovingWall { velocity }) = (velocities.get_mut(tag), sb.condition) {
|
||||
*slot = velocity.map(|c| c * dt / h);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Running a fluid step: the time loop, output frames and force histories, in model units.
|
||||
|
||||
use modelcraft_model::Model;
|
||||
use fieldcraft_model::Model;
|
||||
|
||||
use crate::flow::Flow;
|
||||
use crate::setup::{Obstacle, Setup};
|
||||
|
||||
@@ -30,8 +30,8 @@
|
||||
//! lower triangle index, so the assignment is deterministic. This is a classification for
|
||||
//! reporting, not a meshing decision, so floating-point distances are adequate.
|
||||
|
||||
use modelcraft_mesh::predicates::{Point2, Point3, Sign, orient2d, orient3d, snap_to_domain};
|
||||
use modelcraft_mesh::surface::BoundarySurface;
|
||||
use fieldcraft_mesh::predicates::{Point2, Point3, Sign, orient2d, orient3d, snap_to_domain};
|
||||
use fieldcraft_mesh::surface::BoundarySurface;
|
||||
|
||||
use crate::{Error, Result};
|
||||
|
||||
@@ -373,7 +373,7 @@ impl TriangleTree {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use modelcraft_mesh::surface::SurfaceOptions;
|
||||
use fieldcraft_mesh::surface::SurfaceOptions;
|
||||
|
||||
/// The surface of the box [lo, hi], outward, two triangles per side, side k tagged k.
|
||||
pub(crate) fn cube(lo: [f64; 3], hi: [f64; 3]) -> BoundarySurface {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
//! Benchmark suite: comparisons with published numerical reference values (not analytic; the
|
||||
//! analytic suite in `verification.rs` is the gate). Each case names its source. The cases are
|
||||
//! long, so they are ignored by default:
|
||||
//! `cargo test --release -p modelcraft-lbm --test benchmark -- --ignored --nocapture`.
|
||||
//! `cargo test --release -p fieldcraft-lbm --test benchmark -- --ignored --nocapture`.
|
||||
|
||||
mod common;
|
||||
|
||||
use common::*;
|
||||
use modelcraft_lbm::{Limits, solve_fluid};
|
||||
use modelcraft_model::{FaceCondition, Procedure, Profile};
|
||||
use fieldcraft_lbm::{Limits, solve_fluid};
|
||||
use fieldcraft_model::{FaceCondition, Procedure, Profile};
|
||||
|
||||
/// Benchmark "2D-1" (steady flow around a cylinder at Re = 20): M. Schäfer, S. Turek, *Benchmark
|
||||
/// computations of laminar flow around a cylinder*, in: Flow Simulation with High-Performance
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use modelcraft_lbm::{Flow, Limits, Obstacle, Setup};
|
||||
use modelcraft_mesh::surface::{BoundarySurface, SurfaceOptions};
|
||||
use modelcraft_model::{BoxFaces, Fluid, FluidDomain, Material, Model, Procedure, Step};
|
||||
use fieldcraft_lbm::{Flow, Limits, Obstacle, Setup};
|
||||
use fieldcraft_mesh::surface::{BoundarySurface, SurfaceOptions};
|
||||
use fieldcraft_model::{BoxFaces, Fluid, FluidDomain, Material, Model, Procedure, Step};
|
||||
|
||||
/// A domain over [min, max] with walls on every face.
|
||||
pub fn domain(min: [f64; 3], max: [f64; 3], spacing: f64) -> FluidDomain {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
//! Throughput of the solver (ignored; run with
|
||||
//! `cargo test --release -p modelcraft-lbm --test throughput -- --ignored --nocapture`).
|
||||
//! `cargo test --release -p fieldcraft-lbm --test throughput -- --ignored --nocapture`).
|
||||
|
||||
mod common;
|
||||
|
||||
use common::*;
|
||||
use modelcraft_model::{BoxFace, FaceCondition};
|
||||
use fieldcraft_model::{BoxFace, FaceCondition};
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
@@ -26,7 +26,7 @@ fn cell_updates_per_second() {
|
||||
1.0,
|
||||
1.0 / 6.0,
|
||||
d,
|
||||
modelcraft_model::Procedure::Fluid {
|
||||
fieldcraft_model::Procedure::Fluid {
|
||||
domain: "Flow".into(),
|
||||
end_time: None,
|
||||
steps: Some(steps),
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
//! of a periodic array of spheres (Hasimoto 1959), inlet/outlet channel flow, mass conservation
|
||||
//! to round-off, and the stability limits.
|
||||
//!
|
||||
//! Each test prints its errors and measured orders (`cargo test -p modelcraft-lbm -- --nocapture`).
|
||||
//! Each test prints its errors and measured orders (`cargo test -p fieldcraft-lbm -- --nocapture`).
|
||||
|
||||
mod common;
|
||||
|
||||
use common::*;
|
||||
use modelcraft_lbm::{Error, Limits, Setup, solve_fluid};
|
||||
use modelcraft_model::{FaceCondition, Profile};
|
||||
use fieldcraft_lbm::{Error, Limits, Setup, solve_fluid};
|
||||
use fieldcraft_model::{FaceCondition, Profile};
|
||||
|
||||
const PERIODIC: FaceCondition = FaceCondition::Periodic {};
|
||||
|
||||
@@ -100,7 +100,7 @@ fn taylor_green(n: usize) -> (f64, f64) {
|
||||
let (nu, u0, l) = (0.1, 0.1, 2.0 * std::f64::consts::PI);
|
||||
let dx = l / n as f64;
|
||||
let mut d = domain([0.0; 3], [l, l, dx], dx);
|
||||
for f in modelcraft_model::BoxFace::ALL {
|
||||
for f in fieldcraft_model::BoxFace::ALL {
|
||||
*d.faces.get_mut(f) = PERIODIC;
|
||||
}
|
||||
// τ = 0.8: Δt = 0.3 Δx² / (3 ν).
|
||||
@@ -222,7 +222,7 @@ fn sphere_drag(r: f64, l: usize, steps: u64) -> (f64, f64) {
|
||||
let (nu, a) = (5.0 / 6.0, 1e-6);
|
||||
let side = l as f64;
|
||||
let mut d = domain([0.0; 3], [side; 3], 1.0);
|
||||
for f in modelcraft_model::BoxFace::ALL {
|
||||
for f in fieldcraft_model::BoxFace::ALL {
|
||||
*d.faces.get_mut(f) = PERIODIC;
|
||||
}
|
||||
d.acceleration = [a, 0.0, 0.0];
|
||||
@@ -232,7 +232,7 @@ fn sphere_drag(r: f64, l: usize, steps: u64) -> (f64, f64) {
|
||||
1.0,
|
||||
nu,
|
||||
d,
|
||||
modelcraft_model::Procedure::Fluid {
|
||||
fieldcraft_model::Procedure::Fluid {
|
||||
domain: "Flow".into(),
|
||||
end_time: None,
|
||||
steps: Some(steps),
|
||||
@@ -287,7 +287,7 @@ fn channel_with_a_parabolic_inlet_and_a_pressure_outlet_develops_poiseuille_flow
|
||||
// τ = 0.6. The pressure drop changes the density along the channel by Δp / (ρ c_s²) in lattice
|
||||
// units (the weakly compressible error), here 0.07%; the automatic time step would give 1.6%.
|
||||
let dt = 0.1 / 3.0 * dx * dx / nu;
|
||||
let procedure = modelcraft_model::Procedure::Fluid {
|
||||
let procedure = fieldcraft_model::Procedure::Fluid {
|
||||
domain: "Flow".into(),
|
||||
end_time: Some(4.0 * h * h / nu),
|
||||
steps: None,
|
||||
@@ -372,7 +372,7 @@ fn results_are_bit_for_bit_reproducible() {
|
||||
1.0,
|
||||
0.01,
|
||||
d,
|
||||
modelcraft_model::Procedure::Fluid { domain: "Flow".into(), end_time: Some(1.0), steps: None, time_step: None, frames: 3, magic: None },
|
||||
fieldcraft_model::Procedure::Fluid { domain: "Flow".into(), end_time: Some(1.0), steps: None, time_step: None, frames: 3, magic: None },
|
||||
);
|
||||
let a = solve_fluid(&m, None, std::slice::from_ref(&ball), &Limits::default()).unwrap();
|
||||
let b = solve_fluid(&m, None, std::slice::from_ref(&ball), &Limits::default()).unwrap();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "modelcraft-mcp"
|
||||
description = "ModelCraft's MCP server: JSON-RPC 2.0 over stdio, driving a headless session or the running app"
|
||||
name = "fieldcraft-mcp"
|
||||
description = "FieldCraft's MCP server: JSON-RPC 2.0 over stdio, driving a headless session or the running app"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
@@ -9,10 +9,10 @@ license.workspace = true
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
modelcraft-engine = { workspace = true }
|
||||
modelcraft-io = { workspace = true }
|
||||
modelcraft-render = { workspace = true }
|
||||
fieldcraft-engine = { workspace = true }
|
||||
fieldcraft-io = { workspace = true }
|
||||
fieldcraft-render = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
modelcraft-testkit = { workspace = true }
|
||||
fieldcraft-testkit = { workspace = true }
|
||||
+12
-12
@@ -8,10 +8,10 @@ use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::time::Duration;
|
||||
|
||||
use modelcraft_engine::Session;
|
||||
use modelcraft_engine::view::{Palette, render_png};
|
||||
use modelcraft_io::roots::Roots;
|
||||
use modelcraft_render::StandardView;
|
||||
use fieldcraft_engine::Session;
|
||||
use fieldcraft_engine::view::{Palette, render_png};
|
||||
use fieldcraft_io::roots::Roots;
|
||||
use fieldcraft_render::StandardView;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
/// What a picture shows.
|
||||
@@ -33,7 +33,7 @@ pub struct Picture {
|
||||
pub info: Value,
|
||||
}
|
||||
|
||||
/// Something that runs ModelCraft commands and draws pictures.
|
||||
/// Something that runs FieldCraft commands and draws pictures.
|
||||
pub trait Backend {
|
||||
/// Run a command (`engine.execute`).
|
||||
fn execute(&mut self, command: &str, params: Value) -> Result<Value, String>;
|
||||
@@ -93,7 +93,7 @@ impl Backend for Headless {
|
||||
}
|
||||
|
||||
fn commands(&mut self) -> Result<Value, String> {
|
||||
Ok(modelcraft_engine::commands().iter().map(|c| json!({"id": c.id, "label": c.label, "params": c.params})).collect())
|
||||
Ok(fieldcraft_engine::commands().iter().map(|c| json!({"id": c.id, "label": c.label, "params": c.params})).collect())
|
||||
}
|
||||
|
||||
fn picture(&mut self, r: &PictureRequest) -> Result<Picture, String> {
|
||||
@@ -122,12 +122,12 @@ impl Backend for Headless {
|
||||
}
|
||||
|
||||
fn describe(&self) -> String {
|
||||
"a headless in-process ModelCraft session (no window; screenshots are CPU renders)".into()
|
||||
"a headless in-process FieldCraft session (no window; screenshots are CPU renders)".into()
|
||||
}
|
||||
}
|
||||
|
||||
/// A running ModelCraft app reached through its loopback control channel
|
||||
/// (`modelcraft --control PORT`).
|
||||
/// A running FieldCraft app reached through its loopback control channel
|
||||
/// (`fieldcraft --control PORT`).
|
||||
pub struct Remote {
|
||||
addr: String,
|
||||
conn: Option<(BufReader<TcpStream>, TcpStream)>,
|
||||
@@ -198,7 +198,7 @@ impl Remote {
|
||||
let reply = match self.roundtrip(&line) {
|
||||
Ok(r) => r,
|
||||
// One retry on a fresh connection (the app may have restarted).
|
||||
Err(_) => self.roundtrip(&line).map_err(|e| format!("ModelCraft at {}: {e}", self.addr))?,
|
||||
Err(_) => self.roundtrip(&line).map_err(|e| format!("FieldCraft at {}: {e}", self.addr))?,
|
||||
};
|
||||
let v: Value = serde_json::from_str(&reply).map_err(|e| format!("bad reply from the app: {e}"))?;
|
||||
if v.get("ok").and_then(Value::as_bool) == Some(true) {
|
||||
@@ -212,7 +212,7 @@ impl Remote {
|
||||
/// A temporary PNG path the app writes and the server reads back (both on this computer).
|
||||
fn temp_png() -> PathBuf {
|
||||
let n = SHOT.fetch_add(1, Ordering::Relaxed);
|
||||
std::env::temp_dir().join(format!("modelcraft-mcp-{}-{n}.png", std::process::id()))
|
||||
std::env::temp_dir().join(format!("fieldcraft-mcp-{}-{n}.png", std::process::id()))
|
||||
}
|
||||
|
||||
impl Backend for Remote {
|
||||
@@ -262,6 +262,6 @@ impl Backend for Remote {
|
||||
}
|
||||
|
||||
fn describe(&self) -> String {
|
||||
format!("connected to the ModelCraft app at {} (the user watches its window)", self.addr)
|
||||
format!("connected to the FieldCraft app at {} (the user watches its window)", self.addr)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
//! ModelCraft's MCP server (docs/mcp.md).
|
||||
//! FieldCraft's MCP server (docs/mcp.md).
|
||||
//!
|
||||
//! JSON-RPC 2.0 over stdio, one message per line, implementing the Model Context Protocol's
|
||||
//! lifecycle, tools and resources. Every tool runs engine commands (the same ones the UI,
|
||||
@@ -10,7 +10,7 @@
|
||||
//! The server treats its input as hostile: a message line is capped at [`MAX_LINE`] bytes and
|
||||
//! answered with an error past it, a JSON-RPC batch at [`MAX_BATCH_MESSAGES`] messages, a
|
||||
//! `batch` tool call at [`tools::MAX_STEPS`] commands, and tool arguments are checked against
|
||||
//! the tool's schema before anything runs. Read and write roots ([`modelcraft_io::roots`])
|
||||
//! the tool's schema before anything runs. Read and write roots ([`fieldcraft_io::roots`])
|
||||
//! confine every file a tool touches.
|
||||
//!
|
||||
//! Adapted from SolidCraft's `crates/mcp` (MIT OR Apache-2.0), see ATTRIBUTION.md.
|
||||
@@ -24,9 +24,9 @@ pub mod tools;
|
||||
pub use backend::{Backend, Headless, Picture, PictureRequest, Remote};
|
||||
pub use server::{MAX_BATCH_MESSAGES, MAX_LINE, PROTOCOL_VERSION, SUPPORTED_VERSIONS, Server};
|
||||
|
||||
use modelcraft_io::roots::Roots;
|
||||
use fieldcraft_io::roots::Roots;
|
||||
|
||||
/// How the server is started (`modelcraft-cli mcp`).
|
||||
/// How the server is started (`fieldcraft-cli mcp`).
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Options {
|
||||
/// The control channel of a running app (`127.0.0.1:7070`); `None` runs headless.
|
||||
@@ -46,7 +46,7 @@ pub fn backend(options: Options) -> Result<(Box<dyn Backend>, Option<String>), S
|
||||
)),
|
||||
Some(addr) => match Remote::connect(&addr) {
|
||||
Ok(r) => Ok((Box::new(r), None)),
|
||||
Err(e) => Ok((headless(options.roots), Some(format!("ModelCraft at {addr} cannot be reached ({e}); running headless")))),
|
||||
Err(e) => Ok((headless(options.roots), Some(format!("FieldCraft at {addr} cannot be reached ({e}); running headless")))),
|
||||
},
|
||||
None => Ok((headless(options.roots), None)),
|
||||
}
|
||||
|
||||
@@ -23,9 +23,9 @@ const INVALID_PARAMS: i64 = -32602;
|
||||
const INTERNAL_ERROR: i64 = -32603;
|
||||
const RESOURCE_NOT_FOUND: i64 = -32002;
|
||||
|
||||
const INSTRUCTIONS: &str = "ModelCraft turns geometry into finite element models and solves them. The model is \
|
||||
const INSTRUCTIONS: &str = "FieldCraft turns geometry into finite element models and solves them. The model is \
|
||||
unit-consistent and never converts (SolidCraft geometry is in mm, so use mm-t-s-N-MPa: E in MPa, density in t/mm³, \
|
||||
forces in N); Z is up. A typical analysis: `open` a .solidcraft design (or a .modelcraft model, or a face-tagged \
|
||||
forces in N); Z is up. A typical analysis: `open` a .solidcraft design (or a .fieldcraft model, or a face-tagged \
|
||||
.3mf); `inspect_model` for its bodies and their persistent face names; `mesh` (method tet: {body, tolerance}); \
|
||||
then `execute` the model definition commands (model.set.create {name, geometry: [{body, face}]}, \
|
||||
model.material.create, model.section.create, model.boundary_condition.create, model.load.create, model.step.create \
|
||||
@@ -182,7 +182,7 @@ impl Server {
|
||||
Ok(json!({
|
||||
"protocolVersion": version,
|
||||
"capabilities": {"tools": {"listChanged": false}, "resources": {"listChanged": false}},
|
||||
"serverInfo": {"name": "modelcraft", "title": "ModelCraft", "version": env!("CARGO_PKG_VERSION")},
|
||||
"serverInfo": {"name": "fieldcraft", "title": "FieldCraft", "version": env!("CARGO_PKG_VERSION")},
|
||||
"instructions": format!("{INSTRUCTIONS} Backend: {}.", self.backend.describe()),
|
||||
}))
|
||||
}
|
||||
@@ -198,16 +198,16 @@ impl Server {
|
||||
Ok(call_tool(self.backend.as_mut(), name, &args))
|
||||
}
|
||||
"resources/list" => Ok(json!({"resources": [
|
||||
{"uri": "modelcraft://model", "name": "Model", "description": "model.info: node and element counts, units, definitions", "mimeType": "application/json"},
|
||||
{"uri": "modelcraft://commands", "name": "Command catalog", "description": "every command with its parameter docs", "mimeType": "application/json"},
|
||||
{"uri": "fieldcraft://model", "name": "Model", "description": "model.info: node and element counts, units, definitions", "mimeType": "application/json"},
|
||||
{"uri": "fieldcraft://commands", "name": "Command catalog", "description": "every command with its parameter docs", "mimeType": "application/json"},
|
||||
]})),
|
||||
"resources/templates/list" => Ok(json!({"resourceTemplates": []})),
|
||||
"prompts/list" => Ok(json!({"prompts": []})),
|
||||
"resources/read" => {
|
||||
let uri = params.get("uri").and_then(Value::as_str).unwrap_or("");
|
||||
let v = match uri {
|
||||
"modelcraft://model" => self.backend.execute("model.info", json!({})),
|
||||
"modelcraft://commands" => self.backend.commands(),
|
||||
"fieldcraft://model" => self.backend.execute("model.info", json!({})),
|
||||
"fieldcraft://commands" => self.backend.commands(),
|
||||
_ => return Err((RESOURCE_NOT_FOUND, format!("unknown resource `{}`", uri.chars().take(200).collect::<String>()))),
|
||||
}
|
||||
.map_err(|e| (INTERNAL_ERROR, e))?;
|
||||
|
||||
+12
-12
@@ -3,7 +3,7 @@
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use modelcraft_io::roots::Roots;
|
||||
use fieldcraft_io::roots::Roots;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::{Headless, MAX_LINE, Options, Server};
|
||||
@@ -38,7 +38,7 @@ fn ok(s: &mut Server, name: &str, args: Value) -> Value {
|
||||
struct Dir(PathBuf);
|
||||
impl Dir {
|
||||
fn new(tag: &str) -> Dir {
|
||||
let d = std::env::temp_dir().join(format!("modelcraft-mcp-{tag}-{}", std::process::id()));
|
||||
let d = std::env::temp_dir().join(format!("fieldcraft-mcp-{tag}-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&d);
|
||||
std::fs::create_dir_all(&d).unwrap();
|
||||
Dir(std::fs::canonicalize(&d).unwrap())
|
||||
@@ -55,7 +55,7 @@ fn initialize_lists_tools_and_resources() {
|
||||
let mut s = server();
|
||||
let r = send(&mut s, json!({"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"protocolVersion": "2025-03-26", "capabilities": {}}}));
|
||||
assert_eq!(r["result"]["protocolVersion"], "2025-03-26");
|
||||
assert_eq!(r["result"]["serverInfo"]["name"], "modelcraft");
|
||||
assert_eq!(r["result"]["serverInfo"]["name"], "fieldcraft");
|
||||
assert!(r["result"]["instructions"].as_str().unwrap().contains("headless"));
|
||||
let r = send(&mut s, json!({"jsonrpc": "2.0", "id": 2, "method": "initialize", "params": {"protocolVersion": "1999-01-01"}}));
|
||||
assert_eq!(r["result"]["protocolVersion"], crate::PROTOCOL_VERSION);
|
||||
@@ -83,9 +83,9 @@ fn initialize_lists_tools_and_resources() {
|
||||
] {
|
||||
assert!(names.contains(&t), "{t}");
|
||||
}
|
||||
let r = send(&mut s, json!({"jsonrpc": "2.0", "id": 4, "method": "resources/read", "params": {"uri": "modelcraft://model"}}));
|
||||
let r = send(&mut s, json!({"jsonrpc": "2.0", "id": 4, "method": "resources/read", "params": {"uri": "fieldcraft://model"}}));
|
||||
assert!(r["result"]["contents"][0]["text"].as_str().unwrap().contains("nodes"));
|
||||
let r = send(&mut s, json!({"jsonrpc": "2.0", "id": 5, "method": "resources/read", "params": {"uri": "modelcraft://commands"}}));
|
||||
let r = send(&mut s, json!({"jsonrpc": "2.0", "id": 5, "method": "resources/read", "params": {"uri": "fieldcraft://commands"}}));
|
||||
assert!(r["result"]["contents"][0]["text"].as_str().unwrap().contains("solve.static"));
|
||||
assert_eq!(send(&mut s, json!({"jsonrpc": "2.0", "id": 6, "method": "ping"}))["result"], json!({}));
|
||||
}
|
||||
@@ -159,7 +159,7 @@ fn hostile_tool_arguments_are_tool_errors() {
|
||||
("execute", json!({"command": "mesh.element.add", "params": {"label": 1, "kind": "hex20", "nodes": vec![u32::MAX; 20]}})),
|
||||
("execute", json!({"command": "file.open", "params": {"path": "\u{0}"}})),
|
||||
("open", json!({"path": "/etc/passwd"})),
|
||||
("open", json!({"path": "/no/such/model.modelcraft"})),
|
||||
("open", json!({"path": "/no/such/model.fieldcraft"})),
|
||||
("open", json!({"path": huge.clone() + ".solidcraft"})),
|
||||
("save", json!({})),
|
||||
("write_deck", json!({"format": "abaqus", "path": "/no/such/dir/x.inp"})),
|
||||
@@ -221,7 +221,7 @@ fn file_access_stays_inside_the_roots() {
|
||||
for f in [&input, &output, &outside] {
|
||||
std::fs::create_dir_all(f).unwrap();
|
||||
}
|
||||
let design = modelcraft_testkit::corpus::Analytic::Cylinder { radius: 5.0, height: 10.0 }.design().unwrap();
|
||||
let design = fieldcraft_testkit::corpus::Analytic::Cylinder { radius: 5.0, height: 10.0 }.design().unwrap();
|
||||
std::fs::write(outside.join("c.solidcraft"), &design).unwrap();
|
||||
std::fs::write(input.join("c.solidcraft"), &design).unwrap();
|
||||
#[cfg(unix)]
|
||||
@@ -236,16 +236,16 @@ fn file_access_stays_inside_the_roots() {
|
||||
refused(&mut s, "open", json!({"path": input.join("link.solidcraft")}));
|
||||
refused(&mut s, "execute", json!({"command": "geometry.open", "params": {"path": input.join("../outside/c.solidcraft")}}));
|
||||
ok(&mut s, "open", json!({"path": input.join("c.solidcraft")}));
|
||||
refused(&mut s, "save", json!({"path": outside.join("m.modelcraft")}));
|
||||
refused(&mut s, "save", json!({"path": input.join("m.modelcraft")}));
|
||||
refused(&mut s, "execute", json!({"command": "file.save_as", "params": {"path": outside.join("m.modelcraft")}}));
|
||||
refused(&mut s, "save", json!({"path": outside.join("m.fieldcraft")}));
|
||||
refused(&mut s, "save", json!({"path": input.join("m.fieldcraft")}));
|
||||
refused(&mut s, "execute", json!({"command": "file.save_as", "params": {"path": outside.join("m.fieldcraft")}}));
|
||||
refused(&mut s, "write_deck", json!({"format": "abaqus", "path": outside.join("m.inp")}));
|
||||
refused(&mut s, "screenshot", json!({"path": outside.join("x.png")}));
|
||||
refused(&mut s, "screenshot", json!({"path": input.join("x.png")}));
|
||||
refused(&mut s, "execute", json!({"command": "file.recovery.list", "params": {"dir": outside}}));
|
||||
ok(&mut s, "save", json!({"path": output.join("m.modelcraft")}));
|
||||
ok(&mut s, "save", json!({"path": output.join("m.fieldcraft")}));
|
||||
ok(&mut s, "screenshot", json!({"path": output.join("x.png")}));
|
||||
assert!(output.join("m.modelcraft").is_file() && output.join("x.png").is_file());
|
||||
assert!(output.join("m.fieldcraft").is_file() && output.join("x.png").is_file());
|
||||
assert_eq!(std::fs::read_dir(&outside).unwrap().count(), 1, "nothing written outside");
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use modelcraft_io::roots::Access;
|
||||
use fieldcraft_io::roots::Access;
|
||||
use serde_json::{Map, Value, json};
|
||||
|
||||
use crate::backend::{Backend, PictureRequest};
|
||||
@@ -29,7 +29,7 @@ const ANALYSES: &[(&str, &str)] =
|
||||
const VIEWS: &[&str] = &["iso", "home", "fit", "front", "back", "top", "bottom", "left", "right"];
|
||||
|
||||
fn available<T: Copy>(table: &'static [T], command: fn(T) -> &'static str) -> impl Iterator<Item = T> {
|
||||
table.iter().copied().filter(move |t| modelcraft_engine::find_command(command(*t)).is_some())
|
||||
table.iter().copied().filter(move |t| fieldcraft_engine::find_command(command(*t)).is_some())
|
||||
}
|
||||
|
||||
fn obj(props: Value, required: &[&str]) -> Value {
|
||||
@@ -137,17 +137,17 @@ pub fn tool_definitions() -> Vec<Value> {
|
||||
tool(
|
||||
"open",
|
||||
"Open a file",
|
||||
"Open by extension: a .modelcraft model (file.open: replaces the model; its design and meshes open again), a \
|
||||
"Open by extension: a .fieldcraft model (file.open: replaces the model; its design and meshes open again), a \
|
||||
SolidCraft design .solidcraft (geometry.open: its bodies become the geometry) or a face-tagged .3mf mesh \
|
||||
(geometry.import.mesh: added as a surface).",
|
||||
obj(json!({"path": string("Path to a .modelcraft, .solidcraft or .3mf file")}), &["path"]),
|
||||
obj(json!({"path": string("Path to a .fieldcraft, .solidcraft or .3mf file")}), &["path"]),
|
||||
false,
|
||||
true,
|
||||
),
|
||||
tool(
|
||||
"save",
|
||||
"Save the model",
|
||||
"Save the model as a .modelcraft file (file.save): to `path`, or to the file it came from.",
|
||||
"Save the model as a .fieldcraft file (file.save): to `path`, or to the file it came from.",
|
||||
obj(json!({"path": string("Path to save to (default: the model's file)")}), &[]),
|
||||
false,
|
||||
false,
|
||||
@@ -406,10 +406,10 @@ fn dispatch(b: &mut dyn Backend, name: &str, a: &Map<String, Value>) -> Result<V
|
||||
let path = str_arg(a, "path").ok_or("`path` is required")?;
|
||||
let ext = Path::new(path).extension().and_then(|e| e.to_str()).map(str::to_ascii_lowercase);
|
||||
let command = match ext.as_deref() {
|
||||
Some("modelcraft") => "file.open",
|
||||
Some("fieldcraft") => "file.open",
|
||||
Some("solidcraft") => "geometry.open",
|
||||
Some("3mf") => "geometry.import.mesh",
|
||||
_ => return Err("open: a .modelcraft model, a .solidcraft design or a .3mf mesh".into()),
|
||||
_ => return Err("open: a .fieldcraft model, a .solidcraft design or a .3mf mesh".into()),
|
||||
};
|
||||
let r = b.execute(command, json!({"path": path}))?;
|
||||
Ok(json!({"command": command, "result": r}))
|
||||
@@ -534,7 +534,7 @@ fn capture(b: &mut dyn Backend, a: &Map<String, Value>) -> Result<(Vec<u8>, Valu
|
||||
let picture = b.picture(&request)?;
|
||||
let mut info = picture.info;
|
||||
if let Some(path) = &keep {
|
||||
modelcraft_io::fs::write_atomic(path, &picture.png, false).map_err(|e| e.to_string())?;
|
||||
fieldcraft_io::fs::write_atomic(path, &picture.png, false).map_err(|e| e.to_string())?;
|
||||
info["path"] = json!(path.to_string_lossy());
|
||||
}
|
||||
info["bytes"] = json!(picture.png.len());
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use modelcraft_io::roots::Roots;
|
||||
use modelcraft_mcp::{Headless, Server};
|
||||
use fieldcraft_io::roots::Roots;
|
||||
use fieldcraft_mcp::{Headless, Server};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
struct Client {
|
||||
@@ -57,18 +57,18 @@ fn base64_decode(s: &str) -> Vec<u8> {
|
||||
|
||||
#[test]
|
||||
fn a_corpus_part_is_meshed_loaded_solved_and_rendered_over_mcp() {
|
||||
let dir: PathBuf = std::env::temp_dir().join(format!("modelcraft-mcp-workflow-{}", std::process::id()));
|
||||
let dir: PathBuf = std::env::temp_dir().join(format!("fieldcraft-mcp-workflow-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let dir = std::fs::canonicalize(&dir).unwrap();
|
||||
let part = modelcraft_testkit::corpus::read_recipe(&modelcraft_testkit::corpus::corpus_dir().join("stepped-block")).unwrap();
|
||||
let part = fieldcraft_testkit::corpus::read_recipe(&fieldcraft_testkit::corpus::corpus_dir().join("stepped-block")).unwrap();
|
||||
let design = dir.join("steps.solidcraft");
|
||||
std::fs::write(&design, modelcraft_testkit::corpus::design_bytes(&part).unwrap()).unwrap();
|
||||
std::fs::write(&design, fieldcraft_testkit::corpus::design_bytes(&part).unwrap()).unwrap();
|
||||
|
||||
let roots = Roots::new(Some(&dir), Some(&dir)).unwrap();
|
||||
let mut c = Client { server: Server::new(Box::new(Headless::new(roots))), next: 0 };
|
||||
let init = c.request("initialize", json!({"protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": {"name": "test", "version": "1"}}));
|
||||
assert_eq!(init["serverInfo"]["name"], "modelcraft");
|
||||
assert_eq!(init["serverInfo"]["name"], "fieldcraft");
|
||||
assert!(c.server.handle_line(r#"{"jsonrpc":"2.0","method":"notifications/initialized"}"#).is_none());
|
||||
|
||||
// Open the design and look at its faces.
|
||||
@@ -134,8 +134,8 @@ fn a_corpus_part_is_meshed_loaded_solved_and_rendered_over_mcp() {
|
||||
assert_eq!(info["results"]["step"], "Load", "{info}");
|
||||
|
||||
// Save the model and write a deck, both inside the root.
|
||||
c.json("save", json!({"path": dir.join("steps.modelcraft")}));
|
||||
c.json("save", json!({"path": dir.join("steps.fieldcraft")}));
|
||||
c.json("write_deck", json!({"format": "abaqus", "path": dir.join("steps.inp")}));
|
||||
assert!(dir.join("steps.modelcraft").is_file() && dir.join("steps.inp").is_file());
|
||||
assert!(dir.join("steps.fieldcraft").is_file() && dir.join("steps.inp").is_file());
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "modelcraft-mesh"
|
||||
description = "ModelCraft's meshing: robust predicates, surface preparation, tetrahedral and hexahedral meshing, quality metrics"
|
||||
name = "fieldcraft-mesh"
|
||||
description = "FieldCraft's meshing: robust predicates, surface preparation, tetrahedral and hexahedral meshing, quality metrics"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
@@ -9,11 +9,11 @@ license.workspace = true
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
modelcraft-model = { workspace = true }
|
||||
fieldcraft-model = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
# Test oracles only (never linked into ModelCraft): exact rationals, and georust's port of
|
||||
# Test oracles only (never linked into FieldCraft): exact rationals, and georust's port of
|
||||
# Shewchuk's public-domain predicates.c as a differential reference.
|
||||
num-bigint = "0.4"
|
||||
num-rational = { version = "0.4", default-features = false, features = ["num-bigint", "std"] }
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
//! Every element is checked with the certified Jacobian test ([`crate::quality`]); a block
|
||||
//! whose corners are not in hex8 order or whose curves fold it is an error.
|
||||
|
||||
use modelcraft_model::{ElementKind, MAX_COORD};
|
||||
use fieldcraft_model::{ElementKind, MAX_COORD};
|
||||
|
||||
use crate::quad::{lattice_index, transfinite_faces, transfinite_grid_blended, transfinite_volume};
|
||||
use crate::quality::{Validity, element_validity, planar_quad_validity};
|
||||
|
||||
@@ -54,7 +54,7 @@ mod tests;
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use modelcraft_model::ElementKind;
|
||||
use fieldcraft_model::ElementKind;
|
||||
|
||||
use crate::delaunay::Triangulation;
|
||||
use crate::predicates::{GenericPoint, Point3, Sign};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use modelcraft_model::ElementKind;
|
||||
use fieldcraft_model::ElementKind;
|
||||
|
||||
use super::*;
|
||||
use crate::quality::{GateOptions, enclosed_volume, validity_gate_elements};
|
||||
|
||||
@@ -54,7 +54,7 @@ use crate::size::SizeField;
|
||||
use crate::surface::BoundarySurface;
|
||||
use crate::vec3::{P, cross, dot, sub};
|
||||
use crate::{Error, Result};
|
||||
use modelcraft_model::ElementKind;
|
||||
use fieldcraft_model::ElementKind;
|
||||
|
||||
/// Which structured method to use.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
@@ -323,7 +323,7 @@ fn finish(ctx: &Ctx, raw: Raw, method: HexMethod) -> Result<HexMesh> {
|
||||
if hexes.len() > ctx.opts.max_elements {
|
||||
return Err(Error::Limit(format!("the mesh has {} hexahedra; the limit is {}", hexes.len(), ctx.opts.max_elements)));
|
||||
}
|
||||
modelcraft_model::check_coordinates(nodes.as_flattened())?;
|
||||
fieldcraft_model::check_coordinates(nodes.as_flattened())?;
|
||||
let at = |nodes: &[P], i: u32| nodes.get(i as usize).copied().ok_or_else(|| Error::Failed(format!("node {i} out of range")));
|
||||
// Boundary faces: hex faces used once.
|
||||
let mut uses: HashMap<[u32; 4], u32> = HashMap::with_capacity(hexes.len() * 3);
|
||||
@@ -548,6 +548,6 @@ fn mid_edge_nodes(ctx: &Ctx, nodes: &mut Vec<P>, hexes: &[[u32; 8]], boundary: &
|
||||
}
|
||||
out.push(m);
|
||||
}
|
||||
modelcraft_model::check_coordinates(nodes.as_flattened())?;
|
||||
fieldcraft_model::check_coordinates(nodes.as_flattened())?;
|
||||
Ok(out)
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet, HashMap};
|
||||
|
||||
use modelcraft_model::{ElementKind, Model};
|
||||
use fieldcraft_model::{ElementKind, Model};
|
||||
|
||||
use crate::vec3::{P, dot, sub};
|
||||
use crate::{Error, Result};
|
||||
|
||||
@@ -35,7 +35,7 @@ pub enum Error {
|
||||
#[error("invalid boundary surface: {0}")]
|
||||
Surface(surface::Defects),
|
||||
#[error(transparent)]
|
||||
Model(#[from] modelcraft_model::Error),
|
||||
Model(#[from] fieldcraft_model::Error),
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
Loaded 100 of 268 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user