forked from 1b-agent/fieldcraft-upstream
engine: geometry.face.split, a face split along a datum plane (#508)
geometry.face.split {name, face, datum} records a face split; it is
applied after the merges whenever the design is opened or cut again,
follows its datum (which it holds), undoes, and is saved with the
model. geometry.face.split.delete removes it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
1 parent
7cd3dcfaa0
commit
54e367970f
12 files changed
+172
-10
No files matched your search
@@ -67,6 +67,7 @@ is committed and listed here with its exports.
|
||||
| assets/icons/geometry.partition.svg, assets/icons/geometry.partition.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/geometry.partition.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||
| assets/icons/geometry.faces.merge.svg, assets/icons/geometry.faces.merge.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/geometry.faces.merge.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||
| assets/icons/geometry.face.absorb.svg, assets/icons/geometry.face.absorb.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/geometry.face.absorb.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||
| assets/icons/geometry.face.split.svg, assets/icons/geometry.face.split.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/geometry.face.split.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||
| assets/icons/geometry.stitch.svg, assets/icons/geometry.stitch.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/geometry.stitch.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||
| assets/icons/geometry.tessellate.svg, assets/icons/geometry.tessellate.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/geometry.tessellate.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||
| assets/icons/interaction.bolt.svg, assets/icons/interaction.bolt.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/interaction.bolt.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>geometry.face.split</title>
|
||||
<g id="Layer_1" data-name="Layer 1">
|
||||
<path d="M3 8 L12 4 L21 8 L12 12 Z" fill="#2d4e70" stroke="#dde2ea" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M3 8 L3 16 L12 20 L12 12 M12 20 L21 16 L21 8" fill="none" stroke="#dde2ea" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M7.5 10 L16.5 6" fill="none" stroke="#f0b44a" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M5 4.5 L9 2.5 M15 9.5 L19 7.5" fill="none" stroke="#4f9ce8" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="1.5 1.5"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 845 B |
Binary file not shown.
@@ -0,0 +1 @@
|
||||
{"shapes": [{"d": "M3 8 L12 4 L21 8 L12 12 Z", "fill": "soft", "stroke": "line"}, {"d": "M3 8 L3 16 L12 20 L12 12 M12 20 L21 16 L21 8", "stroke": "line"}, {"line": [7.5, 10, 16.5, 6], "stroke": "warm", "width": 2}, {"d": "M5 4.5 L9 2.5 M15 9.5 L19 7.5", "stroke": "accent", "width": 1.5, "dash": [1.5, 1.5]}]}
|
||||
@@ -309,6 +309,14 @@ pub static CATALOGUE: &[Capability] = &[
|
||||
"Prepare",
|
||||
"Absorb tiny and sliver faces into a neighbour for meshing, one picked or every one Check Geometry found",
|
||||
),
|
||||
covering(
|
||||
"geometry.face.split",
|
||||
"Split Face",
|
||||
Geometry,
|
||||
"Prepare",
|
||||
"Split a face along a datum plane for meshing only: two faces on its surface, the cut an edge between them",
|
||||
&["geometry.face.split.delete"],
|
||||
),
|
||||
covering(
|
||||
"geometry.stitch",
|
||||
"Stitch",
|
||||
|
||||
@@ -470,15 +470,24 @@ fn cuts(model: &Model) -> Vec<Cut> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// `design` cut by `model`'s partitions, then with its face merges (the design as opened when
|
||||
/// the model has neither).
|
||||
/// `design` cut by `model`'s partitions, then with its face merges and its face splits (those
|
||||
/// along a suppressed datum left out; the design as opened when the model has none).
|
||||
pub(crate) fn partitioned(design: &Design, model: &Model) -> Result<Design> {
|
||||
let merges: Vec<Merge> = model
|
||||
.face_merges()
|
||||
.iter()
|
||||
.map(|m| Merge { name: m.name.clone(), body: m.body.clone(), faces: m.faces.clone(), into: m.into.clone() })
|
||||
.collect();
|
||||
Ok(design.partitioned(&cuts(model))?.merged(&merges)?)
|
||||
let splits: Vec<fieldcraft_geometry::face_split::FaceSplit> = model
|
||||
.face_splits()
|
||||
.iter()
|
||||
.filter(|s| !model.datum_suppressed(&s.datum))
|
||||
.filter_map(|s| {
|
||||
let (point, normal) = model.datum(&s.datum)?.plane()?;
|
||||
Some(fieldcraft_geometry::face_split::FaceSplit { name: s.name.clone(), body: s.body.clone(), face: s.face.clone(), point, normal })
|
||||
})
|
||||
.collect();
|
||||
Ok(design.partitioned(&cuts(model))?.merged(&merges)?.split(&splits)?)
|
||||
}
|
||||
|
||||
/// Cut the session's design by its model's partitions again (and merge its faces again). The bodies that are gone (cells
|
||||
|
||||
@@ -1740,6 +1740,12 @@ pub const STITCH: &[Field] = &[
|
||||
.req(),
|
||||
];
|
||||
pub const STITCH_DELETE: &[Field] = existing!("Stitch", C::Stitch);
|
||||
pub const FACE_SPLIT: &[Field] = &[
|
||||
NAME,
|
||||
group("face", "Face", FACE_ROW).help("The face to split.").req(),
|
||||
reference("datum", "Datum plane", C::DatumPlane).help("The plane the face is split along.").req(),
|
||||
];
|
||||
pub const FACE_SPLIT_DELETE: &[Field] = existing!("Face split", C::FaceSplit);
|
||||
pub const FACES_MERGE_DELETE: &[Field] = existing!("Face merge", C::FaceMerge);
|
||||
pub const MESH_PLAN: &[Field] = &[
|
||||
reference("body", "Body", C::Body).help("The body of the design to plan (as opened, not a partition cell).").req(),
|
||||
|
||||
@@ -76,6 +76,8 @@ pub static ALL: &[CommandSpec] = &[
|
||||
virtual_faces::FACES_MERGE,
|
||||
virtual_faces::FACES_MERGE_DELETE,
|
||||
virtual_faces::FACE_ABSORB,
|
||||
virtual_faces::FACE_SPLIT,
|
||||
virtual_faces::FACE_SPLIT_DELETE,
|
||||
stitch::STITCH,
|
||||
stitch::STITCH_DELETE,
|
||||
mesh::NODE_ADD,
|
||||
|
||||
@@ -45,6 +45,26 @@ pub const FACE_ABSORB: CommandSpec = CommandSpec {
|
||||
run: absorb,
|
||||
};
|
||||
|
||||
pub const FACE_SPLIT: CommandSpec = CommandSpec {
|
||||
id: "geometry.face.split",
|
||||
label: "Split Face",
|
||||
params: r#"{"name": "Band", "face": {"body": "Base", "face": "F1:+z"}, "datum": "Mid" (a datum plane)} — splits a face along the curve where a datum plane crosses it, for meshing only (virtual topology; partitions cut whole bodies): the meshers, the checks and the viewport take it as two faces on its surface, `split:<name>:a` on the plane's negative side and `split:<name>:b` on the other, with the cut as an edge between them (a row of nodes along it; seed it like any edge), and the face's edges cut where the plane crosses them. The B-rep is not changed. Applied after the face merges, by persistent names, whenever the design is opened (a merge cannot name a split face); moving the datum moves the cut, and the datum cannot be deleted while a split uses it. The body loses its mesh (`replaced`); sets naming the face no longer resolve (`stale_sets`). A plane that does not cross the face is an error. Delete with geometry.face.split.delete. One undo step"#,
|
||||
form: Some(super::forms::FACE_SPLIT),
|
||||
kind: CommandKind::Model,
|
||||
requires: &[Requirement::Design, Requirement::DatumPlane],
|
||||
run: split,
|
||||
};
|
||||
|
||||
pub const FACE_SPLIT_DELETE: CommandSpec = CommandSpec {
|
||||
id: "geometry.face.split.delete",
|
||||
label: "Delete Face Split",
|
||||
params: r#"{"name": "Band"} — removes a face split: the face is one face again; the body loses its mesh (`replaced`)"#,
|
||||
form: Some(super::forms::FACE_SPLIT_DELETE),
|
||||
kind: CommandKind::Model,
|
||||
requires: &[Requirement::FaceSplit],
|
||||
run: split_delete,
|
||||
};
|
||||
|
||||
/// Most faces one merge lists.
|
||||
const MAX_FACES: usize = fieldcraft_model::virtual_faces::MAX_FACE_MERGE_FACES;
|
||||
|
||||
@@ -75,12 +95,12 @@ fn stale_sets(s: &Session, body: &str) -> Vec<String> {
|
||||
s.model.sets().iter().filter(|(_, set)| set.geometry.iter().any(gone)).map(|(n, _)| n.clone()).collect()
|
||||
}
|
||||
|
||||
/// Apply the model's partitions and merges to the design again, failing with the error of a merge
|
||||
/// named in `merges` when it has one; the meshes of `bodies` go (their faces changed).
|
||||
fn reapply(s: &mut Session, merges: &[String], bodies: &[String], reply: Value) -> Result<Value> {
|
||||
/// Apply the model's partitions, merges and splits to the design again, failing with the error of
|
||||
/// one of `features` (`merge <name>`, `split <name>`) when it has one; the meshes of `bodies` go
|
||||
/// (their faces changed).
|
||||
fn reapply(s: &mut Session, features: &[String], bodies: &[String], reply: Value) -> Result<Value> {
|
||||
let mut cleared = super::datum::recut(s)?;
|
||||
let design = s.design.as_deref().ok_or_else(|| EngineError::Params("open a design first".into()))?;
|
||||
let features: Vec<String> = merges.iter().map(|m| format!("merge {m}")).collect();
|
||||
let errors: Vec<String> =
|
||||
design.errors.iter().filter(|e| features.contains(&e.feature)).map(|e| format!("{}: {}", e.feature, e.message)).collect();
|
||||
if !errors.is_empty() {
|
||||
@@ -110,7 +130,7 @@ fn merge(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
s.model.add_face_merge(FaceMerge { name: name.clone(), body: body.clone(), faces: faces.clone(), into: None })?;
|
||||
let face = merged_face_name(&name);
|
||||
let reply = json!({"name": name, "body": body, "face": face});
|
||||
let mut reply = reapply(s, std::slice::from_ref(&name), std::slice::from_ref(&body), reply)?;
|
||||
let mut reply = reapply(s, &[format!("merge {name}")], std::slice::from_ref(&body), reply)?;
|
||||
let shape = s.design.as_deref().and_then(|d| d.body(&body));
|
||||
let brep = shape.and_then(|b| b.face_index(&face)).and_then(|f| shape?.merged_faces().into_iter().find(|(g, _)| *g == f)).map_or(0, |(_, n)| n);
|
||||
if let Some(o) = reply.as_object_mut() {
|
||||
@@ -200,7 +220,29 @@ fn absorb(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
return Err(EngineError::Params(format!("no face could be absorbed: {}", json!(skipped))));
|
||||
}
|
||||
let bodies: Vec<String> = shapes.into_iter().map(|b| b.name).collect();
|
||||
reapply(s, &merges, &bodies, json!({"absorbed": absorbed, "skipped": skipped}))
|
||||
let features: Vec<String> = merges.iter().map(|m| format!("merge {m}")).collect();
|
||||
reapply(s, &features, &bodies, json!({"absorbed": absorbed, "skipped": skipped}))
|
||||
}
|
||||
|
||||
fn split(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let name = super::name(p)?.to_string();
|
||||
let r: GeometryRef = super::req(p, "face")?;
|
||||
let Entity::Face(face) = r.entity else { return Err(EngineError::invalid("face", "give {\"body\", \"face\"}")) };
|
||||
let datum: String = super::req(p, "datum")?;
|
||||
super::datum::datum_plane_of(&s.model, &datum)?;
|
||||
s.model.add_face_split(fieldcraft_model::FaceSplit { name: name.clone(), body: r.body.clone(), face: face.clone(), datum })?;
|
||||
let feature = format!("split:{name}");
|
||||
let faces = [format!("{feature}:a"), format!("{feature}:b")];
|
||||
let reply = json!({"name": name, "body": r.body, "faces": faces});
|
||||
reapply(s, &[format!("split {name}")], std::slice::from_ref(&r.body), reply)
|
||||
}
|
||||
|
||||
fn split_delete(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let name = super::name(p)?;
|
||||
let removed = s.model.remove_face_split(name)?;
|
||||
let mut cleared = super::datum::recut(s)?;
|
||||
cleared.extend(super::replace::clear(&mut s.model, [removed.body.as_str()]));
|
||||
Ok(cleared.report_if_any(s, json!({"deleted": name, "body": removed.body})))
|
||||
}
|
||||
|
||||
fn delete(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
@@ -507,4 +549,75 @@ mod tests {
|
||||
assert_eq!(r["absorbed"][0]["name"], "Riser", "{r}");
|
||||
assert!(s.model.face_merges()[0].into.is_some());
|
||||
}
|
||||
|
||||
/// The plate's top face (z = 10), by name.
|
||||
fn top(s: &Session) -> String {
|
||||
let b = &s.design.as_ref().unwrap().bodies[0];
|
||||
b.faces()
|
||||
.iter()
|
||||
.zip(b.face_surfaces().unwrap())
|
||||
.find(|(_, x)| matches!(x, Some(FaceSurface::Plane { normal, point }) if normal[2] > 0.5 && point[2] > 9.0))
|
||||
.unwrap()
|
||||
.0
|
||||
.name
|
||||
.clone()
|
||||
}
|
||||
|
||||
/// Mesh nodes on the top along x = `x`.
|
||||
fn nodes_along(s: &Session, x: f64) -> usize {
|
||||
s.model.nodes().values().filter(|n| (n.xyz[0] - x).abs() < 1e-6 && (n.xyz[2] - 10.0).abs() < 1e-6).count()
|
||||
}
|
||||
|
||||
/// Splitting the top along a datum plane gives two faces the tet mesher tags, with a row of
|
||||
/// nodes along the cut; the split follows the datum, holds it, is saved and replayed, and
|
||||
/// undoes.
|
||||
#[test]
|
||||
fn a_split_face_meshes_with_a_node_row_along_the_cut() {
|
||||
let (mut s, dir) = super::super::datum::tests::plate();
|
||||
let body = s.design.as_ref().unwrap().bodies[0].name.clone();
|
||||
let face = json!({"body": body, "face": top(&s)});
|
||||
s.execute("geometry.datum", &json!({"name": "Cut", "by": {"type": "offset", "plane": "yz", "offset": 30.3}})).unwrap();
|
||||
let mesh = json!({"size": 3});
|
||||
s.execute("mesh.tet.generate", &mesh).unwrap();
|
||||
let before = nodes_along(&s, 30.3);
|
||||
let r = s.execute("geometry.face.split", &json!({"name": "Band", "face": face, "datum": "Cut"})).unwrap();
|
||||
assert_eq!(r["faces"], json!(["split:Band:a", "split:Band:b"]), "{r}");
|
||||
assert!(r["replaced"].is_array(), "{r}");
|
||||
assert_eq!(faces(&s).len(), 9);
|
||||
s.execute("mesh.tet.generate", &mesh).unwrap();
|
||||
let after = nodes_along(&s, 30.3);
|
||||
assert!(after >= 10 && after > before, "a node row along the 30 mm cut: {before} -> {after}");
|
||||
let c = s.model.classification();
|
||||
let used: BTreeSet<String> =
|
||||
c.elements().values().flat_map(|t| t.faces.values()).filter_map(|&t| c.entity(t)?.entity.name().map(str::to_string)).collect();
|
||||
assert!(used.contains("split:Band:a") && used.contains("split:Band:b"), "{used:?}");
|
||||
// The datum holds; moving it moves the cut.
|
||||
assert!(s.execute("geometry.datum.delete", &json!({"name": "Cut"})).is_err());
|
||||
s.execute("geometry.datum.edit", &json!({"name": "Cut", "by": {"type": "offset", "plane": "yz", "offset": 35.5}})).unwrap();
|
||||
s.execute("mesh.tet.generate", &mesh).unwrap();
|
||||
assert!(nodes_along(&s, 35.5) >= 10);
|
||||
// Saved and replayed.
|
||||
let file = dir.join("split.fieldcraft");
|
||||
s.execute("file.save", &json!({"path": file.to_str().unwrap()})).unwrap();
|
||||
let mut t = Session::default();
|
||||
t.execute("file.open", &json!({"path": file.to_str().unwrap()})).unwrap();
|
||||
assert_eq!(faces(&t), faces(&s));
|
||||
// Bad splits change nothing; delete brings the face back.
|
||||
let journal = s.journal.len();
|
||||
for p in [
|
||||
json!({"name": "X", "face": face, "datum": "nope"}),
|
||||
json!({"name": "X", "face": {"body": body, "face": "nope"}, "datum": "Cut"}),
|
||||
json!({"name": "X", "face": {"body": body}, "datum": "Cut"}),
|
||||
json!({"name": "Band", "face": face, "datum": "Cut"}),
|
||||
json!({"name": "X", "face": {"body": body, "face": "split:Band:b"}, "datum": "Cut"}),
|
||||
] {
|
||||
assert!(s.execute("geometry.face.split", &p).is_err(), "{p}");
|
||||
}
|
||||
assert_eq!(s.journal.len(), journal);
|
||||
s.execute("geometry.face.split.delete", &json!({"name": "Band"})).unwrap();
|
||||
assert_eq!(faces(&s).len(), 8);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(faces(&s).len(), 9);
|
||||
std::fs::remove_dir_all(dir).ok();
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,8 @@ pub enum Requirement {
|
||||
FaceMerge,
|
||||
/// A stitch.
|
||||
Stitch,
|
||||
/// A face split.
|
||||
FaceSplit,
|
||||
/// An edge seed.
|
||||
Seed,
|
||||
/// At least one mesh node.
|
||||
@@ -85,6 +87,7 @@ impl Requirement {
|
||||
Requirement::Partition => !m.partitions().is_empty(),
|
||||
Requirement::FaceMerge => !m.face_merges().is_empty(),
|
||||
Requirement::Stitch => !m.stitches().is_empty(),
|
||||
Requirement::FaceSplit => !m.face_splits().is_empty(),
|
||||
Requirement::Seed => !m.seeds().is_empty(),
|
||||
Requirement::Nodes => !m.nodes().is_empty(),
|
||||
Requirement::Mesh => has_elements,
|
||||
@@ -127,6 +130,7 @@ impl Requirement {
|
||||
Requirement::Partition => "a partition",
|
||||
Requirement::FaceMerge => "a face merge",
|
||||
Requirement::Stitch => "a stitch",
|
||||
Requirement::FaceSplit => "a face split",
|
||||
Requirement::Seed => "an edge seed",
|
||||
Requirement::Nodes => "mesh nodes",
|
||||
Requirement::Mesh | Requirement::Analysable => "a mesh",
|
||||
@@ -159,6 +163,7 @@ impl Requirement {
|
||||
Requirement::Partition => "geometry.partition",
|
||||
Requirement::FaceMerge => "geometry.faces.merge",
|
||||
Requirement::Stitch => "geometry.stitch",
|
||||
Requirement::FaceSplit => "geometry.face.split",
|
||||
Requirement::Seed => "mesh.seed.edge",
|
||||
Requirement::Nodes => "mesh.node.add",
|
||||
Requirement::Mesh | Requirement::Analysable => "mesh.tet.generate",
|
||||
@@ -218,6 +223,7 @@ impl crate::schema::Collection {
|
||||
C::Partition => Requirement::Partition,
|
||||
C::FaceMerge => Requirement::FaceMerge,
|
||||
C::Stitch => Requirement::Stitch,
|
||||
C::FaceSplit => Requirement::FaceSplit,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -360,6 +366,7 @@ mod tests {
|
||||
C::Partition,
|
||||
C::FaceMerge,
|
||||
C::Stitch,
|
||||
C::FaceSplit,
|
||||
];
|
||||
let empty = Session::default();
|
||||
for c in all {
|
||||
|
||||
@@ -221,6 +221,8 @@ pub enum Collection {
|
||||
FaceMerge,
|
||||
/// A stitch (`geometry.stitch`).
|
||||
Stitch,
|
||||
/// A face split (`geometry.face.split`).
|
||||
FaceSplit,
|
||||
}
|
||||
|
||||
impl Collection {
|
||||
@@ -248,6 +250,7 @@ impl Collection {
|
||||
Collection::Partition => "partition",
|
||||
Collection::FaceMerge => "face merge",
|
||||
Collection::Stitch => "stitch",
|
||||
Collection::FaceSplit => "face split",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -279,6 +282,7 @@ impl Session {
|
||||
Collection::Partition => Box::new(m.partitions().iter().map(|p| p.name.as_str())),
|
||||
Collection::FaceMerge => Box::new(m.face_merges().iter().map(|f| f.name.as_str())),
|
||||
Collection::Stitch => Box::new(m.stitches().iter().map(|f| f.name.as_str())),
|
||||
Collection::FaceSplit => Box::new(m.face_splits().iter().map(|f| f.name.as_str())),
|
||||
};
|
||||
names.take(MAX_NAMES).map(str::to_string).collect()
|
||||
}
|
||||
|
||||
+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.
|
||||
|
||||
81 ready, 37 coming soon.
|
||||
82 ready, 37 coming soon.
|
||||
|
||||
## Geometry
|
||||
|
||||
@@ -24,6 +24,7 @@ the control channel and AI agents over MCP (docs/mcp.md) run the same commands.
|
||||
| Prepare | Partition — Cut bodies into cells by a datum plane; the two faces of each cut are paired by name | ready | `geometry.partition`, (`geometry.partition.delete`), (`geometry.partition.suppress`) |
|
||||
| Prepare | Merge Faces — Mesh adjacent faces as one (virtual topology): a fillet in pieces, a face split by an imprint; the B-rep is unchanged | ready | `geometry.faces.merge`, (`geometry.faces.merge.delete`) |
|
||||
| Prepare | Absorb Faces — Absorb tiny and sliver faces into a neighbour for meshing, one picked or every one Check Geometry found | ready | `geometry.face.absorb` |
|
||||
| Prepare | Split Face — Split a face along a datum plane for meshing only: two faces on its surface, the cut an edge between them | ready | `geometry.face.split`, (`geometry.face.split.delete`) |
|
||||
| Prepare | Stitch — Close an imported surface's gaps under a tolerance: open points welded, T-junctions split | ready | `geometry.stitch`, (`geometry.stitch.delete`) |
|
||||
| Prepare | Midsurface — Extract midsurfaces of thin parts for shell meshes | coming soon (M8) | |
|
||||
|
||||
|
||||
Reference in new issue
Block a user