forked from 1b-agent/fieldcraft-upstream
mesh.plan.apply: name the faces of the meshed bodies left untagged
The reply's untagged_faces lists every face of the plan's bodies no element face carries; the acceptance checks require it empty, and a symmetric filleted base checks a set by every face of every cell (#494). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
1 parent
937e557e5a
commit
d5fb100182
1 file changed
+70
-2
@@ -30,7 +30,10 @@
|
||||
//! (apply an edited plan object) and applied again after the design changes. The projection
|
||||
//! blocking mesher (#399) consumes the same plan: its direction, templates and cuts.
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use fieldcraft_geometry::{Curve, Design, FaceSurface, FeatureNode, Shape};
|
||||
use fieldcraft_model::geometry::Entity;
|
||||
use fieldcraft_model::{CutRole, Datum, MeshPlan, Partition, PlanCut, PlanFeature, PlanMesher, PlanRing, PlanUnmapped, RingKind};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
@@ -63,7 +66,7 @@ pub const PLAN: CommandSpec = CommandSpec {
|
||||
pub const APPLY: CommandSpec = CommandSpec {
|
||||
id: "mesh.plan.apply",
|
||||
label: "Apply Mesh Plan",
|
||||
params: r#"{"plan": "Plan-Base" (a saved plan) | {plan object, as mesh.plan replies it; edited}, "name"?: "Plan-Base" (the name an object is saved under; default Plan-<body>), "symmetry"?: true (a projection plan on a body mirror symmetric across planes through its middle square to the sweep meshes one cell and mirrors it: false meshes the whole body)} — carries the plan out with today's meshers: replaces the plan's earlier partitions (`<plan>-cut<k>`) with its `cuts` (datum planes and partitions of the plan's body only; a cut the kernel cannot make, or that leaves a cell no mesher takes, is left out and reported in `skipped_cuts`), then meshes the body: its cells swept together with structured hex (mesh.cells, `default` hex) when there are cuts, else the body swept (mesh.hex.generate, swept), at the plan's size; each of its `copies` is meshed the same way by a plan of its own (`Plan-<copy>`, saved). No fallback: a cell that does not mesh all-hex is the error. Replies the cuts made and skipped, the cells, the element count, the smallest scaled Jacobian, `all_hex`, whether the cells joined node to node into one mesh (`conforming`), the mapped features, what today's meshers cannot build yet (`pending`: the O-grid and butterfly rings, which need the projection blocking mesher, #399), the `unmapped` features, the copies and the mesher's reply (`mesh`); `replaced` names the meshes it replaced. A plan object is checked and saved first"#,
|
||||
params: r#"{"plan": "Plan-Base" (a saved plan) | {plan object, as mesh.plan replies it; edited}, "name"?: "Plan-Base" (the name an object is saved under; default Plan-<body>), "symmetry"?: true (a projection plan on a body mirror symmetric across planes through its middle square to the sweep meshes one cell and mirrors it: false meshes the whole body)} — carries the plan out with today's meshers: replaces the plan's earlier partitions (`<plan>-cut<k>`) with its `cuts` (datum planes and partitions of the plan's body only; a cut the kernel cannot make, or that leaves a cell no mesher takes, is left out and reported in `skipped_cuts`), then meshes the body: its cells swept together with structured hex (mesh.cells, `default` hex) when there are cuts, else the body swept (mesh.hex.generate, swept), at the plan's size; each of its `copies` is meshed the same way by a plan of its own (`Plan-<copy>`, saved). No fallback: a cell that does not mesh all-hex is the error. Replies the cuts made and skipped, the cells, the element count, the smallest scaled Jacobian, `all_hex`, whether the cells joined node to node into one mesh (`conforming`), the mapped features, what today's meshers cannot build yet (`pending`: the O-grid and butterfly rings, which need the projection blocking mesher, #399), the `unmapped` features, the copies and the mesher's reply (`mesh`); `untagged_faces` names each face of the meshed bodies no element face is tagged with (a set by it finds nothing: only a face narrower than the elements beside it); `replaced` names the meshes it replaced. A plan object is checked and saved first"#,
|
||||
form: Some(super::forms::MESH_PLAN_APPLY),
|
||||
kind: CommandKind::Model,
|
||||
requires: &[Requirement::Design],
|
||||
@@ -1045,12 +1048,14 @@ fn apply(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
take_replaced(&mut reply, &r);
|
||||
copies.push(json!({"body": copy, "plan": copy_name, "cells": r["cells"], "elements": r["elements"], "min_scaled_jacobian": r["min_scaled_jacobian"], "conforming": r["conforming"]}));
|
||||
}
|
||||
let untagged = untagged_faces(s, &plan_bodies(s, p)?);
|
||||
let mins: Vec<f64> = std::iter::once(&reply).chain(&copies).filter_map(|r| r["min_scaled_jacobian"].as_f64()).collect();
|
||||
if let Some(o) = reply.as_object_mut() {
|
||||
if !copies.is_empty() {
|
||||
o.insert("copies".into(), json!(copies));
|
||||
o.insert("min_scaled_jacobian".into(), json!(mins.iter().copied().reduce(f64::min)));
|
||||
}
|
||||
o.insert("untagged_faces".into(), json!(untagged));
|
||||
let results: Vec<&str> = s.solved_steps().collect();
|
||||
if o.get("replaced").is_some_and(|r| r.as_array().is_some_and(|a| !a.is_empty())) && !results.is_empty() {
|
||||
o.insert("results_cleared".into(), json!(results));
|
||||
@@ -1059,6 +1064,33 @@ fn apply(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Ok(reply)
|
||||
}
|
||||
|
||||
/// The faces of `bodies` (as the design has them) that no element face of the mesh is tagged
|
||||
/// with: a set, load or relaxation by such a face finds nothing. Only a face narrower than the
|
||||
/// elements beside it should be one.
|
||||
fn untagged_faces(s: &Session, bodies: &[String]) -> Vec<Value> {
|
||||
let c = s.model.classification();
|
||||
let mut tagged: BTreeSet<u32> = BTreeSet::new();
|
||||
for t in c.elements().values() {
|
||||
tagged.extend(t.faces.values().copied());
|
||||
}
|
||||
let tagged: BTreeSet<(&str, &str)> = tagged
|
||||
.iter()
|
||||
.filter_map(|&t| c.entity(t))
|
||||
.filter_map(|g| match &g.entity {
|
||||
Entity::Face(name) => Some((g.body.as_str(), name.as_str())),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
let Some(design) = s.design.as_deref() else { return Vec::new() };
|
||||
bodies
|
||||
.iter()
|
||||
.filter_map(|b| design.body(b))
|
||||
.flat_map(|b| b.faces().iter().map(move |f| (b.name.as_str(), f.name.as_str())))
|
||||
.filter(|key| !tagged.contains(key))
|
||||
.map(|(body, face)| json!({"body": body, "face": face}))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The bodies `mesh.plan.apply` fills (replace.rs): the cells of the plan's body and of its
|
||||
/// copies, as the design has them now (the body itself when it is not cut).
|
||||
pub(crate) fn plan_bodies(s: &Session, p: &Value) -> Result<Vec<String>> {
|
||||
@@ -1897,6 +1929,41 @@ mod acceptance {
|
||||
assert!(lo > 0.65, "per corner min scaled Jacobian {worst:?}");
|
||||
}
|
||||
|
||||
/// The enclosure base meshed by its symmetry (one quarter, mirrored twice): each corner
|
||||
/// boss cuts the inner fillet into two pieces and a sliver narrower than the elements, and
|
||||
/// every face of every cell, the slivers and their mirror images among them, has element
|
||||
/// faces tagged with it, so a set by any face finds the mesh (#494).
|
||||
#[test]
|
||||
fn a_symmetric_filleted_base_tags_every_face_of_every_cell() {
|
||||
let mut commands = vec![
|
||||
json!({"command": "solid.box", "params": {"length": 56, "width": 42, "height": 16, "body_name": "Base"}}),
|
||||
json!({"command": "solid.fillet", "params": {"edges": [[0, 0, 8], [56, 0, 8], [56, 42, 8], [0, 42, 8]], "radius": 5}}),
|
||||
json!({"command": "solid.shell", "params": {"faces": [[28, 21, 16]], "thickness": 3, "body": "Base"}}),
|
||||
];
|
||||
for (x, y) in [(-1.0, -1.0), (57.0, -1.0), (-1.0, 43.0), (57.0, 43.0)] {
|
||||
commands.push(json!({"command": "solid.cylinder", "params": {"diameter": 13, "height": 16, "base": [x, y, 0], "operation": "join", "targets": ["Base"]}}));
|
||||
}
|
||||
let (_d, mut s) = open(json!(commands));
|
||||
let plan = s.execute("mesh.plan", &json!({"body": "Base", "size": 1.5, "layers": 2})).unwrap();
|
||||
let r = s.execute("mesh.plan.apply", &json!({"plan": plan["name"]})).unwrap();
|
||||
assert_eq!(r["cells"].as_array().map(Vec::len), Some(4), "{r}");
|
||||
assert_eq!(r["untagged_faces"], json!([]), "{r}");
|
||||
let design = s.design.clone().unwrap();
|
||||
let mut blends = 0;
|
||||
for cell in r["cells"].as_array().unwrap() {
|
||||
let body = design.body(cell.as_str().unwrap()).unwrap();
|
||||
for (k, face) in body.faces().iter().enumerate() {
|
||||
blends += usize::from(face.name.contains("blend"));
|
||||
let set = format!("F{k}");
|
||||
s.execute("model.set.create", &json!({"name": set, "geometry": [{"body": body.name, "face": face.name}]})).unwrap();
|
||||
let faces = s.model.resolve_set(&set).map(|x| x.faces.len());
|
||||
assert!(faces.as_ref().is_ok_and(|n| *n > 0), "face {} of {}: {faces:?}", face.name, body.name);
|
||||
s.execute("model.set.delete", &json!({"name": set})).unwrap();
|
||||
}
|
||||
}
|
||||
assert!(blends >= 4, "the inner fillet's pieces");
|
||||
}
|
||||
|
||||
/// An enclosure base: a box with rounded vertical edges, shelled open at the top, a screw
|
||||
/// boss joined at each corner (centred outside it, nearly touching the inner fillet) with a
|
||||
/// blind hole, and standoffs on the floor. Each boss's ring frames the whole circle,
|
||||
@@ -2000,9 +2067,10 @@ mod tests_support {
|
||||
}
|
||||
|
||||
/// All hex, one conforming mesh on the geometry, the volume the body's, no element below
|
||||
/// `min_sj` or above `max_edge_ratio`, no free faces inside.
|
||||
/// `min_sj` or above `max_edge_ratio`, no free faces inside, every face of the body tagged.
|
||||
pub(super) fn accept(s: &mut Session, r: &Value, min_sj: f64, max_edge_ratio: f64) {
|
||||
assert_eq!((r["all_hex"].as_bool(), r["conforming"].as_bool()), (Some(true), Some(true)), "{r}");
|
||||
assert_eq!(r["untagged_faces"], json!([]), "faces no element face is tagged with");
|
||||
let q = s.execute("mesh.quality", &json!({})).unwrap();
|
||||
let sj = q["metrics"]["scaled_jacobian"]["min"].as_f64().unwrap();
|
||||
let er = q["metrics"]["edge_ratio"]["max"].as_f64().unwrap();
|
||||
|
||||
Reference in new issue
Block a user