engine: parameter schemas for every command

cmd/forms.rs describes each command's parameters: model definitions (sets,
materials with elasticity, plasticity, hyperelasticity, damage, thermal and fluid
options, sections, reference points, couplings, masses, springs, boundary
conditions, loads, fluid domains, steps including nonlinear and explicit),
meshers, solvers, deck export, files and results. Tests check that every command
has a form and that a form's defaults are accepted or refused only for the
required fields left empty.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
jyvikoandClaude Opus 5.5 authored and Irakli Kourtis committed 2026-10-14 13:20:04 -04:00
1 parent 5c5516aae0
commit 8848ef9e7c
16 files changed
+1071 -73

No files matched your search

+1 -1
View File
@@ -18,7 +18,7 @@ pub const BLOCK: CommandSpec = CommandSpec {
id: "mesh.block",
label: "Mesh Block",
params: r#"{"body": "Plate", "kind": "hex8|hex20|tet4|tet10|wedge6", "corners": [[x, y, z] × 8] (hex8 order: 1-2-3-4 counter-clockwise seen from 5-6-7-8), "divisions": [n12, n14, n15] (elements along 1→2, 1→4, 1→5), "bias": [1, 1, 1] (optional: last over first element size along each direction), "edges": [{"corners": [1, 2], "arc": {"center": [x, y, z]}} | {"corners": [2, 3], "ellipse": {"center": [x, y, z], "axes": [[a], [b]] (perpendicular)}} | {"corners": [5, 6], "name": "tip_edge"}] (optional: curved edges, the shorter arc between the corners, and edge names), "faces": {"1": "bottom", "4": "tip"} (optional: names of block faces, numbered as hex8 faces: 1 = 1-2-3-4, 2 = 5-6-7-8, 3 = 1-2-6-5, 4 = 2-3-7-6, 5 = 3-4-8-7, 6 = 4-1-5-8), "vertices": {"1": "A"} (optional: names of corners), "merge": distance (optional, default 1e-6 of the block's diagonal; 0 never joins)} — adds a mapped mesh of the block (transfinite interpolation of its edges; quadratic nodes on the curves), uses existing nodes within `merge` of new ones so blocks that share a face join (the shared face must have the same elements on both sides, or nothing changes), and tags the elements with the body and element faces, edges and nodes with the named faces, edges and vertices. 2D: "kind": "quad4|quad8|tri3|tri6", four "corners" at z = 0 (counter-clockwise from +z), "divisions": [n12, n14], "bias": [r, r], "face": "area" (the face of the body the elements fill, default `area`), edges and vertices numbered among the four corners; for plane stress, plane strain and axisymmetric sections"#,
form: None,
form: Some(super::forms::BLOCK),
kind: CommandKind::Model,
run: block_mesh,
};
+5 -5
View File
@@ -43,7 +43,7 @@ pub const PAIR: CommandSpec = CommandSpec {
r#""friction": {"static": 0.3, "kinetic": 0.2, "decay": 10} (Coulomb, μ = μk + (μs − μk) e^(−decay × slip rate); default frictionless), "penalty_scale": 1 (0 < scale ≤ 1: a factor on the stiffest penalty that leaves the time increment alone), "initial_penetration": "adjust" (move nodes that start inside onto the surface, strain-free, and report them) | "refuse" (refuse the analysis, naming the first)"#,
r#"} — contact between two surfaces, each one's nodes kept out of the other's faces (explicit dynamics; fails if the name is taken)"#
),
form: None,
form: Some(super::forms::CONTACT_PAIR),
kind: CommandKind::Model,
run: |s: &mut Session, p: &Value| {
let n = name(p)?;
@@ -61,7 +61,7 @@ pub const GENERAL: CommandSpec = CommandSpec {
r#""friction": {"static": 0.3, "kinetic": 0.2, "decay": 10} (Coulomb, μ = μk + (μs − μk) e^(−decay × slip rate); default frictionless), "penalty_scale": 1 (0 < scale ≤ 1: a factor on the stiffest penalty that leaves the time increment alone), "initial_penetration": "adjust" (move nodes that start inside onto the surface, strain-free, and report them) | "refuse" (refuse the analysis, naming the first)"#,
r#"} — single-surface contact of everything that may touch (explicit dynamics; fails if the name is taken)"#
),
form: None,
form: Some(super::forms::CONTACT_GENERAL),
kind: CommandKind::Model,
run: |s: &mut Session, p: &Value| {
let n = name(p)?;
@@ -78,7 +78,7 @@ pub const EDIT: CommandSpec = CommandSpec {
id: "interaction.contact.edit",
label: "Edit Contact",
params: r#"{"name": "impact", "surfaces": {"type": "pair", "main": "plate", "secondary": "ball"} | {"type": "general", "surface": "skin" (optional)}, "friction": {…}, "penalty_scale": 1, "initial_penetration": "adjust|refuse"} — replaces the contact of that name (the model's form; see interaction.contact.pair)"#,
form: None,
form: Some(super::forms::CONTACT_EDIT),
kind: CommandKind::Model,
run: |s: &mut Session, p: &Value| {
let n = name(p)?;
@@ -94,7 +94,7 @@ pub const DELETE: CommandSpec = CommandSpec {
id: "interaction.contact.delete",
label: "Delete Contact",
params: r#"{"name": "impact"}"#,
form: None,
form: Some(super::forms::CONTACT_DELETE),
kind: CommandKind::Model,
run: |s: &mut Session, p: &Value| {
s.model.remove_contact(name(p)?)?;
@@ -109,7 +109,7 @@ pub const FIND: CommandSpec = CommandSpec {
r#"{"tolerance": 0.1 (distance within which surfaces touch; default a tenth of the mean exterior face size), "create": false (true: add two face sets and a contact pair per touching pair of parts, named find-N-main, find-N-secondary and find-N), "#,
r#""friction", "penalty_scale", "initial_penetration" (of the created pairs, as interaction.contact.pair)} — the parts of the mesh (solid elements connected through their nodes) whose exterior faces come within the tolerance of each other"#
),
form: None,
form: Some(super::forms::CONTACT_FIND),
kind: CommandKind::Model,
run: find,
};
+1 -1
View File
@@ -12,7 +12,7 @@ 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: 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"#,
form: None,
form: Some(super::forms::DECK),
kind: CommandKind::Session,
run: write,
};
+43 -17
View File
@@ -25,37 +25,37 @@ fn fields<T: DeserializeOwned>(p: &Value, what: &str) -> Result<T> {
/// Create, edit and delete commands for one kind of definition.
macro_rules! definition_commands {
($what:literal, $ty:ty, $id:literal, $label:literal, $doc:literal, $add:ident, $replace:ident, $remove:ident,
($what:literal, $ty:ty, $id:literal, $label:literal, $doc:literal, $form:expr, $delete_form:expr, $add:ident, $replace:ident, $remove:ident,
$CREATE:ident, $EDIT:ident, $DELETE:ident) => {
pub const $CREATE: CommandSpec = CommandSpec {
id: concat!("model.", $id, ".create"),
label: concat!("New ", $label),
params: concat!($doc, " — fails if the name is taken"),
form: None,
form: Some($form),
kind: CommandKind::Model,
run: |s: &mut Session, p: &Value| {
let n = name(p)?;
s.model.$add(n, fields::<$ty>(p, $what)?)?;
Ok(json!({"name": n}))
s.model.$add(n, fields::<$ty>(p, $what)?)?;
Ok(json!({"name": n}))
},
};
pub const $EDIT: CommandSpec = CommandSpec {
id: concat!("model.", $id, ".edit"),
label: concat!("Edit ", $label),
params: concat!($doc, " — replaces the existing definition of that name"),
form: None,
form: Some($form),
kind: CommandKind::Model,
run: |s: &mut Session, p: &Value| {
let n = name(p)?;
let n = name(p)?;
s.model.$replace(n, fields::<$ty>(p, $what)?)?;
Ok(json!({"name": n}))
Ok(json!({"name": n}))
},
};
pub const $DELETE: CommandSpec = CommandSpec {
id: concat!("model.", $id, ".delete"),
label: concat!("Delete ", $label),
params: r#"{"name": "…"} — refused while another definition refers to it"#,
form: None,
form: Some($delete_form),
kind: CommandKind::Model,
run: |s: &mut Session, p: &Value| {
s.model.$remove(name(p)?)?;
@@ -71,6 +71,8 @@ definition_commands!(
"set",
"Set",
r#"{"name": "root", "geometry": [{"body": "Body1", "face": "F2:top"}, {"body": "Body1", "edge": "…"}, {"body": "Body1", "vertex": "…"}, {"body": "Body1"}], "nodes": [labels], "elements": [labels], "faces": [{"element": 1, "face": 3}], "edges": [{"element": 1, "edge": 2}]} — any of the member lists (element edges: where a 2D model's surface loads act); geometry members resolve against the mesh's geometry tags after every remesh (a face name also selects its split pieces `name#0`, `name#1`, …)"#,
super::forms::SET,
super::forms::SET_DELETE,
add_set,
replace_set,
remove_set,
@@ -85,6 +87,8 @@ definition_commands!(
"material",
"Material",
r#"{"name": "Steel", "elastic": {"type": "isotropic", "young": 210000, "poisson": 0.3} | {"type": "engineering_constants", "young": [E1, E2, E3], "poisson": [ν12, ν13, ν23], "shear": [G12, G13, G23]}, "density": 7.85e-9, "fluid": {"type": "newtonian", "viscosity": 1.0e-9} (dynamic viscosity; a fluid needs a density), "plastic": {…} (see model.material.plasticity), "damage": {…}, "erosion": {…} (see model.material.damage), "hyperelastic": {"type": "neo_hookean"} | {"type": "mooney_rivlin", "c01": 0.1} (C10 = μ/2 − C01) | {"type": "yeoh", "c20": -0.01, "c30": 0.001} (C10 = μ/2) (large strain in nonlinear steps, from the isotropic constants: μ = E/(2(1 + ν)), κ = E/(3(1 − 2ν)); without it an elastic material is St Venant–Kirchhoff there)} — in the model's unit system (model.units), never converted"#,
super::forms::MATERIAL,
super::forms::MATERIAL_DELETE,
add_material,
replace_material,
remove_material,
@@ -106,7 +110,7 @@ pub const MATERIAL_PLASTICITY: CommandSpec = CommandSpec {
id: "model.material.plasticity",
label: "Plasticity",
params: r#"{"name": "Steel", "plastic": {"type": "bilinear", "yield_stress": 250, "hardening_modulus": 1000, "hardening": "isotropic|kinematic"} | {"type": "table", "points": [{"strain": 0, "stress": 250}, {"strain": 0.1, "stress": 300}]} | {"type": "johnson_cook", "a": 792, "b": 510, "n": 0.26, "c": 0.014, "m": 1.03, "reference_strain_rate": 1, "room_temperature": 294, "melt_temperature": 1793, "specific_heat": 4.77e8, "taylor_quinney": 0.9} | null} — von Mises plasticity of an isotropic material (null removes it); explicit dynamics uses it, the linear solver the elastic constants alone"#,
form: None,
form: Some(super::forms::MATERIAL_PLASTICITY),
kind: CommandKind::Model,
run: |s: &mut Session, p: &Value| {
change_material(s, p, |m, p| {
@@ -120,7 +124,7 @@ pub const MATERIAL_DAMAGE: CommandSpec = CommandSpec {
id: "model.material.damage",
label: "Damage",
params: r#"{"name": "Steel", "damage": {"type": "johnson_cook", "d1": 0.05, "d2": 3.44, "d3": -2.12, "d4": 0.002, "d5": 0.61} | null, "erosion": {"plastic_strain": 1.0} | null} — damage to failure and element erosion of a plastic material (each key optional; null removes it); a failed element is eroded in explicit dynamics"#,
form: None,
form: Some(super::forms::MATERIAL_DAMAGE),
kind: CommandKind::Model,
run: |s: &mut Session, p: &Value| {
change_material(s, p, |m, p| {
@@ -146,6 +150,8 @@ definition_commands!(
"section",
"Section",
r#"{"name": "web", "set": "all", "material": "Steel", "kind": {"type": "solid"} | {"type": "shell", "thickness": 2, "offset": 0 (-0.5..=0.5 of the thickness), "points": 5} | {"type": "beam", "profile": {"type": "rectangle", "width", "height"} | {"type": "circle", "radius"} | {"type": "pipe", "radius", "thickness"} | {"type": "i", "height", "width", "web", "flange"} | {"type": "box", "width", "height", "thickness"}, "orientation": [x, y, z]} | {"type": "rod", "area": 10} | {"type": "plane_stress", "thickness": 1} | {"type": "plane_strain", "thickness": 1 (default)} | {"type": "axisymmetric"} (2D: elements in the XY plane; axisymmetric about Y with x the radius, forces totals over the circumference), "formulation": {"integration": "full|reduced", "incompatible_modes": false, "hybrid": false}}"#,
super::forms::SECTION,
super::forms::SECTION_DELETE,
add_section,
replace_section,
remove_section,
@@ -160,6 +166,8 @@ definition_commands!(
"reference_point",
"Reference Point",
r#"{"name": "RP", "xyz": [x, y, z], "label": 0} — label: its node label in decks (default: the highest free label, kept through remeshes)"#,
super::forms::REFERENCE_POINT,
super::forms::REFERENCE_POINT_DELETE,
add_reference_point,
replace_reference_point,
remove_reference_point,
@@ -174,6 +182,8 @@ definition_commands!(
"coupling",
"Coupling",
r#"{"name": "pad", "reference_point": "RP", "set": "hole", "kind": "kinematic|distributing", "dofs": [1, 2, 3, 4, 5, 6] (default all)}"#,
super::forms::COUPLING,
super::forms::COUPLING_DELETE,
add_coupling,
replace_coupling,
remove_coupling,
@@ -188,6 +198,8 @@ definition_commands!(
"mass",
"Point Mass",
r#"{"name": "motor", "target": {"set": "…"} | {"reference_point": "…"}, "mass": 2.5 | [mx, my, mz] (per global direction), "inertia": [I11, I22, I33, I12, I13, I23] (tensor about the node in global axes; needs rotations: a reference point)} — at every node of the target, each with the full value"#,
super::forms::MASS,
super::forms::MASS_DELETE,
add_mass,
replace_mass,
remove_mass,
@@ -202,6 +214,8 @@ definition_commands!(
"spring",
"Spring or Dashpot",
r#"{"name": "mount", "type": "spring", "stiffness": 1000 | "type": "dashpot", "coefficient": 5, "first": {"set": "…"} | {"reference_point": "…"}, "second": (optional; without it each node of `first` connects to the ground; with it each end is one node), "direction": {"dof": 1..6} (4–6: torsional) | {"vector": [x, y, z]} | "line" (along the two ends, two-point only)} — dashpots act in dynamic steps; static and frequency steps ignore them"#,
super::forms::SPRING,
super::forms::SPRING_DELETE,
add_spring,
replace_spring,
remove_spring,
@@ -216,6 +230,8 @@ definition_commands!(
"boundary_condition",
"Boundary Condition",
r#"{"name": "clamp", "type": "fixed", "target": {"set": "root"} | {"reference_point": "RP"}} | {"name", "type": "displacement", "target", "values": [u1, u2, u3, ur1, ur2, ur3] (null leaves a degree of freedom free)} | {"name", "type": "symmetry", "target", "plane": "x|y|z" (the plane's normal)} — active in the steps that list it (model.step.activate)"#,
super::forms::BOUNDARY_CONDITION,
super::forms::BOUNDARY_CONDITION_DELETE,
add_boundary_condition,
replace_boundary_condition,
remove_boundary_condition,
@@ -230,6 +246,8 @@ definition_commands!(
"load",
"Load",
r#"{"name": "push", "type": "force|moment", "target": {"set": "…"} | {"reference_point": "…"}, "vector": [x, y, z], "distribution": "total" (default: the vector is the total, split by area over the set's faces, or equally over its nodes) | "per_node" (at every node: depends on the mesh)} | {"name", "type": "traction", "set", "vector" (per area, on the set's faces)} | {"name", "type": "pressure", "set", "magnitude" (positive pushes in)} | {"name", "type": "gravity", "acceleration": [x, y, z], "set" (optional, default the whole model)} | {"name", "type": "body_force", "set", "vector" (per volume)} | {"name", "type": "centrifugal", "angular_velocity" (rad per time unit), "point": [x, y, z] (on the axis), "axis": [x, y, z], "set" (optional)} — 2D models: in-plane vectors, surface loads on element edges; active in the steps that list it (model.step.activate)"#,
super::forms::LOAD,
super::forms::LOAD_DELETE,
add_load,
replace_load,
remove_load,
@@ -244,6 +262,8 @@ definition_commands!(
"fluid_domain",
"Fluid Domain",
r#"{"name": "Flow", "material": "Water" (a density and fluid behaviour), "min": [x, y, z], "max": [x, y, z], "spacing": 1.0 (cubic cells; every extent a whole number of spacings), "faces": {"x_min": {"type": "velocity", "velocity": [u, v, w], "profile": "uniform|parabolic"}, "x_max": {"type": "pressure", "pressure": 0}, "y_min": {"type": "wall"} | {"type": "moving_wall", "velocity": [u, v, w] (in the face's plane)} | {"type": "slip"} | {"type": "periodic"} (in opposite pairs), …} (default walls), "obstacles": ["Body1"] (solid bodies of the design or imported surfaces, by name), "surfaces": [{"face": {"body": "Body1", "face": "F2:top"}, "condition": {"type": "moving_wall", "velocity": [u, v, w]}}] (obstacle faces are walls unless listed), "initial_velocity": [u, v, w], "acceleration": [x, y, z] (gravity or a driving force per mass), "tolerance": 0.1 (obstacle triangulation; default a tenth of the spacing)}"#,
super::forms::FLUID_DOMAIN,
super::forms::FLUID_DOMAIN_DELETE,
add_fluid_domain,
replace_fluid_domain,
remove_fluid_domain,
@@ -256,7 +276,7 @@ pub const FLUID_BOUNDARY: CommandSpec = CommandSpec {
id: "model.fluid_domain.boundary",
label: "Inlet, Outlet or Wall",
params: r#"{"domain": "Flow", "at": "x_min" (a box face) | {"body": "Body1", "face": "F2:top"} (an obstacle face), "condition": {"type": "velocity", "velocity": [u, v, w], "profile": "uniform|parabolic"} | {"type": "pressure", "pressure": 0} | {"type": "wall"} | {"type": "moving_wall", "velocity": [u, v, w]} | {"type": "slip"} | {"type": "periodic"}} — sets one boundary's condition (obstacle faces take walls and moving walls)"#,
form: None,
form: Some(super::forms::FLUID_BOUNDARY),
kind: CommandKind::Model,
run: fluid_boundary,
};
@@ -273,7 +293,7 @@ pub const STEP_CREATE: CommandSpec = CommandSpec {
id: "model.step.create",
label: "New Step",
params: r#"{"name": "Bend", "procedure": {"type": "static"} | {"type": "nonlinear_static", "nonlinear_geometry": true, "initial_increment": 0.1, "min_increment": 1e-5, "max_increment": 1.0 (fractions of the step), "max_increments": 100, "max_iterations": 16 (per increment), "tolerances": {"force": 1e-6, "displacement": 1e-5, "energy": 1e-10}, "line_search": false, "load_path": [[0, 0], [0.5, 1], [1, 0]] (optional: the load factor against the step fraction, piecewise linear from [0, 0] to fraction 1; increments end at its points)} (loads and prescribed displacements are the load factor times their values; it ramps from 0 to 1 without a load path) | {"type": "frequency", "modes": 10, "min_hz": 0, "max_hz": 500} | {"type": "fluid", "domain": "Flow", "end_time": 1.0 | "steps": 1000, "time_step": (default: chosen for Mach 0.087 and τ ≤ 1), "frames": 10, "magic": 0.1875 (the two-relaxation-time Λ)} | {"type": "explicit_dynamic", "end_time": 0.001, "dt_scale": 0.9, "max_increments": 10000000, "output_interval", "history_interval", "history_nodes": "set", "initial_velocities": [{"set", "velocity": [vx, vy, vz]}], "rigid_walls": [{"set", "point", "normal", "friction": {"static": 0.3, "kinetic": 0.2, "decay": 10} (Coulomb; default frictionless)}], "hourglass": {"type": "viscous|stiffness", "coefficient": 0.1}, "bulk_viscosity": {"linear": 0.06, "quadratic": 1.5}}, "output": ["displacement", "reaction_force", "stress", "strain"] (default by procedure), "boundary_conditions": [names], "loads": [names] (active in this step; nothing carries over between steps), "before": "step" (default: last)}"#,
form: None,
form: Some(super::forms::STEP_CREATE),
kind: CommandKind::Model,
run: step_create,
};
@@ -282,7 +302,7 @@ pub const STEP_EDIT: CommandSpec = CommandSpec {
id: "model.step.edit",
label: "Edit Step",
params: r#"{"name": "Bend", "procedure", "output", "boundary_conditions", "loads"} — as model.step.create; what is left out keeps its value"#,
form: None,
form: Some(super::forms::STEP_EDIT),
kind: CommandKind::Model,
run: step_edit,
};
@@ -291,7 +311,7 @@ pub const STEP_DELETE: CommandSpec = CommandSpec {
id: "model.step.delete",
label: "Delete Step",
params: r#"{"name": "Bend"}"#,
form: None,
form: Some(super::forms::STEP_DELETE),
kind: CommandKind::Model,
run: |s, p| s.model.remove_step(name(p)?).map(|_| Value::Null).map_err(Into::into),
};
@@ -300,13 +320,19 @@ pub const STEP_ACTIVATE: CommandSpec = CommandSpec {
id: "model.step.activate",
label: "Activate in Step",
params: r#"{"step": "Bend", "boundary_conditions": [names], "loads": [names], "active": true} — switches the listed items on (or off with false) in that step only"#,
form: None,
form: Some(super::forms::STEP_ACTIVATE),
kind: CommandKind::Model,
run: step_activate,
};
pub const ACTIVITY: CommandSpec =
CommandSpec { id: "model.activity", label: "Step Table", params: "{}", form: None, kind: CommandKind::Model, run: |s, _| Ok(activity(&s.model)) };
pub const ACTIVITY: CommandSpec = CommandSpec {
id: "model.activity",
label: "Step Table",
params: "{}",
form: Some(&[]),
kind: CommandKind::Model,
run: |s, _| Ok(activity(&s.model)),
};
fn step_create(s: &mut Session, p: &Value) -> Result<Value> {
let n = name(p)?;
+2 -2
View File
@@ -36,7 +36,7 @@ pub const UNDO: CommandSpec = CommandSpec {
id: "edit.undo",
label: "Undo",
params: r#"{"count"?: 1} — takes back the last command that changed the model (it leaves the journal and can be redone); `count` takes back that many, as far as the history reaches"#,
form: None,
form: Some(super::forms::COUNT),
kind: CommandKind::Session,
run: |s, p| repeat(s, p, Session::undo),
};
@@ -45,7 +45,7 @@ pub const REDO: CommandSpec = CommandSpec {
id: "edit.redo",
label: "Redo",
params: r#"{"count"?: 1} — runs the last undone command's change again; `count` redoes that many, as far as there are undone commands"#,
form: None,
form: Some(super::forms::COUNT),
kind: CommandKind::Session,
run: |s, p| repeat(s, p, Session::redo),
};
+7 -7
View File
@@ -23,7 +23,7 @@ pub const OPEN: CommandSpec = CommandSpec {
id: "file.open",
label: "Open Model",
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"#,
form: None,
form: Some(super::forms::FILE_OPEN),
kind: CommandKind::Session,
run: open,
};
@@ -32,7 +32,7 @@ pub const SAVE: CommandSpec = CommandSpec {
id: "file.save",
label: "Save Model",
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`"#,
form: None,
form: Some(super::forms::FILE_SAVE),
kind: CommandKind::Session,
run: save,
};
@@ -41,7 +41,7 @@ pub const SAVE_AS: CommandSpec = CommandSpec {
id: "file.save_as",
label: "Save Model As",
params: r#"{"path": "bracket.fieldcraft"} — saves to a new file, which becomes the model's file"#,
form: None,
form: Some(super::forms::FILE_SAVE_AS),
kind: CommandKind::Session,
run: save_as,
};
@@ -50,7 +50,7 @@ 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: FIELDCRAFT_RECOVERY_DIR or the per-user data folder)"#,
form: None,
form: Some(super::forms::RECOVERY_LIST),
kind: CommandKind::Session,
run: recovery_list,
};
@@ -59,7 +59,7 @@ pub const RECOVER: CommandSpec = CommandSpec {
id: "file.recover",
label: "Recover Model",
params: r#"{"id": "…", "dir"?: "…"} — opens an autosaved model (an id from file.recovery.list) with its original file path, as unsaved changes; the autosave stays until file.recovery.discard"#,
form: None,
form: Some(super::forms::RECOVER),
kind: CommandKind::Session,
run: recover,
};
@@ -68,7 +68,7 @@ pub const RECOVERY_DISCARD: CommandSpec = CommandSpec {
id: "file.recovery.discard",
label: "Discard Recovered Model",
params: r#"{"id": "…"} or {"all": true}; "dir"?: "…" — deletes autosaves of apps that are no longer running"#,
form: None,
form: Some(super::forms::RECOVERY_DISCARD),
kind: CommandKind::Session,
run: recovery_discard,
};
@@ -77,7 +77,7 @@ pub const SAMPLE: CommandSpec = CommandSpec {
id: "file.sample",
label: "Open Sample",
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)"#,
form: None,
form: Some(super::forms::SAMPLE),
kind: CommandKind::Session,
run: open_sample,
};
+970
View File
@@ -0,0 +1,970 @@
//! The commands' parameter schemas ([`crate::schema`]), area by area. Each describes the JSON
//! the command's `run` reads, as its `params` doc string does in words.
use crate::schema::Collection as C;
use crate::schema::Dim as D;
use crate::schema::*;
// ---- Shared pieces ----
/// `{"set": …}` or `{"reference_point": …}`.
const TARGET_OPTIONS: &[Field] = &[reference("set", "Set", C::Set), reference("reference_point", "Reference point", C::ReferencePoint)];
const fn target(key: &'static str, label: &'static str) -> Field {
either(key, label, TARGET_OPTIONS)
}
/// The name of an existing definition, for delete commands.
macro_rules! existing {
($label:literal, $c:expr) => {
&[reference("name", $label, $c).req()]
};
}
pub const SET_DELETE: &[Field] = existing!("Set", C::Set);
pub const MATERIAL_DELETE: &[Field] = existing!("Material", C::Material);
pub const SECTION_DELETE: &[Field] = existing!("Section", C::Section);
pub const REFERENCE_POINT_DELETE: &[Field] = existing!("Reference point", C::ReferencePoint);
pub const COUPLING_DELETE: &[Field] = existing!("Coupling", C::Coupling);
pub const MASS_DELETE: &[Field] = existing!("Point mass", C::Mass);
pub const SPRING_DELETE: &[Field] = existing!("Spring", C::Spring);
pub const BOUNDARY_CONDITION_DELETE: &[Field] = existing!("Boundary condition", C::BoundaryCondition);
pub const LOAD_DELETE: &[Field] = existing!("Load", C::Load);
pub const FLUID_DOMAIN_DELETE: &[Field] = existing!("Fluid domain", C::FluidDomain);
pub const CONTACT_DELETE: &[Field] = existing!("Contact", C::Contact);
pub const STEP_DELETE: &[Field] = existing!("Step", C::Step);
pub const SET_RESOLVE: &[Field] = existing!("Set", C::Set);
// ---- Model definitions ----
pub const SET: &[Field] = &[
NAME,
rows(
"geometry",
"Geometry",
&[
reference("body", "Body", C::Body).req(),
text("face", "Face").hint("persistent face name"),
text("edge", "Edge"),
text("vertex", "Vertex"),
],
)
.hint("whole bodies, or their faces, edges or vertices"),
json("nodes", "Nodes").hint("[labels]"),
json("elements", "Elements").hint("[labels]"),
json("faces", "Element faces").hint("[{\"element\", \"face\"}]"),
json("edges", "Element edges").hint("[{\"element\", \"edge\"}]"),
];
const ELASTIC: Field = variants(
"elastic",
"Elasticity",
&[
variant(
"isotropic",
"Isotropic",
&[num("young", "Young's modulus", D::Stress).req(), range("poisson", "Poisson's ratio", D::None, -1.0, 0.5).req()],
),
variant(
"engineering_constants",
"Orthotropic",
&[
vector("young", "E1, E2, E3", D::Stress, &["E1", "E2", "E3"]).req(),
vector("poisson", "ν12, ν13, ν23", D::None, &["ν12", "ν13", "ν23"]).req(),
vector("shear", "G12, G13, G23", D::Stress, &["G12", "G13", "G23"]).req(),
],
),
],
)
.def(r#"{"type": "isotropic"}"#);
const HARDENING: &[(&str, &str)] = &[("isotropic", "Isotropic"), ("kinematic", "Kinematic")];
const PLASTIC: Field = variants(
"plastic",
"Plasticity",
&[
variant(
"bilinear",
"Bilinear",
&[
positive("yield_stress", "Yield stress", D::Stress).req(),
num("hardening_modulus", "Hardening modulus", D::Stress).def("0"),
choice("hardening", "Hardening", HARDENING).def("\"isotropic\""),
],
),
variant(
"table",
"Table",
&[rows(
"points",
"Hardening curve",
&[positive("strain", "Plastic strain", D::None).req(), positive("stress", "Stress", D::Stress).req()],
)
.req()],
),
variant(
"johnson_cook",
"Johnson–Cook",
&[
positive("a", "A", D::Stress).req(),
num("b", "B", D::Stress).req(),
num("n", "n", D::None).req(),
num("c", "C", D::None).def("0"),
num("m", "m", D::None).def("1"),
positive("reference_strain_rate", "Reference strain rate", D::None).def("1").hint("per time unit"),
num("room_temperature", "Room temperature", D::Temperature).req(),
num("melt_temperature", "Melt temperature", D::Temperature).req(),
positive("specific_heat", "Specific heat", D::SpecificHeat).hint("isothermal when empty"),
range("taylor_quinney", "Taylor–Quinney", D::None, 0.0, 1.0).def("0.9"),
],
),
],
);
const DAMAGE: Field = variants(
"damage",
"Damage",
&[variant(
"johnson_cook",
"Johnson–Cook",
&[
num("d1", "D1", D::None).req(),
num("d2", "D2", D::None).req(),
num("d3", "D3", D::None).req(),
num("d4", "D4", D::None),
num("d5", "D5", D::None),
],
)],
);
const EROSION: Field = group("erosion", "Erosion", &[positive("plastic_strain", "Plastic strain", D::None).req()]);
const HYPERELASTIC: Field = variants(
"hyperelastic",
"Hyperelasticity",
&[
variant("neo_hookean", "Neo-Hookean", &[]),
variant("mooney_rivlin", "Mooney–Rivlin", &[num("c01", "C01", D::Stress).req()]),
variant("yeoh", "Yeoh", &[num("c20", "C20", D::Stress).req(), num("c30", "C30", D::Stress).req()]),
],
)
.hint("large strain in nonlinear steps, from the isotropic constants");
const THERMAL: Field = group(
"thermal",
"Thermal",
&[
json("conductivity", "Conductivity").hint("k or [k1, k2, k3]"),
positive("specific_heat", "Specific heat", D::SpecificHeat),
json("expansion", "Expansion").hint("α or [α1, α2, α3], 1/K"),
num("reference_temperature", "Reference temperature", D::Temperature).def("0"),
],
);
pub const MATERIAL: &[Field] = &[
NAME,
ELASTIC,
positive("density", "Density", D::Density),
PLASTIC,
HYPERELASTIC,
DAMAGE,
EROSION,
THERMAL,
variants("fluid", "Fluid", &[variant("newtonian", "Newtonian", &[positive("viscosity", "Viscosity", D::Viscosity).req()])])
.hint("a fluid needs a density"),
];
pub const MATERIAL_PLASTICITY: &[Field] = &[reference("name", "Material", C::Material).req(), PLASTIC.req().def(r#"{"type": "bilinear"}"#)];
pub const MATERIAL_DAMAGE: &[Field] =
&[reference("name", "Material", C::Material).req(), DAMAGE.def(r#"{"type": "johnson_cook"}"#).hint("give damage, erosion or both"), EROSION];
const PROFILE: Field = variants(
"profile",
"Profile",
&[
variant("rectangle", "Rectangle", &[positive("width", "Width", D::Length).req(), positive("height", "Height", D::Length).req()]),
variant("circle", "Circle", &[positive("radius", "Radius", D::Length).req()]),
variant("pipe", "Pipe", &[positive("radius", "Radius", D::Length).req(), positive("thickness", "Wall", D::Length).req()]),
variant(
"i",
"I-beam",
&[
positive("height", "Height", D::Length).req(),
positive("width", "Width", D::Length).req(),
positive("web", "Web", D::Length).req(),
positive("flange", "Flange", D::Length).req(),
],
),
variant(
"box",
"Box",
&[
positive("width", "Width", D::Length).req(),
positive("height", "Height", D::Length).req(),
positive("thickness", "Wall", D::Length).req(),
],
),
],
)
.req()
.def(r#"{"type": "rectangle"}"#);
pub const SECTION: &[Field] = &[
NAME,
reference("set", "Region", C::Set).req(),
reference("material", "Material", C::Material).req(),
variants(
"kind",
"Kind",
&[
variant("solid", "Solid", &[]),
variant(
"shell",
"Shell",
&[
positive("thickness", "Thickness", D::Length).req(),
range("offset", "Offset", D::None, -0.5, 0.5).def("0").hint("fraction of the thickness"),
int("points", "Integration points", 1, 15).def("5"),
],
),
variant("beam", "Beam", &[PROFILE, xyz("orientation", "Orientation", D::None).req().def("[0, 0, 1]")]),
variant("rod", "Rod", &[positive("area", "Area", D::Area).req()]),
variant("plane_stress", "Plane stress", &[positive("thickness", "Thickness", D::Length).req().def("1")]),
variant("plane_strain", "Plane strain", &[positive("thickness", "Thickness", D::Length).def("1")]),
variant("axisymmetric", "Axisymmetric", &[]),
],
)
.req()
.def(r#"{"type": "solid"}"#),
group(
"formulation",
"Formulation",
&[
choice("integration", "Integration", &[("full", "Full"), ("reduced", "Reduced")]).def("\"full\""),
flag("incompatible_modes", "Incompatible modes").def("false"),
flag("hybrid", "Hybrid").def("false"),
],
),
];
pub const REFERENCE_POINT: &[Field] = &[
NAME,
xyz("xyz", "Position", D::Length).req().def("[0, 0, 0]"),
int("label", "Node label", 0, u32::MAX as i64).hint("0: the highest free label"),
];
const DOFS: Field = json("dofs", "Degrees of freedom").hint("[1, …, 6], default all");
pub const COUPLING: &[Field] = &[
NAME,
reference("reference_point", "Reference point", C::ReferencePoint).req(),
reference("set", "Region", C::Set).req(),
choice("kind", "Kind", &[("kinematic", "Kinematic"), ("distributing", "Distributing")]).req().def("\"kinematic\""),
DOFS,
];
pub const MASS: &[Field] = &[
NAME,
TARGET,
positive("mass", "Mass", D::Mass).hint("per node; [mx, my, mz] in JSON"),
vector("inertia", "Inertia", D::Inertia, &["I11", "I22", "I33", "I12", "I13", "I23"]).hint("about the node; needs a reference point"),
];
const SPRING_FIRST: Field = target("first", "First end").req();
const SPRING_SECOND: Field = target("second", "Second end").hint("ground when empty");
const SPRING_DIRECTION: Field =
either("direction", "Direction", &[int("dof", "DOF", 1, 6), xyz("vector", "Vector", D::None), unit("line", "Along the ends")])
.req()
.def(r#"{"dof": 1}"#);
pub const SPRING: &[Field] = &[
NAME,
variants(
"",
"Type",
&[
variant("spring", "Spring", &[SPRING_FIRST, SPRING_SECOND, SPRING_DIRECTION, positive("stiffness", "Stiffness", D::Stiffness).req()]),
variant("dashpot", "Dashpot", &[SPRING_FIRST, SPRING_SECOND, SPRING_DIRECTION, positive("coefficient", "Coefficient", D::Damping).req()]),
],
)
.req()
.def("\"spring\""),
];
/// The target of a boundary condition, force, moment or temperature.
const TARGET: Field = target("target", "Target").req();
pub const BOUNDARY_CONDITION: &[Field] = &[
NAME,
variants(
"",
"Type",
&[
variant("fixed", "Fixed", &[TARGET]),
variant(
"displacement",
"Displacement",
&[
TARGET,
free_vector("values", "Values", D::Length, &["u1", "u2", "u3", "ur1", "ur2", "ur3"])
.req()
.def("[0, 0, 0, null, null, null]")
.hint("empty: free"),
],
),
variant("symmetry", "Symmetry", &[TARGET, choice("plane", "Plane normal", &[("x", "X"), ("y", "Y"), ("z", "Z")]).req().def("\"x\"")]),
variant("temperature", "Temperature", &[TARGET, num("value", "Temperature", D::Temperature).req()]),
],
)
.req()
.def("\"fixed\""),
];
const DISTRIBUTION: Field = choice("distribution", "Distribution", &[("total", "Total"), ("per_node", "Per node")]).def("\"total\"");
const LOAD_SET: Field = reference("set", "Region", C::Set).req();
pub const LOAD: &[Field] = &[
NAME,
variants(
"",
"Type",
&[
variant("force", "Force", &[TARGET, xyz("vector", "Force", D::Force).req(), DISTRIBUTION]),
variant("moment", "Moment", &[TARGET, xyz("vector", "Moment", D::Moment).req(), DISTRIBUTION]),
variant("pressure", "Pressure", &[LOAD_SET, num("magnitude", "Pressure", D::Stress).req().hint("positive pushes in")]),
variant("traction", "Traction", &[LOAD_SET, xyz("vector", "Traction", D::Stress).req()]),
variant(
"gravity",
"Gravity",
&[xyz("acceleration", "Acceleration", D::Acceleration).req(), reference("set", "Region", C::Set).hint("the whole model")],
),
variant("body_force", "Body force", &[LOAD_SET, xyz("vector", "Force per volume", D::ForcePerVolume).req()]),
variant(
"centrifugal",
"Centrifugal",
&[
num("angular_velocity", "Angular velocity", D::AngularVelocity).req(),
xyz("point", "Point on axis", D::Length).req().def("[0, 0, 0]"),
xyz("axis", "Axis", D::None).req().def("[0, 0, 1]"),
reference("set", "Region", C::Set).hint("the whole model"),
],
),
variant("surface_flux", "Surface heat flux", &[LOAD_SET, num("flux", "Flux", D::HeatFlux).req()]),
variant(
"convection",
"Convection",
&[LOAD_SET, positive("film", "Film coefficient", D::FilmCoefficient).req(), num("sink", "Sink temperature", D::Temperature).req()],
),
variant("heat_source", "Heat source", &[LOAD_SET, num("power", "Power per volume", D::None).req()]),
variant("temperature", "Temperature", &[TARGET, num("value", "Temperature", D::Temperature).req()]),
],
)
.req()
.def("\"force\""),
];
const FACE_CONDITION: &[Variant] = &[
variant("wall", "Wall", &[]),
variant("moving_wall", "Moving wall", &[xyz("velocity", "Velocity", D::Velocity).req()]),
variant("slip", "Slip", &[]),
variant(
"velocity",
"Velocity inlet",
&[
xyz("velocity", "Velocity", D::Velocity).req(),
choice("profile", "Profile", &[("uniform", "Uniform"), ("parabolic", "Parabolic")]).def("\"uniform\""),
],
),
variant("pressure", "Pressure outlet", &[num("pressure", "Pressure", D::Stress).req().def("0")]),
variant("periodic", "Periodic", &[]),
];
const fn face(key: &'static str, label: &'static str) -> Field {
variants(key, label, FACE_CONDITION).hint("wall")
}
pub const FLUID_DOMAIN: &[Field] = &[
NAME,
reference("material", "Fluid", C::Material).req(),
xyz("min", "Box min", D::Length).req(),
xyz("max", "Box max", D::Length).req(),
positive("spacing", "Cell size", D::Length).req(),
group(
"faces",
"Box faces",
&[
face("x_min", "X min"),
face("x_max", "X max"),
face("y_min", "Y min"),
face("y_max", "Y max"),
face("z_min", "Z min"),
face("z_max", "Z max"),
],
),
references("obstacles", "Obstacles", C::Body),
json("surfaces", "Obstacle faces").hint("[{\"face\", \"condition\"}]; walls by default"),
xyz("initial_velocity", "Initial velocity", D::Velocity),
xyz("acceleration", "Acceleration", D::Acceleration),
positive("tolerance", "Obstacle tolerance", D::Length).hint("a tenth of the cell size"),
];
pub const FLUID_BOUNDARY: &[Field] = &[
reference("domain", "Domain", C::FluidDomain).req(),
json("at", "Boundary").req().def("\"x_min\"").hint("\"x_min\" … \"z_max\", or {\"body\", \"face\"}"),
variants("condition", "Condition", FACE_CONDITION).req().def(r#"{"type": "velocity"}"#),
];
const PROCEDURE: Field = variants(
"procedure",
"Procedure",
&[
variant("static", "Static, linear", &[]),
variant(
"nonlinear_static",
"Static, nonlinear",
&[
flag("nonlinear_geometry", "Nonlinear geometry").def("true"),
range("initial_increment", "Initial increment", D::None, 0.0, 1.0).def("0.1"),
range("min_increment", "Min increment", D::None, 0.0, 1.0).def("1e-5"),
range("max_increment", "Max increment", D::None, 0.0, 1.0).def("1.0"),
int("max_increments", "Max increments", 1, 1_000_000).def("100"),
int("max_iterations", "Iterations per increment", 1, 1000).def("16"),
group(
"tolerances",
"Tolerances",
&[
positive("force", "Force", D::None).def("1e-6"),
positive("displacement", "Displacement", D::None).def("1e-5"),
positive("energy", "Energy", D::None).def("1e-10"),
],
),
flag("line_search", "Line search").def("false"),
json("load_path", "Load path").hint("[[fraction, factor], …]; a ramp when empty"),
],
),
variant(
"frequency",
"Frequency",
&[
int("modes", "Modes", 1, 10_000).req().def("10"),
positive("min_hz", "Min frequency", D::Hertz),
positive("max_hz", "Max frequency", D::Hertz),
],
),
variant(
"explicit_dynamic",
"Dynamic, explicit",
&[
positive("end_time", "End time", D::Time).req(),
range("dt_scale", "Time step scale", D::None, 0.0, 1.0).def("0.9"),
int("max_increments", "Max increments", 1, 10_000_000_000).def("10000000"),
positive("output_interval", "Field output every", D::Time),
positive("history_interval", "History every", D::Time),
reference("history_nodes", "History nodes", C::Set),
rows(
"initial_velocities",
"Initial velocities",
&[reference("set", "Set", C::Set).req(), xyz("velocity", "Velocity", D::Velocity).req()],
),
rows(
"rigid_walls",
"Rigid walls",
&[
reference("set", "Set", C::Set).req(),
xyz("point", "Point", D::Length).req(),
xyz("normal", "Normal", D::None).req(),
FRICTION,
],
),
group(
"hourglass",
"Hourglass control",
&[
choice("type", "Type", &[("viscous", "Viscous"), ("stiffness", "Stiffness")]).def("\"viscous\""),
positive("coefficient", "Coefficient", D::None),
],
),
group("bulk_viscosity", "Bulk viscosity", &[positive("linear", "Linear", D::None), positive("quadratic", "Quadratic", D::None)]),
flag("nonlinear_geometry", "Nonlinear geometry").def("true"),
choice("stress_rate", "Stress rate", &[("jaumann", "Jaumann"), ("green_naghdi", "Green–Naghdi")]),
],
),
variant(
"heat_transfer",
"Heat transfer",
&[group(
"transient",
"Transient",
&[
positive("end_time", "End time", D::Time).req(),
positive("increment", "Increment", D::Time).req(),
range("theta", "θ", D::None, 0.0, 1.0),
num("initial_temperature", "Initial temperature", D::Temperature),
],
)
.hint("steady state when off")],
),
variant("coupled_temperature_displacement", "Coupled temperature–displacement", &[]),
variant(
"fluid",
"Fluid (lattice Boltzmann)",
&[
reference("domain", "Domain", C::FluidDomain).req(),
positive("end_time", "End time", D::Time).hint("or Steps"),
int("steps", "Steps", 1, 1_000_000_000_000),
positive("time_step", "Time step", D::Time).hint("chosen for Mach 0.087"),
int("frames", "Frames", 1, 100_000).def("10"),
positive("magic", "TRT Λ", D::None).hint("0.1875"),
],
),
],
);
const OUTPUT: Field = json("output", "Output").hint("[\"displacement\", \"stress\", …]; by procedure when empty");
const STEP_BCS: Field = references("boundary_conditions", "Boundary conditions", C::BoundaryCondition);
const STEP_LOADS: Field = references("loads", "Loads", C::Load);
pub const STEP_CREATE: &[Field] =
&[NAME, PROCEDURE.req().def(r#"{"type": "static"}"#), OUTPUT, STEP_BCS, STEP_LOADS, reference("before", "Insert before", C::Step).hint("last")];
pub const STEP_EDIT: &[Field] = &[reference("name", "Step", C::Step).req(), PROCEDURE, OUTPUT, STEP_BCS, STEP_LOADS];
pub const STEP_ACTIVATE: &[Field] = &[
reference("step", "Step", C::Step).req(),
references("boundary_conditions", "Boundary conditions", C::BoundaryCondition),
references("loads", "Loads", C::Load),
flag("active", "Active").def("true"),
];
pub const UNITS: &[Field] = &[choice(
"system",
"Unit system",
&[
("mm-t-s-N-MPa", "mm, t, s, N, MPa"),
("m-kg-s-N-Pa", "m, kg, s, N, Pa"),
("mm-kg-ms-kN-GPa", "mm, kg, ms, kN, GPa"),
("mm-g-ms-N-MPa", "mm, g, ms, N, MPa"),
("in-lbf-s-psi", "in, lbf·s²/in, s, lbf, psi"),
("unspecified", "Unspecified"),
],
)
.req()
.def("\"mm-t-s-N-MPa\"")];
// ---- Contact ----
const FRICTION: Field = group(
"friction",
"Friction",
&[
positive("static", "Static μ", D::None).hint("0"),
positive("kinetic", "Kinetic μ", D::None).hint("the static μ"),
positive("decay", "Decay", D::None).hint("0; per slip rate"),
],
)
.hint("frictionless");
const PENALTY: Field = range("penalty_scale", "Penalty scale", D::None, 0.0, 1.0).def("1");
const PENETRATION: Field =
choice("initial_penetration", "Initial penetration", &[("adjust", "Adjust nodes"), ("refuse", "Refuse")]).def("\"adjust\"");
pub const CONTACT_PAIR: &[Field] = &[
NAME,
reference("main", "Main surface", C::Set).req(),
reference("secondary", "Secondary surface", C::Set).req(),
FRICTION,
PENALTY,
PENETRATION,
];
pub const CONTACT_GENERAL: &[Field] = &[NAME, reference("surface", "Surface", C::Set).hint("every exterior face"), FRICTION, PENALTY, PENETRATION];
pub const CONTACT_EDIT: &[Field] = &[
reference("name", "Contact", C::Contact).req(),
variants(
"surfaces",
"Surfaces",
&[
variant("pair", "Pair", &[reference("main", "Main", C::Set).req(), reference("secondary", "Secondary", C::Set).req()]),
variant("general", "General", &[reference("surface", "Surface", C::Set).hint("every exterior face")]),
],
)
.req(),
FRICTION,
PENALTY,
PENETRATION,
];
pub const CONTACT_FIND: &[Field] = &[
positive("tolerance", "Tolerance", D::Length).hint("a tenth of the mean face size"),
flag("create", "Create pairs").def("false"),
FRICTION,
PENALTY,
PENETRATION,
];
// ---- Meshing ----
const BODY: Field = reference("body", "Body", C::Body).hint("every body of the design");
const SURFACE: Field = int("surface", "Imported surface", 0, 1_000_000).hint("instead of a body");
const TOLERANCE: Field = positive("tolerance", "Chordal tolerance", D::Millimetre).hint("1e-3 of the diagonal");
const WELD: Field = range("weld", "Weld", D::None, 0.0, 1e-3).hint("1e-9 of the diagonal");
const WELD_DISTANCE: Field = positive("weld_distance", "Weld distance", D::Millimetre).hint("the surface's own");
const FEATURE_ANGLE: Field = range("feature_angle", "Feature angle", D::Degree, 0.0, 180.0).hint("45");
pub const TET: &[Field] = &[
BODY,
SURFACE,
positive("size", "Element size", D::Millimetre).hint("1/20 of the diagonal"),
choice("order", "Order", &[("linear", "Linear (tet4)"), ("quadratic", "Quadratic (tet10)")]).def("\"linear\""),
TOLERANCE,
flag("quality", "Improve quality").def("true"),
range("quality_target", "Quality target", D::Degree, 5.0, 60.0).hint("40"),
range("curvature_angle", "Curvature angle", D::Degree, 0.0, 90.0).hint("22.5"),
range("grading", "Grading", D::None, 0.05, 2.0).hint("0.3"),
int("max_passes", "Improvement passes", 0, 200).hint("20"),
int("max_points", "Max points", 1, 20_000_000).hint("1000000"),
int("max_steiner", "Max Steiner points", 0, 10_000_000).hint("50000"),
int("max_tets", "Max tetrahedra", 1, 50_000_000).hint("5000000"),
WELD,
WELD_DISTANCE,
FEATURE_ANGLE,
];
const HEX_SIZE: Field = positive("size", "Element size", D::Millimetre).hint("1/25 of the diagonal");
const HEX_ORDER: Field = int("order", "Order", 1, 2).def("1").hint("1: hex8, 2: hex20");
pub const HEX_GENERATE: &[Field] = &[
BODY,
SURFACE,
HEX_SIZE,
HEX_ORDER,
choice(
"method",
"Method",
&[("auto", "Automatic"), ("swept", "Any sweep"), ("extrude", "Extrude"), ("sweep", "Sweep"), ("revolve", "Revolve"), ("mapped", "Mapped")],
)
.def("\"auto\""),
TOLERANCE,
WELD,
WELD_DISTANCE,
FEATURE_ANGLE,
];
pub const HEX_SWEEP: &[Field] = &[
BODY,
SURFACE,
HEX_SIZE,
HEX_ORDER,
choice("method", "Method", &[("swept", "Any sweep"), ("extrude", "Extrude"), ("sweep", "Sweep"), ("revolve", "Revolve")]).def("\"swept\""),
TOLERANCE,
WELD,
WELD_DISTANCE,
FEATURE_ANGLE,
];
pub const HEX_MAPPED: &[Field] = &[BODY, SURFACE, HEX_SIZE, HEX_ORDER, TOLERANCE, WELD, WELD_DISTANCE, FEATURE_ANGLE];
pub const HEX_BLOCKS: &[Field] =
&[references("bodies", "Cells", C::Body).hint("every body of the design"), HEX_SIZE, HEX_ORDER, TOLERANCE, WELD, WELD_DISTANCE, FEATURE_ANGLE];
pub const HEX_OCTREE: &[Field] = &[
BODY,
SURFACE,
HEX_SIZE,
TOLERANCE,
int("max_depth", "Refinement levels", 0, 5).hint("3"),
int("max_elements", "Max elements", 1, 10_000_000),
int("attempts", "Attempts", 1, 8).hint("4"),
int("max_sweeps", "Optimiser sweeps", 0, 10_000).hint("300"),
WELD,
WELD_DISTANCE,
FEATURE_ANGLE,
];
pub const BLOCK: &[Field] = &[
text("body", "Body").req().def("\"Block\""),
choice(
"kind",
"Element",
&[
("hex8", "hex8"),
("hex20", "hex20"),
("tet4", "tet4"),
("tet10", "tet10"),
("wedge6", "wedge6"),
("quad4", "quad4 (2D)"),
("quad8", "quad8 (2D)"),
("tri3", "tri3 (2D)"),
("tri6", "tri6 (2D)"),
],
)
.req()
.def("\"hex8\""),
json("corners", "Corners").req().hint("[[x, y, z] × 8] (2D: × 4 at z = 0)"),
json("divisions", "Divisions").req().hint("[n12, n14, n15] (2D: [n12, n14])"),
json("bias", "Bias").hint("[1, 1, 1]"),
json("edges", "Curved and named edges"),
json("faces", "Face names").hint("{\"1\": \"bottom\"}"),
json("vertices", "Corner names"),
text("face", "2D face name").hint("area"),
positive("merge", "Merge distance", D::Length).hint("1e-6 of the diagonal"),
];
pub const PLANAR: &[Field] = &[
positive("size", "Element size", D::Length).req(),
choice("kind", "Element", &[("quad4", "quad4"), ("quad8", "quad8"), ("tri3", "tri3"), ("tri6", "tri6")]).def("\"quad4\""),
BODY,
text("face", "Face").hint("a planar face of the body"),
TOLERANCE,
json("polygon", "Polygon").hint("[[x, y], …] instead of a face"),
json("holes", "Holes"),
text("name", "Body name").hint("Plane"),
int("max_elements", "Max elements", 1, 4_000_000),
];
pub const QUALITY: &[Field] = &[json("elements", "Elements").hint("[labels]; all when empty"), int("bins", "Histogram bins", 1, 1000).def("10")];
pub const SURFACE_CHECK: &[Field] =
&[reference("body", "Body", C::Body).req().hint("or an imported surface"), SURFACE, TOLERANCE, WELD, WELD_DISTANCE, FEATURE_ANGLE];
pub const NODE_ADD: &[Field] = &[int("label", "Label", 1, u32::MAX as i64).req(), xyz("xyz", "Position", D::Length).req().def("[0, 0, 0]")];
pub const LABEL: &[Field] = &[int("label", "Label", 1, u32::MAX as i64).req()];
pub const ELEMENT_ADD: &[Field] = &[
int("label", "Label", 1, u32::MAX as i64).req(),
choice(
"kind",
"Kind",
&[
("rod2", "rod2"),
("beam2", "beam2"),
("tri3", "tri3"),
("tri6", "tri6"),
("quad4", "quad4"),
("quad8", "quad8"),
("tet4", "tet4"),
("tet10", "tet10"),
("wedge6", "wedge6"),
("hex8", "hex8"),
("hex20", "hex20"),
],
)
.req()
.def("\"hex8\""),
json("nodes", "Nodes").req().hint("[labels]"),
];
pub const DELAUNAY_POINTS: &[Field] = &[json("points", "Points").req().hint("[[x, y, z], …]")];
pub const TAG: &[Field] = &[
group("geometry", "Geometry", &[reference("body", "Body", C::Body).req(), text("face", "Face"), text("edge", "Edge"), text("vertex", "Vertex")])
.req(),
json("elements", "Elements"),
json("faces", "Element faces"),
json("edges", "Element edges"),
json("nodes", "Nodes"),
];
// ---- Geometry and files ----
pub const OPEN_DESIGN: &[Field] = &[path("path", "Design", false).req()];
pub const TESSELLATE: &[Field] = &[reference("body", "Body", C::Body).req(), TOLERANCE];
pub const IMPORT_MESH: &[Field] = &[path("path", "3MF file", false).req()];
pub const FILE_OPEN: &[Field] = &[path("path", "Model", false).req()];
pub const FILE_SAVE: &[Field] = &[path("path", "File", true).hint("the model's file")];
pub const FILE_SAVE_AS: &[Field] = &[path("path", "File", true).req()];
pub const RECOVERY_LIST: &[Field] = &[path("dir", "Folder", false).hint("the recovery folder")];
pub const RECOVER: &[Field] = &[text("id", "Autosave").req(), path("dir", "Folder", false).hint("the recovery folder")];
pub const RECOVERY_DISCARD: &[Field] = &[text("id", "Autosave"), flag("all", "All"), path("dir", "Folder", false).hint("the recovery folder")];
pub const SAMPLE: &[Field] =
&[choice("name", "Sample", &[("cantilever", "Cantilever"), ("impact", "Impact"), ("parts", "Parts")]).req().def("\"cantilever\"")];
pub const COUNT: &[Field] = &[int("count", "Count", 1, 1_000_000).def("1")];
pub const DECK: &[Field] = &[
choice("format", "Format", &[("abaqus", "Abaqus input (.inp)"), ("nastran", "Nastran bulk data (.bdf)"), ("lsdyna", "LS-DYNA keyword (.k)")])
.req()
.def("\"abaqus\""),
path("path", "File", true).req(),
references("steps", "Steps", C::Step).hint("all"),
];
// ---- Solving ----
const STEP: Field = reference("step", "Step", C::Step).hint("the only one");
const SOLVED: Field = reference("step", "Step", C::SolvedStep).hint("the step shown");
const MAX_DOFS: Field = int("max_dofs", "Max DOFs", 1, 6_000_000).hint("6000000");
const MAX_FACTOR: Field = int("max_factor_entries", "Max factor entries", 1, 400_000_000).hint("400000000");
const THREADS: Field = int("threads", "Threads", 1, 256).hint("the session's");
pub const SOLVE_STATIC: &[Field] = &[STEP, MAX_DOFS, MAX_FACTOR, THREADS];
pub const SOLVE_NONLINEAR: &[Field] =
&[STEP, MAX_DOFS, MAX_FACTOR, int("max_newton_iterations", "Max Newton iterations", 1, 100_000).hint("100000"), THREADS];
pub const SOLVE_FREQUENCY: &[Field] = &[
STEP,
choice("mass", "Mass matrix", &[("consistent", "Consistent"), ("lumped", "Lumped")]).def("\"consistent\""),
num("shift", "Shift", D::None).hint("0"),
MAX_DOFS,
MAX_FACTOR,
int("max_iterations", "Max Lanczos vectors", 1, 3000).hint("3000"),
THREADS,
];
pub const SOLVE_EXPLICIT: &[Field] = &[
STEP,
MAX_DOFS,
int("max_increments", "Max increments", 1, 1_000_000_000).hint("1000000000"),
int("max_output_values", "Max output values", 1, 50_000_000).hint("50000000"),
int("max_contact_faces", "Max contact faces", 1, 10_000_000).hint("10000000"),
THREADS,
];
pub const SOLVE_HISTORY: &[Field] = &[SOLVED.req(), int("every", "Keep one in", 1, 1_000_000_000)];
pub const SOLVE_FLUID: &[Field] = &[
STEP,
int("max_cells", "Max cells", 1, 16_000_000),
int("max_steps", "Max steps", 1, 10_000_000),
positive("max_cell_updates", "Max cell updates", D::None),
positive("max_frame_bytes", "Max frame bytes", D::None),
THREADS,
];
pub const SOLVE_PROBE: &[Field] = &[
SOLVED.req(),
json("nodes", "Nodes").hint("[labels]"),
reference("set", "Set", C::Set),
json("at", "Points").hint("[[x, y, z], …]: nearest nodes"),
int("mode", "Mode", 1, 1_000_000),
int("frame", "Frame", 0, 1_000_000_000),
flag("elements", "Element results"),
json("points", "Fluid points").hint("[[x, y, z], …]"),
json("forces", "Fluid walls").hint("[\"x_min\", \"F2:top\"]"),
];
// ---- Results ----
pub const DEFORMED: &[Field] = &[SOLVED, flag("show", "Show").def("true"), json("scale", "Scale").def("\"auto\"").hint("\"auto\" or a factor")];
pub const CONTOUR: &[Field] = &[
SOLVED,
choice(
"field",
"Field",
&[
("von_mises", "Von Mises stress"),
("displacement", "Displacement"),
("displacement_x", "Displacement X"),
("displacement_y", "Displacement Y"),
("displacement_z", "Displacement Z"),
("max_principal", "Max principal"),
("mid_principal", "Mid principal"),
("min_principal", "Min principal"),
("stress_xx", "Stress XX"),
("stress_yy", "Stress YY"),
("stress_zz", "Stress ZZ"),
("stress_xy", "Stress XY"),
("stress_xz", "Stress XZ"),
("stress_yz", "Stress YZ"),
("none", "None"),
],
)
.req()
.def("\"von_mises\""),
int("bands", "Bands", 2, 24).def("12"),
];
pub const MODES: &[Field] = &[SOLVED, int("mode", "Mode", 1, 1_000_000).def("1")];
pub const ANIMATE: &[Field] = &[flag("on", "Animate").def("true")];
pub const FRAME: &[Field] = &[SOLVED, int("frame", "Frame", 0, 1_000_000_000), positive("time", "Time", D::Time).hint("the last frame")];
pub const REACTIONS: &[Field] = &[SOLVED, references("sets", "Sets", C::Set).hint("the step's BC sets"), xyz("about", "About point", D::Length)];
#[cfg(test)]
mod tests {
use serde_json::{Value, json};
use super::*;
use crate::{EngineError, Session};
/// The form's starting parameters (and those with the required names and references filled
/// in from the model) are accepted, or refused only for a required field still empty; never
/// for a value the form put there itself. Errors other than parameter errors (no design, no
/// results) come after the parameters were read.
#[test]
fn form_defaults_are_accepted_or_need_only_required_fields() {
let (mut checked, mut failures) = (0, Vec::new());
for c in crate::commands() {
let Some(form) = c.form else { continue };
if c.id.starts_with("file.") && c.id != "file.save_as" || c.id == "edit.undo" || c.id == "edit.redo" {
continue; // they replace or write the session's model; their forms are paths and counts
}
for fill_refs in [false, true] {
let limits = crate::Limits { max_steiner: 10, max_tets: 100, max_hex_elements: 100, threads: 1 };
let mut s = Session { limits, ..Session::default() };
crate::cmd::define::tests::defined_bar(&mut s);
s.execute("solve.run", &json!({})).unwrap();
let mut p = prefill(form, &json!({}));
if fill_refs {
fill_required(&s, form, &mut p);
}
let missing: Vec<String> = problems(form, &p).into_iter().filter(|p| p.message == "required").map(|p| p.path).collect();
match s.execute(c.id, &p) {
// State, not parameters: the meshers' default is the open design's bodies, the
// bar has no explicit step; activation and probes need one of several fields.
Err(EngineError::Params(e)) if STATE.iter().any(|m| e.contains(m)) => {}
Err(EngineError::Params(e)) if !missing.iter().any(|k| e.contains(k.rsplit('/').next().unwrap_or(k))) => {
failures.push(format!("{}: {p} refused for `{e}`, but only {missing:?} were missing", c.id));
}
Err(EngineError::Params(_)) => {}
_ => checked += 1,
}
}
}
assert!(failures.is_empty(), "{failures:#?}");
assert!(checked > 20, "{checked}");
}
/// The commands whose form is the JSON text alone (no schema yet). None: keep it that way.
#[test]
fn every_command_has_a_form() {
let without: Vec<&str> = crate::commands().iter().filter(|c| c.form.is_none()).map(|c| c.id).collect();
assert_eq!(without, Vec::<&str>::new());
// A command that takes nothing has an empty form, and says so.
for c in crate::commands() {
assert_eq!(c.form.is_some_and(<[Field]>::is_empty), c.params.trim_start().starts_with("{}"), "{}", c.id);
}
}
const STATE: &[&str] = &["open a design first", "`boundary_conditions` or `loads`", "give one of", "is not an explicit", "has a result"];
/// Fill top-level required names (a fresh one) and references (the first existing item).
fn fill_required(s: &Session, form: &[Field], p: &mut Value) {
let Some(obj) = p.as_object_mut() else { return };
for f in form {
if !f.required || obj.contains_key(f.key) {
continue;
}
match f.kind {
Kind::Text => {
obj.insert(f.key.into(), json!("Fresh"));
}
Kind::Ref(c) => {
if let Some(n) = s.names(c).first() {
obj.insert(f.key.into(), json!(n));
}
}
_ => {}
}
}
}
/// Every default is JSON; every variant field's default names a variant.
#[test]
fn defaults_parse() {
fn walk(fields: &[Field]) {
for f in fields {
if let Some(d) = f.default {
assert!(serde_json::from_str::<serde_json::Value>(d).is_ok(), "{}: {d}", f.key);
}
match f.kind {
Kind::Group(g) | Kind::Rows { fields: g, .. } | Kind::Either(g) => walk(g),
Kind::Variants { variants, .. } => variants.iter().for_each(|v| walk(v.fields)),
_ => {}
}
}
}
for c in crate::commands() {
if let Some(form) = c.form {
walk(form);
}
}
}
}
+4 -4
View File
@@ -12,7 +12,7 @@ pub const OPEN: CommandSpec = CommandSpec {
id: "geometry.open",
label: "Open Design",
params: r#"{"path": "part.solidcraft"} — rebuilds the SolidCraft design and makes its bodies the geometry; opening it again after an edit picks up the change"#,
form: None,
form: Some(super::forms::OPEN_DESIGN),
kind: CommandKind::Model,
run: open,
};
@@ -21,7 +21,7 @@ pub const BODIES: CommandSpec = CommandSpec {
id: "geometry.bodies",
label: "Bodies",
params: "{} — the open design's bodies: measures and persistent face names (by face index)",
form: None,
form: Some(&[]),
kind: CommandKind::Model,
run: bodies,
};
@@ -30,7 +30,7 @@ pub const TESSELLATE: CommandSpec = CommandSpec {
id: "geometry.tessellate",
label: "Tessellate Body",
params: r#"{"body": "Body1", "tolerance": 0.05} — triangulates the body's boundary within the chordal tolerance (mm); reports triangles per face"#,
form: None,
form: Some(super::forms::TESSELLATE),
kind: CommandKind::Model,
run: tessellate,
};
@@ -39,7 +39,7 @@ pub const IMPORT_MESH: CommandSpec = CommandSpec {
id: "geometry.import.mesh",
label: "Import Tagged Mesh",
params: r#"{"path": "part.3mf"} — adds each build item of a 3MF package as a surface; triangles keep their B-rep face tags (docs/formats/tagged-mesh.md)"#,
form: None,
form: Some(super::forms::IMPORT_MESH),
kind: CommandKind::Model,
run: import_mesh,
};
+4 -4
View File
@@ -15,7 +15,7 @@ pub const HEX_GENERATE: CommandSpec = CommandSpec {
id: "mesh.hex.generate",
label: "Generate Hex Mesh",
params: r#"{"body": "Body1" (default: every body of the open design) or "surface": 0 (an imported surface), "size": 2.0 (element size, mm; default 1/25 of the bounding-box diagonal), "order": 1 (hex8) or 2 (hex20, mid-edge nodes on the geometry), "method": "auto" (default) | "swept" (any sweep) | "extrude" | "sweep" | "revolve" | "mapped", "tolerance": 0.05 (chordal tolerance of the tessellation, mm; default 1e-3 of the diagonal)} — replaces the mesh with structured hexahedra: multi-level sweeps along a direction or around an axis, one-to-one sweeps through linking faces, or mapped blocks. Boundary element faces are tagged with their B-rep faces (named as in geometry.bodies; by index when a face has no name; `weld`, `weld_distance` and `feature_angle` as in mesh.tet.generate), so sets by geometry resolve. Reports per body the method, counts and quality; a body no structured method fits is an error that says why"#,
form: None,
form: Some(super::forms::HEX_GENERATE),
kind: CommandKind::Model,
run: hex_generate,
};
@@ -118,7 +118,7 @@ pub const HEX_SWEEP: CommandSpec = CommandSpec {
id: "mesh.hex.sweep",
label: "Sweep Hex Mesh",
params: r#"the parameters of mesh.hex.generate, with "method": "swept" (default: extrude, sweep or revolve, whichever fits first) | "extrude" | "sweep" | "revolve" — mesh.hex.generate limited to the sweeping methods"#,
form: None,
form: Some(super::forms::HEX_SWEEP),
kind: CommandKind::Model,
run: hex_sweep,
};
@@ -127,7 +127,7 @@ pub const HEX_MAPPED: CommandSpec = CommandSpec {
id: "mesh.hex.mapped",
label: "Mapped Hex Mesh",
params: r#"the parameters of mesh.hex.generate without "method" — mesh.hex.generate with the mapped method: a six-sided block (six faces of four edges, three edges at each of eight corners)"#,
form: None,
form: Some(super::forms::HEX_MAPPED),
kind: CommandKind::Model,
run: hex_mapped,
};
@@ -136,7 +136,7 @@ pub const HEX_BLOCKS: CommandSpec = CommandSpec {
id: "mesh.hex.blocks",
label: "Block Hex Mesh",
params: r#"{"bodies": ["Cell1", "Cell2"] (default: every body of the open design), "size": 2.0 (element size, mm; default 1/25 of the bounding-box diagonal of all of them), "order": 1 (hex8) or 2 (hex20, mid-edge nodes on the geometry), "tolerance": 0.05 (chordal tolerance of the tessellation, mm; default 1e-3 of that diagonal)} — replaces the mesh with one conforming all-hex mesh of a part partitioned into cells (bodies that meet face to face, such as the pieces of solid.split_body in SolidCraft): every cell a mapped block (six four-sided faces; faces that meet smoothly, such as the two halves of a hole, count as one), division counts assigned over all cells together, each shared face meshed once and its nodes merged, radial lines graded off holes and fillets. Elements are tagged with their cell's body and boundary faces with its B-rep faces (`weld`, `weld_distance` and `feature_angle` as in mesh.tet.generate). Reports the cells, the shared faces, counts and quality; a cell that is not a block, or cells that touch without sharing whole faces, is an error that says which"#,
form: None,
form: Some(super::forms::HEX_BLOCKS),
kind: CommandKind::Model,
run: hex_blocks,
};
+9 -9
View File
@@ -19,7 +19,7 @@ pub const NODE_ADD: CommandSpec = CommandSpec {
id: "mesh.node.add",
label: "Add Node",
params: r#"{"label": 1, "xyz": [x, y, z]} — adds the node, or moves it if the label exists"#,
form: None,
form: Some(super::forms::NODE_ADD),
kind: CommandKind::Model,
run: node_add,
};
@@ -28,7 +28,7 @@ pub const NODE_REMOVE: CommandSpec = CommandSpec {
id: "mesh.node.remove",
label: "Remove Node",
params: r#"{"label": 1} — only a node no element uses"#,
form: None,
form: Some(super::forms::LABEL),
kind: CommandKind::Model,
run: node_remove,
};
@@ -37,7 +37,7 @@ pub const ELEMENT_ADD: CommandSpec = CommandSpec {
id: "mesh.element.add",
label: "Add Element",
params: r#"{"label": 1, "kind": "rod2|beam2|tri3|tri6|quad4|quad8|tet4|tet10|wedge6|hex8|hex20", "nodes": [labels]} — adds or replaces"#,
form: None,
form: Some(super::forms::ELEMENT_ADD),
kind: CommandKind::Model,
run: element_add,
};
@@ -46,7 +46,7 @@ pub const ELEMENT_REMOVE: CommandSpec = CommandSpec {
id: "mesh.element.remove",
label: "Remove Element",
params: r#"{"label": 1}"#,
form: None,
form: Some(super::forms::LABEL),
kind: CommandKind::Model,
run: element_remove,
};
@@ -55,7 +55,7 @@ pub const QUALITY: CommandSpec = CommandSpec {
id: "mesh.quality",
label: "Mesh Quality",
params: r#"{"elements": [labels] (optional, default all), "bins": 10 (1..=1000)} — certified validity (det J > 0 everywhere), volume, and min/max/mean/worst/histogram per metric (angles in degrees; histograms over each metric's fixed range, out-of-range values in the end bins)"#,
form: None,
form: Some(super::forms::QUALITY),
kind: CommandKind::Model,
run: quality_report,
};
@@ -64,7 +64,7 @@ pub const DELAUNAY_POINTS: CommandSpec = CommandSpec {
id: "mesh.delaunay.points",
label: "Delaunay of Points",
params: r#"{"points": [[x, y, z], ...] (at most 20000)} — the exact Delaunay tetrahedralization (a debugging report; the model is not changed): {"dimension", "vertices", "duplicates": [[point, kept point], ...], "tets": [[i, j, k, l], ...]} with 0-based point indices in tet4 order; collinear or coplanar points give no tets"#,
form: None,
form: Some(super::forms::DELAUNAY_POINTS),
kind: CommandKind::Model,
run: delaunay_points,
};
@@ -90,7 +90,7 @@ pub const SURFACE_CHECK: CommandSpec = CommandSpec {
id: "mesh.surface.check",
label: "Check Boundary Surface",
params: r#"{"body": "Body1", "tolerance": 0.05} or {"surface": 0}, optional "weld": 1e-9 (0 or 1e-15..=1e-3 of the bounding-box diagonal), "weld_distance": mm (default: the surface's own, twice SolidCraft's linear tolerance for a tessellated body, 0 for an imported mesh), "feature_angle": 45 (degrees, (0, 180]) — welds the tessellated body or imported surface and checks it can bound a volume mesh: closed, manifold, outward, no self-intersections; reports shells, genus, volume and feature edges, or the located defects"#,
form: None,
form: Some(super::forms::SURFACE_CHECK),
kind: CommandKind::Model,
run: surface_check,
};
@@ -99,7 +99,7 @@ pub const TET_GENERATE: CommandSpec = CommandSpec {
id: "mesh.tet.generate",
label: "Generate Tet Mesh",
params: r#"{"body": "Body1" (default: every body of the open design) or "surface": 0 (an imported surface), "tolerance": 0.05 (chordal tolerance of the tessellation, mm; default 1e-3 of the body's bounding-box diagonal), the boundary options of mesh.surface.check ("weld", "weld_distance", "feature_angle"), optional "size": mm (target edge length; default a twentieth of the bounding-box diagonal), "order": "linear" | "quadratic" (tet4 or tet10; quadratic mid-edge nodes lie on the B-rep faces), "quality": true (false: the raw constrained Delaunay tetrahedralization), "quality_target": 40 (degrees: tetrahedra whose smallest dihedral-angle sine is at least sin of this are left alone by improvement, 5..=60), "curvature_angle": 22.5 (degrees per element along curved boundary, 0 for none, at most 90), "grading": 0.3 (largest size change per unit length, 0.05..=2), "max_passes": 20 (improvement passes, 0..=200), "max_points": 1000000 (1..=20000000), "max_steiner": 50000 (0..=10000000), "max_tets": 5000000 (1..=50000000) — the constrained Delaunay tetrahedralization of the welded boundary (every boundary triangle a union of tet faces), refined to the size field with its boundary vertices on the B-rep faces, smoothed and improved (Delaunay refinement, ODT smoothing, Klingner–Shewchuk improvement), with a quality report. Adds the elements and their nodes (labels after the largest existing ones), tags the elements with the body and each boundary element face with its geometry face, and fails, changing nothing, if a surface is invalid or a mesh does not pass the validity gate. Reports the totals and, under `bodies`, each body's counts, size and quality report"#,
form: None,
form: Some(super::forms::TET),
kind: CommandKind::Model,
run: tet_generate,
};
@@ -348,7 +348,7 @@ pub const PLANAR: CommandSpec = CommandSpec {
id: "mesh.planar",
label: "2D Mesh",
params: r#"{"size": 1.0, "kind": "quad4|quad8|tri3|tri6" (default quad4), and either "polygon": [[x, y], ...] with optional "holes": [[[x, y], ...], ...] and "name": "Plane" (the body name its tags use), or "body": "Body1", "face": "F2:top", "tolerance": 0.05 (a planar face of an open design, parallel to XY); optional "max_elements": 1000000 (1..=4000000)} — meshes the planar region with 2D elements in the XY plane (a face at z = c is meshed at z = 0) for plane stress, plane strain or axisymmetric sections (x the radius, Y the axis): free quadrilaterals (or constrained Delaunay triangles) of about `size`, mid-side nodes on the boundary for quadratic kinds. Adds nodes and elements after the largest existing labels and tags them: the elements with the face (polygon: face `area` of body `name`), the boundary element edges with edges named after the neighbouring face of the body (polygon: `e1`, `e2`, … side by side, the outer loop then each hole, and vertices `v1`, … at the corners), so sets on those edges carry loads and boundary conditions"#,
form: None,
form: Some(super::forms::PLANAR),
kind: CommandKind::Model,
run: planar,
};
+1
View File
@@ -6,6 +6,7 @@ pub(crate) mod deck;
pub(crate) mod define;
mod edit;
pub(crate) mod file;
pub(crate) mod forms;
mod geometry;
mod hex;
pub(crate) mod mesh;
+5 -4
View File
@@ -4,25 +4,26 @@ use serde_json::{Value, json};
use super::{listed, name};
use crate::{CommandKind, CommandSpec, EngineError, Result, Session};
pub const INFO: CommandSpec = CommandSpec { id: "model.info", label: "Model Info", params: "{}", form: None, kind: CommandKind::Model, run: info };
pub const INFO: CommandSpec =
CommandSpec { id: "model.info", label: "Model Info", params: "{}", form: Some(&[]), kind: CommandKind::Model, run: info };
pub const UNITS: CommandSpec = CommandSpec {
id: "model.units",
label: "Unit System",
params: r#"{"system": "m-kg-s-N-Pa|mm-t-s-N-MPa|mm-kg-ms-kN-GPa|mm-g-ms-N-MPa|in-lbf-s-psi|unspecified"} — labels the system the model's numbers are in; nothing is converted"#,
form: None,
form: Some(super::forms::UNITS),
kind: CommandKind::Model,
run: units,
};
pub const CHECK: CommandSpec =
CommandSpec { id: "model.check", label: "Check Model", params: "{}", form: None, kind: CommandKind::Model, run: check };
CommandSpec { id: "model.check", label: "Check Model", params: "{}", form: Some(&[]), kind: CommandKind::Model, run: check };
pub const SET_RESOLVE: CommandSpec = CommandSpec {
id: "model.set.resolve",
label: "Resolve Set",
params: r#"{"name": "root"} — the set's nodes, elements and element faces on the current mesh (the first 100 of each); fails naming every member that is not on the mesh"#,
form: None,
form: Some(super::forms::SET_RESOLVE),
kind: CommandKind::Model,
run: set_resolve,
};
+1 -1
View File
@@ -15,7 +15,7 @@ pub const HEX_OCTREE: CommandSpec = CommandSpec {
id: "mesh.hex.octree",
label: "Octree Hex Mesh",
params: r#"{"body": "Body1" (default: every body of the open design) or "surface": 0 (an imported surface), "size": 2.0 (element size in the interior, mm; default 1/25 of the bounding-box diagonal), "tolerance": 0.05 (chordal tolerance of the tessellation, mm; default 1e-3 of the diagonal), "max_depth": 3 (refinement levels below the size, each a third, 0..=5), "max_elements": 2000000 (1..=10000000; default the session limit), "attempts": 4 (builds, refining where one failed, 1..=8), "max_sweeps": 300 (optimiser sweeps, 0..=10000), and the boundary options of mesh.tet.generate ("weld", "weld_distance", "feature_angle")} — replaces the mesh with an unstructured all-hex8 mesh of any part: an adaptive 3-refinement grid with conforming transition templates, a core inside the part, two boundary layers that capture every B-rep face boundary and sharp edge, and untangling. Every element is proven valid; boundary element faces are tagged with their B-rep faces (named as in geometry.bodies; by index when a face has no name), so sets by geometry resolve. Reports per body the elements, quality, volume and grid statistics"#,
form: None,
form: Some(super::forms::HEX_OCTREE),
kind: CommandKind::Model,
run: hex_octree,
};
+7 -7
View File
@@ -12,7 +12,7 @@ pub const DEFORMED: CommandSpec = CommandSpec {
id: "results.deformed",
label: "Deformed Shape",
params: r#"{"step": "Load", "show": true, "scale": "auto"} — shows a solved step's deformed shape (`show: false` the undeformed mesh); `scale` is a factor (0 draws the undeformed shape, so a sequence from 0 swings it out) or "auto" (the largest displacement drawn a tenth of the model's size); `step` defaults to the step shown or the first solved one"#,
form: None,
form: Some(super::forms::DEFORMED),
kind: CommandKind::Session,
run: deformed,
};
@@ -21,7 +21,7 @@ pub const CONTOUR: CommandSpec = CommandSpec {
id: "results.contour",
label: "Contours",
params: r#"{"step": "Load", "field": "von_mises", "bands": 12} — contours of a nodal field: displacement, displacement_x/y/z, von_mises, max/mid/min_principal, stress_xx/yy/zz/xy/xz/yz (stresses for static steps); "none" turns them off; 2 to 24 bands"#,
form: None,
form: Some(super::forms::CONTOUR),
kind: CommandKind::Session,
run: contour,
};
@@ -30,7 +30,7 @@ pub const MODES: CommandSpec = CommandSpec {
id: "results.modes",
label: "Mode Shapes",
params: r#"{"step": "Modes", "mode": 1} — shows a mode shape of a solved frequency step (from 1) and lists every mode's frequency"#,
form: None,
form: Some(super::forms::MODES),
kind: CommandKind::Session,
run: modes,
};
@@ -39,7 +39,7 @@ pub const ANIMATE: CommandSpec = CommandSpec {
id: "results.animate",
label: "Animate",
params: r#"{"on": true} — animates the deformed shape: a mode swings through its positive and negative shape, a static step grows from undeformed to deformed"#,
form: None,
form: Some(super::forms::ANIMATE),
kind: CommandKind::Session,
run: animate,
};
@@ -48,7 +48,7 @@ pub const FRAME: CommandSpec = CommandSpec {
id: "results.frame",
label: "Field Frame",
params: r#"{"step": "Impact", "frame": 3, "time": 0.002} — shows a field frame of a solved explicit step, by index from 0 or the frame nearest a time (neither: the last), or of a nonlinear static step (one per converged increment, `time` its load factor; the deformed shape follows it, its contours show the final state); the deformed shape and contours follow it"#,
form: None,
form: Some(super::forms::FRAME),
kind: CommandKind::Session,
run: frame,
};
@@ -57,7 +57,7 @@ pub const REACTIONS: CommandSpec = CommandSpec {
id: "results.reactions",
label: "Reaction Sums",
params: r#"{"step": "Load", "sets": ["root"], "about": [0, 0, 0]} — reaction force and moment (about the point) summed over each node set, and over the whole model; `sets` defaults to the sets the step's boundary conditions hold"#,
form: None,
form: Some(super::forms::REACTIONS),
kind: CommandKind::Session,
run: reactions,
};
@@ -66,7 +66,7 @@ pub const CLEAR: CommandSpec = CommandSpec {
id: "results.clear",
label: "Hide Results",
params: "{} — back to the model: hides the deformed shape, contours and modes",
form: None,
form: Some(&[]),
kind: CommandKind::Session,
run: clear,
};
+9 -9
View File
@@ -19,7 +19,7 @@ pub const RUN: CommandSpec = CommandSpec {
id: "solve.run",
label: "Solve All Steps",
params: r#"{} — solves every step in order with its procedure's solver (solve.static, solve.frequency, solve.explicit, solve.fluid with their default caps) and keeps the results; stops at the first step that fails, naming it"#,
form: None,
form: Some(&[]),
kind: CommandKind::Model,
run: run_all,
};
@@ -28,7 +28,7 @@ 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: `FIELDCRAFT_THREADS` or the cores) changes only the time, never the result"#,
form: None,
form: Some(super::forms::SOLVE_STATIC),
kind: CommandKind::Model,
run: run_static,
};
@@ -37,7 +37,7 @@ pub const NONLINEAR: CommandSpec = CommandSpec {
id: "solve.nonlinear",
label: "Solve Nonlinear Static Step",
params: r#"{"step": "Inflate", "max_dofs": 6000000, "max_factor_entries": 400000000, "max_newton_iterations": 100000, "threads": 8} — solves a nonlinear static step (the only one when `step` is left out) in load increments with Newton–Raphson iterations and keeps the result in the session: the final displacements, reactions, Cauchy stresses and logarithmic strains (small strains without nonlinear geometry), the equivalent plastic strain of plastic materials (the largest in the summary), every increment's load factor, iterations and residuals (force, displacement and energy, relative to their tolerances' scales), and a displacement frame per converged increment; the caps may only be lowered (`max_newton_iterations` over the whole step); `threads` as for solve.static"#,
form: None,
form: Some(super::forms::SOLVE_NONLINEAR),
kind: CommandKind::Model,
run: run_nonlinear,
};
@@ -46,7 +46,7 @@ pub const HEAT: CommandSpec = CommandSpec {
id: "solve.heat",
label: "Solve Heat Transfer Step",
params: r#"{"step": "Heat", "max_dofs": 6000000, "max_factor_entries": 400000000, "threads": 8} — steady or transient heat conduction of a heat transfer step (the only one when `step` is left out): temperatures, heat flux and the heat flow at prescribed temperatures, kept in the session; the caps may only be lowered; `threads` as for solve.static"#,
form: None,
form: Some(super::forms::SOLVE_STATIC),
kind: CommandKind::Model,
run: run_heat,
};
@@ -55,7 +55,7 @@ pub const FREQUENCY: CommandSpec = CommandSpec {
id: "solve.frequency",
label: "Solve Frequency Step",
params: r#"{"step": "Modes", "mass": "consistent|lumped", "shift": 0.0, "max_dofs": 6000000, "max_factor_entries": 400000000, "max_iterations": 3000, "threads": 8} — natural frequencies and mode shapes of a frequency step (its modes and range); `max_iterations` caps the Lanczos vectors; the result stays in the session; `threads` as for solve.static"#,
form: None,
form: Some(super::forms::SOLVE_FREQUENCY),
kind: CommandKind::Model,
run: run_frequency,
};
@@ -64,7 +64,7 @@ pub const EXPLICIT: CommandSpec = CommandSpec {
id: "solve.explicit",
label: "Solve Explicit Dynamic Step",
params: r#"{"step": "Impact", "max_dofs": 6000000, "max_increments": 1000000000, "max_output_values": 50000000, "max_contact_faces": 10000000, "threads": 8} — transient response of an explicit dynamic step (the only one when `step` is left out) by central differences, with the model's contacts (interaction.contact.*) and the step's rigid walls; keeps its histories and field frames in the session and reports each contact's peak force, first and last contact, deepest penetration, friction dissipated and the nodes adjusted at the start; the caps may only be lowered (the step's own increment cap applies too); `threads` as for solve.static"#,
form: None,
form: Some(super::forms::SOLVE_EXPLICIT),
kind: CommandKind::Model,
run: run_explicit,
};
@@ -73,7 +73,7 @@ pub const HISTORY: CommandSpec = CommandSpec {
id: "solve.history",
label: "Result Histories",
params: r#"{"step": "Impact", "every": 1} — the time histories of a solved explicit step: time, increment, energies, momentum, constraint force, the normal force across each contact (in `contacts` order) and the history nodes' displacement and velocity; `every` keeps one sample in so many (by default enough to return at most 10000)"#,
form: None,
form: Some(super::forms::SOLVE_HISTORY),
kind: CommandKind::Model,
run: history,
};
@@ -82,7 +82,7 @@ pub const FLUID: CommandSpec = CommandSpec {
id: "solve.fluid",
label: "Solve Fluid Step",
params: r#"{"step": "Flow", "max_cells": 16000000, "max_steps": 10000000, "max_cell_updates": 1e12, "max_frame_bytes": 2e9, "threads": 8} — runs a fluid step with the lattice Boltzmann solver (the only fluid step when `step` is left out): obstacles are triangulated from the open design or taken from imported surfaces; returns the time step, relaxation time, Mach number, mass and the final force on every wall; the caps may only be lowered; `threads` as for solve.static"#,
form: None,
form: Some(super::forms::SOLVE_FLUID),
kind: CommandKind::Model,
run: run_fluid,
};
@@ -91,7 +91,7 @@ pub const PROBE: CommandSpec = CommandSpec {
id: "solve.probe",
label: "Probe Results",
params: r#"{"step": "Load", "nodes": [1, 2] (or "set": "fillet", a set's nodes), "mode": 1, "frame": 0} — the stored result at nodes (with `max_von_mises`, the largest among them, for static steps): displacement, rotation, reaction, reaction moment, stress and von Mises (static, with `elements` each element's results: solid, 2D and truss nodal stresses (and, in a nonlinear step, the equivalent plastic strain at the integration points of a plastic element), shell section forces (N, M, Q per length in the element's axes) and bottom, middle and top stresses, beam end forces (N, V_y, V_z, T, M_y, M_z in its axes)), the shape of mode `mode` with its rotations (frequency, from 1), or displacement and velocity in field frame `frame` (explicit, from 0; the last by default) at `nodes` (or at the nodes nearest to points: "at": [[x, y, z], ...], lowest label on a tie, each entry then giving its `xyz` and `distance`) and, with `elements` (explicit), each element's stress, von Mises stress, strain, equivalent plastic strain, damage, temperature and whether it has eroded; a coupled reference point's label gives its six displacements (translations, rotations) and reactions (forces, moments). Fluid steps: {"step": "Flow", "points": [[x, y, z]], "frame": 10 (default the last)} gives velocity and pressure in the cells at the points; {"step": "Flow", "forces": ["x_min", "F2:top", "Body1/F2:top"]} gives those walls' force histories"#,
form: None,
form: Some(super::forms::SOLVE_PROBE),
kind: CommandKind::Model,
run: probe,
};
+2 -2
View File
@@ -11,13 +11,13 @@ pub const TAG: CommandSpec = CommandSpec {
id: "mesh.tag",
label: "Tag Mesh with Geometry",
params: r#"{"geometry": {"body": "Body1"} | {"body", "face"} | {"body", "edge"} | {"body", "vertex"}, "elements": [labels] (filling a body, face or edge), "faces": [{"element": 1, "face": 3}] (on a face), "edges": [{"element": 1, "edge": 2}] (on an edge), "nodes": [labels] (on a vertex)} — records where the mesh lies on the geometry, so sets named by geometry resolve"#,
form: None,
form: Some(super::forms::TAG),
kind: CommandKind::Model,
run: tag,
};
pub const TAG_CLEAR: CommandSpec =
CommandSpec { id: "mesh.tag.clear", label: "Clear Geometry Tags", params: "{}", form: None, kind: CommandKind::Model, run: tag_clear };
CommandSpec { id: "mesh.tag.clear", label: "Clear Geometry Tags", params: "{}", form: Some(&[]), kind: CommandKind::Model, run: tag_clear };
#[derive(serde::Deserialize)]
#[serde(deny_unknown_fields)]