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 <noreply@anthropic.com> 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
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user