forked from 1b-agent/fieldcraft-upstream
engine: mesh.quality.mark marks the elements below quality limits as a set
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
1 parent
33d69fc53d
commit
75b8c7e26e
10 files changed
+237
-1
No files matched your search
@@ -93,6 +93,7 @@ is committed and listed here with its exports.
|
||||
| assets/icons/mesh.order.quadratic.svg, assets/icons/mesh.order.quadratic.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.order.quadratic.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||
| assets/icons/mesh.quad.faces.svg, assets/icons/mesh.quad.faces.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.quad.faces.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||
| assets/icons/mesh.quality.svg, assets/icons/mesh.quality.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.quality.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||
| assets/icons/mesh.quality.mark.svg, assets/icons/mesh.quality.mark.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.quality.mark.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||
| assets/icons/results.export.vtu.svg, assets/icons/results.export.vtu.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/results.export.vtu.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||
| assets/icons/mesh.export.stl.svg, assets/icons/mesh.export.stl.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.export.stl.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||
| assets/icons/mesh.region.remesh.svg, assets/icons/mesh.region.remesh.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.region.remesh.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 24 24">
|
||||
<title>mesh.quality.mark</title>
|
||||
<g id="Layer_1" data-name="Layer 1">
|
||||
<path d="M3 3 L11 3 L11 11 L3 11 Z" fill="#2d4e70" stroke="#dde2ea" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M13 3 L21 3 L21 11 L13 11 Z" fill="#2d4e70" stroke="#dde2ea" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M3 13 L11 13 L11 21 L3 21 Z" fill="#2d4e70" stroke="#dde2ea" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M13 14 L21 13 L20 21 L14 19 Z" fill="#f0b44a" stroke="#dde2ea" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 822 B |
Binary file not shown.
@@ -0,0 +1 @@
|
||||
{"shapes": [{"rect": [3, 3, 8, 8], "fill": "soft", "stroke": "line"}, {"rect": [13, 3, 8, 8], "fill": "soft", "stroke": "line"}, {"rect": [3, 13, 8, 8], "fill": "soft", "stroke": "line"}, {"d": "M13 14 L21 13 L20 21 L14 19 Z", "fill": "warm", "stroke": "line"}]}
|
||||
@@ -405,6 +405,13 @@ pub static CATALOGUE: &[Capability] = &[
|
||||
soon("mesh.region.remesh", "Remesh Region", Mesh, "Edit", "M6", "Remesh a region in place; split and collapse elements"),
|
||||
soon("mesh.import.orphan", "Orphan Mesh", Mesh, "Edit", "M6", "Import a mesh without geometry"),
|
||||
ready("mesh.quality", "Quality", Mesh, "Verify", "Quality metrics and the certified validity check, failing elements listed"),
|
||||
ready(
|
||||
"mesh.quality.mark",
|
||||
"Mark Bad Elements",
|
||||
Mesh,
|
||||
"Verify",
|
||||
"Mark the elements below quality limits as a set, located and drawn in the marker colour",
|
||||
),
|
||||
soon("mesh.verify", "Verify Set", Mesh, "Verify", "M2", "Quality and validity per set"),
|
||||
ready("mesh.export.stl", "Export STL", Mesh, "Verify", "Write the mesh's outer surface (or a set's faces) as binary STL"),
|
||||
ready_more("mesh.tag", "Tag Geometry", Mesh, "Verify", "Tag elements, faces, edges and nodes with the geometry they mesh", &["mesh.tag.clear"]),
|
||||
|
||||
@@ -1275,6 +1275,25 @@ pub const QUALITY: &[Field] = &[
|
||||
int("bins", "Histogram bins", 1, 1000).help("Number of bars in the quality histogram.").def("10"),
|
||||
];
|
||||
|
||||
pub const QUALITY_MARK: &[Field] = &[
|
||||
labels("elements", "Elements", LabelOf::Element).help("The elements to check; all when empty.").hint("all"),
|
||||
group(
|
||||
"limits",
|
||||
"Limits",
|
||||
&[
|
||||
num("scaled_jacobian", "Scaled Jacobian at least", D::None)
|
||||
.help("Elements with a lower scaled Jacobian are marked (1 is a perfect element).")
|
||||
.hint("0.2"),
|
||||
num("min_dihedral", "Min dihedral at least", D::Degree).help("Tetrahedra with a smaller dihedral angle are marked.").hint("10"),
|
||||
num("max_dihedral", "Max dihedral at most", D::Degree).help("Tetrahedra with a larger dihedral angle are marked.").hint("165"),
|
||||
num("aspect_ratio", "Aspect ratio at most", D::None).help("Elements with a larger aspect ratio are marked.").hint("10"),
|
||||
num("min_angle", "Min angle at least", D::Degree).help("Shells with a smaller corner angle are marked.").hint("15"),
|
||||
num("max_angle", "Max angle at most", D::Degree).help("Shells with a larger corner angle are marked.").hint("165"),
|
||||
],
|
||||
)
|
||||
.help("Elements worse than a limit are marked; a limit left empty keeps its default."),
|
||||
];
|
||||
|
||||
pub const SURFACE_CHECK: &[Field] = &[
|
||||
reference("body", "Body", C::Body).help("The body whose surface to check.").req().hint("or an imported surface"),
|
||||
SURFACE,
|
||||
|
||||
@@ -67,6 +67,16 @@ pub const QUALITY: CommandSpec = CommandSpec {
|
||||
run: quality_report,
|
||||
};
|
||||
|
||||
pub const QUALITY_MARK: CommandSpec = CommandSpec {
|
||||
id: "mesh.quality.mark",
|
||||
label: "Mark Bad Elements",
|
||||
params: r#"{"limits"?: {"scaled_jacobian": 0.2, "min_dihedral": 10, "max_dihedral": 165, "aspect_ratio": 10, "min_angle": 15, "max_angle": 165} (the defaults; given limits are merged over them, null drops one; any metric of mesh.quality may be limited: a floor for those where higher is better, a ceiling for max_dihedral, aspect_ratio, edge_ratio, max_angle and skew; angles in degrees), "elements"?: [labels] (default all)} — after meshing, marks the elements that fail a limit (and every solid not proven valid) as the element set `quality_marked`, replaced on every run and removed when none fail; the viewport and `fieldcraft-cli snapshot` draw its elements in the marker colour. Replies `summary`, `measured`, `marked`, `invalid`, the `limits` used and `elements` (worst first: not valid, then lowest scaled Jacobian; at most 1000, `truncated` says when more): each `label`, `kind`, `body`, `at` (its centroid), `valid` and `failed` ({metric, value, limit} per limit it fails). One undo step"#,
|
||||
form: Some(super::forms::QUALITY_MARK),
|
||||
kind: CommandKind::Model,
|
||||
requires: &[Requirement::Mesh],
|
||||
run: quality_mark,
|
||||
};
|
||||
|
||||
pub const EXPORT_STL: CommandSpec = CommandSpec {
|
||||
id: "mesh.export.stl",
|
||||
label: "Export STL",
|
||||
@@ -650,6 +660,103 @@ fn quality_report(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}))
|
||||
}
|
||||
|
||||
/// The element set `mesh.quality.mark` keeps the marked elements in.
|
||||
pub const MARKED_SET: &str = "quality_marked";
|
||||
|
||||
/// The default limits of `mesh.quality.mark`.
|
||||
const MARK_LIMITS: [(quality::Metric, f64); 6] = [
|
||||
(quality::Metric::ScaledJacobian, 0.2),
|
||||
(quality::Metric::MinDihedral, 10.0),
|
||||
(quality::Metric::MaxDihedral, 165.0),
|
||||
(quality::Metric::AspectRatio, 10.0),
|
||||
(quality::Metric::MinAngle, 15.0),
|
||||
(quality::Metric::MaxAngle, 165.0),
|
||||
];
|
||||
|
||||
/// Most marked elements `mesh.quality.mark` lists.
|
||||
const MAX_MARKED_LISTED: usize = 1000;
|
||||
|
||||
fn quality_mark(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let mut limits: std::collections::BTreeMap<quality::Metric, f64> = MARK_LIMITS.into_iter().collect();
|
||||
match p.get("limits") {
|
||||
None | Some(Value::Null) => {}
|
||||
Some(Value::Object(given)) => {
|
||||
for (name, v) in given {
|
||||
let metric = quality::Metric::parse(name).ok_or_else(|| {
|
||||
let known: Vec<&str> = quality::Metric::ALL.iter().map(|m| m.name()).collect();
|
||||
EngineError::Params(format!("no quality metric `{}` (one of {})", name.chars().take(40).collect::<String>(), known.join(", ")))
|
||||
})?;
|
||||
match v {
|
||||
Value::Null => {
|
||||
limits.remove(&metric);
|
||||
}
|
||||
v => {
|
||||
let limit = v
|
||||
.as_f64()
|
||||
.filter(|x| x.is_finite())
|
||||
.ok_or_else(|| EngineError::Params(format!("the limit of `{name}` must be a number")))?;
|
||||
limits.insert(metric, limit);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(_) => return Err(EngineError::Params("`limits` must be an object of metric names and numbers".into())),
|
||||
}
|
||||
let selection = match p.get("elements") {
|
||||
None => None,
|
||||
Some(_) => Some(labels(p, "elements", MAX_ELEMENTS)?),
|
||||
};
|
||||
let limits: Vec<(quality::Metric, f64)> = limits.into_iter().collect();
|
||||
let (measured, marked) = quality::mark_elements(&s.model, selection.as_deref(), &limits)?;
|
||||
let set = fieldcraft_model::Set { elements: marked.iter().map(|m| m.label).collect(), ..Default::default() };
|
||||
if set.elements.is_empty() {
|
||||
if s.model.set(MARKED_SET).is_some() {
|
||||
s.model.remove_set(MARKED_SET)?;
|
||||
}
|
||||
} else if s.model.set(MARKED_SET).is_some() {
|
||||
s.model.replace_set(MARKED_SET, set)?;
|
||||
} else {
|
||||
s.model.add_set(MARKED_SET, set)?;
|
||||
}
|
||||
let invalid = marked.iter().filter(|m| m.is_invalid()).count();
|
||||
let c = s.model.classification();
|
||||
let body = |label: u32| c.elements().get(&label).and_then(|t| t.region).and_then(|r| c.entity(r)).map(|g| g.body.clone());
|
||||
let elements: Vec<Value> = marked
|
||||
.iter()
|
||||
.take(MAX_MARKED_LISTED)
|
||||
.map(|m| {
|
||||
json!({
|
||||
"label": m.label,
|
||||
"kind": m.kind.name(),
|
||||
"body": body(m.label),
|
||||
"at": m.centroid,
|
||||
"valid": m.validity.map(|v| v == quality::Validity::Valid),
|
||||
"failed": m.failed.iter().map(|(metric, v, limit)| json!({"metric": metric.name(), "value": number(*v), "limit": limit})).collect::<Vec<_>>(),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
let mut summary = format!("{} of {measured} elements marked", marked.len());
|
||||
if invalid > 0 {
|
||||
summary.push_str(&format!(", {invalid} not valid"));
|
||||
}
|
||||
if let Some(worst) = marked.first().and_then(|m| m.scaled_jacobian) {
|
||||
summary.push_str(&format!("; worst scaled Jacobian {worst:.3}"));
|
||||
}
|
||||
if !marked.is_empty() {
|
||||
summary.push_str(&format!(" (set {MARKED_SET})"));
|
||||
}
|
||||
Ok(json!({
|
||||
"summary": summary,
|
||||
"set": (!marked.is_empty()).then_some(MARKED_SET),
|
||||
"measured": measured,
|
||||
"marked": marked.len(),
|
||||
"invalid": invalid,
|
||||
"limits": limits.iter().map(|(m, v)| (m.name().to_string(), json!(v))).collect::<serde_json::Map<_, _>>(),
|
||||
"elements": elements,
|
||||
"truncated": marked.len() > MAX_MARKED_LISTED,
|
||||
}))
|
||||
}
|
||||
|
||||
/// A quality report in a line, for the status bar and the CLI: the elements measured, how many
|
||||
/// are invalid or undetermined, and the scaled Jacobian's minimum and mean.
|
||||
fn quality_summary(r: &QualityReport) -> String {
|
||||
@@ -825,6 +932,73 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn mark_session() -> Session {
|
||||
let mut s = Session::default();
|
||||
let corners = json!([[0, 0, 0], [2, 0, 0], [2, 1, 0], [0, 1, 0], [0, 0, 1], [2, 0, 1], [2, 1, 1], [0, 1, 1]]);
|
||||
s.execute("mesh.block", &json!({"body": "Bar", "kind": "hex8", "corners": corners, "divisions": [2, 1, 1]})).unwrap();
|
||||
s
|
||||
}
|
||||
|
||||
/// Two good hexes mark nothing; a corner pulled in marks its element, located, as the set
|
||||
/// the viewport draws; looser limits empty it; undo brings it back.
|
||||
#[test]
|
||||
fn marks_bad_elements_as_a_set() {
|
||||
let mut s = mark_session();
|
||||
let r = s.execute("mesh.quality.mark", &json!({})).unwrap();
|
||||
assert_eq!((r["measured"].as_u64(), r["marked"].as_u64()), (Some(2), Some(0)), "{r}");
|
||||
assert!(s.model.set(super::MARKED_SET).is_none() && r["set"].is_null());
|
||||
assert_eq!(r["limits"]["scaled_jacobian"], 0.2);
|
||||
// Pull the far top corner of the second hex nearly onto its middle.
|
||||
let (&corner, _) = s.model.nodes().iter().find(|(_, n)| n.xyz == [2.0, 1.0, 1.0]).unwrap();
|
||||
let (&bad, _) = s.model.elements().iter().find(|(_, e)| e.nodes.contains(&corner)).unwrap();
|
||||
s.execute("mesh.node.add", &json!({"label": corner, "xyz": [1.7, 0.7, 0.7]})).unwrap();
|
||||
let r = s.execute("mesh.quality.mark", &json!({})).unwrap();
|
||||
assert_eq!((r["marked"].as_u64(), r["invalid"].as_u64()), (Some(1), Some(0)), "{r}");
|
||||
assert_eq!(r["set"], super::MARKED_SET);
|
||||
let e = &r["elements"][0];
|
||||
assert_eq!((e["label"].as_u64(), e["kind"].as_str(), e["body"].as_str()), (Some(bad as u64), Some("hex8"), Some("Bar")), "{e}");
|
||||
assert!(e["failed"].as_array().unwrap().iter().any(|f| f["metric"] == "scaled_jacobian"), "{e}");
|
||||
let at: Vec<f64> = e["at"].as_array().unwrap().iter().map(|v| v.as_f64().unwrap()).collect();
|
||||
assert!(at[0] > 1.0 && at[0] < 2.0, "{at:?}");
|
||||
assert!(r["summary"].as_str().unwrap().starts_with("1 of 2 elements marked"), "{r}");
|
||||
assert_eq!(s.model.set(super::MARKED_SET).unwrap().elements, [bad]);
|
||||
assert_eq!(crate::view::marked(&s).into_iter().collect::<Vec<_>>(), [bad]);
|
||||
// The picture draws its faces in the marker colour, and only its.
|
||||
let p = crate::view::Palette::LIGHT;
|
||||
let (scene, _, _) = crate::view::scene(&s, &p);
|
||||
let red = scene.tris.iter().filter(|t| t.color == p.marked).count();
|
||||
assert_eq!(red, 5 * 2, "the bad hex's outer faces (all but the shared one), as triangles");
|
||||
// Without limits the (valid) element passes and the set goes.
|
||||
let all_off = json!({"limits": {"scaled_jacobian": null, "min_dihedral": null, "max_dihedral": null, "aspect_ratio": null, "min_angle": null, "max_angle": null}});
|
||||
let r = s.execute("mesh.quality.mark", &all_off).unwrap();
|
||||
assert_eq!(r["marked"], 0, "{r}");
|
||||
assert!(s.model.set(super::MARKED_SET).is_none());
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(s.model.set(super::MARKED_SET).unwrap().elements, [bad], "undo restores the marks");
|
||||
// Only the elements asked about.
|
||||
let other = s.model.elements().keys().copied().find(|l| *l != bad).unwrap();
|
||||
assert_eq!(s.execute("mesh.quality.mark", &json!({"elements": [other]})).unwrap()["marked"], 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mark_rejects_hostile_parameters() {
|
||||
let mut s = Session::default();
|
||||
assert!(s.execute("mesh.quality.mark", &json!({})).is_err(), "needs a mesh");
|
||||
let mut s = mark_session();
|
||||
for p in [
|
||||
json!({"limits": {"nope": 1}}),
|
||||
json!({"limits": {"scaled_jacobian": "low"}}),
|
||||
json!({"limits": {"scaled_jacobian": true}}),
|
||||
json!({"limits": [0.2]}),
|
||||
json!({"limits": {"x".repeat(10000): 1}}),
|
||||
json!({"elements": [0]}),
|
||||
json!({"elements": [99]}),
|
||||
json!({"elements": "all"}),
|
||||
] {
|
||||
assert!(s.execute("mesh.quality.mark", &p).is_err(), "{p}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reports_quality_and_inverted_elements() {
|
||||
let mut s = Session::default();
|
||||
|
||||
@@ -75,6 +75,7 @@ pub static ALL: &[CommandSpec] = &[
|
||||
mesh::ELEMENT_REMOVE,
|
||||
replace::CLEAR,
|
||||
mesh::QUALITY,
|
||||
mesh::QUALITY_MARK,
|
||||
mesh::EXPORT_STL,
|
||||
mesh::DELAUNAY_POINTS,
|
||||
mesh::SURFACE_CHECK,
|
||||
|
||||
@@ -41,6 +41,8 @@ pub struct Palette {
|
||||
pub contour: [Rgb; 5],
|
||||
/// Values above a clamped legend's maximum.
|
||||
pub contour_over: Rgb,
|
||||
/// The elements `mesh.quality.mark` marked.
|
||||
pub marked: Rgb,
|
||||
/// The analysis setup's symbols.
|
||||
pub glyphs: GlyphColors,
|
||||
}
|
||||
@@ -73,6 +75,7 @@ impl Palette {
|
||||
no_result: Rgb(0x9a, 0xa0, 0xaa),
|
||||
contour: [Rgb(0x2b, 0x4f, 0xd8), Rgb(0x2f, 0xb8, 0xe0), Rgb(0x3c, 0xc8, 0x64), Rgb(0xf0, 0xd2, 0x3c), Rgb(0xe0, 0x40, 0x2e)],
|
||||
contour_over: CONTOUR_OVER,
|
||||
marked: Rgb(0xe0, 0x2e, 0x2e),
|
||||
glyphs: GlyphColors {
|
||||
constraint: Rgb(0x00, 0x8a, 0xa8),
|
||||
load: Rgb(0xd0, 0x24, 0x24),
|
||||
@@ -218,6 +221,24 @@ fn model_scene(d: &DisplayMesh, p: &Palette) -> Scene {
|
||||
s
|
||||
}
|
||||
|
||||
/// The elements `mesh.quality.mark` marked (its set), drawn in the marker colour.
|
||||
pub fn marked(s: &Session) -> std::collections::BTreeSet<u32> {
|
||||
s.model.set(crate::cmd::mesh::MARKED_SET).map(|set| set.elements.iter().copied().collect()).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Colour the faces of the `marked` elements in `scene`, whose triangles start with the element
|
||||
/// faces of `display` in order (as [`model_scene`] and the app's viewport build them).
|
||||
pub fn mark(scene: &mut Scene, display: &DisplayMesh, marked: &std::collections::BTreeSet<u32>, color: Rgb) {
|
||||
if marked.is_empty() {
|
||||
return;
|
||||
}
|
||||
for (t, d) in scene.tris.iter_mut().zip(&display.tris) {
|
||||
if marked.contains(&d.element) {
|
||||
t.color = color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The model part of a picture: the model or the results view's plot, the shape the symbols sit
|
||||
/// on (the deformed one of a plot), the box around it and what it shows.
|
||||
struct Picture {
|
||||
@@ -251,6 +272,7 @@ fn picture(s: &Session, p: &Palette, section: Option<Section>, toward: Vec3) ->
|
||||
let glyphs = plot.map_or_else(|| s.glyphs(), |(v, _)| s.step_glyphs(&v.step));
|
||||
let Some((view, result)) = plot else {
|
||||
let mut scene = model_scene(&base, p);
|
||||
mark(&mut scene, &base, &marked(s), p.marked);
|
||||
let bounds = base.bounds;
|
||||
let shows = |n: &str| s.shows(fieldcraft_io::ItemKind::Datum, n);
|
||||
let datums = fieldcraft_render::glyphs::datums(&s.model, &base.bounds, &shows, None, &p.glyphs, &mut scene);
|
||||
|
||||
+2
-1
@@ -6,7 +6,7 @@ Every entry is a toolbar button. **Ready** entries run their command; **coming s
|
||||
shown greyed with the roadmap milestone that brings them (ROADMAP.md). Scripts, the command line,
|
||||
the control channel and AI agents over MCP (docs/mcp.md) run the same commands.
|
||||
|
||||
76 ready, 39 coming soon.
|
||||
77 ready, 39 coming soon.
|
||||
|
||||
## Geometry
|
||||
|
||||
@@ -57,6 +57,7 @@ the control channel and AI agents over MCP (docs/mcp.md) run the same commands.
|
||||
| Edit | Remesh Region — Remesh a region in place; split and collapse elements | coming soon (M6) | |
|
||||
| Edit | Orphan Mesh — Import a mesh without geometry | coming soon (M6) | |
|
||||
| Verify | Quality — Quality metrics and the certified validity check, failing elements listed | ready | `mesh.quality` |
|
||||
| Verify | Mark Bad Elements — Mark the elements below quality limits as a set, located and drawn in the marker colour | ready | `mesh.quality.mark` |
|
||||
| Verify | Verify Set — Quality and validity per set | coming soon (M2) | |
|
||||
| Verify | Export STL — Write the mesh's outer surface (or a set's faces) as binary STL | ready | `mesh.export.stl` |
|
||||
| Verify | Tag Geometry — Tag elements, faces, edges and nodes with the geometry they mesh | ready | `mesh.tag`, `mesh.tag.clear` |
|
||||
|
||||
Reference in new issue
Block a user