engine: geometry.defeature takes features out of the design for meshing (#433)

Suppresses the SolidCraft features named (holes filled, fillets and chamfers back to
their sharp edges, bosses removed) and rebuilds the design; a feature that cannot be
suppressed alone is merged into its neighbour for meshing only. Kept in the model,
replayed on open, undoable; geometry.defeature.delete turns them back on.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
jyvikoandClaude Opus 5.5 authored and Irakli Kourtis committed 2026-10-16 17:43:42 -04:00
1 parent 3f0cae70a2
commit f297ffca3a
13 files changed
+284 -9

No files matched your search

+1
View File
@@ -68,6 +68,7 @@ is committed and listed here with its exports.
| 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.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.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.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.defeature.svg, assets/icons/geometry.defeature.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/geometry.defeature.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.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/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 | | 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 |
+10
View File
@@ -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.defeature</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="M9 8 C9 8.828 10.343 9.5 12 9.5 C13.657 9.5 15 8.828 15 8 C15 7.172 13.657 6.5 12 6.5 C10.343 6.5 9 7.172 9 8 Z" fill="#f0b44a" stroke="#f0b44a" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.5 11 L16.5 5" fill="none" stroke="#4f9ce8" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 886 B

Binary file not shown.
+1
View File
@@ -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"}, {"d": "M9 8 A3 1.5 0 1 0 15 8 A3 1.5 0 1 0 9 8 Z", "fill": "warm", "stroke": "warm", "width": 1}, {"d": "M7.5 11 L16.5 5", "stroke": "accent", "width": 1.75}]}
+8
View File
@@ -317,6 +317,14 @@ pub static CATALOGUE: &[Capability] = &[
"Split a face along a datum plane for meshing only: two faces on its surface, the cut an edge between them", "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"], &["geometry.face.split.delete"],
), ),
covering(
"geometry.defeature",
"Defeature",
Geometry,
"Prepare",
"Take small holes, fillets, chamfers and bosses out for meshing: the design rebuilt without their features",
&["geometry.defeature.delete"],
),
covering( covering(
"geometry.stitch", "geometry.stitch",
"Stitch", "Stitch",
+12 -7
View File
@@ -470,14 +470,19 @@ fn cuts(model: &Model) -> Vec<Cut> {
.collect() .collect()
} }
/// `design` cut by `model`'s partitions, then with its face merges and its face splits (those /// `design` without the features `model` defeatures, cut by its partitions, then with its face
/// along a suppressed datum left out; the design as opened when the model has none). /// merges (after those of the features defeatured virtually) 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> { pub(crate) fn partitioned(design: &Design, model: &Model) -> Result<Design> {
let merges: Vec<Merge> = model let defeatures: Vec<fieldcraft_geometry::defeature::Defeature> =
.face_merges() model.defeatures().iter().map(|d| fieldcraft_geometry::defeature::Defeature { name: d.name.clone(), features: d.features.clone() }).collect();
.iter() let (design, mut merges) = design.defeatured(&defeatures)?;
.map(|m| Merge { name: m.name.clone(), body: m.body.clone(), faces: m.faces.clone(), into: m.into.clone() }) merges.extend(model.face_merges().iter().map(|m| Merge {
.collect(); name: m.name.clone(),
body: m.body.clone(),
faces: m.faces.clone(),
into: m.into.clone(),
}));
let splits: Vec<fieldcraft_geometry::face_split::FaceSplit> = model let splits: Vec<fieldcraft_geometry::face_split::FaceSplit> = model
.face_splits() .face_splits()
.iter() .iter()
+228
View File
@@ -0,0 +1,228 @@
//! Defeaturing for meshing: `geometry.defeature` takes holes, fillets, chamfers and small bosses
//! out of the open design by suppressing their SolidCraft features (virtually, as a face merge,
//! when a feature cannot be suppressed on its own).
//!
//! A defeature is part of the model (`fieldcraft_model::Defeature`, by feature id); the geometry
//! layer applies it to the design as opened (`fieldcraft_geometry::defeature`), before the
//! partitions, whenever the design is opened or cut again ([`super::datum::partitioned`]), so the
//! meshers, the viewport and the checks all see the part without them.
use std::collections::BTreeSet;
use fieldcraft_model::Defeature;
use serde::Deserialize;
use serde_json::{Value, json};
use crate::requirement::Requirement;
use crate::{CommandKind, CommandSpec, EngineError, Result, Session};
pub const DEFEATURE: CommandSpec = CommandSpec {
id: "geometry.defeature",
label: "Defeature",
params: r#"{"name": "Small", "features": [3, "F7"] (feature ids, as geometry.features lists them; or the text "3, F7")} — takes features of the open design out for meshing: the design is rebuilt without them, as SolidCraft builds it with them suppressed (a hole filled, a fillet or chamfer back to its sharp edge, a boss gone), and the other faces keep their persistent names. A feature that cannot be suppressed alone (a later feature needs it, or a body would go) is defeatured virtually instead: its faces are merged into the neighbour they share the longest edges with, for meshing only (virtual topology; the B-rep keeps it). A feature that builds a body or a sketch, or one the design lacks, is an error. Kept in the model by feature id and applied again whenever the design is opened (before the partitions); the same name again replaces its features. A suppressed feature affects every body it shaped. The bodies lose their meshes (`replaced`; remesh them); sets naming their faces are `stale_sets`. Replies `suppressed` (feature ids), `virtual` ([{feature, reason}]) and `bodies`. One undo step; geometry.defeature.delete turns them back on"#,
form: Some(super::forms::DEFEATURE),
kind: CommandKind::Model,
requires: &[Requirement::Design],
run: defeature,
};
pub const DEFEATURE_DELETE: CommandSpec = CommandSpec {
id: "geometry.defeature.delete",
label: "Delete Defeature",
params: r#"{"name": "Small"} — turns a defeature's features back on: the design is rebuilt with them; the bodies they shaped lose their meshes (`replaced`)"#,
form: Some(super::forms::DEFEATURE_DELETE),
kind: CommandKind::Model,
requires: &[Requirement::Defeature],
run: delete,
};
/// A feature id: `3` or `"F3"`.
#[derive(Deserialize)]
#[serde(untagged)]
enum FeatureId {
Id(u64),
Name(String),
}
/// The feature ids of `features`: a list, or the form's text (`3, F7`).
fn ids(p: &Value) -> Result<Vec<u64>> {
let given: Vec<FeatureId> = match p.get("features") {
Some(Value::String(t)) if t.len() <= 64 * 1024 => {
t.split([',', ' ', ';']).filter(|w| !w.is_empty()).map(|w| FeatureId::Name(w.to_string())).collect()
}
_ => super::req(p, "features")?,
};
if given.is_empty() || given.len() > fieldcraft_model::virtual_faces::MAX_DEFEATURE_FEATURES {
return Err(EngineError::invalid("features", format!("1 to {} feature ids", fieldcraft_model::virtual_faces::MAX_DEFEATURE_FEATURES)));
}
let mut out = Vec::with_capacity(given.len());
for f in given {
let id = match f {
FeatureId::Id(id) => Some(id),
FeatureId::Name(n) => n.strip_prefix('F').unwrap_or(&n).parse().ok(),
};
match id {
Some(id) if !out.contains(&id) => out.push(id),
Some(_) => {}
None => return Err(EngineError::invalid("features", "feature ids: 3 or \"F3\"")),
}
}
Ok(out)
}
/// The bodies of the open design (as opened) that features `ids` shaped.
fn shaped(s: &Session, ids: &BTreeSet<u64>) -> Vec<String> {
let Some(design) = s.design.as_deref() else { return Vec::new() };
let graph = design.undefeatured().features(None).unwrap_or_default();
let sources: Vec<String> = graph.into_iter().filter(|b| b.features.iter().any(|f| ids.contains(&f.id))).map(|b| b.body).collect();
// A body the partitions cut: each of its cells.
design.bodies.iter().map(|b| b.name.clone()).filter(|n| design.source_of(n).is_some_and(|src| sources.iter().any(|x| x == src))).collect()
}
/// What the design's defeatures did with the features of `name`, as the reply lists them.
fn outcome(s: &Session, name: &str) -> (Vec<u64>, Vec<Value>) {
let report = s.design.as_deref().map(|d| d.defeat_report().to_vec()).unwrap_or_default();
let mine = report.into_iter().filter(|r| r.defeature == name);
let (mut suppressed, mut virt) = (Vec::new(), Vec::new());
for r in mine {
if r.method == "suppressed" {
suppressed.push(r.feature);
} else {
virt.push(json!({"feature": r.feature, "reason": r.reason}));
}
}
(suppressed, virt)
}
fn defeature(s: &mut Session, p: &Value) -> Result<Value> {
let name = super::name(p)?.to_string();
let features = ids(p)?;
let before: BTreeSet<u64> = s.model.defeatures().iter().filter(|d| d.name == name).flat_map(|d| d.features.clone()).collect();
s.model.set_defeature(Defeature { name: name.clone(), features: features.clone() })?;
let mut touched = before;
touched.extend(&features);
let bodies = shaped(s, &touched);
let merges: Vec<String> = features.iter().map(|id| format!("merge {name}-F{id}")).collect();
let mut errors = vec![format!("defeature {name}")];
errors.extend(merges);
let reply = super::virtual_faces::reapply(s, &errors, &bodies, json!({"name": name}))?;
let (suppressed, virt) = outcome(s, &name);
let bodies = shaped(s, &touched);
let mut reply = reply;
if let Some(o) = reply.as_object_mut() {
o.insert("suppressed".into(), json!(suppressed));
o.insert("virtual".into(), json!(virt));
o.insert("bodies".into(), json!(bodies));
}
Ok(reply)
}
fn delete(s: &mut Session, p: &Value) -> Result<Value> {
let name = super::name(p)?;
let removed = s.model.remove_defeature(name)?;
let bodies = shaped(s, &removed.features.iter().copied().collect());
let mut cleared = super::datum::recut(s)?;
cleared.extend(super::replace::clear(&mut s.model, bodies.iter().map(String::as_str)));
Ok(cleared.report_if_any(s, json!({"deleted": name, "features": removed.features, "bodies": bodies})))
}
#[cfg(test)]
mod tests {
use serde_json::json;
use crate::Session;
fn faces(s: &Session) -> usize {
s.design.as_ref().unwrap().bodies[0].faces().len()
}
fn hole(s: &mut Session) -> u64 {
let r = s.execute("geometry.features", &json!({})).unwrap();
r["bodies"][0]["features"].as_array().unwrap().iter().find(|f| f["operation"] == "cut").unwrap()["id"].as_u64().unwrap()
}
/// The sample plate: suppressing its hole gives a block meshed
/// without it (tet and hex), undo and redo take it out and put it back, it is saved and
/// replayed on open, and deleting the defeature brings the hole back.
#[test]
fn the_plate_hole_is_suppressed_meshed_undone_saved_and_restored() {
let (mut s, dir) = super::super::datum::tests::plate();
let body = s.design.as_ref().unwrap().bodies[0].name.clone();
assert_eq!(faces(&s), 8);
let id = hole(&mut s);
s.execute("mesh.tet.generate", &json!({"body": body, "size": 4})).unwrap();
let with_hole = s.model.elements().len();
let r = s.execute("geometry.defeature", &json!({"name": "Holes", "features": [id]})).unwrap();
assert_eq!(r["suppressed"], json!([id]), "{r}");
assert_eq!(r["replaced"][0]["body"].as_str(), Some(body.as_str()), "{r}");
assert_eq!(faces(&s), 6);
// Meshed without it: a tet mesh fills the whole block, and the block sweeps into hexes.
s.execute("mesh.tet.generate", &json!({"body": body, "size": 4})).unwrap();
let volume = |s: &Session| s.design.as_ref().unwrap().bodies[0].measure().unwrap().volume;
assert!((volume(&s) - 40.0 * 30.0 * 10.0).abs() < 1e-6, "{}", volume(&s));
assert_ne!(s.model.elements().len(), with_hole);
s.execute("mesh.hex.sweep", &json!({"body": body, "size": 5})).unwrap();
assert!(s.model.elements().values().all(|e| e.nodes.len() == 8), "a block sweeps into hexes only");
// Undo the meshes and the defeature: the hole is back; redo takes it out again.
while !s.model.defeatures().is_empty() {
s.execute("edit.undo", &json!({})).unwrap();
}
assert_eq!(faces(&s), 8);
s.execute("edit.redo", &json!({})).unwrap();
assert_eq!(faces(&s), 6);
// Saved, and replayed on open of the model and on a re-open of the design.
let file = dir.join("defeatured.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!((t.model.defeatures().len(), faces(&t)), (1, 6));
let design = s.sources.design.as_ref().unwrap().path().to_string();
s.execute("geometry.open", &json!({"path": design})).unwrap();
assert_eq!(faces(&s), 6);
// Switched back on.
let r = s.execute("geometry.defeature.delete", &json!({"name": "Holes"})).unwrap();
assert_eq!(r["features"], json!([id]), "{r}");
assert_eq!(faces(&s), 8);
std::fs::remove_dir_all(dir).ok();
}
/// Unknown features, body-building ones and bad ids are refused and leave the model as it was.
#[test]
fn bad_features_are_refused() {
let (mut s, dir) = super::super::datum::tests::plate();
for features in [json!([999]), json!(["Fx"]), json!([]), json!("F1, x"), json!(7)] {
assert!(s.execute("geometry.defeature", &json!({"name": "Bad", "features": features})).is_err(), "{features}");
}
let body = s.design.as_ref().unwrap().bodies[0].name.clone();
let base = s.execute("geometry.features", &json!({"body": body})).unwrap()["bodies"][0]["features"][0]["id"].as_u64().unwrap();
let e = s.execute("geometry.defeature", &json!({"name": "Bad", "features": [base]})).unwrap_err();
assert!(e.to_string().contains("builds a body"), "{e}");
assert!(s.model.defeatures().is_empty() && faces(&s) == 8);
std::fs::remove_dir_all(dir).ok();
}
/// The bracket's 6 mm fillet is defeatured back to a sharp corner and
/// meshed; its holes stay.
#[test]
fn a_bracket_is_meshed_with_its_small_fillet_defeatured() {
let (_dir, mut s) = super::super::datum::tests::scripted(json!([
{"command": "sketch.create", "params": {"plane": "XY", "name": "Profile"}},
{"command": "sketch.line", "params": {"points": [[0, 0], [60, 0], [60, 8], [8, 8], [8, 50], [0, 50]], "closed": true}},
{"command": "sketch.finish", "params": {}},
{"command": "solid.extrude", "params": {"distance": 40, "body_name": "Bracket"}},
{"command": "solid.fillet", "params": {"edges": [[8, 8, 20]], "radius": 6}},
{"command": "solid.hole", "params": {"position": [40, 8, 20], "diameter": 8}},
{"command": "solid.hole", "params": {"position": [8, 32, 20], "diameter": 8}}
]));
let g = s.execute("geometry.features", &json!({"body": "Bracket"})).unwrap();
let fillet: Vec<serde_json::Value> =
g["bodies"][0]["features"].as_array().unwrap().iter().filter(|f| f["type"] == "fillet").map(|f| f["id"].clone()).collect();
assert_eq!(fillet.len(), 1);
let before = faces(&s);
let r = s.execute("geometry.defeature", &json!({"name": "Fillets", "features": fillet})).unwrap();
assert_eq!(r["suppressed"], json!(fillet), "{r}");
assert_eq!(faces(&s), before - 1);
let r = s.execute("mesh.tet.generate", &json!({"body": "Bracket", "size": 4})).unwrap();
assert!(r["elements"].as_u64().unwrap_or(0) > 0, "{r}");
}
}
+7
View File
@@ -1780,6 +1780,13 @@ pub const FACE_SPLIT: &[Field] = &[
]; ];
pub const FACE_SPLIT_DELETE: &[Field] = existing!("Face split", C::FaceSplit); pub const FACE_SPLIT_DELETE: &[Field] = existing!("Face split", C::FaceSplit);
pub const FACES_MERGE_DELETE: &[Field] = existing!("Face merge", C::FaceMerge); pub const FACES_MERGE_DELETE: &[Field] = existing!("Face merge", C::FaceMerge);
pub const DEFEATURE: &[Field] = &[
NAME,
text("features", "Features")
.help("The feature ids to take out for meshing (3 or F3), as the feature graph (geometry.features) lists them; separate them with commas.")
.req(),
];
pub const DEFEATURE_DELETE: &[Field] = existing!("Defeature", C::Defeature);
pub const MESH_PLAN: &[Field] = &[ 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(), reference("body", "Body", C::Body).help("The body of the design to plan (as opened, not a partition cell).").req(),
text("name", "Name").help("The name the plan is saved under.").hint("Plan-<body>"), text("name", "Name").help("The name the plan is saved under.").hint("Plan-<body>"),
+3
View File
@@ -5,6 +5,7 @@ mod cells;
mod contact; mod contact;
pub(crate) mod datum; pub(crate) mod datum;
pub(crate) mod deck; pub(crate) mod deck;
mod defeature;
pub(crate) mod define; pub(crate) mod define;
pub mod diagnostics; pub mod diagnostics;
mod edit; mod edit;
@@ -79,6 +80,8 @@ pub static ALL: &[CommandSpec] = &[
virtual_faces::FACE_ABSORB, virtual_faces::FACE_ABSORB,
virtual_faces::FACE_SPLIT, virtual_faces::FACE_SPLIT,
virtual_faces::FACE_SPLIT_DELETE, virtual_faces::FACE_SPLIT_DELETE,
defeature::DEFEATURE,
defeature::DEFEATURE_DELETE,
stitch::STITCH, stitch::STITCH,
stitch::STITCH_DELETE, stitch::STITCH_DELETE,
mesh::NODE_ADD, mesh::NODE_ADD,
+1 -1
View File
@@ -98,7 +98,7 @@ fn stale_sets(s: &Session, body: &str) -> Vec<String> {
/// Apply the model's partitions, merges and splits to the design again, failing with the error of /// 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 /// one of `features` (`merge <name>`, `split <name>`) when it has one; the meshes of `bodies` go
/// (their faces changed). /// (their faces changed).
fn reapply(s: &mut Session, features: &[String], bodies: &[String], reply: Value) -> Result<Value> { pub(super) fn reapply(s: &mut Session, features: &[String], bodies: &[String], reply: Value) -> Result<Value> {
let mut cleared = super::datum::recut(s)?; let mut cleared = super::datum::recut(s)?;
let design = s.design.as_deref().ok_or_else(|| EngineError::Params("open a design first".into()))?; let design = s.design.as_deref().ok_or_else(|| EngineError::Params("open a design first".into()))?;
let errors: Vec<String> = let errors: Vec<String> =
+7
View File
@@ -38,6 +38,8 @@ pub enum Requirement {
Stitch, Stitch,
/// A face split. /// A face split.
FaceSplit, FaceSplit,
/// A defeature.
Defeature,
/// An edge seed. /// An edge seed.
Seed, Seed,
/// A size control. /// A size control.
@@ -90,6 +92,7 @@ impl Requirement {
Requirement::FaceMerge => !m.face_merges().is_empty(), Requirement::FaceMerge => !m.face_merges().is_empty(),
Requirement::Stitch => !m.stitches().is_empty(), Requirement::Stitch => !m.stitches().is_empty(),
Requirement::FaceSplit => !m.face_splits().is_empty(), Requirement::FaceSplit => !m.face_splits().is_empty(),
Requirement::Defeature => !m.defeatures().is_empty(),
Requirement::Seed => !m.seeds().is_empty(), Requirement::Seed => !m.seeds().is_empty(),
Requirement::SizeControl => !m.size_controls().is_empty(), Requirement::SizeControl => !m.size_controls().is_empty(),
Requirement::Nodes => !m.nodes().is_empty(), Requirement::Nodes => !m.nodes().is_empty(),
@@ -134,6 +137,7 @@ impl Requirement {
Requirement::FaceMerge => "a face merge", Requirement::FaceMerge => "a face merge",
Requirement::Stitch => "a stitch", Requirement::Stitch => "a stitch",
Requirement::FaceSplit => "a face split", Requirement::FaceSplit => "a face split",
Requirement::Defeature => "a defeature",
Requirement::Seed => "an edge seed", Requirement::Seed => "an edge seed",
Requirement::SizeControl => "a local size", Requirement::SizeControl => "a local size",
Requirement::Nodes => "mesh nodes", Requirement::Nodes => "mesh nodes",
@@ -168,6 +172,7 @@ impl Requirement {
Requirement::FaceMerge => "geometry.faces.merge", Requirement::FaceMerge => "geometry.faces.merge",
Requirement::Stitch => "geometry.stitch", Requirement::Stitch => "geometry.stitch",
Requirement::FaceSplit => "geometry.face.split", Requirement::FaceSplit => "geometry.face.split",
Requirement::Defeature => "geometry.defeature",
Requirement::Seed => "mesh.seed.edge", Requirement::Seed => "mesh.seed.edge",
Requirement::SizeControl => "mesh.size.set", Requirement::SizeControl => "mesh.size.set",
Requirement::Nodes => "mesh.node.add", Requirement::Nodes => "mesh.node.add",
@@ -230,6 +235,7 @@ impl crate::schema::Collection {
C::Stitch => Requirement::Stitch, C::Stitch => Requirement::Stitch,
C::FaceSplit => Requirement::FaceSplit, C::FaceSplit => Requirement::FaceSplit,
C::SizeControl => Requirement::SizeControl, C::SizeControl => Requirement::SizeControl,
C::Defeature => Requirement::Defeature,
} }
} }
} }
@@ -373,6 +379,7 @@ mod tests {
C::FaceMerge, C::FaceMerge,
C::Stitch, C::Stitch,
C::FaceSplit, C::FaceSplit,
C::Defeature,
]; ];
let empty = Session::default(); let empty = Session::default();
for c in all { for c in all {
+4
View File
@@ -225,6 +225,8 @@ pub enum Collection {
FaceSplit, FaceSplit,
/// A size control (`mesh.size.set`). /// A size control (`mesh.size.set`).
SizeControl, SizeControl,
/// A defeature (`geometry.defeature`).
Defeature,
} }
impl Collection { impl Collection {
@@ -254,6 +256,7 @@ impl Collection {
Collection::Stitch => "stitch", Collection::Stitch => "stitch",
Collection::FaceSplit => "face split", Collection::FaceSplit => "face split",
Collection::SizeControl => "size control", Collection::SizeControl => "size control",
Collection::Defeature => "defeature",
} }
} }
} }
@@ -287,6 +290,7 @@ impl Session {
Collection::Stitch => Box::new(m.stitches().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())), Collection::FaceSplit => Box::new(m.face_splits().iter().map(|f| f.name.as_str())),
Collection::SizeControl => Box::new(m.size_controls().keys().map(String::as_str)), Collection::SizeControl => Box::new(m.size_controls().keys().map(String::as_str)),
Collection::Defeature => Box::new(m.defeatures().iter().map(|f| f.name.as_str())),
}; };
names.take(MAX_NAMES).map(str::to_string).collect() names.take(MAX_NAMES).map(str::to_string).collect()
} }
+2 -1
View File
@@ -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.
83 ready, 37 coming soon. 84 ready, 37 coming soon.
## Geometry ## Geometry
@@ -25,6 +25,7 @@ the control channel and AI agents over MCP (docs/mcp.md) run the same commands.
| 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 | 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 | 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 | 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 | Defeature — Take small holes, fillets, chamfers and bosses out for meshing: the design rebuilt without their features | ready | `geometry.defeature`, (`geometry.defeature.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 | 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) | | | Prepare | Midsurface — Extract midsurfaces of thin parts for shell meshes | coming soon (M8) | |