forked from 1b-agent/fieldcraft-upstream
every_command_has_a_form failed on develop since #399. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2002 lines
95 KiB
Rust
2002 lines
95 KiB
Rust
//! 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).help("A named set of nodes, elements or faces."),
|
||
reference("reference_point", "Reference point", C::ReferencePoint).help("A reference point: a single node that stands for a rigid connection."),
|
||
];
|
||
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().help(concat!("The ", $label, " to act on, by name."))]
|
||
};
|
||
}
|
||
|
||
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 TIE_DELETE: &[Field] = existing!("Tie", C::Tie);
|
||
pub const STEP_DELETE: &[Field] = existing!("Step", C::Step);
|
||
pub const SET_RESOLVE: &[Field] = existing!("Set", C::Set);
|
||
|
||
// ---- Model definitions ----
|
||
|
||
pub const SET: &[Field] = &[
|
||
NAME,
|
||
rows(
|
||
"geometry",
|
||
"Geometry",
|
||
&[
|
||
reference("body", "Body", C::Body).help("A body of the open design.").req(),
|
||
text("face", "Face").help("A persistent face name of the body (e.g. F3); the whole body when empty.").hint("persistent face name"),
|
||
text("edge", "Edge").help("A persistent edge name of the body."),
|
||
text("vertex", "Vertex").help("A persistent vertex name of the body."),
|
||
],
|
||
)
|
||
.help("Bodies of the design, or their named faces, edges or vertices. Kept by name, so the set survives remeshing.")
|
||
.hint("whole bodies, or their faces, edges or vertices"),
|
||
labels("nodes", "Nodes", LabelOf::Node).help("Mesh node labels; Use selection takes the picked nodes.").hint("labels: 1, 2, 10-20"),
|
||
labels("elements", "Elements", LabelOf::Element)
|
||
.help("Mesh element labels; Use selection takes the picked elements.")
|
||
.hint("labels: 1, 2, 10-20"),
|
||
json("faces", "Element faces").help("Element faces, as a JSON list of element label and local face number.").hint("[{\"element\", \"face\"}]"),
|
||
json("edges", "Element edges").help("Element edges, as a JSON list of element label and local edge number.").hint("[{\"element\", \"edge\"}]"),
|
||
];
|
||
|
||
const ELASTIC: Field = variants(
|
||
"elastic",
|
||
"Elasticity",
|
||
&[
|
||
variant(
|
||
"isotropic",
|
||
"Isotropic",
|
||
&[
|
||
num("young", "Young's modulus", D::Stress)
|
||
.help("Stiffness in tension: stress over elastic strain. Steel ≈ 210 GPa (210000 MPa), aluminium ≈ 70 GPa, rubber a few MPa.")
|
||
.req(),
|
||
range("poisson", "Poisson's ratio", D::None, -1.0, 0.5)
|
||
.help("Lateral contraction over axial stretch, between -1 and 0.5. Metals ≈ 0.3, rubber close to 0.5.")
|
||
.req(),
|
||
],
|
||
),
|
||
variant(
|
||
"engineering_constants",
|
||
"Orthotropic",
|
||
&[
|
||
vector("young", "E1, E2, E3", D::Stress, &["E1", "E2", "E3"]).help("Young's moduli along the three material axes.").req(),
|
||
vector("poisson", "ν12, ν13, ν23", D::None, &["ν12", "ν13", "ν23"])
|
||
.help("Poisson's ratios between the material axes (contraction along the second axis for a stretch along the first).")
|
||
.req(),
|
||
vector("shear", "G12, G13, G23", D::Stress, &["G12", "G13", "G23"]).help("Shear moduli in the three material planes.").req(),
|
||
],
|
||
),
|
||
],
|
||
)
|
||
.help("How the material deforms elastically: the same in every direction (isotropic) or per material axis (orthotropic).")
|
||
.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).help("The initial yield stress: where elastic behaviour ends and plastic flow starts. 6061-T6 aluminium ≈ 276 MPa, mild steel ≈ 250 MPa.").req(),
|
||
num("hardening_modulus", "Hardening modulus", D::Stress).help("Slope of stress against plastic strain after yield. 0 is perfectly plastic; metals are often 1–5 % of Young's modulus (e.g. 2000 MPa for steel).").def("0"),
|
||
choice("hardening", "Hardening", HARDENING).help("Isotropic: the yield surface grows equally in every direction. Kinematic: it moves with the loading, so reversed loading yields earlier (Bauschinger effect; for cyclic loads).").def("\"isotropic\""),
|
||
],
|
||
),
|
||
variant(
|
||
"table",
|
||
"Table",
|
||
&[rows(
|
||
"points",
|
||
"Hardening curve",
|
||
&[positive("strain", "Plastic strain", D::None).help("Equivalent plastic strain of the point; the first point is 0.").req(), positive("stress", "Stress", D::Stress).help("Yield stress at that plastic strain.").req()],
|
||
).help("Points of yield stress against plastic strain, plastic strain rising from 0 at the initial yield.")
|
||
.req()],
|
||
),
|
||
variant(
|
||
"johnson_cook",
|
||
"Johnson–Cook",
|
||
&[
|
||
positive("a", "A", D::Stress).help("Johnson–Cook initial yield stress at the reference strain rate and room temperature. Steel ≈ 300–800 MPa.").req(),
|
||
num("b", "B", D::Stress).help("Johnson–Cook hardening coefficient: the stress added by plastic strain.").req(),
|
||
num("n", "n", D::None).help("Johnson–Cook hardening exponent, typically 0.1–0.6.").req(),
|
||
num("c", "C", D::None).help("Johnson–Cook strain-rate coefficient, typically 0.01–0.03; 0 ignores the strain rate.").def("0"),
|
||
num("m", "m", D::None).help("Johnson–Cook thermal softening exponent, typically about 1.").def("1"),
|
||
positive("reference_strain_rate", "Reference strain rate", D::None).help("The plastic strain rate at which A, B and n were measured, per time unit (often 1/s).").def("1").hint("per time unit"),
|
||
num("room_temperature", "Room temperature", D::Temperature).help("The temperature at which A, B and n hold; no thermal softening below it.").req(),
|
||
num("melt_temperature", "Melt temperature", D::Temperature).help("The temperature at which strength drops to zero. Steel ≈ 1800 K, aluminium ≈ 900 K.").req(),
|
||
positive("specific_heat", "Specific heat", D::SpecificHeat).help("Heat that raises a unit mass by one degree, for heating by plastic work. Steel ≈ 4.6e8 mm²/(s²·K) in mm-t-s.").hint("isothermal when empty"),
|
||
range("taylor_quinney", "Taylor–Quinney", D::None, 0.0, 1.0).help("Fraction of plastic work turned into heat, 0 to 1; usually 0.9.").def("0.9"),
|
||
],
|
||
),
|
||
],
|
||
).help("Permanent deformation once the stress passes the yield stress: bilinear, a table of points, or Johnson–Cook.");
|
||
|
||
const DAMAGE: Field = variants(
|
||
"damage",
|
||
"Damage",
|
||
&[variant(
|
||
"johnson_cook",
|
||
"Johnson–Cook",
|
||
&[
|
||
num("d1", "D1", D::None).help("Johnson–Cook failure strain constant. Typical metals: −0.8 to 0.2.").req(),
|
||
num("d2", "D2", D::None).help("Johnson–Cook failure strain constant multiplying the stress-triaxiality exponential.").req(),
|
||
num("d3", "D3", D::None).help("Johnson–Cook stress-triaxiality exponent constant, usually negative.").req(),
|
||
num("d4", "D4", D::None).help("Johnson–Cook strain-rate constant; 0 when empty."),
|
||
num("d5", "D5", D::None).help("Johnson–Cook temperature constant; 0 when empty."),
|
||
],
|
||
)],
|
||
)
|
||
.help("When the material fails: Johnson–Cook failure strain built from D1–D5.");
|
||
const EROSION: Field = group(
|
||
"erosion",
|
||
"Erosion",
|
||
&[positive("plastic_strain", "Plastic strain", D::None)
|
||
.help("Equivalent plastic strain at which an element is removed, e.g. 0.3–1 for ductile metals.")
|
||
.req()],
|
||
)
|
||
.help("Removes an element from an explicit analysis once it reaches the plastic strain given.");
|
||
|
||
const HYPERELASTIC: Field = variants(
|
||
"hyperelastic",
|
||
"Hyperelasticity",
|
||
&[
|
||
variant("neo_hookean", "Neo-Hookean", &[]),
|
||
variant(
|
||
"mooney_rivlin",
|
||
"Mooney–Rivlin",
|
||
&[num("c01", "C01", D::Stress).help("Mooney–Rivlin second constant; C10 comes from the isotropic shear modulus.").req()],
|
||
),
|
||
variant(
|
||
"yeoh",
|
||
"Yeoh",
|
||
&[
|
||
num("c20", "C20", D::Stress).help("Yeoh second-order constant.").req(),
|
||
num("c30", "C30", D::Stress).help("Yeoh third-order constant.").req(),
|
||
],
|
||
),
|
||
],
|
||
)
|
||
.help("A large-strain rubber model (nonlinear steps), built on the isotropic constants.")
|
||
.hint("large strain in nonlinear steps, from the isotropic constants");
|
||
|
||
const THERMAL: Field = group(
|
||
"thermal",
|
||
"Thermal",
|
||
&[
|
||
json("conductivity", "Conductivity")
|
||
.help("Thermal conductivity, one value or one per axis. Steel ≈ 50 W/(m·K), i.e. 50 in mm-t-s-N.")
|
||
.hint("k or [k1, k2, k3]"),
|
||
positive("specific_heat", "Specific heat", D::SpecificHeat)
|
||
.help("Heat that raises a unit mass by one degree. Steel ≈ 460 J/(kg·K), 4.6e8 in mm-t-s."),
|
||
json("expansion", "Expansion")
|
||
.help("Thermal expansion coefficient, one value or one per axis, in 1/K. Steel ≈ 1.2e-5, aluminium ≈ 2.3e-5.")
|
||
.hint("α or [α1, α2, α3], 1/K"),
|
||
num("reference_temperature", "Reference temperature", D::Temperature).help("The temperature at which there is no thermal strain.").def("0"),
|
||
],
|
||
)
|
||
.help("Heat conduction, specific heat and thermal expansion, for heat-transfer and thermal-stress steps.");
|
||
|
||
pub const MATERIAL: &[Field] = &[
|
||
NAME,
|
||
ELASTIC,
|
||
positive("density", "Density", D::Density)
|
||
.help("Mass per volume; needed for dynamics, frequencies and gravity. Steel ≈ 7.85e-9 t/mm³ (7850 kg/m³), aluminium ≈ 2.7e-9 t/mm³."),
|
||
PLASTIC,
|
||
HYPERELASTIC,
|
||
DAMAGE,
|
||
EROSION,
|
||
THERMAL,
|
||
variants(
|
||
"fluid",
|
||
"Fluid",
|
||
&[variant(
|
||
"newtonian",
|
||
"Newtonian",
|
||
&[positive("viscosity", "Viscosity", D::Viscosity).help("Dynamic viscosity. Water ≈ 1e-3 Pa·s (1e-9 MPa·s), air ≈ 1.8e-5 Pa·s.").req()],
|
||
)],
|
||
)
|
||
.help("Makes the material a fluid for a lattice Boltzmann flow analysis.")
|
||
.hint("a fluid needs a density"),
|
||
];
|
||
|
||
pub const MATERIAL_PLASTICITY: &[Field] =
|
||
&[reference("name", "Material", C::Material).help("The material to change.").req(), PLASTIC.req().def(r#"{"type": "bilinear"}"#)];
|
||
pub const MATERIAL_DAMAGE: &[Field] = &[
|
||
reference("name", "Material", C::Material).help("The material to change.").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).help("Cross-section width, along the section's first axis.").req(),
|
||
positive("height", "Height", D::Length).help("Cross-section height, along the section's second axis.").req(),
|
||
],
|
||
),
|
||
variant("circle", "Circle", &[positive("radius", "Radius", D::Length).help("Outer radius of the cross-section.").req()]),
|
||
variant(
|
||
"pipe",
|
||
"Pipe",
|
||
&[
|
||
positive("radius", "Radius", D::Length).help("Outer radius of the pipe.").req(),
|
||
positive("thickness", "Wall", D::Length).help("Wall thickness of the pipe.").req(),
|
||
],
|
||
),
|
||
variant(
|
||
"i",
|
||
"I-beam",
|
||
&[
|
||
positive("height", "Height", D::Length).help("Overall height of the I-beam.").req(),
|
||
positive("width", "Width", D::Length).help("Flange width.").req(),
|
||
positive("web", "Web", D::Length).help("Web thickness.").req(),
|
||
positive("flange", "Flange", D::Length).help("Flange thickness.").req(),
|
||
],
|
||
),
|
||
variant(
|
||
"box",
|
||
"Box",
|
||
&[
|
||
positive("width", "Width", D::Length).help("Outer width of the box.").req(),
|
||
positive("height", "Height", D::Length).help("Outer height of the box.").req(),
|
||
positive("thickness", "Wall", D::Length).help("Wall thickness of the box.").req(),
|
||
],
|
||
),
|
||
],
|
||
)
|
||
.help("The beam's cross-section shape.")
|
||
.req()
|
||
.def(r#"{"type": "rectangle"}"#);
|
||
|
||
pub const SECTION: &[Field] = &[
|
||
NAME,
|
||
reference("set", "Region", C::Set).help("The set of elements the section applies to.").req(),
|
||
reference("material", "Material", C::Material).help("The material of the section.").req(),
|
||
variants(
|
||
"kind",
|
||
"Kind",
|
||
&[
|
||
variant("solid", "Solid", &[]),
|
||
variant(
|
||
"shell",
|
||
"Shell",
|
||
&[
|
||
positive("thickness", "Thickness", D::Length).help("Shell thickness.").req(),
|
||
range("offset", "Offset", D::None, -0.5, 0.5)
|
||
.help("Where the reference surface sits, as a fraction of the thickness: 0 mid-surface, ±0.5 top or bottom.")
|
||
.def("0")
|
||
.hint("fraction of the thickness"),
|
||
int("points", "Integration points", 1, 15)
|
||
.help("Points through the thickness. 5 is usual; more for plasticity through the thickness.")
|
||
.def("5"),
|
||
],
|
||
),
|
||
variant(
|
||
"beam",
|
||
"Beam",
|
||
&[
|
||
PROFILE,
|
||
xyz("orientation", "Orientation", D::None)
|
||
.help("A direction that sets the beam section's first axis (not along the beam).")
|
||
.req()
|
||
.def("[0, 0, 1]"),
|
||
],
|
||
),
|
||
variant("rod", "Rod", &[positive("area", "Area", D::Area).help("Cross-section area of the rod.").req()]),
|
||
variant(
|
||
"plane_stress",
|
||
"Plane stress",
|
||
&[positive("thickness", "Thickness", D::Length).help("Out-of-plane thickness of the plane-stress elements.").req().def("1")],
|
||
),
|
||
variant(
|
||
"plane_strain",
|
||
"Plane strain",
|
||
&[positive("thickness", "Thickness", D::Length).help("Out-of-plane thickness of the plane-strain slice.").def("1")],
|
||
),
|
||
variant("axisymmetric", "Axisymmetric", &[]),
|
||
],
|
||
)
|
||
.help("What the elements are: solids, shells, beams, rods or 2D elements.")
|
||
.req()
|
||
.def(r#"{"type": "solid"}"#),
|
||
group(
|
||
"formulation",
|
||
"Formulation",
|
||
&[
|
||
choice("integration", "Integration", &[("full", "Full"), ("reduced", "Reduced")])
|
||
.help("Full: every integration point. Reduced: fewer points, cheaper and less stiff, but may need hourglass control.")
|
||
.def("\"full\""),
|
||
flag("incompatible_modes", "Incompatible modes")
|
||
.help("Extra bending modes for linear hexahedra, so a coarse hex8 mesh bends without locking.")
|
||
.def("false"),
|
||
flag("hybrid", "Hybrid").help("Pressure as an extra unknown, for nearly incompressible materials such as rubber.").def("false"),
|
||
],
|
||
)
|
||
.help("How the solid elements are integrated; the defaults suit most models."),
|
||
];
|
||
|
||
pub const REFERENCE_POINT: &[Field] = &[
|
||
NAME,
|
||
xyz("xyz", "Position", D::Length).help("The point's coordinates.").req().def("[0, 0, 0]"),
|
||
int("label", "Node label", 0, u32::MAX as i64).help("The node label written to decks.").hint("0: the highest free label"),
|
||
];
|
||
|
||
const DOFS: Field = multi("dofs", "Degrees of freedom", &[("1", "UX"), ("2", "UY"), ("3", "UZ"), ("4", "URX"), ("5", "URY"), ("6", "URZ")])
|
||
.help("Which of the six DOFs to couple (1–3 translations, 4–6 rotations); all when empty.")
|
||
.hint("all");
|
||
|
||
pub const COUPLING: &[Field] = &[
|
||
NAME,
|
||
reference("reference_point", "Reference point", C::ReferencePoint).help("The reference point the region follows.").req(),
|
||
reference("set", "Region", C::Set).help("The nodes or faces coupled to the reference point.").req(),
|
||
choice("kind", "Kind", &[("kinematic", "Kinematic"), ("distributing", "Distributing")])
|
||
.help("Kinematic: the region moves rigidly with the point. Distributing: forces spread over the region, which stays free to deform.")
|
||
.req()
|
||
.def("\"kinematic\""),
|
||
DOFS,
|
||
];
|
||
|
||
pub const MASS: &[Field] = &[
|
||
NAME,
|
||
TARGET,
|
||
positive("mass", "Mass", D::Mass).help("A lumped mass at each node of the target.").hint("per node; [mx, my, mz] in JSON"),
|
||
vector("inertia", "Inertia", D::Inertia, &["I11", "I22", "I33", "I12", "I13", "I23"])
|
||
.help("Rotary inertia about the reference point: three moments and three products.")
|
||
.hint("about the node; needs a reference point"),
|
||
];
|
||
|
||
const SPRING_FIRST: Field = target("first", "First end").help("Where the spring starts: a set or a reference point.").req();
|
||
const SPRING_SECOND: Field = target("second", "Second end").help("Where the spring ends; ground when empty.").hint("ground when empty");
|
||
const SPRING_DIRECTION: Field = either(
|
||
"direction",
|
||
"Direction",
|
||
&[
|
||
int("dof", "DOF", 1, 6).help("1–3 translation along X, Y, Z; 4–6 rotation about them."),
|
||
xyz("vector", "Vector", D::None).help("A direction vector; its length does not matter."),
|
||
unit("line", "Along the ends").help("Along the line from the first end to the second."),
|
||
],
|
||
)
|
||
.help("The direction the spring acts in: one DOF, a vector, or the line between its 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).help("Force per unit stretch. For a 10 N/mm spring enter 10 in mm-t-s-N.").req(),
|
||
],
|
||
),
|
||
variant(
|
||
"dashpot",
|
||
"Dashpot",
|
||
&[
|
||
SPRING_FIRST,
|
||
SPRING_SECOND,
|
||
SPRING_DIRECTION,
|
||
positive("coefficient", "Coefficient", D::Damping).help("Damping force per unit velocity.").req(),
|
||
],
|
||
),
|
||
],
|
||
)
|
||
.help("What kind of connector: a spring (stiffness) or a dashpot (damping).")
|
||
.req()
|
||
.def("\"spring\""),
|
||
];
|
||
|
||
/// The target of a boundary condition, force, moment or temperature.
|
||
const TARGET: Field = target("target", "Target").help("Where it applies: a set or a reference point.").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"])
|
||
.help("Displacements u1–u3 along X, Y, Z and rotations ur1–ur3; empty entries stay free.")
|
||
.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")])
|
||
.help("The axis normal to the symmetry plane: its translation and the two rotations about the other axes are held.")
|
||
.req()
|
||
.def("\"x\""),
|
||
],
|
||
),
|
||
variant("temperature", "Temperature", &[TARGET, num("value", "Temperature", D::Temperature).help("The prescribed temperature.").req()]),
|
||
],
|
||
)
|
||
.help("What is held: everything fixed, given displacements, a symmetry plane or a temperature.")
|
||
.req()
|
||
.def("\"fixed\""),
|
||
];
|
||
|
||
const DISTRIBUTION: Field = choice("distribution", "Distribution", &[("total", "Total"), ("per_node", "Per node")])
|
||
.help("Total: the value is split over the target's nodes. Per node: every node gets the full value.")
|
||
.def("\"total\"");
|
||
const LOAD_SET: Field = reference("set", "Region", C::Set).help("The faces or elements the load acts on.").req();
|
||
|
||
pub const LOAD: &[Field] = &[
|
||
NAME,
|
||
variants(
|
||
"",
|
||
"Type",
|
||
&[
|
||
variant(
|
||
"force",
|
||
"Force",
|
||
&[TARGET, xyz("vector", "Force", D::Force).help("The force, X, Y and Z components in the global axes.").req(), DISTRIBUTION],
|
||
),
|
||
variant(
|
||
"moment",
|
||
"Moment",
|
||
&[TARGET, xyz("vector", "Moment", D::Moment).help("The moment, about the X, Y and Z axes.").req(), DISTRIBUTION],
|
||
),
|
||
variant(
|
||
"pressure",
|
||
"Pressure",
|
||
&[
|
||
LOAD_SET,
|
||
num("magnitude", "Pressure", D::Stress)
|
||
.help("Pressure normal to the faces; positive pushes into the surface. 1 MPa = 1 N/mm².")
|
||
.req()
|
||
.hint("positive pushes in"),
|
||
],
|
||
),
|
||
variant(
|
||
"traction",
|
||
"Traction",
|
||
&[LOAD_SET, xyz("vector", "Traction", D::Stress).help("Force per area on the faces, X, Y and Z components.").req()],
|
||
),
|
||
variant(
|
||
"gravity",
|
||
"Gravity",
|
||
&[
|
||
xyz("acceleration", "Acceleration", D::Acceleration)
|
||
.help("Gravity or other acceleration. Earth gravity downward is [0, 0, -9810] mm/s² in mm-t-s.")
|
||
.req(),
|
||
reference("set", "Region", C::Set).help("Where gravity acts; the whole model when empty.").hint("the whole model"),
|
||
],
|
||
),
|
||
variant(
|
||
"body_force",
|
||
"Body force",
|
||
&[LOAD_SET, xyz("vector", "Force per volume", D::ForcePerVolume).help("A body force per unit volume, X, Y and Z components.").req()],
|
||
),
|
||
variant(
|
||
"centrifugal",
|
||
"Centrifugal",
|
||
&[
|
||
num("angular_velocity", "Angular velocity", D::AngularVelocity)
|
||
.help("Spin speed in radians per time unit. 3000 rpm ≈ 314 rad/s.")
|
||
.req(),
|
||
xyz("point", "Point on axis", D::Length).help("A point on the axis of rotation.").req().def("[0, 0, 0]"),
|
||
xyz("axis", "Axis", D::None).help("Direction of the axis of rotation.").req().def("[0, 0, 1]"),
|
||
reference("set", "Region", C::Set).help("Where the centrifugal load acts; the whole model when empty.").hint("the whole model"),
|
||
],
|
||
),
|
||
variant(
|
||
"surface_flux",
|
||
"Surface heat flux",
|
||
&[LOAD_SET, num("flux", "Flux", D::HeatFlux).help("Heat flow per area into the surface.").req()],
|
||
),
|
||
variant(
|
||
"convection",
|
||
"Convection",
|
||
&[
|
||
LOAD_SET,
|
||
positive("film", "Film coefficient", D::FilmCoefficient)
|
||
.help("Convective heat transfer coefficient. Still air ≈ 5–25 W/(m²·K), water 500–10000.")
|
||
.req(),
|
||
num("sink", "Sink temperature", D::Temperature).help("The temperature of the surrounding fluid.").req(),
|
||
],
|
||
),
|
||
variant(
|
||
"heat_source",
|
||
"Heat source",
|
||
&[LOAD_SET, num("power", "Power per volume", D::None).help("Heat generated per unit volume.").req()],
|
||
),
|
||
variant("temperature", "Temperature", &[TARGET, num("value", "Temperature", D::Temperature).help("The prescribed temperature.").req()]),
|
||
],
|
||
)
|
||
.help("What kind of load: forces, pressures, gravity, heat and more.")
|
||
.req()
|
||
.def("\"force\""),
|
||
];
|
||
|
||
const FACE_CONDITION: &[Variant] = &[
|
||
variant("wall", "Wall", &[]),
|
||
variant("moving_wall", "Moving wall", &[xyz("velocity", "Velocity", D::Velocity).help("The velocity of the moving wall.").req()]),
|
||
variant("slip", "Slip", &[]),
|
||
variant(
|
||
"velocity",
|
||
"Velocity inlet",
|
||
&[
|
||
xyz("velocity", "Velocity", D::Velocity).help("Inflow velocity.").req(),
|
||
choice("profile", "Profile", &[("uniform", "Uniform"), ("parabolic", "Parabolic")])
|
||
.help("Uniform across the inlet, or parabolic (fully developed flow).")
|
||
.def("\"uniform\""),
|
||
],
|
||
),
|
||
variant(
|
||
"pressure",
|
||
"Pressure outlet",
|
||
&[num("pressure", "Pressure", D::Stress).help("Pressure at the outlet, relative to the domain's reference.").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).help("The fluid material: needs a density and a viscosity.").req(),
|
||
xyz("min", "Box min", D::Length).help("The box corner with the smallest coordinates.").req(),
|
||
xyz("max", "Box max", D::Length).help("The box corner with the largest coordinates.").req(),
|
||
positive("spacing", "Cell size", D::Length)
|
||
.help("Lattice cell edge length. Smaller is more accurate and costs as the cube of the refinement.")
|
||
.req(),
|
||
group(
|
||
"faces",
|
||
"Box faces",
|
||
&[
|
||
face("x_min", "X min").help("Condition on the box face at the smallest X."),
|
||
face("x_max", "X max").help("Condition on the box face at the largest X."),
|
||
face("y_min", "Y min").help("Condition on the box face at the smallest Y."),
|
||
face("y_max", "Y max").help("Condition on the box face at the largest Y."),
|
||
face("z_min", "Z min").help("Condition on the box face at the smallest Z."),
|
||
face("z_max", "Z max").help("Condition on the box face at the largest Z."),
|
||
],
|
||
)
|
||
.help("The condition on each face of the box."),
|
||
references("obstacles", "Obstacles", C::Body).help("Bodies of the design the flow goes around."),
|
||
json("surfaces", "Obstacle faces")
|
||
.help("Conditions on single obstacle faces, as JSON; walls by default.")
|
||
.hint("[{\"face\", \"condition\"}]; walls by default"),
|
||
xyz("initial_velocity", "Initial velocity", D::Velocity).help("The fluid's velocity at the start."),
|
||
xyz("acceleration", "Acceleration", D::Acceleration).help("A body acceleration on the fluid, such as gravity."),
|
||
positive("tolerance", "Obstacle tolerance", D::Length)
|
||
.help("How close a cell must be to an obstacle surface to count as on it.")
|
||
.hint("a tenth of the cell size"),
|
||
];
|
||
|
||
pub const FLUID_BOUNDARY: &[Field] = &[
|
||
reference("domain", "Domain", C::FluidDomain).help("The fluid domain to change.").req(),
|
||
json("at", "Boundary")
|
||
.help("A box face (x_min … z_max) or an obstacle face, as JSON.")
|
||
.req()
|
||
.def("\"x_min\"")
|
||
.hint("\"x_min\" … \"z_max\", or {\"body\", \"face\"}"),
|
||
variants("condition", "Condition", FACE_CONDITION).help("The flow condition on that boundary.").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")
|
||
.help("Account for large displacements and rotations. Needed for buckling-like and large-deflection problems.")
|
||
.def("true"),
|
||
range("initial_increment", "Initial increment", D::None, 0.0, 1.0)
|
||
.help("First increment, as a fraction of the step (0.1 = 10 %).")
|
||
.def("0.1"),
|
||
range("min_increment", "Min increment", D::None, 0.0, 1.0).help("Smallest increment allowed before the step gives up.").def("1e-5"),
|
||
range("max_increment", "Max increment", D::None, 0.0, 1.0).help("Largest increment, as a fraction of the step.").def("1.0"),
|
||
int("max_increments", "Max increments", 1, 1_000_000).help("The most increments the step may take.").def("100"),
|
||
int("max_iterations", "Iterations per increment", 1, 1000).help("Newton iterations before an increment is cut back.").def("16"),
|
||
group(
|
||
"tolerances",
|
||
"Tolerances",
|
||
&[
|
||
positive("force", "Force", D::None).help("Residual force, relative to the applied forces.").def("1e-6"),
|
||
positive("displacement", "Displacement", D::None)
|
||
.help("Displacement correction, relative to the increment's displacement.")
|
||
.def("1e-5"),
|
||
positive("energy", "Energy", D::None).help("Work of the residual, relative to the increment's work.").def("1e-10"),
|
||
],
|
||
)
|
||
.help("Convergence tolerances; the defaults suit most models."),
|
||
flag("line_search", "Line search")
|
||
.help("Scale each Newton correction to reduce the residual; helps with hard convergence.")
|
||
.def("false"),
|
||
json("load_path", "Load path")
|
||
.help("How the load factor varies through the step, as JSON pairs of step fraction and factor; a linear ramp when empty.")
|
||
.hint("[[fraction, factor], …]; a ramp when empty"),
|
||
group(
|
||
"imperfection",
|
||
"Imperfection",
|
||
&[
|
||
reference("step", "Buckling step", C::Step).help("The solved buckling step whose mode is used.").req(),
|
||
int("mode", "Mode", 1, 100_000).help("Which buckling mode, from 1.").req().def("1"),
|
||
num("scale", "Largest component", D::Length)
|
||
.help("The largest displacement of the scaled mode, e.g. 1 % of the thickness.")
|
||
.req(),
|
||
],
|
||
)
|
||
.help("Seed a buckling mode shape into the geometry, for post-buckling analysis."),
|
||
group(
|
||
"arc_length",
|
||
"Arc length",
|
||
&[
|
||
choice("constraint", "Constraint", &[("cylindrical", "Cylindrical"), ("spherical", "Spherical")])
|
||
.help("Cylindrical: displacements only. Spherical: displacements and load factor.")
|
||
.def("\"cylindrical\""),
|
||
positive("max_load_factor", "Max load factor", D::None).help("Stop when the load factor reaches this.").def("1"),
|
||
json("displacement_limit", "Displacement limit")
|
||
.help("Stop when a node's displacement reaches a value, as JSON.")
|
||
.hint("{\"node\": 1, \"direction\": 3, \"displacement\": -10}"),
|
||
],
|
||
)
|
||
.help("Follow the load-displacement path through limit points (snap-through), with the load factor as an unknown."),
|
||
],
|
||
),
|
||
variant(
|
||
"buckle",
|
||
"Buckling",
|
||
&[
|
||
int("modes", "Modes", 1, 100_000).help("How many buckling modes to find.").req().def("4"),
|
||
num("shift", "Shift", D::None).help("Look for eigenvalues near this value; 0 finds the lowest."),
|
||
],
|
||
),
|
||
variant(
|
||
"frequency",
|
||
"Frequency",
|
||
&[
|
||
int("modes", "Modes", 1, 10_000).help("How many natural frequencies to find.").req().def("10"),
|
||
positive("min_hz", "Min frequency", D::Hertz).help("Lowest frequency of interest."),
|
||
positive("max_hz", "Max frequency", D::Hertz).help("Highest frequency of interest."),
|
||
],
|
||
),
|
||
variant(
|
||
"explicit_dynamic",
|
||
"Dynamic, explicit",
|
||
&[
|
||
positive("end_time", "End time", D::Time).help("Duration of the step. An impact is typically a few milliseconds.").req(),
|
||
range("dt_scale", "Time step scale", D::None, 0.0, 1.0)
|
||
.help("Fraction of the stable time step used, 0 to 1; 0.9 is usual.")
|
||
.def("0.9"),
|
||
int("max_increments", "Max increments", 1, 10_000_000_000).help("The most time increments the step may take.").def("10000000"),
|
||
positive("output_interval", "Field output every", D::Time)
|
||
.help("Time between saved result frames; ten frames over the step when empty."),
|
||
positive("history_interval", "History every", D::Time).help("Time between history samples."),
|
||
reference("history_nodes", "History nodes", C::Set).help("The nodes whose displacements are recorded over time."),
|
||
rows(
|
||
"initial_velocities",
|
||
"Initial velocities",
|
||
&[
|
||
reference("set", "Set", C::Set).help("The set given the velocity.").req(),
|
||
xyz("velocity", "Velocity", D::Velocity).help("Its starting velocity.").req(),
|
||
],
|
||
)
|
||
.help("Starting velocities of sets, for impacts and drops."),
|
||
rows(
|
||
"rigid_walls",
|
||
"Rigid walls",
|
||
&[
|
||
reference("set", "Set", C::Set).help("The nodes the wall stops.").req(),
|
||
xyz("point", "Point", D::Length).help("A point on the wall.").req(),
|
||
xyz("normal", "Normal", D::None).help("The wall's normal, pointing to the side the nodes stay on.").req(),
|
||
FRICTION,
|
||
],
|
||
)
|
||
.help("Flat walls the set's nodes cannot pass."),
|
||
group(
|
||
"hourglass",
|
||
"Hourglass control",
|
||
&[
|
||
choice("type", "Type", &[("viscous", "Viscous"), ("stiffness", "Stiffness")])
|
||
.help("Viscous: damping, for high-speed events. Stiffness: springs, for slower ones.")
|
||
.def("\"viscous\""),
|
||
positive("coefficient", "Coefficient", D::None).help("Strength of the hourglass control, typically 0.03–0.15."),
|
||
],
|
||
)
|
||
.help("Suppresses zero-energy modes of reduced-integration elements."),
|
||
group(
|
||
"bulk_viscosity",
|
||
"Bulk viscosity",
|
||
&[
|
||
positive("linear", "Linear", D::None).help("Linear bulk viscosity coefficient, typically 0.06."),
|
||
positive("quadratic", "Quadratic", D::None).help("Quadratic bulk viscosity coefficient, typically 1.2."),
|
||
],
|
||
)
|
||
.help("Damps shock-wave ringing."),
|
||
flag("nonlinear_geometry", "Nonlinear geometry").help("Account for large displacements and rotations.").def("true"),
|
||
choice("stress_rate", "Stress rate", &[("jaumann", "Jaumann"), ("green_naghdi", "Green–Naghdi")])
|
||
.help("How stresses rotate with the material: Jaumann or Green–Naghdi."),
|
||
group(
|
||
"mass_scaling",
|
||
"Mass scaling",
|
||
&[
|
||
positive("target_increment", "Target increment", D::Time).help("The time step mass scaling aims for.").req(),
|
||
reference("set", "Set", C::Set).help("Where mass is added; the whole model when empty."),
|
||
],
|
||
)
|
||
.help("Adds mass to small elements so the time step reaches the target; check the added mass stays small."),
|
||
],
|
||
),
|
||
variant(
|
||
"heat_transfer",
|
||
"Heat transfer",
|
||
&[group(
|
||
"transient",
|
||
"Transient",
|
||
&[
|
||
positive("end_time", "End time", D::Time).help("Duration of the transient.").req(),
|
||
positive("increment", "Increment", D::Time).help("Time step of the transient.").req(),
|
||
range("theta", "θ", D::None, 0.0, 1.0).help("Time integration: 0.5 Crank–Nicolson, 1 backward Euler (default, most stable)."),
|
||
num("initial_temperature", "Initial temperature", D::Temperature).help("Temperature at the start of the transient."),
|
||
],
|
||
)
|
||
.help("Solve over time instead of for the steady state.")
|
||
.hint("steady state when off")],
|
||
),
|
||
variant("coupled_temperature_displacement", "Coupled temperature–displacement", &[]),
|
||
variant(
|
||
"fluid",
|
||
"Fluid (lattice Boltzmann)",
|
||
&[
|
||
reference("domain", "Domain", C::FluidDomain).help("The fluid domain to solve.").req(),
|
||
positive("end_time", "End time", D::Time).help("Simulated flow time, or give Steps.").hint("or Steps"),
|
||
int("steps", "Steps", 1, 1_000_000_000_000).help("Lattice time steps, instead of End time."),
|
||
positive("time_step", "Time step", D::Time)
|
||
.help("The lattice time step; chosen for a Mach number of 0.087 when empty.")
|
||
.hint("chosen for Mach 0.087"),
|
||
int("frames", "Frames", 1, 100_000).help("How many result frames to keep.").def("10"),
|
||
positive("magic", "TRT Λ", D::None).help("The two-relaxation-time magic parameter; 0.1875 is usual.").hint("0.1875"),
|
||
],
|
||
),
|
||
],
|
||
)
|
||
.help("What the step solves: linear or nonlinear static, buckling, frequencies, explicit dynamics, heat transfer or flow.");
|
||
|
||
const OUTPUT: Field = multi(
|
||
"output",
|
||
"Output",
|
||
&[
|
||
("\"displacement\"", "Displacement"),
|
||
("\"reaction_force\"", "Reaction force"),
|
||
("\"stress\"", "Stress"),
|
||
("\"strain\"", "Strain"),
|
||
("\"temperature\"", "Temperature"),
|
||
("\"heat_flux\"", "Heat flux"),
|
||
],
|
||
)
|
||
.help("Which results to keep; by procedure when none is ticked.")
|
||
.hint("by procedure");
|
||
const STEP_BCS: Field =
|
||
references("boundary_conditions", "Boundary conditions", C::BoundaryCondition).help("The boundary conditions active in the step.");
|
||
const STEP_LOADS: Field = references("loads", "Loads", C::Load).help("The loads active in the step.");
|
||
|
||
pub const STEP_CREATE: &[Field] = &[
|
||
NAME,
|
||
PROCEDURE.req().def(r#"{"type": "static"}"#),
|
||
OUTPUT,
|
||
STEP_BCS,
|
||
STEP_LOADS,
|
||
reference("before", "Insert before", C::Step).help("The step the new one goes before; last when empty.").hint("last"),
|
||
];
|
||
|
||
pub const STEP_EDIT: &[Field] = &[reference("name", "Step", C::Step).help("The step to change.").req(), PROCEDURE, OUTPUT, STEP_BCS, STEP_LOADS];
|
||
|
||
pub const STEP_ACTIVATE: &[Field] = &[
|
||
reference("step", "Step", C::Step).help("The step to change.").req(),
|
||
references("boundary_conditions", "Boundary conditions", C::BoundaryCondition).help("The boundary conditions to switch on or off in the step."),
|
||
references("loads", "Loads", C::Load).help("The loads to switch on or off in the step."),
|
||
flag("active", "Active").help("On: they act in the step. Off: they are switched off from it on.").def("true"),
|
||
];
|
||
|
||
pub const DATUM_SUPPRESS: &[Field] = &[
|
||
reference("name", "Datum", C::Datum).help("The datum to keep but leave out of the model.").req(),
|
||
flag("suppressed", "Suppressed").help("On: left out (its partitions stop cutting). Off: back in the model.").def("true"),
|
||
];
|
||
|
||
pub const PARTITION_SUPPRESS: &[Field] = &[
|
||
reference("name", "Partition", C::Partition).help("The partition to keep but leave out: its cells merge back.").req(),
|
||
flag("suppressed", "Suppressed").help("On: the design is cut without it. Off: it cuts again.").def("true"),
|
||
];
|
||
|
||
pub const RENAME: &[Field] = &[
|
||
choice(
|
||
"kind",
|
||
"Kind",
|
||
&[
|
||
("set", "Set"),
|
||
("material", "Material"),
|
||
("section", "Section"),
|
||
("step", "Step"),
|
||
("boundary_condition", "Boundary condition"),
|
||
("load", "Load"),
|
||
("reference_point", "Reference point"),
|
||
("coupling", "Coupling"),
|
||
("contact", "Contact"),
|
||
("tie", "Tie"),
|
||
("point_mass", "Point mass"),
|
||
("spring", "Spring"),
|
||
("fluid_domain", "Fluid domain"),
|
||
("datum", "Datum"),
|
||
],
|
||
)
|
||
.help("What kind of definition to rename.")
|
||
.req(),
|
||
text("from", "Name").help("Its current name.").req(),
|
||
text("to", "New name").help("Every definition that refers to it is changed to the new name.").req(),
|
||
];
|
||
|
||
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"),
|
||
],
|
||
).help("The units the model's numbers are in. FieldCraft never converts: enter every value in this system. Designs from SolidCraft are in mm, so mm-t-s-N-MPa fits them.")
|
||
.req()
|
||
.def("\"mm-t-s-N-MPa\"")];
|
||
|
||
// ---- Contact ----
|
||
|
||
const FRICTION: Field = group(
|
||
"friction",
|
||
"Friction",
|
||
&[
|
||
positive("static", "Static μ", D::None)
|
||
.help("Friction coefficient at rest. Steel on steel ≈ 0.15–0.8, rubber on concrete ≈ 0.6–1.")
|
||
.hint("0"),
|
||
positive("kinetic", "Kinetic μ", D::None)
|
||
.help("Friction coefficient while sliding, usually a little lower than the static.")
|
||
.hint("the static μ"),
|
||
positive("decay", "Decay", D::None).help("How fast friction falls from static to kinetic with slip rate.").hint("0; per slip rate"),
|
||
],
|
||
)
|
||
.help("Coulomb friction between the surfaces; frictionless when off.")
|
||
.hint("frictionless");
|
||
const PENALTY: Field = range("penalty_scale", "Penalty scale", D::None, 0.0, 1.0)
|
||
.help("Scales the contact stiffness. Lower softens contact and helps convergence, but lets surfaces interpenetrate more.")
|
||
.def("1");
|
||
const PENETRATION: Field = choice("initial_penetration", "Initial penetration", &[("adjust", "Adjust nodes"), ("refuse", "Refuse")])
|
||
.help("Adjust: move secondary nodes onto the main surface at the start. Refuse: report overlaps as errors.")
|
||
.def("\"adjust\"");
|
||
|
||
pub const CONTACT_PAIR: &[Field] = &[
|
||
NAME,
|
||
reference("main", "Main surface", C::Set).help("The stiffer or coarser surface.").req(),
|
||
reference("secondary", "Secondary surface", C::Set).help("The finer surface; its nodes are kept out of the main surface.").req(),
|
||
FRICTION,
|
||
PENALTY,
|
||
PENETRATION,
|
||
];
|
||
pub const CONTACT_GENERAL: &[Field] = &[
|
||
NAME,
|
||
reference("surface", "Surface", C::Set).help("The faces that may touch each other; every exterior face when empty.").hint("every exterior face"),
|
||
FRICTION,
|
||
PENALTY,
|
||
PENETRATION,
|
||
];
|
||
pub const CONTACT_EDIT: &[Field] = &[
|
||
reference("name", "Contact", C::Contact).help("The contact to change.").req(),
|
||
variants(
|
||
"surfaces",
|
||
"Surfaces",
|
||
&[
|
||
variant(
|
||
"pair",
|
||
"Pair",
|
||
&[
|
||
reference("main", "Main", C::Set).help("The stiffer or coarser surface.").req(),
|
||
reference("secondary", "Secondary", C::Set).help("The finer surface.").req(),
|
||
],
|
||
),
|
||
variant(
|
||
"general",
|
||
"General",
|
||
&[reference("surface", "Surface", C::Set)
|
||
.help("The faces that may touch; every exterior face when empty.")
|
||
.hint("every exterior face")],
|
||
),
|
||
],
|
||
)
|
||
.help("A pair of surfaces, or general contact among many faces.")
|
||
.req(),
|
||
FRICTION,
|
||
PENALTY,
|
||
PENETRATION,
|
||
];
|
||
pub const TIE: &[Field] = &[
|
||
NAME,
|
||
reference("main", "Main surface", C::Set).help("The surface the secondary is tied to; usually the coarser mesh.").req(),
|
||
reference("secondary", "Secondary surface", C::Set).help("The surface whose nodes are tied to the main surface.").req(),
|
||
positive("position_tolerance", "Position tolerance", D::Length)
|
||
.help("Secondary nodes farther than this from the main surface are left untied.")
|
||
.hint("half the main element's thickness"),
|
||
];
|
||
pub const TIE_EDIT: &[Field] = &[
|
||
reference("name", "Tie", C::Tie).help("The tie to change.").req(),
|
||
reference("main", "Main surface", C::Set).help("The surface the secondary is tied to; usually the coarser mesh.").req(),
|
||
reference("secondary", "Secondary surface", C::Set).help("The surface whose nodes are tied to the main surface.").req(),
|
||
positive("position_tolerance", "Position tolerance", D::Length)
|
||
.help("Secondary nodes farther than this from the main surface are left untied.")
|
||
.hint("half the main element's thickness"),
|
||
];
|
||
pub const CONTACT_FIND: &[Field] = &[
|
||
positive("tolerance", "Tolerance", D::Length).help("Faces closer than this count as touching.").hint("a tenth of the mean face size"),
|
||
flag("create", "Create pairs").help("Also create a contact for each touching pair found.").def("false"),
|
||
FRICTION,
|
||
PENALTY,
|
||
PENETRATION,
|
||
];
|
||
|
||
// ---- Meshing ----
|
||
|
||
const BODY: Field =
|
||
reference("body", "Body", C::Body).help("The body to mesh; every body of the design when empty.").hint("every body of the design");
|
||
const SURFACE: Field =
|
||
int("surface", "Imported surface", 0, 1_000_000).help("An imported surface mesh to mesh instead of a design body.").hint("instead of a body");
|
||
const TOLERANCE: Field = positive("tolerance", "Chordal tolerance", D::Millimetre)
|
||
.help("Largest distance between the tessellation and the true curved surface. Smaller follows curves closer, with more elements.")
|
||
.hint("1e-3 of the diagonal");
|
||
const WELD: Field = range("weld", "Weld", D::None, 0.0, 1e-3)
|
||
.help("Points closer than this fraction of the diagonal are merged when the surface is assembled.")
|
||
.hint("1e-9 of the diagonal");
|
||
const WELD_DISTANCE: Field = positive("weld_distance", "Weld distance", D::Millimetre)
|
||
.help("Points closer than this are merged when the surface is assembled.")
|
||
.hint("the surface's own");
|
||
const FEATURE_ANGLE: Field = range("feature_angle", "Feature angle", D::Degree, 0.0, 180.0)
|
||
.help("Edges where faces meet at more than this angle are kept as sharp features.")
|
||
.hint("45");
|
||
|
||
pub const TET: &[Field] = &[
|
||
BODY,
|
||
SURFACE,
|
||
positive("size", "Element size", D::Millimetre)
|
||
.help("Target element edge length. Several elements through the thinnest wall are needed for accurate stresses.")
|
||
.hint("1/20 of the diagonal"),
|
||
choice("order", "Order", &[("linear", "Linear (tet4)"), ("quadratic", "Quadratic (tet10)")])
|
||
.help("Linear (tet4) is fast but stiff in bending; quadratic (tet10) is far more accurate for stress.")
|
||
.def("\"linear\""),
|
||
TOLERANCE,
|
||
flag("quality", "Improve quality").help("Smooth and swap elements after meshing to remove slivers.").def("true"),
|
||
range("quality_target", "Quality target", D::Degree, 5.0, 60.0).help("The smallest dihedral angle quality improvement aims for.").hint("40"),
|
||
range("curvature_angle", "Curvature angle", D::Degree, 0.0, 90.0)
|
||
.help("Angle an element may span on a curved surface; smaller refines curved regions.")
|
||
.hint("22.5"),
|
||
range("grading", "Grading", D::None, 0.05, 2.0).help("How fast element size may grow from small to large; smaller is smoother.").hint("0.3"),
|
||
int("max_passes", "Improvement passes", 0, 200).help("The most quality-improvement passes.").hint("20"),
|
||
int("max_points", "Max points", 1, 20_000_000).help("Cap on the points the mesher creates.").hint("1000000"),
|
||
int("max_steiner", "Max Steiner points", 0, 10_000_000).help("Cap on the points added to recover the surface.").hint("50000"),
|
||
int("max_tets", "Max tetrahedra", 1, 50_000_000).help("Cap on the tetrahedra created.").hint("5000000"),
|
||
WELD,
|
||
WELD_DISTANCE,
|
||
FEATURE_ANGLE,
|
||
];
|
||
|
||
const HEX_SIZE: Field = positive("size", "Element size", D::Millimetre).help("Target element edge length.").hint("1/25 of the diagonal");
|
||
const HEX_ORDER: Field =
|
||
int("order", "Order", 1, 2).help("1: linear hex8. 2: quadratic hex20, more accurate in bending.").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")],
|
||
)
|
||
.help(
|
||
"How to build the hexes. Automatic tries, in order, an extrusion, a sweep, a revolution and a mapped block, and \
|
||
uses the first that fits; Any sweep tries the first three.",
|
||
)
|
||
.notes(HEX_METHOD_NOTES)
|
||
.def("\"auto\""),
|
||
TOLERANCE,
|
||
WELD,
|
||
WELD_DISTANCE,
|
||
FEATURE_ANGLE,
|
||
];
|
||
|
||
/// What each structured hex method needs of the body, one line each.
|
||
const HEX_METHOD_NOTES: &[(&str, &str)] = &[
|
||
("auto", "Automatic: the first of Extrude, Sweep, Revolve and Mapped that fits the body."),
|
||
("swept", "Any sweep: the first of Extrude, Sweep and Revolve that fits the body."),
|
||
("extrude", "Extrude: the body must be one cross-section carried along a straight direction (levels allowed)."),
|
||
("sweep", "Sweep: a source face carried to a target face through four-sided linking faces."),
|
||
("revolve", "Revolve: the body must be one cross-section turned around an axis."),
|
||
("mapped", "Mapped: the body must have six sides, each with four edges (curved faces allowed)."),
|
||
];
|
||
|
||
pub const HEX_SWEEP: &[Field] = &[
|
||
BODY,
|
||
SURFACE,
|
||
HEX_SIZE,
|
||
HEX_ORDER,
|
||
choice("method", "Method", &[("swept", "Any sweep"), ("extrude", "Extrude"), ("sweep", "Sweep"), ("revolve", "Revolve")])
|
||
.help("How to sweep: any sweep, extrusion, sweep along a path or revolution.")
|
||
.notes(HEX_METHOD_NOTES)
|
||
.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)
|
||
.help("The cells (bodies) to mesh as blocks; every body of the design when empty.")
|
||
.hint("every body of the design"),
|
||
HEX_SIZE,
|
||
HEX_ORDER,
|
||
TOLERANCE,
|
||
WELD,
|
||
WELD_DISTANCE,
|
||
FEATURE_ANGLE,
|
||
];
|
||
|
||
const CELL_METHODS: &[(&str, &str)] = &[
|
||
("auto", "Auto (hex, else tet)"),
|
||
("hex", "Hex"),
|
||
("swept", "Swept"),
|
||
("extrude", "Extrude"),
|
||
("sweep", "Sweep"),
|
||
("revolve", "Revolve"),
|
||
("mapped", "Mapped"),
|
||
("tet", "Tet"),
|
||
];
|
||
|
||
pub const CELLS: &[Field] = &[
|
||
references("bodies", "Cells", C::Body)
|
||
.help("The cells (bodies) to mesh: tick them, or pick them in the viewport (Select: Bodies) and Use selection. Every body of the design when none is ticked.")
|
||
.hint("every body of the design"),
|
||
ref_choices("methods", "Methods", C::Body, CELL_METHODS).help("A meshing method for single cells; the others take Other cells' method."),
|
||
choice(
|
||
"default",
|
||
"Other cells",
|
||
&[("auto", "Auto (hex, else tet)"), ("hex", "Hex"), ("swept", "Swept"), ("mapped", "Mapped"), ("tet", "Tet"), ("blocks", "All hex (conforming blocks)")],
|
||
)
|
||
.help("The method for cells not listed in Methods. All hex meshes every cell as a mapped block, with the division counts of all cells assigned together so shared faces match (mesh.hex.blocks).")
|
||
.notes(&[("blocks", "All hex: every cell must have six sides, each with four edges; leave Methods empty.")])
|
||
.def("\"auto\""),
|
||
HEX_SIZE,
|
||
HEX_ORDER,
|
||
TOLERANCE,
|
||
];
|
||
|
||
pub const HEX_PROJECT: &[Field] = &[
|
||
reference("body", "Body", C::Body).help("The body to mesh with a projected block topology.").req(),
|
||
HEX_SIZE,
|
||
choice(
|
||
"template",
|
||
"Template",
|
||
&[("grid", "Block grid (carve)"), ("sphere", "Sphere (7 blocks)"), ("butterfly", "Butterfly (5-block disc, extruded)"), ("blocks", "Blocks (given vertices)")],
|
||
)
|
||
.help("The block topology projected onto the body: a grid of blocks you may carve, the seven-block sphere, the five-block butterfly for round sections, or blocks you list.")
|
||
.def("\"grid\""),
|
||
xyz("center", "Centre", POINT).help("Sphere or butterfly centre.").hint("from the body's bounding box"),
|
||
positive("radius", "Radius", D::Millimetre).help("Sphere or butterfly radius.").hint("from the body"),
|
||
xyz("axis", "Axis", D::None).help("Butterfly axis direction.").hint("0, 0, 1"),
|
||
range("inset", "Inset", D::None, 0.05, 0.95).help("Inner square or cube corner at this fraction of the radius.").def("0.55"),
|
||
json("grid", "Grid").help("The block grid: planes per axis or a cell count, and blocks to carve away, as JSON.").hint(r#"{"cells": [ni, nj, nk], "carve": [{"i": [a, b], "j": [c, d], "k": [e, f]}]}"#),
|
||
json("blocks", "Blocks").help("Template Blocks: vertices and hex8 blocks, as JSON.").hint(r#"{"vertices": [[x, y, z], ...], "blocks": [[v1..v8], ...]}"#),
|
||
json("operations", "Operations").help("O-grids, extrusions, connections, removals and merges applied in order, as JSON.").hint(r#"[{"op": "o_grid", "block": 0, "open": ["+z"]}]"#),
|
||
json("faces", "Face targets").help("Block faces sent to chosen B-rep faces; other outer faces go to the nearest surfaces.").hint(r#"[{"block": 0, "face": "+x", "to": ["F1:+x"]}]"#),
|
||
json("divisions", "Divisions").help("Element counts per block direction.").hint(r#"[{"block": 0, "direction": 1, "count": 8}]"#),
|
||
choice("smoothing", "Smoothing", &[("elliptic", "Elliptic"), ("elliptic-controlled", "Elliptic, controlled (Thomas–Middlecoff)"), ("laplace", "Laplace"), ("none", "None")])
|
||
.help("How interior nodes are smoothed after projection, with the boundary held on the geometry.")
|
||
.def("\"elliptic\""),
|
||
int("iterations", "Iterations", 1, 100_000).help("Smoothing iterations.").def("100"),
|
||
range("min_quality", "Minimum quality", D::None, 0.0, 1.0).help("Smallest scaled Jacobian accepted.").def("0.2"),
|
||
flag("preview", "Preview only").help("Return the block topology without meshing."),
|
||
TOLERANCE,
|
||
];
|
||
|
||
pub const CLEAR_MESH: &[Field] = &[references("bodies", "Bodies", C::Body)
|
||
.help("The bodies whose meshes go. When none is ticked: the meshes of bodies the open design no longer has because its partitions changed (model.check lists them).")
|
||
.hint("the bodies partitions removed")];
|
||
|
||
pub const MIRROR: &[Field] = &[
|
||
json("plane", "Plane")
|
||
.help("The mirror plane: a datum plane's name, or a point and normal as JSON.")
|
||
.req()
|
||
.hint(r#""Datum" or {"point": [x, y, z], "normal": [nx, ny, nz]}"#),
|
||
references("bodies", "Bodies", C::Body).help("The meshed bodies to mirror; every meshed body when empty.").hint("every meshed body"),
|
||
json("as", "Images")
|
||
.help("Names for the mirrored bodies, as JSON.")
|
||
.hint(r#"{"body": "image body"}; default: the design's body at the reflected place"#),
|
||
];
|
||
|
||
pub const SYMMETRY: &[Field] = &[
|
||
NAME,
|
||
reference("datum", "Datum plane", C::DatumPlane).help("The symmetry plane.").req(),
|
||
reference("set", "Set", C::Set).help("The faces on the plane; found from the design when empty.").hint("the faces of the design on the plane"),
|
||
references("bodies", "Bodies", C::Body).help("The bodies cut by the symmetry plane; every body when empty.").hint("every body"),
|
||
];
|
||
|
||
pub const HEX_OCTREE: &[Field] = &[
|
||
BODY,
|
||
SURFACE,
|
||
HEX_SIZE,
|
||
TOLERANCE,
|
||
int("max_depth", "Refinement levels", 0, 5).help("How many times the octree may refine near the surface.").hint("3"),
|
||
int("max_elements", "Max elements", 1, 10_000_000).help("Cap on the elements created."),
|
||
int("attempts", "Attempts", 1, 8).help("How many times to retry with adjusted settings.").hint("4"),
|
||
int("max_sweeps", "Optimiser sweeps", 0, 10_000).help("The most smoothing sweeps.").hint("300"),
|
||
WELD,
|
||
WELD_DISTANCE,
|
||
FEATURE_ANGLE,
|
||
];
|
||
|
||
pub const BLOCK: &[Field] = &[
|
||
text("body", "Body").help("Name of the new block 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)"),
|
||
],
|
||
)
|
||
.help("The element type of the block.")
|
||
.req()
|
||
.def("\"hex8\""),
|
||
points("corners", "Corners", D::Length, 8).help("The eight corner points (four for 2D, at z = 0).").req(),
|
||
json("divisions", "Divisions").help("Elements along each edge direction, as JSON.").req().hint("[n12, n14, n15] (2D: [n12, n14])"),
|
||
json("bias", "Bias").help("Element size ratio along each direction, as JSON; 1 is uniform.").hint("[1, 1, 1]"),
|
||
json("edges", "Curved and named edges").help("Edge curves and names, as JSON."),
|
||
json("faces", "Face names").help("Names of the block's faces, as JSON.").hint("{\"1\": \"bottom\"}"),
|
||
json("vertices", "Corner names").help("Names of the block's corners, as JSON."),
|
||
text("face", "2D face name").help("Name of a 2D block's face.").hint("area"),
|
||
positive("merge", "Merge distance", D::Length).help("Nodes closer than this merge with existing nodes.").hint("1e-6 of the diagonal"),
|
||
];
|
||
|
||
pub const PLANAR: &[Field] = &[
|
||
positive("size", "Element size", D::Length).help("Target element edge length.").req(),
|
||
choice("kind", "Element", &[("quad4", "quad4"), ("quad8", "quad8"), ("tri3", "tri3"), ("tri6", "tri6")])
|
||
.help("The 2D element type.")
|
||
.def("\"quad4\""),
|
||
BODY,
|
||
text("face", "Face").help("A planar face of the body to mesh.").hint("a planar face of the body"),
|
||
TOLERANCE,
|
||
json("polygon", "Polygon").help("An outline to mesh instead of a face, as JSON points.").hint("[[x, y], …] instead of a face"),
|
||
json("holes", "Holes").help("Holes in the polygon, as JSON point lists."),
|
||
text("name", "Body name").help("Name of the new 2D body.").hint("Plane"),
|
||
int("max_elements", "Max elements", 1, 4_000_000).help("Cap on the elements created."),
|
||
];
|
||
|
||
pub const QUALITY: &[Field] = &[
|
||
labels("elements", "Elements", LabelOf::Element).help("The elements to measure; all when empty.").hint("all"),
|
||
int("bins", "Histogram bins", 1, 1000).help("Number of bars in the quality histogram.").def("10"),
|
||
];
|
||
|
||
pub const SURFACE_CHECK: &[Field] = &[
|
||
reference("body", "Body", C::Body).help("The body whose surface to check.").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).help("The node label.").req(),
|
||
xyz("xyz", "Position", D::Length).help("The node's coordinates.").req().def("[0, 0, 0]"),
|
||
];
|
||
pub const LABEL: &[Field] = &[int("label", "Label", 1, u32::MAX as i64).help("The label of the node or element.").req()];
|
||
pub const ELEMENT_ADD: &[Field] = &[
|
||
int("label", "Label", 1, u32::MAX as i64).help("The element label.").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"),
|
||
],
|
||
)
|
||
.help("The element type.")
|
||
.req()
|
||
.def("\"hex8\""),
|
||
labels("nodes", "Nodes", LabelOf::Node).help("The element's node labels in order.").req().hint("labels in order: 1, 2, 3, 4"),
|
||
];
|
||
|
||
const MERGE_TOLERANCE: Field =
|
||
positive("tolerance", "Tolerance", D::Length).help("Nodes closer than this are one node.").hint("a millionth of the mesh's size");
|
||
|
||
pub const NODE_MERGE: &[Field] = &[
|
||
MERGE_TOLERANCE,
|
||
reference("set", "Set", C::Set).help("Merge only the nodes of this set (and of its elements and faces).").hint("every node"),
|
||
choice("keep", "Keep", &[("lowest", "Lowest label"), ("highest", "Highest label")]).help("Which node of each group survives.").def("\"lowest\""),
|
||
];
|
||
|
||
pub const CHECK_DUPLICATES: &[Field] = &[MERGE_TOLERANCE];
|
||
|
||
pub const NODE_RELAX: &[Field] = &[
|
||
xyz("center", "Centre", D::Length).help("The centre of the sphere of influence; click the mesh to place it.").hint("click the mesh"),
|
||
positive("radius", "Radius", D::Length).help("Nodes closer to the centre than this move; Alt + scroll over the view changes it."),
|
||
int("around_element", "Around element", 1, 99_999_999)
|
||
.help("Instead of a centre: a sphere round this element (radius default twice its longest edge).")
|
||
.hint("none"),
|
||
labels("nodes", "Nodes", LabelOf::Node).help("Instead of a sphere: these nodes.").hint("none"),
|
||
references("bodies", "Bodies", C::Body).help("Only the nodes of these bodies.").hint("every body"),
|
||
int("iterations", "Iterations", 1, 100).help("Sweeps over the nodes.").def("5"),
|
||
range("strength", "Strength", D::None, 0.0, 1.0).help("How far each node moves toward its relaxed position, 0 to 1.").def("1"),
|
||
choice("falloff", "Falloff", &[("smooth", "Smooth"), ("linear", "Linear"), ("none", "None")])
|
||
.help("How the strength falls from the centre to the radius, so the edit blends into the mesh.")
|
||
.def("\"smooth\""),
|
||
text("field", "Keep as").help("The relaxation field to keep, to apply again after a remesh.").hint("Relax1, Relax2, …"),
|
||
flag("reapply", "Reapply after remesh").help("Apply the field again by itself after each remesh.").def("false"),
|
||
];
|
||
|
||
pub const NODE_RELAX_APPLY: &[Field] = &[
|
||
text("field", "Field").req().help("The relaxation field kept by Relax Nodes."),
|
||
references("bodies", "Bodies", C::Body).help("Only the nodes of these bodies.").hint("the field's bodies"),
|
||
range("polish", "Polish", D::None, 0.0, 1.0).help("Strength of one guarded smoothing sweep after the field is applied; 0 for none.").def("0.3"),
|
||
];
|
||
|
||
pub const RENUMBER: &[Field] = &[
|
||
choice("what", "Renumber", &[("both", "Nodes and elements"), ("nodes", "Nodes"), ("elements", "Elements")])
|
||
.help("Which labels change.")
|
||
.def("\"both\""),
|
||
choice("by", "Order", &[("compact", "Compact (label order)"), ("bandwidth", "Bandwidth (reverse Cuthill–McKee)"), ("body", "Body by body")])
|
||
.help("The order of the new labels.")
|
||
.def("\"compact\""),
|
||
int("start", "Start", 1, 99_999_999).help("The first new label.").def("1"),
|
||
int("offset", "Offset", -99_999_999, 99_999_999).help("Instead of an order and a start: add this to every label.").hint("none"),
|
||
];
|
||
|
||
pub const SEED_EDGE: &[Field] = &[
|
||
json("edges", "Edges")
|
||
.help("The edges to seed, as JSON; easiest with Use selection.")
|
||
.req()
|
||
.hint("[{\"body\", \"edge\"}, …] — Use selection with Select: Edges"),
|
||
int("count", "Count", 1, 100_000).help("Number of elements along each edge.").hint("or an element size"),
|
||
positive("size", "Element size", D::Length).help("Element length along the edge, instead of a count.").hint("or a count"),
|
||
group(
|
||
"bias",
|
||
"Bias",
|
||
&[
|
||
range("ratio", "Ratio", D::None, 1.0, 1e4).help("Largest element over smallest; 1 is uniform.").req().def("1"),
|
||
choice("toward", "Small toward", &[("start", "Start"), ("end", "End"), ("both", "Both ends")])
|
||
.help("Which end gets the small elements.")
|
||
.req()
|
||
.def("\"end\""),
|
||
],
|
||
)
|
||
.help("Grade the elements along the edge."),
|
||
choice("constraint", "Constraint", &[("exact", "Exact"), ("min", "At least"), ("free", "Free (a goal)")])
|
||
.help("Exact: the mesher must match the count. At least: it may add more. Free: a goal only.")
|
||
.def("\"exact\""),
|
||
];
|
||
|
||
pub const SEED_CLEAR: &[Field] = &[json("edges", "Edges").help("The edges to clear, as JSON; every seed when empty.").hint("every seed")];
|
||
|
||
pub const DELAUNAY_POINTS: &[Field] = &[points("points", "Points", D::Length, 20_000).help("The points to triangulate.").req()];
|
||
|
||
pub const TAG: &[Field] = &[
|
||
group(
|
||
"geometry",
|
||
"Geometry",
|
||
&[
|
||
reference("body", "Body", C::Body).help("A body of the open design.").req(),
|
||
text("face", "Face").help("A persistent face name."),
|
||
text("edge", "Edge").help("A persistent edge name."),
|
||
text("vertex", "Vertex").help("A persistent vertex name."),
|
||
],
|
||
)
|
||
.help("The geometry the mesh entities are tagged with.")
|
||
.req(),
|
||
labels("elements", "Elements", LabelOf::Element).help("Element labels."),
|
||
json("faces", "Element faces").help("Element faces, as JSON."),
|
||
json("edges", "Element edges").help("Element edges, as JSON."),
|
||
labels("nodes", "Nodes", LabelOf::Node).help("Node labels."),
|
||
];
|
||
|
||
// ---- Geometry and files ----
|
||
|
||
pub const OPEN_DESIGN: &[Field] = &[
|
||
path("path", "Design", false).help("The SolidCraft design file to open.").req(),
|
||
choice("model", "Model", &[("keep", "Keep the current model"), ("new", "Start a new model")])
|
||
.help("Keep the current model with the new geometry, or start a new one.")
|
||
.def("\"keep\""),
|
||
];
|
||
const POINT: Dim = D::Millimetre;
|
||
pub const DATUM_PLANE: &[Field] = &[
|
||
NAME,
|
||
variants(
|
||
"by",
|
||
"Definition",
|
||
&[
|
||
variant(
|
||
"offset",
|
||
"Principal plane",
|
||
&[
|
||
choice("plane", "Plane", &[("yz", "YZ (normal X)"), ("zx", "ZX (normal Y)"), ("xy", "XY (normal Z)")])
|
||
.help("The principal plane.")
|
||
.req(),
|
||
num("offset", "Offset", POINT).help("Distance along the plane's normal.").def("0"),
|
||
],
|
||
),
|
||
variant(
|
||
"three_points",
|
||
"Three points",
|
||
&[
|
||
xyz("a", "Point A", POINT).help("First point.").req(),
|
||
xyz("b", "Point B", POINT).help("Second point.").req(),
|
||
xyz("c", "Point C", POINT).help("Third point.").req(),
|
||
],
|
||
),
|
||
variant(
|
||
"point_normal",
|
||
"Point and normal",
|
||
&[
|
||
xyz("point", "Point", POINT).help("A point on the plane.").req(),
|
||
xyz("normal", "Normal", D::None).help("The plane's normal direction.").req(),
|
||
],
|
||
),
|
||
variant(
|
||
"datum_points",
|
||
"Three datum points",
|
||
&[
|
||
reference("a", "Point A", C::DatumPoint).help("First datum point.").req(),
|
||
reference("b", "Point B", C::DatumPoint).help("Second datum point.").req(),
|
||
reference("c", "Point C", C::DatumPoint).help("Third datum point.").req(),
|
||
],
|
||
),
|
||
variant(
|
||
"normal_to_axis",
|
||
"Normal to an axis",
|
||
&[
|
||
reference("axis", "Axis", C::DatumAxis).help("The datum axis the plane is normal to.").req(),
|
||
reference("point", "Through", C::DatumPoint)
|
||
.help("A datum point on the plane; the axis's own point when empty.")
|
||
.hint("the axis's point"),
|
||
],
|
||
),
|
||
],
|
||
)
|
||
.help("How the plane is defined.")
|
||
.req()
|
||
.def(r#"{"type": "offset"}"#),
|
||
];
|
||
pub const DATUM_AXIS: &[Field] = &[
|
||
NAME,
|
||
variants(
|
||
"by",
|
||
"Definition",
|
||
&[
|
||
variant(
|
||
"principal",
|
||
"Principal axis",
|
||
&[
|
||
choice("axis", "Axis", &[("x", "X"), ("y", "Y"), ("z", "Z")]).help("The principal direction.").req(),
|
||
xyz("through", "Through", POINT).help("A point the axis passes through; the origin when empty.").hint("the origin"),
|
||
],
|
||
),
|
||
variant(
|
||
"two_points",
|
||
"Two points",
|
||
&[
|
||
xyz("a", "Point A", POINT).help("First point on the axis.").req(),
|
||
xyz("b", "Point B", POINT).help("Second point on the axis.").req(),
|
||
],
|
||
),
|
||
variant(
|
||
"point_direction",
|
||
"Point and direction",
|
||
&[
|
||
xyz("point", "Point", POINT).help("A point on the axis.").req(),
|
||
xyz("direction", "Direction", D::None).help("The axis direction.").req(),
|
||
],
|
||
),
|
||
variant(
|
||
"datum_points",
|
||
"Two datum points",
|
||
&[
|
||
reference("a", "Point A", C::DatumPoint).help("First datum point on the axis.").req(),
|
||
reference("b", "Point B", C::DatumPoint).help("Second datum point on the axis.").req(),
|
||
],
|
||
),
|
||
],
|
||
)
|
||
.help("How the axis is defined.")
|
||
.req()
|
||
.def(r#"{"type": "principal"}"#),
|
||
];
|
||
pub const RIGID_WALL: &[Field] = &[
|
||
reference("step", "Step", C::Step)
|
||
.help("The explicit dynamics step the wall acts in; the model's only one when empty.")
|
||
.hint("the explicit step"),
|
||
reference("set", "Set", C::Set).help("The nodes the wall stops: pick them and Use selection.").req(),
|
||
xyz("point", "Point", D::Length).help("A point on the wall.").req().def("[0, 0, 0]"),
|
||
xyz("normal", "Normal", D::None).help("The wall's normal, pointing to the side the nodes stay on.").req().def("[0, 0, 1]"),
|
||
FRICTION,
|
||
];
|
||
pub const RIGID_WALL_DELETE: &[Field] = &[
|
||
reference("step", "Step", C::Step).help("The explicit dynamics step.").req(),
|
||
int("index", "Wall", 0, MAX_ROWS as i64).help("The wall's place in the step's rigid walls, from 0.").req(),
|
||
];
|
||
pub const CENTRE: &[Field] = &[
|
||
rows(
|
||
"geometry",
|
||
"Geometry",
|
||
&[
|
||
reference("body", "Body", C::Body).help("A body of the open design.").req(),
|
||
text("face", "Face").help("A persistent face name of the body."),
|
||
text("edge", "Edge").help("A persistent edge name of the body."),
|
||
text("vertex", "Vertex").help("A persistent vertex name of the body."),
|
||
],
|
||
)
|
||
.help("Faces or edges (a hole's faces or rim give its centre), or one vertex."),
|
||
labels("nodes", "Nodes", LabelOf::Node).help("Mesh node labels: their mean.").hint("labels: 1, 2, 10-20"),
|
||
];
|
||
pub const DATUM_POINT: &[Field] = &[
|
||
NAME,
|
||
variants(
|
||
"by",
|
||
"Definition",
|
||
&[
|
||
variant("coordinates", "Coordinates", &[xyz("point", "Point", POINT).help("The point's coordinates.").req()]),
|
||
variant(
|
||
"vertex",
|
||
"Vertex",
|
||
&[
|
||
reference("body", "Body", C::Body).help("The body the vertex belongs to.").req(),
|
||
text("vertex", "Vertex")
|
||
.help("The vertex's persistent name: pick it with Select: Vertices and Use selection (geometry.bodies lists every body's vertices).")
|
||
.req(),
|
||
],
|
||
),
|
||
variant(
|
||
"edge",
|
||
"On an edge",
|
||
&[
|
||
reference("body", "Body", C::Body).help("The body the edge belongs to.").req(),
|
||
text("edge", "Edge").help("The edge's persistent name.").req(),
|
||
range("fraction", "Fraction", D::None, 0.0, 1.0).help("How far along the edge from its start: 0.5 is its midpoint.").def("0.5"),
|
||
],
|
||
),
|
||
variant(
|
||
"face_centre",
|
||
"Face centre",
|
||
&[
|
||
reference("body", "Body", C::Body).help("The body the face belongs to.").req(),
|
||
text("face", "Face").help("The face's persistent name.").req(),
|
||
],
|
||
),
|
||
variant(
|
||
"axis_plane",
|
||
"Axis and plane",
|
||
&[
|
||
reference("axis", "Axis", C::DatumAxis).help("The datum axis.").req(),
|
||
reference("plane", "Plane", C::DatumPlane).help("The datum plane the axis crosses.").req(),
|
||
],
|
||
),
|
||
],
|
||
)
|
||
.help("How the point is defined.")
|
||
.req()
|
||
.def(r#"{"type": "coordinates"}"#),
|
||
];
|
||
pub const VIEW_ITEMS: &[Field] = &[rows(
|
||
"items",
|
||
"Items",
|
||
&[
|
||
choice(
|
||
"kind",
|
||
"Kind",
|
||
&[
|
||
("body", "Body"),
|
||
("surface", "Surface"),
|
||
("datum", "Datum"),
|
||
("boundary_condition", "Boundary condition"),
|
||
("load", "Load"),
|
||
("reference_point", "Reference point"),
|
||
("coupling", "Coupling"),
|
||
],
|
||
)
|
||
.help("What kind of item it is.")
|
||
.req(),
|
||
text("name", "Name").help("The item's name.").req(),
|
||
],
|
||
)
|
||
.help("The viewport items.")
|
||
.req()];
|
||
pub const DATUM_DELETE: &[Field] = &[
|
||
reference("name", "Datum", C::Datum).req().help("The Datum to act on, by name."),
|
||
flag("cascade", "Delete its partitions").help("Also delete the partitions that cut by it; without it, a datum in use is refused."),
|
||
];
|
||
pub const PARTITION: &[Field] = &[
|
||
NAME,
|
||
reference("datum", "Datum plane", C::DatumPlane).help("The plane that divides the bodies.").req(),
|
||
references("bodies", "Bodies", C::Body).help("The bodies to divide; every body the plane cuts when empty.").hint("every body the plane divides"),
|
||
];
|
||
pub const PARTITION_DELETE: &[Field] = existing!("Partition", C::Partition);
|
||
pub const TESSELLATE: &[Field] = &[reference("body", "Body", C::Body).help("The body to tessellate.").req(), TOLERANCE];
|
||
pub const IMPORT_MESH: &[Field] = &[path("path", "3MF file", false).help("A 3MF surface mesh with named faces.").req()];
|
||
pub const FILE_OPEN: &[Field] = &[path("path", "Model", false).help("The FieldCraft model file to open.").req()];
|
||
pub const FILE_SAVE: &[Field] = &[path("path", "File", true).help("Where to save; the model's own file when empty.").hint("the model's file")];
|
||
pub const FILE_SAVE_AS: &[Field] = &[path("path", "File", true).help("The file to save to.").req()];
|
||
pub const RECOVERY_LIST: &[Field] =
|
||
&[path("dir", "Folder", false).help("The recovery folder; the default one when empty.").hint("the recovery folder")];
|
||
pub const CRASH_REPORTS: &[Field] = &[
|
||
int("limit", "Limit", 1, 1000).help("At most this many reports, newest first.").hint("20"),
|
||
path("dir", "Folder", false).help("The crash folder; the default one when empty.").hint("the crash folder"),
|
||
];
|
||
pub const CRASH_REPORT: &[Field] = &[
|
||
text("id", "Report").help("The crash report to show.").req(),
|
||
path("dir", "Folder", false).help("The crash folder; the default one when empty.").hint("the crash folder"),
|
||
];
|
||
pub const CRASH_REPORTS_DELETE: &[Field] = &[
|
||
text("id", "Report").help("The crash report to delete.").hint("or All"),
|
||
flag("all", "All").help("Delete every crash report instead of one."),
|
||
path("dir", "Folder", false).help("The crash folder; the default one when empty.").hint("the crash folder"),
|
||
];
|
||
pub const BUG_REPORT: &[Field] = &[
|
||
text("title", "Title").help("The issue's title; one is made up when empty."),
|
||
text("observed", "What happened").help("What went wrong; the last error when empty."),
|
||
text("crash", "Crash report").help("A crash report to report (its id)."),
|
||
flag("share_session", "Share my session").help("Attach the commands that built the model (they can name your files).").def("false"),
|
||
flag("include_model", "Include the model").help("Attach the model file too (with Share my session).").def("false"),
|
||
];
|
||
pub const CRASH_TEST: &[Field] =
|
||
&[choice("mode", "Mode", &[("panic", "Panic"), ("recovered", "Recovered panic"), ("abort", "Abort")]).help("How to crash.").req()];
|
||
pub const RECOVER: &[Field] = &[
|
||
text("id", "Autosave").help("The autosave to recover.").req(),
|
||
path("dir", "Folder", false).help("The recovery folder; the default one when empty.").hint("the recovery folder"),
|
||
];
|
||
pub const RECOVERY_DISCARD: &[Field] = &[
|
||
text("id", "Autosave").help("The autosave to discard."),
|
||
flag("all", "All").help("Discard every autosave."),
|
||
path("dir", "Folder", false).help("The recovery folder; the default one when empty.").hint("the recovery folder"),
|
||
];
|
||
pub const SAMPLE: &[Field] = &[choice(
|
||
"name",
|
||
"Sample",
|
||
&[
|
||
("cantilever", "Cantilever"),
|
||
("impact", "Impact"),
|
||
("parts", "Parts"),
|
||
("ibeam", "I-beam cantilever"),
|
||
("drop-test", "Drop test"),
|
||
("rubber-seal", "Rubber seal"),
|
||
("flow-past", "Flow past a bracket"),
|
||
("heated-bracket", "Heated bracket"),
|
||
("bracket-hex", "Hex-meshed bracket"),
|
||
("crank-arm", "Bicycle crank arm"),
|
||
("crank-mirror", "Mirrored crank arm"),
|
||
],
|
||
)
|
||
.help("The sample model to open.")
|
||
.req()
|
||
.def("\"cantilever\"")];
|
||
pub const COUNT: &[Field] = &[int("count", "Count", 1, 1_000_000).help("How many steps.").def("1")];
|
||
|
||
pub const DECK: &[Field] = &[
|
||
choice("format", "Format", &[("abaqus", "Abaqus input (.inp)"), ("nastran", "Nastran bulk data (.bdf)"), ("lsdyna", "LS-DYNA keyword (.k)")])
|
||
.help("The solver deck format.")
|
||
.req()
|
||
.def("\"abaqus\""),
|
||
path("path", "File", true).help("Where to write the deck.").req(),
|
||
references("steps", "Steps", C::Step).help("The steps to write; all when empty.").hint("all"),
|
||
];
|
||
|
||
// ---- Solving ----
|
||
|
||
const STEP: Field = reference("step", "Step", C::Step).help("The step to solve; the only one when empty.").hint("the only one");
|
||
const SOLVED: Field = reference("step", "Step", C::SolvedStep).help("The solved step; the step shown when empty.").hint("the step shown");
|
||
const MAX_DOFS: Field = int("max_dofs", "Max DOFs", 1, 6_000_000).help("Cap on the degrees of freedom solved.").hint("6000000");
|
||
const MAX_FACTOR: Field = int("max_factor_entries", "Max factor entries", 1, super::solve::FACTOR_ENTRY_CEILING as i64)
|
||
.help("Cap on factor entries (8 bytes each): 400M by default; raise deliberately up to 2B (16 GB of values alone). Indices, workspace and other solver storage need additional memory.")
|
||
.hint("400000000");
|
||
const THREADS: Field = int("threads", "Threads", 1, 256).help("Threads to use; the session's setting when empty.").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).help("Cap on the Newton iterations over the whole step.").hint("100000"),
|
||
THREADS,
|
||
];
|
||
pub const SOLVE_FREQUENCY: &[Field] = &[
|
||
STEP,
|
||
choice("mass", "Mass matrix", &[("consistent", "Consistent"), ("lumped", "Lumped")])
|
||
.help("Consistent is more accurate; lumped is cheaper.")
|
||
.def("\"consistent\""),
|
||
num("shift", "Shift", D::None).help("Look for frequencies near this eigenvalue; 0 finds the lowest.").hint("0"),
|
||
MAX_DOFS,
|
||
MAX_FACTOR,
|
||
int("max_iterations", "Max Lanczos vectors", 1, 3000).help("Cap on the Lanczos vectors.").hint("3000"),
|
||
THREADS,
|
||
];
|
||
pub const SOLVE_BUCKLE: &[Field] =
|
||
&[STEP, MAX_DOFS, MAX_FACTOR, int("max_iterations", "Max Lanczos vectors", 1, 3000).help("Cap on the Lanczos vectors.").hint("3000"), THREADS];
|
||
pub const SOLVE_EXPLICIT: &[Field] = &[
|
||
STEP,
|
||
MAX_DOFS,
|
||
int("max_increments", "Max increments", 1, 1_000_000_000).help("Cap on the time increments.").hint("1000000000"),
|
||
int("max_output_values", "Max output values", 1, 50_000_000).help("Cap on the result values kept.").hint("50000000"),
|
||
int("max_contact_faces", "Max contact faces", 1, 10_000_000).help("Cap on the faces in contact search.").hint("10000000"),
|
||
THREADS,
|
||
];
|
||
pub const SOLVE_HISTORY: &[Field] = &[SOLVED.req(), int("every", "Keep one in", 1, 1_000_000_000).help("Keep one history sample in this many.")];
|
||
pub const SOLVE_FLUID: &[Field] = &[
|
||
STEP,
|
||
int("max_cells", "Max cells", 1, 16_000_000).help("Cap on the lattice cells."),
|
||
int("max_steps", "Max steps", 1, 10_000_000).help("Cap on the lattice time steps."),
|
||
positive("max_cell_updates", "Max cell updates", D::None).help("Cap on cells × steps."),
|
||
positive("max_frame_bytes", "Max frame bytes", D::None).help("Cap on the memory for result frames."),
|
||
THREADS,
|
||
];
|
||
pub const SOLVE_PROBE: &[Field] = &[
|
||
SOLVED.req(),
|
||
labels("nodes", "Nodes", LabelOf::Node).help("Node labels to read.").hint("labels: 1, 2, 10-20"),
|
||
reference("set", "Set", C::Set).help("A set whose nodes to read."),
|
||
points("at", "Points", D::Length, 10_000).help("Points to read at the nearest nodes."),
|
||
int("mode", "Mode", 1, 1_000_000).help("The mode to read, for frequency and buckling steps."),
|
||
int("frame", "Frame", 0, 1_000_000_000).help("The frame to read, for explicit and transient steps."),
|
||
flag("elements", "Element results").help("Also read element results (stresses) around the nodes."),
|
||
points("points", "Fluid points", D::Length, 10_000).help("Points in the fluid to read."),
|
||
json("forces", "Fluid walls").help("Walls whose fluid forces to sum, as JSON.").hint("[\"x_min\", \"F2:top\"]"),
|
||
];
|
||
|
||
// ---- Results ----
|
||
|
||
pub const DEFORMED: &[Field] = &[
|
||
SOLVED,
|
||
flag("show", "Show").help("Show the deformed shape.").def("true"),
|
||
number_or_auto("scale", "Scale", D::None, Some(0.0)).help("Displacement magnification: auto, or a factor (1 is true scale).").def("\"auto\""),
|
||
];
|
||
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"),
|
||
("contact_pressure", "Contact pressure"),
|
||
("contact_gap", "Contact gap"),
|
||
("contact_status", "Contact status"),
|
||
("contact_shear", "Contact shear"),
|
||
("contact_slip", "Contact slip"),
|
||
("none", "None"),
|
||
],
|
||
)
|
||
.help("The result shown as colours.")
|
||
.req()
|
||
.def("\"von_mises\""),
|
||
int("bands", "Bands", 2, 24).help("Number of colour bands.").def("12"),
|
||
group(
|
||
"range",
|
||
"Legend range",
|
||
&[
|
||
num("min", "Min", D::None).help("Values below it take the lowest colour.").hint("the field's minimum"),
|
||
num("max", "Max", D::None).help("Values above it are drawn in the over colour.").hint("the field's maximum"),
|
||
],
|
||
)
|
||
.help("Clamp the legend, e.g. below a singular peak; empty follows the field."),
|
||
];
|
||
pub const MODES: &[Field] = &[SOLVED, int("mode", "Mode", 1, 1_000_000).help("The mode shape shown.").def("1")];
|
||
pub const ANIMATE: &[Field] = &[flag("on", "Animate").help("Play the deformation or the frames.").def("true")];
|
||
pub const FRAME: &[Field] = &[
|
||
SOLVED,
|
||
int("frame", "Frame", 0, 1_000_000_000).help("The frame shown."),
|
||
positive("time", "Time", D::Time).help("Show the frame nearest this time; the last when empty.").hint("the last frame"),
|
||
];
|
||
pub const EXPORT_CSV: &[Field] = &[
|
||
choice("what", "Export", &[("reactions", "Reaction sums"), ("probe", "Probed nodes")]).help("What to write.").req().def("\"reactions\""),
|
||
path("path", "File", true).help("The CSV file to write.").req(),
|
||
SOLVED,
|
||
references("sets", "Sets", C::Set).help("Reactions: the sets to sum; the step's BC sets when empty.").hint("the step's BC sets"),
|
||
xyz("about", "About point", D::Length).help("Reactions: the point moments are taken about."),
|
||
labels("nodes", "Nodes", LabelOf::Node).help("Probe: the node labels."),
|
||
reference("set", "Set", C::Set).help("Probe: a set whose nodes to read."),
|
||
points("at", "Points", D::Length, 10_000).help("Probe: points to read at the nearest nodes."),
|
||
int("mode", "Mode", 1, 1_000_000).help("Probe: the mode to read, for frequency and buckling steps."),
|
||
int("frame", "Frame", 0, 1_000_000_000).help("Probe: the frame to read, for explicit and transient steps."),
|
||
];
|
||
pub const REACTIONS: &[Field] = &[
|
||
SOLVED,
|
||
references("sets", "Sets", C::Set)
|
||
.help("The sets whose reaction forces to sum; the step's boundary-condition sets when empty.")
|
||
.hint("the step's BC sets"),
|
||
xyz("about", "About point", D::Length).help("The point moments are taken about."),
|
||
];
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use fieldcraft_model::UnitSystem;
|
||
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.kind == ProblemKind::Required).map(|p| p.path).collect();
|
||
match s.execute(c.id, &p) {
|
||
Err(EngineError::Param(e)) if e.kind == ProblemKind::Required && missing.contains(&e.path) => {}
|
||
Err(EngineError::Param(e)) => {
|
||
failures.push(format!("{}: {p} refused: {}, but only {missing:?} were missing", c.id, e.for_agents()));
|
||
}
|
||
// 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}");
|
||
}
|
||
|
||
/// Every parameter the engine refuses is a field of the command's form: the problem's path
|
||
/// leads to a field, so a form can show it beside that field, by its label. Each field of
|
||
/// each form is given a value of the wrong kind in turn, over the form's starting values.
|
||
#[test]
|
||
fn every_param_error_names_a_field_of_its_form() {
|
||
let hostile = [json!("x"), json!(-1), json!(0), json!(1e300), json!([]), json!({}), json!(null), json!([1, "x"])];
|
||
let (mut seen, 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.starts_with("edit.") || form.iter().any(|f| matches!(f.kind, Kind::Path { .. })) {
|
||
continue; // they replace the session's model or write files
|
||
}
|
||
let base = prefill(form, &json!({}));
|
||
let mut tries = vec![json!({}), base.clone()];
|
||
for f in form.iter().filter(|f| !f.key.is_empty()) {
|
||
for v in &hostile {
|
||
let mut p = base.clone();
|
||
if let Some(o) = p.as_object_mut() {
|
||
o.insert(f.key.into(), v.clone());
|
||
}
|
||
tries.push(p);
|
||
}
|
||
}
|
||
for p in tries {
|
||
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);
|
||
if let Err(EngineError::Param(e)) = s.execute(c.id, &p) {
|
||
seen += 1;
|
||
if e.field.is_none() || field_at(form, &e.path).is_none() {
|
||
failures.push(format!("{}: {p} refused `{}`, not a field of the form ({})", c.id, e.path, e.for_agents()));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
failures.sort();
|
||
failures.dedup();
|
||
assert!(failures.is_empty(), "{failures:#?}");
|
||
assert!(seen > 50, "{seen}");
|
||
}
|
||
|
||
/// A required field left empty says what to do by its kind: a name, a path, else "required".
|
||
#[test]
|
||
fn required_fields_are_phrased_by_kind() {
|
||
let form = |fields: &'static [Field]| problems(fields, &json!({})).iter().map(Problem::for_form).collect::<Vec<_>>();
|
||
assert_eq!(form(RECOVER), ["Autosave: required"]);
|
||
assert_eq!(form(FILE_SAVE_AS), ["File: enter a path"]);
|
||
assert!(form(MATERIAL).contains(&"Name: enter a name".to_string()), "{:?}", form(MATERIAL));
|
||
}
|
||
|
||
/// 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",
|
||
"update checks are off",
|
||
"`boundary_conditions` or `loads`",
|
||
"give one of",
|
||
"is not an explicit",
|
||
"has no explicit dynamics step",
|
||
"has a result",
|
||
"no crash report",
|
||
"FIELDCRAFT_CRASH_TEST",
|
||
"no relaxation field",
|
||
];
|
||
|
||
/// 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 field of every form says what it means, and every number with a dimension shows
|
||
/// its unit, or its dimension when the model has no unit system.
|
||
#[test]
|
||
fn every_field_has_help_and_a_unit() {
|
||
fn walk(id: &str, fields: &[Field], missing: &mut Vec<String>) {
|
||
for f in fields {
|
||
if f.help.trim().is_empty() {
|
||
missing.push(format!("{id}: {} ({}) has no help", f.label, f.key));
|
||
}
|
||
if f.dim().is_some_and(|d| d != D::None) {
|
||
for system in [UnitSystem::Unspecified, UnitSystem::MmTonneS, UnitSystem::MKgS] {
|
||
if f.unit_label(system).is_none_or(|u| u.is_empty()) {
|
||
missing.push(format!("{id}: {} shows no unit in {system:?}", f.label));
|
||
}
|
||
}
|
||
}
|
||
match f.kind {
|
||
Kind::Group(g) | Kind::Rows { fields: g, .. } | Kind::Either(g) => walk(id, g, missing),
|
||
Kind::Variants { variants, .. } => variants.iter().for_each(|v| walk(id, v.fields, missing)),
|
||
_ => {}
|
||
}
|
||
}
|
||
}
|
||
let mut missing = Vec::new();
|
||
for c in crate::commands() {
|
||
walk(c.id, c.form.unwrap_or_default(), &mut missing);
|
||
}
|
||
assert!(missing.is_empty(), "{missing:#?}");
|
||
let stress = positive("s", "S", D::Stress);
|
||
assert_eq!(stress.unit_label(UnitSystem::Unspecified).as_deref(), Some("[stress]"));
|
||
assert_eq!(stress.unit_label(UnitSystem::MmTonneS).as_deref(), Some("MPa"));
|
||
assert!(stress.help_text(UnitSystem::Unspecified).contains(UNITS_HINT));
|
||
}
|
||
|
||
/// 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);
|
||
}
|
||
}
|
||
}
|
||
}
|