mesh: geometry.features, mesh.plan and mesh.plan.apply from the feature graph (#402)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
jyvikoandClaude Opus 5.5 authored and Irakli Kourtis committed 2026-10-15 16:38:51 -04:00
1 parent 8dd7446569
commit efe90ec50e
12 files changed
+1449 -4

No files matched your search

+9 -1
View File
@@ -244,7 +244,7 @@ pub static CATALOGUE: &[Capability] = &[
Geometry,
"Inspect",
"List the open design's bodies, volumes and named faces, edges and vertices",
&["geometry.centre"],
&["geometry.centre", "geometry.features"],
),
ready("geometry.tessellate", "Tessellate", Geometry, "Inspect", "Tessellate a body into a face-tagged boundary surface"),
ready(
@@ -332,6 +332,14 @@ pub static CATALOGUE: &[Capability] = &[
"One mesh of a partitioned part, a method per cell (hex, tet, or all hex as conforming blocks); cells joined across partitions",
&["mesh.hex.blocks"],
),
ready_more(
"mesh.plan",
"Mesh Plan",
Mesh,
"Structured Hex",
"A hex-meshing plan from the features that built a body (sweeps, blocks, O-grids, layers, imprints, copies); saved, editable, applied with the structured meshers",
&["mesh.plan.apply"],
),
ready_more(
"mesh.block",
"Mesh Block",
+9
View File
@@ -1599,6 +1599,15 @@ pub const PARTITION: &[Field] = &[
references("bodies", "Bodies", C::Body).help("The bodies to divide; every body the plane cuts when empty.").hint("every body the plane divides"),
];
pub const PARTITION_DELETE: &[Field] = existing!("Partition", C::Partition);
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(),
text("name", "Name").help("The name the plan is saved under.").hint("Plan-<body>"),
positive("size", "Element size", D::Millimetre).help("The largest element size; thin walls make it smaller.").hint("1/25 of the diagonal"),
int("layers", "Layers through walls", 1, 10).help("Elements through the thinnest wall the features give (a shell, a plate).").hint("2"),
int("max_elements", "Most elements", 1, 5_000_000).help("The size grows past the walls' when more elements would be needed.").hint("300000"),
];
pub const MESH_PLAN_APPLY: &[Field] = &[text("plan", "Plan").help("The name of a saved mesh plan (mesh.plan).").req()];
pub const FEATURES: &[Field] = &[reference("body", "Body", C::Body).help("Only this body's features.").hint("every body")];
pub const TESSELLATE: &[Field] = &[reference("body", "Body", C::Body).help("The body to tessellate.").req(), TOLERANCE];
pub const IMPORT_MESH: &[Field] = &[path("path", "3MF file", false).help("A 3MF surface mesh with named faces.").req()];
pub const FILE_OPEN: &[Field] = &[path("path", "Model", false).help("The FieldCraft model file to open.").req()];
+20
View File
@@ -33,6 +33,16 @@ pub const BODIES: CommandSpec = CommandSpec {
run: bodies,
};
pub const FEATURES: CommandSpec = CommandSpec {
id: "geometry.features",
label: "Feature Graph",
params: r#"{"body"?: "Base" (default: every body)} — the features of the SolidCraft timeline that built each body of the open design, in order (read-only): each feature's `id` (`F<id>` in persistent names), `name`, `type` (extrude, box, cylinder, hole, shell, fillet, combine, pattern, mirror, split, …), `operation` (new_body, join, cut, intersect), `values` (its numeric inputs evaluated: mm, radians, counts), `placement` (a primitive's corner, base or centre), `direction` (an extrude's along the sketch normal, a cylinder's axis, a hole's drilling direction), `two_sided`, `sketch` (plane origin, axes and normal, and the profiles used: lines, arcs and circles in sketch coordinates, holes, curve ids), `transforms` (pattern and mirror copies, 4×4 row-major), `targets` and `tools`, the stored `definition`, the persistent `faces` and `edges` of the body it made, and `error` when it failed to rebuild. A partition cell is described by the body it was cut from"#,
form: Some(super::forms::FEATURES),
kind: CommandKind::Model,
requires: &[Requirement::Design],
run: features,
};
pub const CENTRE: CommandSpec = CommandSpec {
id: "geometry.centre",
label: "Centre of Selection",
@@ -170,6 +180,16 @@ fn bodies(s: &mut Session, _: &Value) -> Result<Value> {
Ok(Value::Array(out))
}
fn features(s: &mut Session, p: &Value) -> Result<Value> {
let body = match p.get("body") {
None | Some(Value::Null) => None,
Some(v) => Some(v.as_str().ok_or_else(|| EngineError::param("body", ProblemKind::NotAName))?),
};
let design = s.design.as_deref().ok_or_else(|| EngineError::Params("open a design first".into()))?;
let graph = design.features(body)?;
Ok(json!({"bodies": graph}))
}
/// Most geometry items and nodes `geometry.centre` takes.
const MAX_CENTRE_ITEMS: usize = 10_000;
+4
View File
@@ -23,6 +23,7 @@ mod mirror;
mod model;
pub(crate) mod nodes;
pub(crate) mod octree;
pub(crate) mod plan;
mod project;
pub(crate) mod relax;
pub mod replace;
@@ -47,6 +48,7 @@ pub static ALL: &[CommandSpec] = &[
model::INFO,
geometry::OPEN,
geometry::BODIES,
geometry::FEATURES,
geometry::CENTRE,
geometry::TESSELLATE,
geometry::IMPORT_MESH,
@@ -83,6 +85,8 @@ pub static ALL: &[CommandSpec] = &[
hex::HEX_BLOCKS,
project::HEX_PROJECT,
cells::CELLS,
plan::PLAN,
plan::APPLY,
seeds::SEED_EDGE,
seeds::SEED_CLEAR,
seeds::SEED_LIST,
File diff suppressed because it is too large. Load diff
+1
View File
@@ -52,6 +52,7 @@ const REMESH: &[&str] = &[
"mesh.hex.sweep",
"mesh.hex.mapped",
"mesh.hex.blocks",
"mesh.plan.apply",
"mesh.hex.octree",
"mesh.cells",
"mesh.block",
+9 -1
View File
@@ -130,6 +130,7 @@ pub static MESHERS: &[(&str, Targets)] = &[
("mesh.hex.blocks", listed_or_all),
("mesh.hex.project", body_or_all),
("mesh.cells", listed_or_all),
("mesh.plan.apply", super::plan::plan_bodies),
("mesh.planar", super::mesh::planar_body),
("mesh.mirror", super::mirror::image_names),
];
@@ -173,7 +174,7 @@ fn made_kind(command: &str, params: &Value) -> Option<ElementKind> {
Some(if params.get("order").and_then(Value::as_str) == Some("quadratic") { ElementKind::Tet10 } else { ElementKind::Tet4 })
}
"mesh.hex.project" => Some(ElementKind::Hex8),
"mesh.hex.generate" | "mesh.hex.sweep" | "mesh.hex.mapped" | "mesh.hex.blocks" => {
"mesh.hex.generate" | "mesh.hex.sweep" | "mesh.hex.mapped" | "mesh.hex.blocks" | "mesh.plan.apply" => {
Some(if params.get("order").and_then(Value::as_i64) == Some(2) { ElementKind::Hex20 } else { ElementKind::Hex8 })
}
_ => None,
@@ -313,6 +314,13 @@ mod tests {
("mesh.hex.blocks", vec![tet_other.clone()], json!({"bodies": ["Box.1"], "size": 2.5}), "Box.1", "Box.2"),
("mesh.hex.project", vec![tet_other.clone()], json!({"body": "Box.1", "size": 2.5, "merge": 0}), "Box.1", "Box.2"),
("mesh.cells", vec![tet_other.clone()], json!({"bodies": ["Box.1"], "size": 2.5, "default": "hex"}), "Box.1", "Box.2"),
(
"mesh.plan.apply",
vec![tet_other.clone(), json!({"command": "mesh.plan", "params": {"body": "Box.1", "size": 2.5}})],
json!({"plan": "Plan-Box_1"}),
"Box.1",
"Box.2",
),
("mesh.planar", vec![tet_other.clone()], json!({"polygon": square, "size": 2.5, "name": "Box.1"}), "Box.1", "Box.2"),
// The image's tets (meshed before) are replaced by the mirrored hexahedra.
(