forked from 1b-agent/fieldcraft-upstream
engine: mesh.export.stl writes the outer surface or a set's faces
Binary by default, ASCII on request; in the Mesh tab's Verify panel. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
1 parent
87710a83ab
commit
1d71a9f8af
12 files changed
+166
-5
No files matched your search
@@ -92,6 +92,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.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.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.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.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 |
|
| 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 |
|
||||||
| assets/icons/mesh.size.adapt.svg, assets/icons/mesh.size.adapt.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.size.adapt.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/mesh.size.adapt.svg, assets/icons/mesh.size.adapt.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.size.adapt.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/mesh.size.global.svg, assets/icons/mesh.size.global.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.size.global.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/mesh.size.global.svg, assets/icons/mesh.size.global.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.size.global.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.export.stl</title>
|
||||||
|
<g id="Layer_1" data-name="Layer 1">
|
||||||
|
<path d="M3 9 L9 5 L15 9 L9 13 Z" fill="#2d4e70" stroke="#dde2ea" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M3 9 L3 17 L9 21 L9 13 M9 21 L15 17 L15 9" fill="none" stroke="#dde2ea" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M3 9 L9 21 M9 13 L15 17" fill="none" stroke="#dde2ea" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M15 15 L21 15 M18.19 16.53 L21 15 L18.19 13.47" fill="none" stroke="#4f9ce8" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 817 B |
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
{"shapes": [{"d": "M3 9 L9 5 L15 9 L9 13 Z", "fill": "soft", "stroke": "line"}, {"d": "M3 9 L3 17 L9 21 L9 13 M9 21 L15 17 L15 9", "stroke": "line"}, {"d": "M3 9 L9 21 M9 13 L15 17", "stroke": "line", "width": 1}, {"d": "M15 15 L21 15 M18.19 16.53 L21 15 L18.19 13.47", "stroke": "accent", "width": 1.5}]}
|
||||||
@@ -399,6 +399,7 @@ pub static CATALOGUE: &[Capability] = &[
|
|||||||
soon("mesh.import.orphan", "Orphan Mesh", Mesh, "Edit", "M6", "Import a mesh without geometry"),
|
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", "Quality", Mesh, "Verify", "Quality metrics and the certified validity check, failing elements listed"),
|
||||||
soon("mesh.verify", "Verify Set", Mesh, "Verify", "M2", "Quality and validity per set"),
|
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"]),
|
ready_more("mesh.tag", "Tag Geometry", Mesh, "Verify", "Tag elements, faces, edges and nodes with the geometry they mesh", &["mesh.tag.clear"]),
|
||||||
// ---- Properties ----
|
// ---- Properties ----
|
||||||
ready_more(
|
ready_more(
|
||||||
|
|||||||
@@ -1704,6 +1704,12 @@ pub const DECK: &[Field] = &[
|
|||||||
references("steps", "Steps", C::Step).help("The steps to write; all when empty.").hint("all"),
|
references("steps", "Steps", C::Step).help("The steps to write; all when empty.").hint("all"),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
pub const EXPORT_STL: &[Field] = &[
|
||||||
|
path("path", "File", true).help("Where to write the STL file.").req(),
|
||||||
|
reference("set", "Set", C::Set).help("Write only this set's element faces; the whole outer surface when empty.").hint("the outer surface"),
|
||||||
|
choice("format", "Format", &[("binary", "Binary"), ("ascii", "ASCII")]).help("Binary is compact; ASCII is readable text.").def("\"binary\""),
|
||||||
|
];
|
||||||
|
|
||||||
// ---- Solving ----
|
// ---- Solving ----
|
||||||
|
|
||||||
const STEP: Field = reference("step", "Step", C::Step).help("The step to solve; the only one when empty.").hint("the only one");
|
const STEP: Field = reference("step", "Step", C::Step).help("The step to solve; the only one when empty.").hint("the only one");
|
||||||
|
|||||||
@@ -67,6 +67,16 @@ pub const QUALITY: CommandSpec = CommandSpec {
|
|||||||
run: quality_report,
|
run: quality_report,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub const EXPORT_STL: CommandSpec = CommandSpec {
|
||||||
|
id: "mesh.export.stl",
|
||||||
|
label: "Export STL",
|
||||||
|
params: r#"{"path": "skin.stl", "set": "top", "format": "binary|ascii"} — writes the mesh's outer surface as STL: every solid element face no other solid shares, and the top side (face 1) of every shell; `set` (optional) writes only that set's element faces instead; `format` defaults to binary. Quadrilaterals are split in two, normals point out of the elements, and coordinates are rounded to single precision (STL stores f32). Replies {path, triangles, bytes}"#,
|
||||||
|
form: Some(super::forms::EXPORT_STL),
|
||||||
|
kind: CommandKind::Session,
|
||||||
|
requires: &[Requirement::Mesh],
|
||||||
|
run: export_stl,
|
||||||
|
};
|
||||||
|
|
||||||
pub const DELAUNAY_POINTS: CommandSpec = CommandSpec {
|
pub const DELAUNAY_POINTS: CommandSpec = CommandSpec {
|
||||||
id: "mesh.delaunay.points",
|
id: "mesh.delaunay.points",
|
||||||
label: "Delaunay of Points",
|
label: "Delaunay of Points",
|
||||||
@@ -553,6 +563,53 @@ fn number(v: f64) -> Value {
|
|||||||
if v.is_finite() { json!(v) } else { json!(if v > 0.0 { "inf" } else { "-inf" }) }
|
if v.is_finite() { json!(v) } else { json!(if v > 0.0 { "inf" } else { "-inf" }) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn export_stl(s: &mut Session, p: &Value) -> Result<Value> {
|
||||||
|
let set: Option<String> = super::opt(p, "set")?;
|
||||||
|
let format: Option<String> = super::opt(p, "format")?;
|
||||||
|
let ascii = match format.as_deref() {
|
||||||
|
None | Some("binary") => false,
|
||||||
|
Some("ascii") => true,
|
||||||
|
Some(other) => {
|
||||||
|
let shown: String = other.chars().take(32).collect();
|
||||||
|
return Err(EngineError::Params(format!("unknown STL format `{shown}`; known: binary, ascii")));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let path = super::file_param(s, p, "path", fieldcraft_io::roots::Access::Write)?;
|
||||||
|
let m = &s.model;
|
||||||
|
if m.elements().is_empty() {
|
||||||
|
return Err(EngineError::Params("the mesh has no elements".into()));
|
||||||
|
}
|
||||||
|
let faces = match &set {
|
||||||
|
Some(name) => {
|
||||||
|
let shown: String = name.chars().take(64).collect();
|
||||||
|
if !m.sets().contains_key(name) {
|
||||||
|
return Err(EngineError::Params(format!("no set `{shown}`")));
|
||||||
|
}
|
||||||
|
let faces = m.resolve_set(name)?.faces;
|
||||||
|
if faces.is_empty() {
|
||||||
|
return Err(EngineError::Params(format!("set `{shown}` has no element faces")));
|
||||||
|
}
|
||||||
|
faces
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
let faces = m.skin_faces();
|
||||||
|
if faces.is_empty() {
|
||||||
|
return Err(EngineError::Params("the mesh has only line elements: no surface to write".into()));
|
||||||
|
}
|
||||||
|
faces
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let bytes = if ascii {
|
||||||
|
let name = std::path::Path::new(&path).file_stem().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default();
|
||||||
|
fieldcraft_io::stl::write_ascii(m, &faces, &name)?.into_bytes()
|
||||||
|
} else {
|
||||||
|
fieldcraft_io::stl::write_binary(m, &faces)?
|
||||||
|
};
|
||||||
|
let triangles = fieldcraft_io::stl::triangles(m, &faces)?.len();
|
||||||
|
fieldcraft_io::fs::write_atomic(std::path::Path::new(&path), &bytes, false)?;
|
||||||
|
Ok(json!({"path": path, "triangles": triangles, "bytes": bytes.len()}))
|
||||||
|
}
|
||||||
|
|
||||||
fn quality_report(s: &mut Session, p: &Value) -> Result<Value> {
|
fn quality_report(s: &mut Session, p: &Value) -> Result<Value> {
|
||||||
let bins = match p.get("bins") {
|
let bins = match p.get("bins") {
|
||||||
None => 10,
|
None => 10,
|
||||||
@@ -671,6 +728,63 @@ fn surface_check(s: &mut Session, p: &Value) -> Result<Value> {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
#[test]
|
||||||
|
fn export_stl_writes_the_skin_or_a_set() {
|
||||||
|
let dir = std::env::temp_dir().join(format!("fieldcraft-stl-{}", std::process::id()));
|
||||||
|
std::fs::create_dir_all(&dir).unwrap();
|
||||||
|
let path = dir.join("skin.stl");
|
||||||
|
let path_str = path.to_string_lossy().into_owned();
|
||||||
|
let mut s = Session::default();
|
||||||
|
assert!(s.execute("mesh.export.stl", &json!({"path": path_str})).is_err(), "no mesh");
|
||||||
|
s.execute("file.sample", &json!({"name": "cantilever"})).unwrap();
|
||||||
|
let journal = s.journal.len();
|
||||||
|
let out = s.execute("mesh.export.stl", &json!({"path": path_str})).unwrap();
|
||||||
|
let bytes = std::fs::read(&path).unwrap();
|
||||||
|
assert!(!bytes.starts_with(b"solid"));
|
||||||
|
let skin = fieldcraft_io::stl::triangles(&s.model, &s.model.skin_faces()).unwrap().len();
|
||||||
|
assert!(skin >= s.model.skin_faces().len() && skin > 0);
|
||||||
|
assert_eq!(out["triangles"], skin);
|
||||||
|
assert_eq!(bytes.len(), 84 + 50 * skin);
|
||||||
|
assert_eq!(out["bytes"], bytes.len());
|
||||||
|
assert_eq!(s.journal.len(), journal, "exporting does not change the model");
|
||||||
|
|
||||||
|
// A set with faces writes only those; one without faces, or an unknown one, is refused.
|
||||||
|
let faces = s.model.skin_faces();
|
||||||
|
let two: Vec<serde_json::Value> = faces.iter().take(2).map(|f| json!({"element": f.element, "face": f.face})).collect();
|
||||||
|
let first = *s.model.elements().keys().next().unwrap();
|
||||||
|
s.execute("model.set.create", &json!({"name": "stl_faces", "faces": two})).unwrap();
|
||||||
|
s.execute("model.set.create", &json!({"name": "stl_elements", "elements": [first]})).unwrap();
|
||||||
|
let out = s.execute("mesh.export.stl", &json!({"path": path_str, "set": "stl_faces"})).unwrap();
|
||||||
|
assert_eq!(out["triangles"], fieldcraft_io::stl::triangles(&s.model, &faces[..2]).unwrap().len());
|
||||||
|
let other = dir.join("other.stl").to_string_lossy().into_owned();
|
||||||
|
let e = s.execute("mesh.export.stl", &json!({"path": other, "set": "nope"})).unwrap_err().to_string();
|
||||||
|
assert!(e.contains("no set `nope`"), "{e}");
|
||||||
|
let e = s.execute("mesh.export.stl", &json!({"path": other, "set": "stl_elements"})).unwrap_err().to_string();
|
||||||
|
assert!(e.contains("has no element faces"), "{e}");
|
||||||
|
let e = s.execute("mesh.export.stl", &json!({"path": other, "format": "obj"})).unwrap_err().to_string();
|
||||||
|
assert!(e.contains("known: binary, ascii"), "{e}");
|
||||||
|
assert!(!dir.join("other.stl").exists());
|
||||||
|
|
||||||
|
let out = s.execute("mesh.export.stl", &json!({"path": path_str, "format": "ascii"})).unwrap();
|
||||||
|
let text = std::fs::read_to_string(&path).unwrap();
|
||||||
|
assert!(text.starts_with("solid skin\n"));
|
||||||
|
assert_eq!(text.matches("facet normal").count(), skin);
|
||||||
|
assert_eq!(out["triangles"], skin);
|
||||||
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn export_stl_refuses_a_mesh_of_lines() {
|
||||||
|
let mut s = Session::default();
|
||||||
|
s.execute("mesh.node.add", &json!({"label": 1, "xyz": [0, 0, 0]})).unwrap();
|
||||||
|
s.execute("mesh.node.add", &json!({"label": 2, "xyz": [1, 0, 0]})).unwrap();
|
||||||
|
s.execute("mesh.element.add", &json!({"label": 1, "kind": "rod2", "nodes": [1, 2]})).unwrap();
|
||||||
|
let path = std::env::temp_dir().join(format!("fieldcraft-stl-lines-{}.stl", std::process::id()));
|
||||||
|
let e = s.execute("mesh.export.stl", &json!({"path": path.to_string_lossy()})).unwrap_err().to_string();
|
||||||
|
assert!(e.contains("only line elements"), "{e}");
|
||||||
|
assert!(!path.exists());
|
||||||
|
}
|
||||||
|
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
use crate::Session;
|
use crate::Session;
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ pub static ALL: &[CommandSpec] = &[
|
|||||||
mesh::ELEMENT_REMOVE,
|
mesh::ELEMENT_REMOVE,
|
||||||
replace::CLEAR,
|
replace::CLEAR,
|
||||||
mesh::QUALITY,
|
mesh::QUALITY,
|
||||||
|
mesh::EXPORT_STL,
|
||||||
mesh::DELAUNAY_POINTS,
|
mesh::DELAUNAY_POINTS,
|
||||||
mesh::SURFACE_CHECK,
|
mesh::SURFACE_CHECK,
|
||||||
mesh::PLANAR,
|
mesh::PLANAR,
|
||||||
|
|||||||
@@ -545,6 +545,10 @@ mod tests {
|
|||||||
json!({"format": "nastran", "path": "/no/such/dir/part.bdf", "steps": vec!["x".repeat(1000); 2000]}),
|
json!({"format": "nastran", "path": "/no/such/dir/part.bdf", "steps": vec!["x".repeat(1000); 2000]}),
|
||||||
json!({"format": "nastran", "path": "/no/such/dir/part.bdf", "steps": {}}),
|
json!({"format": "nastran", "path": "/no/such/dir/part.bdf", "steps": {}}),
|
||||||
json!({"path": 7}),
|
json!({"path": 7}),
|
||||||
|
json!({"path": "x\u{0}.stl"}),
|
||||||
|
json!({"path": "/no/such/dir/skin.stl", "set": 7}),
|
||||||
|
json!({"path": "/no/such/dir/skin.stl", "set": "x".repeat(100_000)}),
|
||||||
|
json!({"path": "/no/such/dir/skin.stl", "format": ["ascii"]}),
|
||||||
// (`max_points` and `max_passes` keep mesh.tet.generate's quality stage small.)
|
// (`max_points` and `max_passes` keep mesh.tet.generate's quality stage small.)
|
||||||
json!({"body": "Body1", "tolerance": 0.05, "max_points": 3000, "max_passes": 1}),
|
json!({"body": "Body1", "tolerance": 0.05, "max_points": 3000, "max_passes": 1}),
|
||||||
json!({"body": null, "tolerance": "fine"}),
|
json!({"body": null, "tolerance": "fine"}),
|
||||||
|
|||||||
+12
-1
@@ -176,7 +176,18 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn tet10_and_wedge6_close_with_outward_normals() {
|
fn tet10_and_wedge6_close_with_outward_normals() {
|
||||||
let mut t = Model::default();
|
let mut t = Model::default();
|
||||||
let p = [[0., 0., 0.], [1., 0., 0.], [0., 1., 0.], [0., 0., 1.], [0.5, 0., 0.], [0.5, 0.5, 0.], [0., 0.5, 0.], [0., 0., 0.5], [0.5, 0., 0.5], [0., 0.5, 0.5]];
|
let p = [
|
||||||
|
[0., 0., 0.],
|
||||||
|
[1., 0., 0.],
|
||||||
|
[0., 1., 0.],
|
||||||
|
[0., 0., 1.],
|
||||||
|
[0.5, 0., 0.],
|
||||||
|
[0.5, 0.5, 0.],
|
||||||
|
[0., 0.5, 0.],
|
||||||
|
[0., 0., 0.5],
|
||||||
|
[0.5, 0., 0.5],
|
||||||
|
[0., 0.5, 0.5],
|
||||||
|
];
|
||||||
for (i, xyz) in p.iter().enumerate() {
|
for (i, xyz) in p.iter().enumerate() {
|
||||||
t.set_node(i as u32 + 1, *xyz).unwrap();
|
t.set_node(i as u32 + 1, *xyz).unwrap();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -403,9 +403,20 @@ mod tests {
|
|||||||
assert_eq!(m.skin_faces().len(), 10, "two hex8 sharing a face");
|
assert_eq!(m.skin_faces().len(), 10, "two hex8 sharing a face");
|
||||||
|
|
||||||
let mut t = Model::default();
|
let mut t = Model::default();
|
||||||
for (i, p) in [[0., 0., 0.], [1., 0., 0.], [0., 1., 0.], [0., 0., 1.], [0.5, 0., 0.], [0.5, 0.5, 0.], [0., 0.5, 0.], [0., 0., 0.5], [0.5, 0., 0.5], [0., 0.5, 0.5]]
|
for (i, p) in [
|
||||||
.iter()
|
[0., 0., 0.],
|
||||||
.enumerate()
|
[1., 0., 0.],
|
||||||
|
[0., 1., 0.],
|
||||||
|
[0., 0., 1.],
|
||||||
|
[0.5, 0., 0.],
|
||||||
|
[0.5, 0.5, 0.],
|
||||||
|
[0., 0.5, 0.],
|
||||||
|
[0., 0., 0.5],
|
||||||
|
[0.5, 0., 0.5],
|
||||||
|
[0., 0.5, 0.5],
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
{
|
{
|
||||||
t.set_node(i as u32 + 1, *p).unwrap();
|
t.set_node(i as u32 + 1, *p).unwrap();
|
||||||
}
|
}
|
||||||
|
|||||||
+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,
|
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.
|
the control channel and AI agents over MCP (docs/mcp.md) run the same commands.
|
||||||
|
|
||||||
73 ready, 39 coming soon.
|
74 ready, 39 coming soon.
|
||||||
|
|
||||||
## Geometry
|
## Geometry
|
||||||
|
|
||||||
@@ -57,6 +57,7 @@ the control channel and AI agents over MCP (docs/mcp.md) run the same commands.
|
|||||||
| Edit | Orphan Mesh — Import a mesh without geometry | 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 | Quality — Quality metrics and the certified validity check, failing elements listed | ready | `mesh.quality` |
|
||||||
| Verify | Verify Set — Quality and validity per set | coming soon (M2) | |
|
| 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` |
|
| Verify | Tag Geometry — Tag elements, faces, edges and nodes with the geometry they mesh | ready | `mesh.tag`, `mesh.tag.clear` |
|
||||||
|
|
||||||
## Properties
|
## Properties
|
||||||
|
|||||||
Reference in new issue
Block a user