diff --git a/crates/engine/src/cmd/cells.rs b/crates/engine/src/cmd/cells.rs index 36e9609..00a8128 100644 --- a/crates/engine/src/cmd/cells.rs +++ b/crates/engine/src/cmd/cells.rs @@ -371,6 +371,27 @@ mod tests { assert_eq!(s.model.ties().len(), 1); } + /// The half crank arm (cut on its symmetry plane only) has no cell that maps or sweeps as + /// the design gives it; the extrusion decomposes its cross-section into blocks itself, alike + /// through mesh.hex.generate (auto) and mesh.cells, with no seeds. + #[test] + fn the_half_crank_extrudes_in_blocks_without_partitions() { + let bytes = fieldcraft_geometry::script_design(crate::sample::CRANK_ARM).unwrap(); + let (_dir, path) = crate::tests::design_file(&bytes); + let mut s = Session::default(); + s.execute("geometry.open", &json!({"path": path})).unwrap(); + s.execute("geometry.datum", &json!({"name": "Sym", "by": {"type": "offset", "plane": "zx", "offset": 0}})).unwrap(); + s.execute("geometry.partition", &json!({"name": "Half", "datum": "Sym"})).unwrap(); + let r = s.execute("mesh.hex.generate", &json!({"body": "CrankArm/a", "size": 3})).unwrap(); + let body = &r["bodies"][0]; + assert_eq!(body["method"], "extrude", "{r}"); + assert!(body["description"].as_str().unwrap().contains(" 0 free)"), "{r}"); + assert!(body["quality"]["min_scaled_jacobian"].as_f64().unwrap() >= 0.37, "{r}"); + let r = s.execute("mesh.cells", &json!({"bodies": ["CrankArm/a"], "default": "extrude", "size": 3})).unwrap(); + assert_eq!(r["cells"][0]["method"], "extrude", "{r}"); + assert_eq!(r["elements"], body["elements"], "{r}"); + } + #[test] fn auto_falls_back_to_tet_and_says_why() { let (_dir, mut s) = cut_plate(); diff --git a/crates/engine/src/cmd/hex.rs b/crates/engine/src/cmd/hex.rs index 98a7cb6..9972b19 100644 --- a/crates/engine/src/cmd/hex.rs +++ b/crates/engine/src/cmd/hex.rs @@ -16,7 +16,7 @@ use crate::{CommandKind, CommandSpec, EngineError, Result, Session}; pub const HEX_GENERATE: CommandSpec = CommandSpec { id: "mesh.hex.generate", label: "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 meshes of the bodies it meshes (as mesh.tet.generate; other bodies keep theirs) 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. Edge seeds (mesh.seed.edge) set the counts and spacing of their edges: exact seeds are held, and seeds that cannot all hold (opposite sides of a mapped face, the layers between two levels) are an error naming them. Reports per body the method, counts and quality; a body no structured method fits is an error that says why"#, + 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 meshes of the bodies it meshes (as mesh.tet.generate; other bodies keep theirs) with structured hexahedra: multi-level sweeps along a direction or around an axis (their cross-section cut into mappable blocks automatically: O-grids round holes, triangle primitives, four-sided blocks; nothing is added to the model), 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. Edge seeds (mesh.seed.edge) set the counts and spacing of their edges: exact seeds are held, and seeds that cannot all hold (opposite sides of a mapped face, the layers between two levels) are an error naming them. Reports per body the method, counts and quality; a body no structured method fits is an error that says why"#, form: Some(super::forms::HEX_GENERATE), kind: CommandKind::Model, requires: &[Requirement::Geometry],