forked from 1b-agent/fieldcraft-upstream
Engine: mesh.seed.edge, mesh.seed.clear and mesh.seed.list
Seed design edges by their persistent names with a count or a size, a bias and a constraint; one undo step each, saved with the model. Seeds on edges a reopened design lacks are reported as stale. Part of #230. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
1 parent
f8e0be970e
commit
e8cada94d0
12 files changed
+295
-11
No files matched your search
@@ -251,7 +251,14 @@ pub static CATALOGUE: &[Capability] = &[
|
||||
soon("geometry.midsurface", "Midsurface", Geometry, "Prepare", "M8", "Extract midsurfaces of thin parts for shell meshes"),
|
||||
// ---- Mesh ----
|
||||
soon("mesh.size.global", "Global Size", Mesh, "Size", "M2", "One element size for the whole part"),
|
||||
soon("mesh.size.seed", "Seed Edges", Mesh, "Size", "M2", "Element counts, sizes and bias per edge and face"),
|
||||
ready_more(
|
||||
"mesh.seed.edge",
|
||||
"Seed Edges",
|
||||
Mesh,
|
||||
"Size",
|
||||
"Element counts or sizes per design edge, with bias and an exact, minimum or free constraint; the meshers honour them",
|
||||
&["mesh.seed.clear", "mesh.seed.list"],
|
||||
),
|
||||
soon("mesh.size.adapt", "Curvature", Mesh, "Size", "M2", "Refine by curvature and proximity"),
|
||||
ready(
|
||||
"mesh.tet.generate",
|
||||
|
||||
@@ -254,7 +254,7 @@ fn partition_delete(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
pub(crate) mod tests {
|
||||
use super::*;
|
||||
|
||||
/// A session with the sample plate (40 × 30 × 10, Ø10 hole at (15, 15)) open.
|
||||
|
||||
@@ -809,6 +809,23 @@ pub const ELEMENT_ADD: &[Field] = &[
|
||||
json("nodes", "Nodes").req().hint("[labels]"),
|
||||
];
|
||||
|
||||
pub const SEED_EDGE: &[Field] = &[
|
||||
json("edges", "Edges").req().hint("[{\"body\", \"edge\"}, …] — Use selection with Select: Edges"),
|
||||
int("count", "Count", 1, 100_000).hint("or an element size"),
|
||||
positive("size", "Element size", D::Length).hint("or a count"),
|
||||
group(
|
||||
"bias",
|
||||
"Bias",
|
||||
&[
|
||||
range("ratio", "Ratio", D::None, 1.0, 1e4).req().def("1"),
|
||||
choice("toward", "Small toward", &[("start", "Start"), ("end", "End"), ("both", "Both ends")]).req().def("\"end\""),
|
||||
],
|
||||
),
|
||||
choice("constraint", "Constraint", &[("exact", "Exact"), ("min", "At least"), ("free", "Free (a goal)")]).def("\"exact\""),
|
||||
];
|
||||
|
||||
pub const SEED_CLEAR: &[Field] = &[json("edges", "Edges").hint("every seed")];
|
||||
|
||||
pub const DELAUNAY_POINTS: &[Field] = &[json("points", "Points").req().hint("[[x, y, z], …]")];
|
||||
|
||||
pub const TAG: &[Field] = &[
|
||||
|
||||
@@ -15,7 +15,7 @@ use crate::{CommandKind, CommandSpec, EngineError, Result, Session};
|
||||
pub const OPEN: CommandSpec = CommandSpec {
|
||||
id: "geometry.open",
|
||||
label: "Open Design",
|
||||
params: r#"{"path": "part.solidcraft", "model"?: "keep"} — rebuilds the SolidCraft design and makes its bodies the geometry. `model`: "keep" (default) keeps the mesh and every definition, so opening the same design again after an edit picks up the change and sets follow their persistent face names; "new" starts a new model with the design (no mesh, sets, materials, sections, BCs, loads or steps; the unit system stays; the model is untitled until saved, and undo restores the previous file). The reply's `related` is false when the model (before the open) refers to bodies (mesh tags, set geometry) and the design has none of them, and `stale` lists what no longer resolves: `mesh_bodies` (bodies the mesh is tagged with that the design lacks) and `sets` (sets naming a body or face it lacks). One undo step"#,
|
||||
params: r#"{"path": "part.solidcraft", "model"?: "keep"} — rebuilds the SolidCraft design and makes its bodies the geometry. `model`: "keep" (default) keeps the mesh and every definition, so opening the same design again after an edit picks up the change and sets follow their persistent face names; "new" starts a new model with the design (no mesh, sets, materials, sections, BCs, loads or steps; the unit system stays; the model is untitled until saved, and undo restores the previous file). The reply's `related` is false when the model (before the open) refers to bodies (mesh tags, set geometry) and the design has none of them, and `stale` lists what no longer resolves: `mesh_bodies` (bodies the mesh is tagged with that the design lacks) `sets` (sets naming a body or face it lacks) and `seeds` (edge seeds on edges it lacks). One undo step"#,
|
||||
form: Some(super::forms::OPEN_DESIGN),
|
||||
kind: CommandKind::Model,
|
||||
requires: &[],
|
||||
@@ -80,6 +80,7 @@ fn open(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
s.set_document(Default::default());
|
||||
fit.mesh_bodies.clear();
|
||||
fit.sets.clear();
|
||||
fit.seeds.clear();
|
||||
}
|
||||
let out = json!({
|
||||
"name": design.name,
|
||||
@@ -87,7 +88,7 @@ fn open(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
"errors": design.errors.iter().map(|e| json!({"feature": e.feature, "message": e.message})).collect::<Vec<_>>(),
|
||||
"model": if new_model { "new" } else { "kept" },
|
||||
"related": fit.related,
|
||||
"stale": {"mesh_bodies": fit.mesh_bodies, "sets": fit.sets},
|
||||
"stale": {"mesh_bodies": fit.mesh_bodies, "sets": fit.sets, "seeds": fit.seeds},
|
||||
});
|
||||
s.design = Some(Arc::new(design));
|
||||
s.sources.design = Some(source);
|
||||
@@ -103,6 +104,8 @@ struct Fit {
|
||||
mesh_bodies: Vec<String>,
|
||||
/// Sets naming a body or a face that is not there.
|
||||
sets: Vec<String>,
|
||||
/// Edge seeds on an edge the design lacks.
|
||||
seeds: Vec<String>,
|
||||
}
|
||||
|
||||
fn fit(s: &Session, design: &Design) -> Fit {
|
||||
@@ -118,6 +121,13 @@ fn fit(s: &Session, design: &Design) -> Fit {
|
||||
related: referenced.is_empty() || referenced.iter().any(|b| has(b)),
|
||||
mesh_bodies: tagged.into_iter().filter(|b| !has(b)).map(str::to_string).collect(),
|
||||
sets: s.model.sets().iter().filter(|(_, set)| !set.geometry.iter().all(resolves)).map(|(n, _)| n.clone()).collect(),
|
||||
seeds: s
|
||||
.model
|
||||
.seeds()
|
||||
.iter()
|
||||
.filter(|seed| super::seeds::seeded_edges(design, &seed.edge).map_or(true, |e| e.is_empty()))
|
||||
.map(|seed| seed.edge.to_string())
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ mod mirror;
|
||||
mod model;
|
||||
pub(crate) mod octree;
|
||||
mod results;
|
||||
pub(crate) mod seeds;
|
||||
pub(crate) mod solve;
|
||||
mod symmetry;
|
||||
mod tags;
|
||||
@@ -57,6 +58,9 @@ pub static ALL: &[CommandSpec] = &[
|
||||
hex::HEX_MAPPED,
|
||||
hex::HEX_BLOCKS,
|
||||
cells::CELLS,
|
||||
seeds::SEED_EDGE,
|
||||
seeds::SEED_CLEAR,
|
||||
seeds::SEED_LIST,
|
||||
mirror::MIRROR,
|
||||
tags::TAG,
|
||||
tags::TAG_CLEAR,
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
//! Edge seeds: `mesh.seed.edge` sets a count or a size (with bias and constraint) on design
|
||||
//! edges, `mesh.seed.clear` removes seeds and `mesh.seed.list` lists them with the count each
|
||||
//! gives on its edge. Seeds live in the model (`fieldcraft_model::seeds`), are saved with it, and
|
||||
//! the meshers honour them.
|
||||
|
||||
use fieldcraft_geometry::{Design, Edge};
|
||||
use fieldcraft_model::{Bias, Divisions, Entity, GeometryRef, Seed, SeedConstraint};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::opt;
|
||||
use crate::requirement::Requirement;
|
||||
use crate::{CommandKind, CommandSpec, EngineError, Result, Session};
|
||||
|
||||
/// Most edges one command names.
|
||||
const MAX_EDGES: usize = 10_000;
|
||||
|
||||
pub const SEED_EDGE: CommandSpec = CommandSpec {
|
||||
id: "mesh.seed.edge",
|
||||
label: "Seed Edges",
|
||||
params: r#"{"edges": [{"body": "Body1", "edge": "F2:side:hi|F2:top"}, …], "count": 8 | "size": 2.5, "bias"?: {"ratio": 4, "toward": "start" | "end" | "both"}, "constraint"?: "exact" (default) | "min" | "free"} — seeds design edges (persistent edge names, geometry.bodies): `count` divisions, or `size` (the edge's length over it, rounded up). `bias` grades the elements geometrically, the largest `ratio` times the smallest, small at the edge's start, end or both ends. `constraint`: "exact" keeps the count (a mesher whose other edges cannot match refuses, naming the conflicting edges), "min" keeps at least it, "free" makes it a goal. Replaces a seed the edge had; an edge name without its `~k` number seeds every edge of that name. The hex meshers (sweep, mapped, blocks, cells) use the counts and spacing; the tet mesher uses the spacing on the edge and grades its size field from it"#,
|
||||
form: Some(super::forms::SEED_EDGE),
|
||||
kind: CommandKind::Model,
|
||||
requires: &[Requirement::Design],
|
||||
run: seed_edge,
|
||||
};
|
||||
|
||||
pub const SEED_CLEAR: CommandSpec = CommandSpec {
|
||||
id: "mesh.seed.clear",
|
||||
label: "Clear Seeds",
|
||||
params: r#"{"edges"?: [{"body", "edge"}, …]} — removes the seeds on these edges (by the names they were seeded with), or every seed"#,
|
||||
form: Some(super::forms::SEED_CLEAR),
|
||||
kind: CommandKind::Model,
|
||||
requires: &[Requirement::Seed],
|
||||
run: seed_clear,
|
||||
};
|
||||
|
||||
pub const SEED_LIST: CommandSpec = CommandSpec {
|
||||
id: "mesh.seed.list",
|
||||
label: "Seeds",
|
||||
params: "{} — the edge seeds, each with the edges of the open design it seeds, their lengths and the count it gives each (`stale`: the design has no such edge)",
|
||||
form: Some(&[]),
|
||||
kind: CommandKind::Model,
|
||||
requires: &[],
|
||||
run: seed_list,
|
||||
};
|
||||
|
||||
/// The `edges` parameter: a list of edge references.
|
||||
fn edge_refs(p: &Value, required: bool) -> Result<Option<Vec<GeometryRef>>> {
|
||||
let refs: Option<Vec<GeometryRef>> = opt(p, "edges")?;
|
||||
let Some(refs) = refs else {
|
||||
return if required { Err(EngineError::Params("`edges` is required: a list of {body, edge}".into())) } else { Ok(None) };
|
||||
};
|
||||
if refs.is_empty() || refs.len() > MAX_EDGES {
|
||||
return Err(EngineError::Params(format!("`edges` must list 1 to {MAX_EDGES} edges")));
|
||||
}
|
||||
if let Some(r) = refs.iter().find(|r| !matches!(r.entity, Entity::Edge(_))) {
|
||||
return Err(EngineError::Params(format!("`edges` lists the {r}; name edges as {{body, edge}}")));
|
||||
}
|
||||
Ok(Some(refs))
|
||||
}
|
||||
|
||||
/// The edges of the design a reference selects (its `~k` pieces too).
|
||||
pub(crate) fn seeded_edges(design: &Design, r: &GeometryRef) -> Result<Vec<Edge>> {
|
||||
let Some(body) = design.body(&r.body) else { return Ok(Vec::new()) };
|
||||
Ok(body.edges()?.into_iter().filter(|e| GeometryRef::new(r.body.clone(), Entity::Edge(e.name.clone())).is_ok_and(|g| r.selects(&g))).collect())
|
||||
}
|
||||
|
||||
fn design(s: &Session) -> Result<&Design> {
|
||||
s.design.as_deref().ok_or_else(|| EngineError::Params("open a design first (geometry.open)".into()))
|
||||
}
|
||||
|
||||
/// A seed's report: its definition and, per edge it seeds, the length and the count it gives.
|
||||
fn report(seed: &Seed, edges: &[Edge]) -> Value {
|
||||
let mut v = json!(seed);
|
||||
v["on"] = edges.iter().map(|e| json!({"edge": e.name, "length": e.length(), "count": seed.count(e.length())})).collect();
|
||||
v["stale"] = json!(edges.is_empty());
|
||||
v
|
||||
}
|
||||
|
||||
fn seed_edge(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let refs = edge_refs(p, true)?.unwrap_or_default();
|
||||
let divisions = match (opt::<u32>(p, "count")?, opt::<f64>(p, "size")?) {
|
||||
(Some(n), None) => Divisions::Count(n),
|
||||
(None, Some(h)) => Divisions::Size(h),
|
||||
_ => return Err(EngineError::Params("give one of `count` (divisions) or `size` (element length)".into())),
|
||||
};
|
||||
let bias: Option<Bias> = opt(p, "bias")?;
|
||||
let constraint: SeedConstraint = opt(p, "constraint")?.unwrap_or_default();
|
||||
let design = design(s)?.clone();
|
||||
let mut out = Vec::with_capacity(refs.len());
|
||||
for r in refs {
|
||||
let edges = seeded_edges(&design, &r)?;
|
||||
if edges.is_empty() {
|
||||
return Err(EngineError::Params(format!("the design has no {r} (geometry.bodies lists the edges)")));
|
||||
}
|
||||
let seed = Seed::new(r, divisions, bias, constraint)?;
|
||||
out.push(report(&seed, &edges));
|
||||
s.model.set_seed(seed)?;
|
||||
}
|
||||
Ok(json!({"seeded": out.len(), "seeds": out}))
|
||||
}
|
||||
|
||||
fn seed_clear(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cleared = match edge_refs(p, false)? {
|
||||
None => s.model.clear_seeds(),
|
||||
Some(refs) => {
|
||||
if let Some(r) = refs.iter().find(|r| s.model.seed(r).is_none()) {
|
||||
return Err(EngineError::Params(format!("the {r} has no seed")));
|
||||
}
|
||||
refs.iter().filter(|r| s.model.remove_seed(r)).count()
|
||||
}
|
||||
};
|
||||
Ok(json!({"cleared": cleared, "left": s.model.seeds().len()}))
|
||||
}
|
||||
|
||||
fn seed_list(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
let mut out = Vec::with_capacity(s.model.seeds().len());
|
||||
for seed in s.model.seeds() {
|
||||
let edges = match s.design.as_deref() {
|
||||
Some(d) => seeded_edges(d, &seed.edge)?,
|
||||
None => Vec::new(),
|
||||
};
|
||||
out.push(report(seed, &edges));
|
||||
}
|
||||
Ok(Value::Array(out))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn plate() -> Session {
|
||||
super::super::datum::tests::plate().0
|
||||
}
|
||||
|
||||
fn thickness_edges(s: &mut Session) -> Vec<Value> {
|
||||
let bodies = s.execute("geometry.bodies", &json!({})).unwrap();
|
||||
bodies[0]["edges"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.filter(|e| (e["length"].as_f64().unwrap() - 10.0).abs() < 1e-6)
|
||||
.map(|e| json!({"body": "Body1", "edge": e["name"]}))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seeds_are_set_replaced_listed_cleared_and_undone() {
|
||||
let mut s = plate();
|
||||
let edges = thickness_edges(&mut s);
|
||||
let r = s.execute("mesh.seed.edge", &json!({"edges": edges[..2], "count": 4, "bias": {"ratio": 3, "toward": "end"}})).unwrap();
|
||||
assert_eq!(r["seeded"], 2);
|
||||
assert_eq!(r["seeds"][0]["on"][0]["count"], 4);
|
||||
assert_eq!(r["seeds"][0]["constraint"], "exact");
|
||||
// A size gives the length over it, rounded up; it replaces the count.
|
||||
let r = s.execute("mesh.seed.edge", &json!({"edges": [edges[0]], "size": 3.0, "constraint": "min"})).unwrap();
|
||||
assert_eq!(r["seeds"][0]["on"][0]["count"], 4);
|
||||
assert_eq!(s.model.seeds().len(), 2);
|
||||
let list = s.execute("mesh.seed.list", &json!({})).unwrap();
|
||||
assert_eq!(list.as_array().unwrap().len(), 2);
|
||||
assert!(list.as_array().unwrap().iter().all(|l| l["stale"] == false));
|
||||
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(s.model.seed(&serde_json::from_value(edges[0].clone()).unwrap()).unwrap().divisions, Divisions::Count(4));
|
||||
s.execute("mesh.seed.clear", &json!({"edges": [edges[1]]})).unwrap();
|
||||
assert_eq!(s.model.seeds().len(), 1);
|
||||
assert!(s.execute("mesh.seed.clear", &json!({"edges": [edges[1]]})).is_err());
|
||||
assert_eq!(s.execute("mesh.seed.clear", &json!({})).unwrap()["cleared"], 1);
|
||||
assert!(s.execute("mesh.seed.clear", &json!({})).is_err(), "nothing to clear");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seeds_survive_save_open_and_a_design_edit() {
|
||||
let (mut s, dir) = super::super::datum::tests::plate();
|
||||
let edges = thickness_edges(&mut s);
|
||||
s.execute("mesh.seed.edge", &json!({"edges": edges, "count": 5, "bias": {"ratio": 2, "toward": "both"}})).unwrap();
|
||||
let file = dir.join("seeded.fieldcraft");
|
||||
s.execute("file.save", &json!({"path": file.to_str().unwrap()})).unwrap();
|
||||
let mut back = Session::default();
|
||||
back.execute("file.open", &json!({"path": file.to_str().unwrap()})).unwrap();
|
||||
assert_eq!(back.model.seeds(), s.model.seeds());
|
||||
// The plate made thicker: the same edges, now 15 mm, keep their seeds.
|
||||
let design = dir.join("plate.solidcraft");
|
||||
let thick = fieldcraft_geometry::sample::Plate { thickness: 15.0, ..Default::default() };
|
||||
std::fs::write(&design, fieldcraft_geometry::sample::plate_design(&thick).unwrap()).unwrap();
|
||||
let r = back.execute("geometry.open", &json!({"path": design.to_str().unwrap()})).unwrap();
|
||||
assert_eq!(r["stale"]["seeds"], json!([]));
|
||||
let list = back.execute("mesh.seed.list", &json!({})).unwrap();
|
||||
for l in list.as_array().unwrap() {
|
||||
assert_eq!(l["on"].as_array().unwrap().len(), 1, "{l}");
|
||||
assert!((l["on"][0]["length"].as_f64().unwrap() - 15.0).abs() < 1e-6, "{l}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hostile_seeds_are_refused_and_change_nothing() {
|
||||
let mut s = plate();
|
||||
let edges = thickness_edges(&mut s);
|
||||
let e = edges[0].clone();
|
||||
for p in [
|
||||
json!({}),
|
||||
json!({"edges": [], "count": 2}),
|
||||
json!({"edges": [e], "count": 2, "size": 1.0}),
|
||||
json!({"edges": [e]}),
|
||||
json!({"edges": [e], "count": 0}),
|
||||
json!({"edges": [e], "count": -3}),
|
||||
json!({"edges": [e], "count": 1e12}),
|
||||
json!({"edges": [e], "size": 0.0}),
|
||||
json!({"edges": [e], "count": 2, "bias": {"ratio": 1e9, "toward": "end"}}),
|
||||
json!({"edges": [e], "count": 2, "bias": {"ratio": 2, "toward": "middle"}}),
|
||||
json!({"edges": [e], "count": 2, "constraint": "loose"}),
|
||||
json!({"edges": [{"body": "Body1", "face": "x"}], "count": 2}),
|
||||
json!({"edges": [{"body": "Body1", "edge": "no|such"}], "count": 2}),
|
||||
json!({"edges": [{"body": "Nope", "edge": "a|b"}], "count": 2}),
|
||||
json!({"edges": "all", "count": 2}),
|
||||
] {
|
||||
assert!(s.execute("mesh.seed.edge", &p).is_err(), "{p}");
|
||||
}
|
||||
assert!(s.model.seeds().is_empty());
|
||||
assert!(s.journal.is_empty() || s.journal.iter().all(|(c, _)| c != "mesh.seed.edge"));
|
||||
}
|
||||
}
|
||||
@@ -696,7 +696,7 @@ mod tests {
|
||||
let before = s.model.clone();
|
||||
let r = s.execute("geometry.open", &json!({"path": path})).unwrap();
|
||||
assert_eq!((r["model"].as_str(), r["related"].as_bool()), (Some("kept"), Some(true)), "{r}");
|
||||
assert_eq!(r["stale"], json!({"mesh_bodies": [], "sets": []}));
|
||||
assert_eq!(r["stale"], json!({"mesh_bodies": [], "sets": [], "seeds": []}));
|
||||
assert_eq!(s.model, before);
|
||||
}
|
||||
|
||||
@@ -711,7 +711,7 @@ mod tests {
|
||||
let mut kept = defined.clone();
|
||||
let r = kept.execute("geometry.open", &json!({"path": path})).unwrap();
|
||||
assert_eq!((r["model"].as_str(), r["related"].as_bool()), (Some("kept"), Some(false)), "{r}");
|
||||
assert_eq!(r["stale"], json!({"mesh_bodies": ["Seal"], "sets": ["all", "hole"]}));
|
||||
assert_eq!(r["stale"], json!({"mesh_bodies": ["Seal"], "sets": ["all", "hole"], "seeds": []}));
|
||||
assert_eq!(kept.model, before);
|
||||
|
||||
// New: the mesh and every definition go, the units stay; one undo brings them back.
|
||||
@@ -721,7 +721,7 @@ mod tests {
|
||||
let before = fresh.model.clone();
|
||||
let r = fresh.execute("geometry.open", &json!({"path": path, "model": "new"})).unwrap();
|
||||
assert_eq!((r["model"].as_str(), r["related"].as_bool()), (Some("new"), Some(false)), "{r}");
|
||||
assert_eq!(r["stale"], json!({"mesh_bodies": [], "sets": []}));
|
||||
assert_eq!(r["stale"], json!({"mesh_bodies": [], "sets": [], "seeds": []}));
|
||||
assert!(fresh.model.elements().is_empty() && fresh.model.nodes().is_empty());
|
||||
assert!(fresh.model.definition_counts().iter().all(|(_, n)| *n == 0));
|
||||
assert_eq!(fresh.model.units(), units);
|
||||
|
||||
@@ -30,6 +30,8 @@ pub enum Requirement {
|
||||
DatumPlane,
|
||||
/// A partition of the design.
|
||||
Partition,
|
||||
/// An edge seed.
|
||||
Seed,
|
||||
/// At least one mesh node.
|
||||
Nodes,
|
||||
/// At least one element.
|
||||
@@ -73,6 +75,7 @@ impl Requirement {
|
||||
Requirement::Datum => !m.datums().is_empty(),
|
||||
Requirement::DatumPlane => m.datums().values().any(|d| d.plane().is_some()),
|
||||
Requirement::Partition => !m.partitions().is_empty(),
|
||||
Requirement::Seed => !m.seeds().is_empty(),
|
||||
Requirement::Nodes => !m.nodes().is_empty(),
|
||||
Requirement::Mesh => has_elements,
|
||||
Requirement::Analysable => {
|
||||
@@ -110,6 +113,7 @@ impl Requirement {
|
||||
Requirement::Datum => "a datum plane or axis",
|
||||
Requirement::DatumPlane => "a datum plane",
|
||||
Requirement::Partition => "a partition",
|
||||
Requirement::Seed => "an edge seed",
|
||||
Requirement::Nodes => "mesh nodes",
|
||||
Requirement::Mesh | Requirement::Analysable => "a mesh",
|
||||
Requirement::ElementMaterial | Requirement::Material => "a material",
|
||||
@@ -137,6 +141,7 @@ impl Requirement {
|
||||
Requirement::Design | Requirement::Geometry => "geometry.open",
|
||||
Requirement::Datum | Requirement::DatumPlane => "geometry.datum",
|
||||
Requirement::Partition => "geometry.partition",
|
||||
Requirement::Seed => "mesh.seed.edge",
|
||||
Requirement::Nodes => "mesh.node.add",
|
||||
Requirement::Mesh | Requirement::Analysable => "mesh.tet.generate",
|
||||
Requirement::ElementMaterial | Requirement::Material => "model.material.create",
|
||||
|
||||
@@ -36,7 +36,10 @@ datum plane, geometry.partition cuts the design's bodies into cells by it (cut f
|
||||
the model and applied again when the design opens); `mesh` method cells meshes the cells each with its own method, \
|
||||
method mirror reflects a meshed half across a datum plane into one conforming mesh; \
|
||||
model.boundary_condition.symmetry holds a half model on its symmetry plane (file.sample crank-mirror shows the \
|
||||
partition, half-mesh and mirror sequence). Failures leave the model unchanged and name what to fix.";
|
||||
partition, half-mesh and mirror sequence). Edge seeds: mesh.seed.edge {edges: [{body, edge}], count | size, bias?, \
|
||||
constraint?} fixes the element count along design edges (inspect_model lists the edge names): seed a thin section's \
|
||||
thickness, a fillet or hole edge (biased toward the stress raiser), and partition cells' shared edges so their \
|
||||
counts match. Failures leave the model unchanged and name what to fix.";
|
||||
|
||||
pub struct Server {
|
||||
backend: Box<dyn Backend>,
|
||||
|
||||
@@ -695,7 +695,7 @@ impl ModelApp {
|
||||
/// empty when everything resolves.
|
||||
fn stale_note(stale: &Value) -> String {
|
||||
let names = |k: &str| stale.get(k).and_then(Value::as_array).into_iter().flatten().filter_map(Value::as_str).collect::<Vec<_>>().join(", ");
|
||||
let parts: Vec<String> = [("mesh of", names("mesh_bodies")), ("sets", names("sets"))]
|
||||
let parts: Vec<String> = [("mesh of", names("mesh_bodies")), ("sets", names("sets")), ("seeds on", names("seeds"))]
|
||||
.into_iter()
|
||||
.filter(|(_, n)| !n.is_empty())
|
||||
.map(|(what, n)| format!("{what} {n}"))
|
||||
|
||||
+2
-2
@@ -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.
|
||||
|
||||
62 ready, 42 coming soon.
|
||||
63 ready, 41 coming soon.
|
||||
|
||||
## Geometry
|
||||
|
||||
@@ -26,7 +26,7 @@ the control channel and AI agents over MCP (docs/mcp.md) run the same commands.
|
||||
| Panel | Capability | Status | Command |
|
||||
|---|---|---|---|
|
||||
| Size | Global Size — One element size for the whole part | coming soon (M2) | |
|
||||
| Size | Seed Edges — Element counts, sizes and bias per edge and face | coming soon (M2) | |
|
||||
| Size | Seed Edges — Element counts or sizes per design edge, with bias and an exact, minimum or free constraint; the meshers honour them | ready | `mesh.seed.edge`, `mesh.seed.clear`, `mesh.seed.list` |
|
||||
| Size | Curvature — Refine by curvature and proximity | coming soon (M2) | |
|
||||
| Tet | Tet Mesh — Quality tet4 or tet10 mesh of a body to a size: refined, smoothed and improved, boundary on the B-rep faces, faces tagged | ready | `mesh.tet.generate` |
|
||||
| Tet | Quadratic — Second-order elements with mid-side nodes on the geometry | coming soon (M2) | |
|
||||
|
||||
+16
@@ -140,6 +140,22 @@ and `mesh` (each one's parameters: `list_commands`):
|
||||
|
||||
`file.sample {"name": "crank-mirror"}` opens the crank arm built this way (docs/examples.md).
|
||||
|
||||
## Edge seeds
|
||||
|
||||
Seed an edge when its element count matters more than the global size: through a thin
|
||||
section's thickness (at least 2–3 elements for bending), around a fillet or hole where the
|
||||
stress peaks (a count or a small size, biased toward the stress raiser), or to give the shared
|
||||
edges of partition cells matching counts.
|
||||
|
||||
1. `inspect_model` lists each body's edges by persistent name (`<face>|<face>`, `~k` when several
|
||||
separate the same faces) with their lengths; the names follow the faces through edits and
|
||||
partitions.
|
||||
2. `execute` `mesh.seed.edge {edges: [{body, edge}], count | size, bias?: {ratio, toward:
|
||||
start|end|both}, constraint?: exact|min|free}`. `exact` keeps the count, `min` keeps at least
|
||||
it, `free` is a goal. The reply gives the count each edge gets.
|
||||
3. `mesh.seed.list` lists the seeds and the edges they reach (`stale` when the design has no such
|
||||
edge); `mesh.seed.clear {edges?}` removes them. Seeds are saved with the model.
|
||||
|
||||
## Limits
|
||||
|
||||
The server treats its input as hostile. A message line may be at most 16 MiB (the control
|
||||
|
||||
Reference in new issue
Block a user