forked from 1b-agent/fieldcraft-upstream
simplify: joining in modelcraft-mesh::join, shared label helper, run_script returns step outputs
Weld and face-to-face check move from the mesh.block command to modelcraft_mesh::join (bounding-box-limited weld, grid-binned hanging-node test), for node merging and imports to reuse. mesh.block and mesh.tet.generate share fresh_labels. The block map evaluates each edge curve once per lattice parameter; vector helpers come from vec3; tet10 and hex20 mid-side nodes follow ElementKind::edges. Session::run_script returns each step's output, which the oracle reads instead of its own loop. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
This commit is contained in:
1 parent
cbff230734
commit
6dda34e837
11 files changed
+369
-359
No files matched your search
+36
-196
@@ -4,11 +4,13 @@
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use modelcraft_mesh::block::{Block, BlockMesh, Curve, block};
|
||||
use modelcraft_model::{ElementKind, Entity, GeometryRef, MAX_LABEL, Model};
|
||||
use modelcraft_mesh::block::{Block, Curve, block};
|
||||
use modelcraft_mesh::join::{check_conforming, weld};
|
||||
use modelcraft_model::{ElementKind, Entity, GeometryRef, Model};
|
||||
use serde::Deserialize;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::fresh_labels;
|
||||
use crate::{CommandKind, CommandSpec, EngineError, Result, Session};
|
||||
|
||||
pub const BLOCK: CommandSpec = CommandSpec {
|
||||
@@ -97,16 +99,15 @@ fn block_mesh(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
return Err(EngineError::Params(format!("edge {:?} gives neither a curve nor a name", e.corners)));
|
||||
}
|
||||
}
|
||||
let faces: Vec<(usize, String)> = p.faces.iter().map(|(k, n)| Ok((numbered(k, 6, "face")?, n.clone()))).collect::<Result<_>>()?;
|
||||
let vertices: Vec<(usize, String)> = p.vertices.iter().map(|(k, n)| Ok((numbered(k, 8, "vertex")?, n.clone()))).collect::<Result<_>>()?;
|
||||
// Check every name before meshing.
|
||||
let body = GeometryRef::body(p.body.clone())?;
|
||||
let face_refs: Vec<(usize, GeometryRef)> =
|
||||
faces.iter().map(|(k, n)| Ok((*k, GeometryRef::face(p.body.clone(), n.clone())?))).collect::<Result<_>>()?;
|
||||
let named = |entries: &BTreeMap<String, String>, n: usize, what: &str, entity: fn(String) -> Entity| -> Result<Vec<(usize, GeometryRef)>> {
|
||||
entries.iter().map(|(k, name)| Ok((numbered(k, n, what)?, GeometryRef::new(p.body.clone(), entity(name.clone()))?))).collect()
|
||||
};
|
||||
let face_refs = named(&p.faces, 6, "face", Entity::Face)?;
|
||||
let vertex_refs = named(&p.vertices, 8, "vertex", Entity::Vertex)?;
|
||||
let edge_refs: Vec<(usize, GeometryRef)> =
|
||||
edge_names.iter().map(|(k, n)| Ok((*k, GeometryRef::new(p.body.clone(), Entity::Edge(n.clone()))?))).collect::<Result<_>>()?;
|
||||
let vertex_refs: Vec<(usize, GeometryRef)> =
|
||||
vertices.iter().map(|(k, n)| Ok((*k, GeometryRef::new(p.body.clone(), Entity::Vertex(n.clone()))?))).collect::<Result<_>>()?;
|
||||
edge_names.into_iter().map(|(k, n)| Ok((k, GeometryRef::new(p.body.clone(), Entity::Edge(n))?))).collect::<Result<_>>()?;
|
||||
|
||||
let mesh = block(&Block { kind, corners: p.corners, divisions: p.divisions, bias: p.bias, curves })?;
|
||||
let diagonal = {
|
||||
@@ -118,62 +119,49 @@ fn block_mesh(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if !(merge.is_finite() && merge >= 0.0 && merge <= 1e-2 * diagonal) {
|
||||
return Err(EngineError::Params(format!("`merge` must be from 0 to 1/100 of the block's diagonal ({diagonal})")));
|
||||
}
|
||||
|
||||
let next = |last: Option<u32>| last.map_or(Some(1), |l| l.checked_add(1));
|
||||
let first_node = next(s.model.nodes().keys().next_back().copied());
|
||||
let first_element = next(s.model.elements().keys().next_back().copied());
|
||||
let fits = |first: Option<u32>, count: usize| first.filter(|&f| u64::from(f) + count as u64 <= u64::from(MAX_LABEL) + 1);
|
||||
let (Some(first_node), Some(first_element)) = (fits(first_node, mesh.points.len()), fits(first_element, mesh.elements.len())) else {
|
||||
return Err(EngineError::Params(format!("the block needs labels beyond {MAX_LABEL}")));
|
||||
};
|
||||
let (first_node, first_element) = fresh_labels(&s.model, mesh.points.len(), mesh.elements.len())?;
|
||||
|
||||
// Each block point: an existing node within `merge`, or a new label.
|
||||
let existing = joined(&s.model, &mesh.points, merge)?;
|
||||
let welded = weld(&s.model, &mesh.points, merge)?;
|
||||
let mut labels = Vec::with_capacity(mesh.points.len());
|
||||
let mut added = 0u32;
|
||||
for (i, xyz) in mesh.points.iter().enumerate() {
|
||||
match existing.get(i).copied().flatten() {
|
||||
Some(label) => labels.push(label),
|
||||
for (xyz, existing) in mesh.points.iter().zip(&welded) {
|
||||
let label = match existing {
|
||||
Some(label) => *label,
|
||||
None => {
|
||||
let label = first_node + added;
|
||||
s.model.set_node(label, *xyz)?;
|
||||
labels.push(label);
|
||||
added += 1;
|
||||
s.model.set_node(first_node + added - 1, *xyz)?;
|
||||
first_node + added - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
let merged: BTreeSet<u32> = existing.iter().flatten().copied().collect();
|
||||
let elements: Vec<Vec<u32>> = mesh.elements.iter().map(|e| e.iter().map(|&i| labels.get(i as usize).copied().unwrap_or(0)).collect()).collect();
|
||||
if !merged.is_empty() {
|
||||
check_conforming(&s.model, kind, &mesh, &elements, &merged, merge)?;
|
||||
};
|
||||
labels.push(label);
|
||||
}
|
||||
let joined: BTreeSet<u32> = welded.iter().flatten().copied().collect();
|
||||
let label_of = |i: u32| labels.get(i as usize).copied().ok_or_else(|| EngineError::Params(format!("block point {i} has no node")));
|
||||
let elements: Vec<(ElementKind, Vec<u32>)> =
|
||||
mesh.elements.iter().map(|e| Ok((kind, e.iter().map(|&i| label_of(i)).collect::<Result<_>>()?))).collect::<Result<_>>()?;
|
||||
check_conforming(&s.model, &elements, &joined, merge)?;
|
||||
|
||||
let body_tag = s.model.geometry_tag(&body)?;
|
||||
for (i, nodes) in elements.into_iter().enumerate() {
|
||||
let label = first_element + i as u32;
|
||||
for (label, (_, nodes)) in (first_element..).zip(elements) {
|
||||
s.model.set_element(label, kind, nodes)?;
|
||||
s.model.tag_element(label, body_tag)?;
|
||||
}
|
||||
for (k, r) in &face_refs {
|
||||
let t = s.model.geometry_tag(r)?;
|
||||
for &(e, f, bf) in &mesh.faces {
|
||||
if usize::from(bf) == *k {
|
||||
s.model.tag_element_face(first_element + e, f, t)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (k, r) in &edge_refs {
|
||||
let t = s.model.geometry_tag(r)?;
|
||||
for &(e, edge, be) in &mesh.edges {
|
||||
if usize::from(be) == *k {
|
||||
s.model.tag_element_edge(first_element + e, edge, t)?;
|
||||
for (refs, pieces, tag) in [
|
||||
(&face_refs, &mesh.faces, Model::tag_element_face as fn(&mut Model, u32, u8, u32) -> modelcraft_model::Result<()>),
|
||||
(&edge_refs, &mesh.edges, Model::tag_element_edge),
|
||||
] {
|
||||
for (k, r) in refs {
|
||||
let t = s.model.geometry_tag(r)?;
|
||||
for &(e, local, _) in pieces.iter().filter(|piece| usize::from(piece.2) == *k) {
|
||||
tag(&mut s.model, first_element + e, local, t)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (k, r) in &vertex_refs {
|
||||
let t = s.model.geometry_tag(r)?;
|
||||
let node = mesh.corners.get(*k).and_then(|&i| labels.get(i as usize)).copied().unwrap_or(0);
|
||||
s.model.tag_node(node, t)?;
|
||||
let corner = mesh.corners.get(*k).copied().unwrap_or(u32::MAX);
|
||||
s.model.tag_node(label_of(corner)?, t)?;
|
||||
}
|
||||
Ok(json!({
|
||||
"body": p.body,
|
||||
@@ -182,158 +170,10 @@ fn block_mesh(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
"first_element": first_element,
|
||||
"nodes": added,
|
||||
"first_node": first_node,
|
||||
"joined": merged.len(),
|
||||
"joined": joined.len(),
|
||||
}))
|
||||
}
|
||||
|
||||
/// For each block point, the existing node within `merge` of it (the nearest; the lowest label
|
||||
/// on a tie). Two points may not join the same node.
|
||||
fn joined(model: &Model, points: &[[f64; 3]], merge: f64) -> Result<Vec<Option<u32>>> {
|
||||
if merge == 0.0 || model.nodes().is_empty() {
|
||||
return Ok(vec![None; points.len()]);
|
||||
}
|
||||
let cell = |x: f64| (x / merge).floor() as i64;
|
||||
let mut grid: BTreeMap<[i64; 3], Vec<(u32, [f64; 3])>> = BTreeMap::new();
|
||||
for (&label, node) in model.nodes() {
|
||||
grid.entry(node.xyz.map(cell)).or_default().push((label, node.xyz));
|
||||
}
|
||||
let mut out = Vec::with_capacity(points.len());
|
||||
let mut taken: BTreeMap<u32, usize> = BTreeMap::new();
|
||||
for (i, p) in points.iter().enumerate() {
|
||||
let c = p.map(cell);
|
||||
let mut best: Option<(f64, u32)> = None;
|
||||
for dx in -1..=1 {
|
||||
for dy in -1..=1 {
|
||||
for dz in -1..=1 {
|
||||
for &(label, xyz) in grid.get(&[c[0] + dx, c[1] + dy, c[2] + dz]).into_iter().flatten() {
|
||||
let d = ((xyz[0] - p[0]).powi(2) + (xyz[1] - p[1]).powi(2) + (xyz[2] - p[2]).powi(2)).sqrt();
|
||||
if d <= merge && best.is_none_or(|(bd, bl)| d < bd || (d == bd && label < bl)) {
|
||||
best = Some((d, label));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some((_, label)) = best
|
||||
&& let Some(other) = taken.insert(label, i)
|
||||
{
|
||||
return Err(EngineError::Params(format!(
|
||||
"block points {other} and {i} both lie within `merge` of node {label}: lower `merge` or refine the block"
|
||||
)));
|
||||
}
|
||||
out.push(best.map(|(_, l)| l));
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Faces as sorted node lists.
|
||||
fn key(nodes: impl Iterator<Item = u32>) -> Vec<u32> {
|
||||
let mut k: Vec<u32> = nodes.collect();
|
||||
k.sort_unstable();
|
||||
k
|
||||
}
|
||||
|
||||
/// Where the block joins existing nodes it must meet the mesh face to face: an element face
|
||||
/// (of the block, or of the mesh) whose corners all landed on shared nodes must be a face of
|
||||
/// the other side too, with the same nodes (no mixed linear and quadratic sides); and no node on
|
||||
/// the block's boundary may lie on an edge of the mesh between two of its nodes, nor the other
|
||||
/// way round (no hanging nodes where the divisions differ). Edges are tested as straight
|
||||
/// segments, so a mismatch along a curved shared edge is caught only through its faces.
|
||||
fn check_conforming(model: &Model, kind: ElementKind, mesh: &BlockMesh, elements: &[Vec<u32>], merged: &BTreeSet<u32>, merge: f64) -> Result<()> {
|
||||
let refuse = |what: String| {
|
||||
Err(EngineError::Params(format!("{what}: the block must meet the mesh face to face, with the same divisions and element kind")))
|
||||
};
|
||||
let corners_of = |k: ElementKind, nodes: &[u32], local: &[usize]| -> Vec<u32> {
|
||||
key(local.iter().filter(|&&i| i < k.corner_count()).filter_map(|&i| nodes.get(i).copied()))
|
||||
};
|
||||
// The block's boundary faces, by sorted corners, with their full node lists.
|
||||
let mut new_faces: BTreeMap<Vec<u32>, Vec<u32>> = BTreeMap::new();
|
||||
for &(e, f, _) in &mesh.faces {
|
||||
let (Some(nodes), Some(local)) = (elements.get(e as usize), usize::from(f).checked_sub(1).and_then(|i| kind.faces().get(i))) else {
|
||||
continue;
|
||||
};
|
||||
new_faces.insert(corners_of(kind, nodes, local), key(local.iter().filter_map(|&i| nodes.get(i).copied())));
|
||||
}
|
||||
let mut old_faces: BTreeMap<Vec<u32>, Vec<u32>> = BTreeMap::new();
|
||||
let mut old_edges: BTreeSet<[u32; 2]> = BTreeSet::new();
|
||||
for e in model.elements().values().filter(|e| e.nodes.iter().any(|n| merged.contains(n))) {
|
||||
for local in e.kind.faces() {
|
||||
old_faces.insert(corners_of(e.kind, &e.nodes, local), key(local.iter().filter_map(|&i| e.nodes.get(i).copied())));
|
||||
}
|
||||
}
|
||||
for (corners, nodes) in &new_faces {
|
||||
if corners.iter().all(|n| merged.contains(n)) && old_faces.get(corners) != Some(nodes) {
|
||||
return refuse(format!("the block face on nodes {corners:?} lies on existing nodes but is no element face there"));
|
||||
}
|
||||
}
|
||||
for (corners, nodes) in &old_faces {
|
||||
if corners.iter().all(|n| merged.contains(n)) && new_faces.get(corners) != Some(nodes) {
|
||||
return refuse(format!("the element face on nodes {corners:?} lies on the block but no block element shares it"));
|
||||
}
|
||||
}
|
||||
// Hanging nodes: boundary nodes of either side on the other side's element edges.
|
||||
let xyz = |label: u32| model.node(label).map(|n| n.xyz);
|
||||
let block_points: BTreeMap<u32, [f64; 3]> = mesh
|
||||
.elements
|
||||
.iter()
|
||||
.zip(elements)
|
||||
.flat_map(|(pts, labels)| pts.iter().zip(labels).filter_map(|(&p, &l)| Some((l, *mesh.points.get(p as usize)?))))
|
||||
.collect();
|
||||
let mut new_edges: BTreeSet<[u32; 2]> = BTreeSet::new();
|
||||
for &(e, f, _) in &mesh.faces {
|
||||
let (Some(nodes), Some(local)) = (elements.get(e as usize), usize::from(f).checked_sub(1).and_then(|i| kind.faces().get(i))) else {
|
||||
continue;
|
||||
};
|
||||
let cs: Vec<u32> = local.iter().filter(|&&i| i < kind.corner_count()).filter_map(|&i| nodes.get(i).copied()).collect();
|
||||
for (i, &a) in cs.iter().enumerate() {
|
||||
let b = cs.get((i + 1) % cs.len()).copied().unwrap_or(a);
|
||||
new_edges.insert(if a < b { [a, b] } else { [b, a] });
|
||||
}
|
||||
}
|
||||
for e in model.elements().values().filter(|e| e.nodes.iter().any(|n| merged.contains(n))) {
|
||||
for local in e.kind.edges() {
|
||||
if let (Some(&a), Some(&b)) = (local.first().and_then(|&i| e.nodes.get(i)), local.get(1).and_then(|&i| e.nodes.get(i))) {
|
||||
old_edges.insert(if a < b { [a, b] } else { [b, a] });
|
||||
}
|
||||
}
|
||||
}
|
||||
let new_boundary: BTreeSet<u32> = new_faces.values().flatten().copied().filter(|n| !merged.contains(n)).collect();
|
||||
let on_segment = |p: [f64; 3], a: [f64; 3], b: [f64; 3]| {
|
||||
let ab = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
|
||||
let ap = [p[0] - a[0], p[1] - a[1], p[2] - a[2]];
|
||||
let len2 = ab[0] * ab[0] + ab[1] * ab[1] + ab[2] * ab[2];
|
||||
if !(len2 > 0.0) {
|
||||
return false;
|
||||
}
|
||||
let t = (ap[0] * ab[0] + ap[1] * ab[1] + ap[2] * ab[2]) / len2;
|
||||
let d = [ap[0] - t * ab[0], ap[1] - t * ab[1], ap[2] - t * ab[2]];
|
||||
let tol = merge / len2.sqrt();
|
||||
t > tol && t < 1.0 - tol && (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt() <= merge
|
||||
};
|
||||
for &n in &new_boundary {
|
||||
let Some(&p) = block_points.get(&n) else { continue };
|
||||
for [a, b] in &old_edges {
|
||||
if let (Some(xa), Some(xb)) = (xyz(*a), xyz(*b))
|
||||
&& on_segment(p, xa, xb)
|
||||
{
|
||||
return refuse(format!("a block node at {p:?} lies on the edge between nodes {a} and {b}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
let old_nodes: BTreeSet<u32> = old_faces.values().flatten().copied().filter(|n| !merged.contains(n)).collect();
|
||||
for n in old_nodes {
|
||||
let Some(p) = xyz(n) else { continue };
|
||||
for [a, b] in &new_edges {
|
||||
if let (Some(&xa), Some(&xb)) = (block_points.get(a), block_points.get(b))
|
||||
&& on_segment(p, xa, xb)
|
||||
{
|
||||
return refuse(format!("node {n} at {p:?} lies on a block edge"));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
|
||||
@@ -6,7 +6,7 @@ use modelcraft_mesh::cdt::{CdtOptions, tetrahedralize};
|
||||
use modelcraft_mesh::delaunay::Triangulation;
|
||||
use modelcraft_mesh::quality::{self, QualityReport};
|
||||
use modelcraft_mesh::surface::{BoundarySurface, Defect, FeatureKind, SurfaceOptions};
|
||||
use modelcraft_model::{ElementKind, GeometryRef, MAX_ELEMENTS, MAX_LABEL};
|
||||
use modelcraft_model::{ElementKind, GeometryRef, MAX_ELEMENTS};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::{label, labels, listed, optional_number, points, xyz};
|
||||
@@ -88,17 +88,6 @@ pub const TET_GENERATE: CommandSpec = CommandSpec {
|
||||
run: tet_generate,
|
||||
};
|
||||
|
||||
/// The first node and element labels of a mesh of `nodes` nodes and `elements` elements added
|
||||
/// after the model's largest labels, or an error when the labels would pass [`MAX_LABEL`].
|
||||
fn next_labels(s: &Session, nodes: usize, elements: usize) -> Result<(u32, u32)> {
|
||||
let next = |last: Option<u32>| last.map_or(Some(1), |l| l.checked_add(1));
|
||||
let fits = |first: Option<u32>, count: usize| first.filter(|&f| u64::from(f) + count as u64 - 1 <= u64::from(MAX_LABEL));
|
||||
match (fits(next(s.model.nodes().keys().next_back().copied()), nodes), fits(next(s.model.elements().keys().next_back().copied()), elements)) {
|
||||
(Some(n), Some(e)) => Ok((n, e)),
|
||||
_ => Err(EngineError::Params(format!("the mesh needs labels beyond {MAX_LABEL}"))),
|
||||
}
|
||||
}
|
||||
|
||||
/// The most Steiner points and tetrahedra `mesh.tet.generate` may be asked for.
|
||||
const TET_MAX_STEINER: u64 = 10_000_000;
|
||||
const TET_MAX_TETS: u64 = 50_000_000;
|
||||
@@ -138,7 +127,7 @@ fn tet_generate(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cdt = tetrahedralize(&surface, &cdt_options)?;
|
||||
let gate = cdt.gate(surface.volume())?.into_result()?;
|
||||
|
||||
let (first_node, first_element) = next_labels(s, cdt.points.len(), cdt.tets.len())?;
|
||||
let (first_node, first_element) = super::fresh_labels(&s.model, cdt.points.len(), cdt.tets.len())?;
|
||||
for (i, xyz) in cdt.points.iter().enumerate() {
|
||||
s.model.set_node(first_node + i as u32, *xyz)?;
|
||||
}
|
||||
@@ -274,7 +263,7 @@ fn planar(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
};
|
||||
let m = mesh_region(&loops, kind, size, &opts)?;
|
||||
|
||||
let (first_node, first_element) = next_labels(s, m.nodes.len(), m.elements.len())?;
|
||||
let (first_node, first_element) = super::fresh_labels(&s.model, m.nodes.len(), m.elements.len())?;
|
||||
for (i, xy) in m.nodes.iter().enumerate() {
|
||||
s.model.set_node(first_node + i as u32, [xy[0], xy[1], 0.0])?;
|
||||
}
|
||||
|
||||
@@ -109,6 +109,19 @@ pub(crate) fn label(p: &Value, key: &str) -> Result<u32> {
|
||||
.ok_or_else(|| EngineError::Params(format!("`{key}` must be a positive whole number")))
|
||||
}
|
||||
|
||||
/// The first node and element labels after the model's largest, for `nodes` new nodes and
|
||||
/// `elements` new elements, all within [`MAX_LABEL`].
|
||||
pub(crate) fn fresh_labels(model: &modelcraft_model::Model, nodes: usize, elements: usize) -> Result<(u32, u32)> {
|
||||
let first = |last: Option<&u32>, count: usize| {
|
||||
let first = last.map_or(Some(1), |l| l.checked_add(1))?;
|
||||
(u64::from(first) + count as u64 <= u64::from(MAX_LABEL) + 1).then_some(first)
|
||||
};
|
||||
match (first(model.nodes().keys().next_back(), nodes), first(model.elements().keys().next_back(), elements)) {
|
||||
(Some(n), Some(e)) => Ok((n, e)),
|
||||
_ => Err(EngineError::Params(format!("the mesh needs labels beyond {MAX_LABEL}"))),
|
||||
}
|
||||
}
|
||||
|
||||
/// A required `[x, y, z]` parameter.
|
||||
pub(crate) fn xyz(p: &Value, key: &str) -> Result<[f64; 3]> {
|
||||
p.get(key).and_then(xyz_value).ok_or_else(|| EngineError::Params(format!("`{key}` must be [x, y, z]")))
|
||||
|
||||
@@ -353,7 +353,7 @@ fn probe(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
(None, Some(_)) => {
|
||||
let at = points(p, "at", MAX_PROBE)?;
|
||||
let found = nearest_nodes(&s.model, result.nodes(), &at)?;
|
||||
(found.iter().map(|(l, _)| *l).collect(), Some(found))
|
||||
(found.iter().map(|(l, _, _)| *l).collect(), Some(found))
|
||||
}
|
||||
(None, None) if matches!(result.as_ref(), StepResult::Explicit(_)) => (Vec::new(), None),
|
||||
_ => return Err(EngineError::Params("give either `nodes` or `at`".into())),
|
||||
@@ -367,9 +367,9 @@ fn probe(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
StepResult::Fluid(r) => probe_fluid(r, p)?,
|
||||
};
|
||||
if let (Some(found), Some(entries)) = (located, out.get_mut("nodes").and_then(Value::as_array_mut)) {
|
||||
for (entry, (label, distance)) in entries.iter_mut().zip(found) {
|
||||
for (entry, (_, xyz, distance)) in entries.iter_mut().zip(found) {
|
||||
if let Some(obj) = entry.as_object_mut() {
|
||||
obj.insert("xyz".into(), json!(s.model.node(label).map(|n| n.xyz)));
|
||||
obj.insert("xyz".into(), json!(xyz));
|
||||
obj.insert("distance".into(), json!(distance));
|
||||
}
|
||||
}
|
||||
@@ -377,16 +377,15 @@ fn probe(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// The node with a result nearest to each point (lowest label on a tie), and its distance.
|
||||
fn nearest_nodes(model: &modelcraft_model::Model, with_result: &[u32], at: &[[f64; 3]]) -> Result<Vec<(u32, f64)>> {
|
||||
/// The node with a result nearest to each point (lowest label on a tie), where it is and how far.
|
||||
fn nearest_nodes(model: &modelcraft_model::Model, with_result: &[u32], at: &[[f64; 3]]) -> Result<Vec<(u32, [f64; 3], f64)>> {
|
||||
let candidates: Vec<(u32, [f64; 3])> = with_result.iter().filter_map(|&l| Some((l, model.node(l)?.xyz))).collect();
|
||||
at.iter()
|
||||
.map(|p| {
|
||||
with_result
|
||||
candidates
|
||||
.iter()
|
||||
.filter_map(|&l| {
|
||||
model.node(l).map(|n| (l, ((n.xyz[0] - p[0]).powi(2) + (n.xyz[1] - p[1]).powi(2) + (n.xyz[2] - p[2]).powi(2)).sqrt()))
|
||||
})
|
||||
.fold(None, |best: Option<(u32, f64)>, (l, d)| if best.is_none_or(|(_, bd)| d < bd) { Some((l, d)) } else { best })
|
||||
.map(|&(l, x)| (l, x, ((x[0] - p[0]).powi(2) + (x[1] - p[1]).powi(2) + (x[2] - p[2]).powi(2)).sqrt()))
|
||||
.min_by(|a, b| a.2.total_cmp(&b.2))
|
||||
.ok_or_else(|| EngineError::Params("the result has no nodes to probe".into()))
|
||||
})
|
||||
.collect()
|
||||
|
||||
@@ -206,14 +206,17 @@ impl Session {
|
||||
}
|
||||
|
||||
/// Run a script: a JSON array of `{"command": id, "params": {...}}`.
|
||||
pub fn run_script(&mut self, script: &Value) -> Result<()> {
|
||||
/// Returns each step's output, in order. Other fields of a step (such as an `id` naming it)
|
||||
/// are left to the caller.
|
||||
pub fn run_script(&mut self, script: &Value) -> Result<Vec<Value>> {
|
||||
let steps = script.as_array().ok_or_else(|| EngineError::Params("a script is a JSON array of steps".into()))?;
|
||||
let mut outputs = Vec::with_capacity(steps.len());
|
||||
for (i, step) in steps.iter().enumerate() {
|
||||
let id = step.get("command").and_then(Value::as_str).ok_or_else(|| EngineError::Params(format!("step {}: no `command`", i + 1)))?;
|
||||
let params = step.get("params").cloned().unwrap_or(Value::Object(Default::default()));
|
||||
self.execute(id, ¶ms).map_err(|e| EngineError::Params(format!("step {} ({id}): {e}", i + 1)))?;
|
||||
outputs.push(self.execute(id, ¶ms).map_err(|e| EngineError::Params(format!("step {} ({id}): {e}", i + 1)))?);
|
||||
}
|
||||
Ok(())
|
||||
Ok(outputs)
|
||||
}
|
||||
|
||||
/// Identifies the current state (see [`Session::is_dirty`]).
|
||||
|
||||
+68
-78
@@ -19,10 +19,9 @@
|
||||
use modelcraft_model::{ElementKind, MAX_COORD};
|
||||
|
||||
use crate::quality::{Validity, element_validity};
|
||||
use crate::vec3::{P, cross, dot, norm, sub};
|
||||
use crate::{Error, Result};
|
||||
|
||||
type P = [f64; 3];
|
||||
|
||||
/// Most elements one block makes.
|
||||
pub const MAX_BLOCK_ELEMENTS: usize = 4_000_000;
|
||||
|
||||
@@ -88,21 +87,9 @@ impl Block {
|
||||
}
|
||||
}
|
||||
|
||||
fn sub(a: P, b: P) -> P {
|
||||
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
|
||||
}
|
||||
fn add_scaled(a: P, s: f64, b: P) -> P {
|
||||
[a[0] + s * b[0], a[1] + s * b[1], a[2] + s * b[2]]
|
||||
}
|
||||
fn dot(a: P, b: P) -> f64 {
|
||||
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
|
||||
}
|
||||
fn cross(a: P, b: P) -> P {
|
||||
[a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]
|
||||
}
|
||||
fn norm(a: P) -> f64 {
|
||||
dot(a, a).sqrt()
|
||||
}
|
||||
|
||||
/// An edge curve resolved against its end points: `point(s)` for `s` in `[0, 1]`.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
@@ -196,28 +183,38 @@ fn parameters(n: usize, bias: f64, order: usize) -> Vec<f64> {
|
||||
out
|
||||
}
|
||||
|
||||
/// The map from the unit cube onto the block.
|
||||
struct Map {
|
||||
/// The map from the unit cube onto the block, on the lattice of `params`: each edge curve is
|
||||
/// evaluated once per lattice parameter along its direction.
|
||||
struct Map<'a> {
|
||||
corners: [P; 8],
|
||||
edges: [Path; 12],
|
||||
params: &'a [Vec<f64>; 3],
|
||||
/// `edges[k][i]`: edge `k` at the `i`-th parameter of its direction (`k / 4`).
|
||||
edges: [Vec<P>; 12],
|
||||
}
|
||||
|
||||
impl Map {
|
||||
fn point(&self, u: f64, v: f64, w: f64) -> P {
|
||||
let e = |k: usize, s: f64| self.edges[k].point(s);
|
||||
impl<'a> Map<'a> {
|
||||
fn new(corners: [P; 8], paths: &[Path; 12], params: &'a [Vec<f64>; 3]) -> Map<'a> {
|
||||
let edges = std::array::from_fn(|k| params[k / 4].iter().map(|&s| paths[k].point(s)).collect());
|
||||
Map { corners, params, edges }
|
||||
}
|
||||
|
||||
/// The point at lattice coordinates `[i, j, k]`.
|
||||
fn point(&self, at: [usize; 3]) -> P {
|
||||
let [u, v, w] = [0, 1, 2].map(|d| self.params[d].get(at[d]).copied().unwrap_or(0.0));
|
||||
let e = |k: usize| self.edges[k].get(at[k / 4]).copied().unwrap_or([f64::NAN; 3]);
|
||||
let terms: [(f64, P); 12] = [
|
||||
((1.0 - v) * (1.0 - w), e(0, u)),
|
||||
(v * (1.0 - w), e(1, u)),
|
||||
((1.0 - v) * w, e(2, u)),
|
||||
(v * w, e(3, u)),
|
||||
((1.0 - u) * (1.0 - w), e(4, v)),
|
||||
(u * (1.0 - w), e(5, v)),
|
||||
((1.0 - u) * w, e(6, v)),
|
||||
(u * w, e(7, v)),
|
||||
((1.0 - u) * (1.0 - v), e(8, w)),
|
||||
(u * (1.0 - v), e(9, w)),
|
||||
(u * v, e(10, w)),
|
||||
((1.0 - u) * v, e(11, w)),
|
||||
((1.0 - v) * (1.0 - w), e(0)),
|
||||
(v * (1.0 - w), e(1)),
|
||||
((1.0 - v) * w, e(2)),
|
||||
(v * w, e(3)),
|
||||
((1.0 - u) * (1.0 - w), e(4)),
|
||||
(u * (1.0 - w), e(5)),
|
||||
((1.0 - u) * w, e(6)),
|
||||
(u * w, e(7)),
|
||||
((1.0 - u) * (1.0 - v), e(8)),
|
||||
(u * (1.0 - v), e(9)),
|
||||
(u * v, e(10)),
|
||||
((1.0 - u) * v, e(11)),
|
||||
];
|
||||
let mut x = [0.0; 3];
|
||||
for (f, p) in terms {
|
||||
@@ -234,22 +231,23 @@ impl Map {
|
||||
/// Lattice offsets of each element of one cell, in units of the lattice (two per cell for
|
||||
/// quadratic kinds), in the model's node order.
|
||||
fn cell_elements(kind: ElementKind) -> Vec<Vec<[usize; 3]>> {
|
||||
let scale = |c: [usize; 3], m: usize| [c[0] * m, c[1] * m, c[2] * m];
|
||||
match kind {
|
||||
ElementKind::Hex8 => vec![CORNERS.to_vec()],
|
||||
ElementKind::Hex20 => {
|
||||
let mut nodes: Vec<[usize; 3]> = CORNERS.iter().map(|c| scale(*c, 2)).collect();
|
||||
for edge in kind.edges() {
|
||||
if let (Some(&a), Some(&b)) = (edge.first(), edge.get(1))
|
||||
&& let (Some(pa), Some(pb)) = (CORNERS.get(a), CORNERS.get(b))
|
||||
{
|
||||
nodes.push([pa[0] + pb[0], pa[1] + pb[1], pa[2] + pb[2]]);
|
||||
}
|
||||
}
|
||||
vec![nodes]
|
||||
// Corners in units of the lattice, then (quadratic kinds) a mid-side node on each edge in
|
||||
// the kind's edge order.
|
||||
let nodes = |corners: &[[usize; 3]]| -> Vec<[usize; 3]> {
|
||||
if !kind.is_quadratic() {
|
||||
return corners.to_vec();
|
||||
}
|
||||
let mut out: Vec<[usize; 3]> = corners.iter().map(|c| c.map(|x| 2 * x)).collect();
|
||||
for edge in kind.edges() {
|
||||
if let (Some(a), Some(b)) = (edge.first().and_then(|&i| corners.get(i)), edge.get(1).and_then(|&i| corners.get(i))) {
|
||||
out.push([a[0] + b[0], a[1] + b[1], a[2] + b[2]]);
|
||||
}
|
||||
}
|
||||
out
|
||||
};
|
||||
match kind {
|
||||
ElementKind::Hex8 | ElementKind::Hex20 => vec![nodes(&CORNERS)],
|
||||
ElementKind::Tet4 | ElementKind::Tet10 => {
|
||||
let m = if kind == ElementKind::Tet10 { 2 } else { 1 };
|
||||
[[0, 1, 2], [0, 2, 1], [1, 0, 2], [1, 2, 0], [2, 0, 1], [2, 1, 0]]
|
||||
.iter()
|
||||
.map(|perm| {
|
||||
@@ -263,13 +261,7 @@ fn cell_elements(kind: ElementKind) -> Vec<Vec<[usize; 3]>> {
|
||||
if odd {
|
||||
v.swap(1, 2);
|
||||
}
|
||||
let mut nodes: Vec<[usize; 3]> = v.iter().map(|c| scale(*c, m)).collect();
|
||||
if m == 2 {
|
||||
for [a, b] in [[0, 1], [1, 2], [2, 0], [0, 3], [1, 3], [2, 3]] {
|
||||
nodes.push([v[a][0] + v[b][0], v[a][1] + v[b][1], v[a][2] + v[b][2]]);
|
||||
}
|
||||
}
|
||||
nodes
|
||||
nodes(&v)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -302,26 +294,22 @@ pub fn block(b: &Block) -> Result<BlockMesh> {
|
||||
Error::Limit(format!("a block makes at most {MAX_BLOCK_ELEMENTS} elements; {:?} divisions of {} are more", b.divisions, kind.name()))
|
||||
})?;
|
||||
|
||||
let mut curves = [Curve::Line; 12];
|
||||
let mut seen = [false; 12];
|
||||
let mut curves: [Option<Curve>; 12] = [None; 12];
|
||||
for (pair, curve) in &b.curves {
|
||||
let k = Block::edge_index(*pair)
|
||||
.ok_or_else(|| Error::Invalid(format!("corners {} and {} are not joined by a block edge", pair[0] + 1, pair[1] + 1)))?;
|
||||
if std::mem::replace(&mut seen[k], true) {
|
||||
if curves[k].replace(*curve).is_some() {
|
||||
return Err(Error::Invalid(format!("the edge from corner {} to {} is given twice", pair[0] + 1, pair[1] + 1)));
|
||||
}
|
||||
if let Some(slot) = curves.get_mut(k) {
|
||||
*slot = *curve;
|
||||
}
|
||||
}
|
||||
let mut paths = [Path::Line { from: [0.0; 3], to: [0.0; 3] }; 12];
|
||||
for (k, [from, to]) in EDGES.iter().enumerate() {
|
||||
paths[k] = Path::new(curves[k], b.corners[*from], b.corners[*to])?;
|
||||
paths[k] = Path::new(curves[k].unwrap_or(Curve::Line), b.corners[*from], b.corners[*to])?;
|
||||
}
|
||||
let map = Map { corners: b.corners, edges: paths };
|
||||
|
||||
let order = if kind.is_quadratic() { 2 } else { 1 };
|
||||
let params: [Vec<f64>; 3] = [0, 1, 2].map(|d| parameters(b.divisions[d], b.bias[d], order));
|
||||
let map = Map::new(b.corners, &paths, ¶ms);
|
||||
let size = [0, 1, 2].map(|d| params[d].len());
|
||||
let index = |c: [usize; 3]| c[0] + size[0] * (c[1] + size[1] * c[2]);
|
||||
|
||||
@@ -338,29 +326,28 @@ pub fn block(b: &Block) -> Result<BlockMesh> {
|
||||
}
|
||||
}
|
||||
}
|
||||
let total = size.iter().product::<usize>();
|
||||
let mut number: Vec<u32> = vec![u32::MAX; total];
|
||||
for e in &lattice {
|
||||
for c in e {
|
||||
if let Some(slot) = number.get_mut(index(*c)) {
|
||||
*slot = 0;
|
||||
}
|
||||
let mut used = vec![false; size.iter().product::<usize>()];
|
||||
for c in lattice.iter().flatten() {
|
||||
if let Some(slot) = used.get_mut(index(*c)) {
|
||||
*slot = true;
|
||||
}
|
||||
}
|
||||
let mut number: Vec<Option<u32>> = vec![None; used.len()];
|
||||
let mut points = Vec::new();
|
||||
for k in 0..size[2] {
|
||||
for j in 0..size[1] {
|
||||
for i in 0..size[0] {
|
||||
if let Some(slot) = number.get_mut(index([i, j, k]))
|
||||
&& *slot == 0
|
||||
let at = index([i, j, k]);
|
||||
if used.get(at) == Some(&true)
|
||||
&& let Some(slot) = number.get_mut(at)
|
||||
{
|
||||
*slot = u32::try_from(points.len()).map_err(|_| Error::Limit("too many points in one block".into()))?;
|
||||
points.push(map.point(params[0][i], params[1][j], params[2][k]));
|
||||
*slot = Some(u32::try_from(points.len()).map_err(|_| Error::Limit("too many points in one block".into()))?);
|
||||
points.push(map.point([i, j, k]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let point_of = |c: [usize; 3]| number.get(index(c)).copied().filter(|&n| n != u32::MAX);
|
||||
let point_of = |c: [usize; 3]| number.get(index(c)).copied().flatten();
|
||||
let last = [0, 1, 2].map(|d| size[d] - 1);
|
||||
let mut corners = [0u32; 8];
|
||||
for (slot, c) in corners.iter_mut().zip(CORNERS) {
|
||||
@@ -370,6 +357,11 @@ pub fn block(b: &Block) -> Result<BlockMesh> {
|
||||
// Block faces in hex8 face order: (direction, at the end?).
|
||||
const FACES: [(usize, bool); 6] = [(2, false), (2, true), (1, false), (0, true), (1, true), (0, false)];
|
||||
let on_face = |c: &[usize; 3], f: (usize, bool)| c[f.0] == if f.1 { last[f.0] } else { 0 };
|
||||
// Each block edge: the direction it runs along and the lattice coordinates fixed on it.
|
||||
let block_edges: [(usize, [usize; 3]); 12] = std::array::from_fn(|be| {
|
||||
let [from, _] = EDGES[be];
|
||||
(be / 4, [0, 1, 2].map(|d| CORNERS[from][d] * last[d]))
|
||||
});
|
||||
let corner_count = kind.corner_count();
|
||||
let mut elements = Vec::with_capacity(lattice.len());
|
||||
let mut faces = Vec::new();
|
||||
@@ -397,11 +389,9 @@ pub fn block(b: &Block) -> Result<BlockMesh> {
|
||||
}
|
||||
for (k, local) in kind.edges().iter().enumerate() {
|
||||
let (Some(a), Some(c)) = (local.first().and_then(|&i| nodes.get(i)), local.get(1).and_then(|&i| nodes.get(i))) else { continue };
|
||||
for (be, [from, to]) in EDGES.iter().enumerate() {
|
||||
// The block edge is where two of the three lattice coordinates are fixed.
|
||||
let along = (0..3).find(|&d| CORNERS[*from][d] != CORNERS[*to][d]).unwrap_or(0);
|
||||
let fixed = |p: &[usize; 3]| (0..3).filter(|&d| d != along).all(|d| p[d] == CORNERS[*from][d] * last[d]);
|
||||
if fixed(a) && fixed(c) {
|
||||
for (be, (along, fixed)) in block_edges.iter().enumerate() {
|
||||
let on = |p: &[usize; 3]| (0..3).filter(|d| d != along).all(|d| p[d] == fixed[d]);
|
||||
if on(a) && on(c) {
|
||||
edges.push((label, k as u8 + 1, be as u8));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
//! Joining new elements to the mesh already in a model: which new points coincide with existing
|
||||
//! nodes ([`weld`]), and whether the new elements then meet the mesh face to face
|
||||
//! ([`check_conforming`]). Mesh generators that add to a model (`mesh.block`) use both; node
|
||||
//! merging and orphan-mesh import will too.
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet, HashMap};
|
||||
|
||||
use modelcraft_model::{ElementKind, Model};
|
||||
|
||||
use crate::vec3::{P, dot, sub};
|
||||
use crate::{Error, Result};
|
||||
|
||||
/// A uniform grid of cells of side `size` over items with a bounding box.
|
||||
struct Grid<T> {
|
||||
size: f64,
|
||||
cells: HashMap<[i64; 3], Vec<T>>,
|
||||
}
|
||||
|
||||
impl<T: Copy> Grid<T> {
|
||||
fn new(size: f64) -> Grid<T> {
|
||||
Grid { size, cells: HashMap::new() }
|
||||
}
|
||||
|
||||
fn cell(&self, p: P) -> [i64; 3] {
|
||||
p.map(|x| (x / self.size).floor() as i64)
|
||||
}
|
||||
|
||||
/// Put `item` in every cell its box `[lo, hi]` overlaps.
|
||||
fn insert(&mut self, lo: P, hi: P, item: T) {
|
||||
let (a, b) = (self.cell(lo), self.cell(hi));
|
||||
for i in a[0]..=b[0] {
|
||||
for j in a[1]..=b[1] {
|
||||
for k in a[2]..=b[2] {
|
||||
self.cells.entry([i, j, k]).or_default().push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The items of the cell holding `p` and of the 26 around it.
|
||||
fn near(&self, p: P) -> impl Iterator<Item = T> + '_ {
|
||||
let c = self.cell(p);
|
||||
(-1..=1_i64).flat_map(move |dx| {
|
||||
(-1..=1_i64).flat_map(move |dy| {
|
||||
(-1..=1_i64).flat_map(move |dz| {
|
||||
let key = [c[0].saturating_add(dx), c[1].saturating_add(dy), c[2].saturating_add(dz)];
|
||||
self.cells.get(&key).into_iter().flatten().copied()
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn bounds(points: impl Iterator<Item = P>) -> Option<(P, P)> {
|
||||
points.fold(None, |b, p| match b {
|
||||
None => Some((p, p)),
|
||||
Some((lo, hi)) => Some(([0, 1, 2].map(|k| lo[k].min(p[k])), [0, 1, 2].map(|k| hi[k].max(p[k])))),
|
||||
})
|
||||
}
|
||||
|
||||
/// For each point, the model node within `tol` of it: the nearest, the lowest label on a tie.
|
||||
/// Two points may not weld to the same node. `tol` = 0 welds nothing.
|
||||
pub fn weld(model: &Model, points: &[P], tol: f64) -> Result<Vec<Option<u32>>> {
|
||||
if !(tol.is_finite() && tol >= 0.0) {
|
||||
return Err(Error::Invalid(format!("the weld distance must be finite and not negative, not {tol}")));
|
||||
}
|
||||
let Some((lo, hi)) = bounds(points.iter().copied()).filter(|_| tol > 0.0) else { return Ok(vec![None; points.len()]) };
|
||||
let inside = |p: P| (0..3).all(|k| p[k] >= lo[k] - tol && p[k] <= hi[k] + tol);
|
||||
let mut grid = Grid::new(tol);
|
||||
for (&label, node) in model.nodes().iter().filter(|(_, n)| inside(n.xyz)) {
|
||||
grid.insert(node.xyz, node.xyz, (label, node.xyz));
|
||||
}
|
||||
let mut taken: HashMap<u32, usize> = HashMap::new();
|
||||
points
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, &p)| {
|
||||
let best = grid
|
||||
.near(p)
|
||||
.map(|(label, xyz)| (dot(sub(xyz, p), sub(xyz, p)), label))
|
||||
.filter(|(d2, _)| *d2 <= tol * tol)
|
||||
.min_by(|a, b| a.0.total_cmp(&b.0).then(a.1.cmp(&b.1)))
|
||||
.map(|(_, label)| label);
|
||||
if let Some(label) = best
|
||||
&& let Some(other) = taken.insert(label, i)
|
||||
{
|
||||
return Err(Error::Invalid(format!("points {other} and {i} both lie within {tol} of node {label}: lower the distance or refine")));
|
||||
}
|
||||
Ok(best)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// A face or edge by its sorted node labels.
|
||||
fn key(nodes: impl Iterator<Item = u32>) -> Vec<u32> {
|
||||
let mut k: Vec<u32> = nodes.collect();
|
||||
k.sort_unstable();
|
||||
k
|
||||
}
|
||||
|
||||
/// The faces (by sorted corners, with their sorted nodes) and the corner edges of elements.
|
||||
struct Skin {
|
||||
faces: BTreeMap<Vec<u32>, Vec<u32>>,
|
||||
edges: BTreeSet<[u32; 2]>,
|
||||
}
|
||||
|
||||
impl Skin {
|
||||
/// Every face and corner edge of the elements; with `boundary`, only the faces that occur
|
||||
/// once, and the edges of those.
|
||||
fn of<'a>(elements: impl Iterator<Item = (ElementKind, &'a [u32])>, boundary: bool) -> Skin {
|
||||
let mut count: BTreeMap<Vec<u32>, (usize, Vec<u32>, Vec<u32>)> = BTreeMap::new();
|
||||
let mut all_edges = BTreeSet::new();
|
||||
for (kind, nodes) in elements {
|
||||
let corners = kind.corner_count();
|
||||
for local in kind.faces() {
|
||||
let cs: Vec<u32> = local.iter().filter(|&&i| i < corners).filter_map(|&i| nodes.get(i).copied()).collect();
|
||||
let entry = count.entry(key(cs.iter().copied())).or_insert_with(|| (0, key(local.iter().filter_map(|&i| nodes.get(i).copied())), cs));
|
||||
entry.0 += 1;
|
||||
}
|
||||
if !boundary {
|
||||
for local in kind.edges() {
|
||||
if let (Some(&a), Some(&b)) = (local.first().and_then(|&i| nodes.get(i)), local.get(1).and_then(|&i| nodes.get(i))) {
|
||||
all_edges.insert([a.min(b), a.max(b)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut faces = BTreeMap::new();
|
||||
let mut edges = all_edges;
|
||||
for (corners, (n, nodes, cycle)) in count {
|
||||
if boundary && n != 1 {
|
||||
continue;
|
||||
}
|
||||
if boundary {
|
||||
for (i, &a) in cycle.iter().enumerate() {
|
||||
let b = cycle.get((i + 1) % cycle.len()).copied().unwrap_or(a);
|
||||
edges.insert([a.min(b), a.max(b)]);
|
||||
}
|
||||
}
|
||||
faces.insert(corners, nodes);
|
||||
}
|
||||
Skin { faces, edges }
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether new elements (their nodes already in the model) meet the model's elements face to
|
||||
/// face where they share the nodes in `joined`: an element face of either side whose corners are
|
||||
/// all shared must be a face of the other side with the same nodes (no mixed linear and
|
||||
/// quadratic sides), and no node of the new boundary may lie on an edge of the old elements
|
||||
/// within `tol`, nor the other way round (no hanging nodes where divisions differ). Edges are
|
||||
/// tested as straight segments, so along a curved shared edge only the face test applies.
|
||||
pub fn check_conforming(model: &Model, new: &[(ElementKind, Vec<u32>)], joined: &BTreeSet<u32>, tol: f64) -> Result<()> {
|
||||
if joined.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let refuse =
|
||||
|what: String| Err(Error::Invalid(format!("{what}: new elements must meet the mesh face to face, with the same divisions and element kind")));
|
||||
let new_skin = Skin::of(new.iter().map(|(k, n)| (*k, n.as_slice())), true);
|
||||
let touching = model.elements().values().filter(|e| e.nodes.iter().any(|n| joined.contains(n)));
|
||||
let old_skin = Skin::of(touching.map(|e| (e.kind, e.nodes.as_slice())), false);
|
||||
for (mine, theirs, what) in [(&new_skin, &old_skin, "a new face"), (&old_skin, &new_skin, "an element face")] {
|
||||
for (corners, nodes) in &mine.faces {
|
||||
if corners.iter().all(|n| joined.contains(n)) && theirs.faces.get(corners) != Some(nodes) {
|
||||
return refuse(format!("{what} on nodes {corners:?} lies on shared nodes but the other side has no such face"));
|
||||
}
|
||||
}
|
||||
}
|
||||
let xyz = |label: u32| model.node(label).map(|n| n.xyz);
|
||||
for (nodes, edges) in [(&new_skin, &old_skin), (&old_skin, &new_skin)] {
|
||||
let segments: Vec<([u32; 2], P, P)> = edges.edges.iter().filter_map(|&[a, b]| Some(([a, b], xyz(a)?, xyz(b)?))).collect();
|
||||
let Some(longest) = segments.iter().map(|(_, a, b)| dot(sub(*b, *a), sub(*b, *a)).sqrt()).reduce(f64::max).filter(|l| *l > 0.0) else {
|
||||
continue;
|
||||
};
|
||||
let mut grid = Grid::new(longest + 2.0 * tol);
|
||||
for (i, (_, a, b)) in segments.iter().enumerate() {
|
||||
grid.insert([0, 1, 2].map(|k| a[k].min(b[k]) - tol), [0, 1, 2].map(|k| a[k].max(b[k]) + tol), i);
|
||||
}
|
||||
let candidates: BTreeSet<u32> = nodes.faces.values().flatten().copied().filter(|n| !joined.contains(n)).collect();
|
||||
for n in candidates {
|
||||
let Some(p) = xyz(n) else { continue };
|
||||
for i in grid.near(p) {
|
||||
let Some(&([a, b], xa, xb)) = segments.get(i) else { continue };
|
||||
let (ab, ap) = (sub(xb, xa), sub(p, xa));
|
||||
let len2 = dot(ab, ab);
|
||||
let t = dot(ap, ab) / len2;
|
||||
let d = sub(ap, ab.map(|x| t * x));
|
||||
let margin = tol / len2.sqrt();
|
||||
if t > margin && t < 1.0 - margin && dot(d, d) <= tol * tol {
|
||||
return refuse(format!("node {n} at {p:?} lies on the edge between nodes {a} and {b}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -10,6 +10,7 @@
|
||||
pub mod block;
|
||||
pub mod cdt;
|
||||
pub mod delaunay;
|
||||
pub mod join;
|
||||
pub mod planar;
|
||||
pub mod quad;
|
||||
pub mod quality;
|
||||
|
||||
@@ -167,7 +167,7 @@ pub fn corpus_dir() -> PathBuf {
|
||||
workspace_root().join("tests").join("corpus")
|
||||
}
|
||||
|
||||
fn read(path: &Path) -> Result<String> {
|
||||
pub(crate) fn read(path: &Path) -> Result<String> {
|
||||
let io = |e: std::io::Error| Error::Io(format!("{}: {e}", path.display()));
|
||||
let len = std::fs::metadata(path).map_err(io)?.len();
|
||||
if len > MAX_FILE_BYTES {
|
||||
|
||||
@@ -24,9 +24,6 @@ use std::time::Instant;
|
||||
use modelcraft_engine::Session;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
/// Largest case file read.
|
||||
const MAX_FILE_BYTES: u64 = 16 << 20;
|
||||
|
||||
/// Fixture band: a result may move by this fraction of its size (and a thousandth of the
|
||||
/// reference's absolute tolerance) before CI calls it a change. Results are deterministic on one
|
||||
/// platform; the band only absorbs last-digit differences of the system maths library.
|
||||
@@ -187,11 +184,7 @@ pub fn fixture_band(value: f64, reference: f64, tolerance: Tolerance) -> f64 {
|
||||
}
|
||||
|
||||
fn read_json(path: &Path) -> Result<Value, String> {
|
||||
let size = std::fs::metadata(path).map_err(|e| format!("{}: {e}", path.display()))?.len();
|
||||
if size > MAX_FILE_BYTES {
|
||||
return Err(format!("{}: larger than {MAX_FILE_BYTES} bytes", path.display()));
|
||||
}
|
||||
let text = std::fs::read_to_string(path).map_err(|e| format!("{}: {e}", path.display()))?;
|
||||
let text = crate::corpus::read(path).map_err(|e| e.to_string())?;
|
||||
serde_json::from_str(&text).map_err(|e| format!("{}: {e}", path.display()))
|
||||
}
|
||||
|
||||
@@ -215,19 +208,13 @@ fn read_fixture(path: &Path) -> Result<Option<BTreeMap<String, Frozen>>, String>
|
||||
.map(Some)
|
||||
}
|
||||
|
||||
/// The result of measuring one quantity.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Measured {
|
||||
pub id: String,
|
||||
pub value: Result<f64, String>,
|
||||
}
|
||||
|
||||
/// Run a case's recipe and measure its quantities. A failing command fails every quantity.
|
||||
pub fn run_case(dir: &Path, expected: &Expected) -> Result<Vec<Measured>, String> {
|
||||
/// Run a case's recipe and measure its quantities, in order. A failing command fails every
|
||||
/// quantity.
|
||||
pub fn run_case(dir: &Path, expected: &Expected) -> Result<Vec<Result<f64, String>>, String> {
|
||||
let mut session = Session::default();
|
||||
let design = dir.join("design.json");
|
||||
if design.is_file() {
|
||||
let script = std::fs::read_to_string(&design).map_err(|e| format!("{}: {e}", design.display()))?;
|
||||
let script = crate::corpus::read(&design).map_err(|e| e.to_string())?;
|
||||
let bytes = modelcraft_geometry::script_design(&script).map_err(|e| format!("design.json: {e}"))?;
|
||||
let opened = modelcraft_geometry::open_design(&bytes).map_err(|e| format!("design.json: {e}"))?;
|
||||
if let Some(e) = opened.errors.first() {
|
||||
@@ -236,17 +223,10 @@ pub fn run_case(dir: &Path, expected: &Expected) -> Result<Vec<Measured>, String
|
||||
session.design = Some(Arc::new(opened));
|
||||
}
|
||||
let recipe = read_json(&dir.join("recipe.json"))?;
|
||||
let steps = recipe.as_array().ok_or("recipe.json must be a JSON array of steps")?;
|
||||
let mut outputs: BTreeMap<String, Value> = BTreeMap::new();
|
||||
for (i, step) in steps.iter().enumerate() {
|
||||
let command = step.get("command").and_then(Value::as_str).ok_or_else(|| format!("recipe step {}: no `command`", i + 1))?;
|
||||
let params = step.get("params").cloned().unwrap_or_else(|| json!({}));
|
||||
let out = session.execute(command, ¶ms).map_err(|e| format!("recipe step {} ({command}): {e}", i + 1))?;
|
||||
if let Some(id) = step.get("id").and_then(Value::as_str) {
|
||||
outputs.insert(id.to_string(), out);
|
||||
}
|
||||
}
|
||||
Ok(expected.quantities.iter().map(|q| Measured { id: q.id.clone(), value: measure(&mut session, &outputs, &q.measure) }).collect())
|
||||
let results = session.run_script(&recipe).map_err(|e| format!("recipe.json: {e}"))?;
|
||||
let outputs: BTreeMap<String, Value> =
|
||||
recipe.as_array().into_iter().flatten().zip(results).filter_map(|(step, out)| Some((step.get("id")?.as_str()?.to_string(), out))).collect();
|
||||
Ok(expected.quantities.iter().map(|q| measure(&mut session, &outputs, &q.measure)).collect())
|
||||
}
|
||||
|
||||
/// Read one number: from a recipe step's output (`"from": id`) or a command run now
|
||||
@@ -368,27 +348,27 @@ pub fn run(root: &Path, options: &Options) -> Result<(), String> {
|
||||
skipped.push(case);
|
||||
continue;
|
||||
}
|
||||
let measured: Vec<Measured> = if expected.pending.is_some() {
|
||||
expected.quantities.iter().map(|q| Measured { id: q.id.clone(), value: Err("pending".into()) }).collect()
|
||||
} else {
|
||||
let start = Instant::now();
|
||||
let m = run_case(dir, &expected)
|
||||
.unwrap_or_else(|e| expected.quantities.iter().map(|q| Measured { id: q.id.clone(), value: Err(e.clone()) }).collect());
|
||||
eprintln!("oracle: {case} ({:.2} s)", start.elapsed().as_secs_f64());
|
||||
m
|
||||
};
|
||||
let fixture_path = dir.join("fixture.json");
|
||||
let fixture = read_fixture(&fixture_path)?;
|
||||
let mut case_rows: Vec<Row> = if expected.pending.is_some() {
|
||||
let row =
|
||||
|q: &Quantity| Row { case: case.clone(), quantity: q.clone(), ours: None, verdict: Verdict::Pending, fixture: FixtureCheck::NotKept };
|
||||
expected.quantities.iter().map(row).collect()
|
||||
} else {
|
||||
let start = Instant::now();
|
||||
let measured = run_case(dir, &expected).unwrap_or_else(|e| vec![Err(e); expected.quantities.len()]);
|
||||
eprintln!("oracle: {case} ({:.2} s)", start.elapsed().as_secs_f64());
|
||||
expected
|
||||
.quantities
|
||||
.iter()
|
||||
.zip(&measured)
|
||||
.map(|(q, m)| {
|
||||
let (verdict, fixture) = judge(q, m, fixture.as_ref().and_then(|f| f.get(&q.id)));
|
||||
Row { case: case.clone(), quantity: q.clone(), ours: m.as_ref().ok().copied(), verdict, fixture }
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
let bless = options.bless.iter().any(|b| *b == case || b == "all");
|
||||
let mut case_rows = Vec::new();
|
||||
for (q, m) in expected.quantities.iter().zip(&measured) {
|
||||
let (verdict, check) = if expected.pending.is_some() {
|
||||
(Verdict::Pending, FixtureCheck::NotKept)
|
||||
} else {
|
||||
judge(q, &m.value, fixture.as_ref().and_then(|f| f.get(&q.id)))
|
||||
};
|
||||
case_rows.push(Row { case: case.clone(), quantity: q.clone(), ours: m.value.as_ref().ok().copied(), verdict, fixture: check });
|
||||
}
|
||||
if bless && expected.pending.is_none() {
|
||||
let passing = case_rows.iter().all(|r| matches!(r.verdict, Verdict::Pass | Verdict::Known));
|
||||
if !passing {
|
||||
@@ -508,14 +488,14 @@ fn error_of(r: &Row) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
fn verdict_text(v: &Verdict) -> String {
|
||||
fn verdict_text(v: &Verdict) -> &'static str {
|
||||
match v {
|
||||
Verdict::Pass => "pass".into(),
|
||||
Verdict::Fail => "**FAIL**".into(),
|
||||
Verdict::Known => "known failure".into(),
|
||||
Verdict::Fixed => "**passes: remove known_failure**".into(),
|
||||
Verdict::Error(_) => "**ERROR**".into(),
|
||||
Verdict::Pending => "pending".into(),
|
||||
Verdict::Pass => "pass",
|
||||
Verdict::Fail => "**FAIL**",
|
||||
Verdict::Known => "known failure",
|
||||
Verdict::Fixed => "**passes: remove known_failure**",
|
||||
Verdict::Error(_) => "**ERROR**",
|
||||
Verdict::Pending => "pending",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -530,7 +510,7 @@ fn fixture_text(f: &FixtureCheck) -> &'static str {
|
||||
}
|
||||
|
||||
fn cell(s: &str) -> String {
|
||||
s.replace('|', "/").replace('\n', " ")
|
||||
s.replace('|', "\\|").replace('\n', " ")
|
||||
}
|
||||
|
||||
/// docs/oracle.md.
|
||||
@@ -573,7 +553,7 @@ benchmark, {} experiment; {} pending cases wait for capabilities still being bui
|
||||
);
|
||||
for (case, _) in &in_kind {
|
||||
for r in rows.iter().filter(|r| &r.case == case) {
|
||||
let mut result = verdict_text(&r.verdict);
|
||||
let mut result = verdict_text(&r.verdict).to_string();
|
||||
if let Some(k) = &r.quantity.known_failure {
|
||||
result = format!("{result}: {k}");
|
||||
}
|
||||
|
||||
@@ -253,7 +253,7 @@ impl ModelApp {
|
||||
|
||||
/// Run a script (a JSON array of `{command, params}`), stopping at the first error.
|
||||
pub fn run_script(&mut self, script: &Value) -> Result<(), String> {
|
||||
let r = self.session.run_script(script).map_err(|e| e.to_string());
|
||||
let r = self.session.run_script(script).map(|_| ()).map_err(|e| e.to_string());
|
||||
self.model_changed();
|
||||
match &r {
|
||||
Ok(()) => self.set_status("Script done", false),
|
||||
|
||||
Reference in new issue
Block a user