forked from 1b-agent/fieldcraft-upstream
mesh: the projection mesher takes fixed boundary patches as given (#490)
A patch of quads already meshed on a face of the body (a neighbour's mesh) is matched to the block faces on it: block vertices to its nodes (its singular nodes first, then along its grid lines), block edges along its grid lines, faces filled quad by quad. Those faces take the patch's divisions and nodes, held through smoothing. mesh.hex.project takes "fixed": [face names] and builds the patch from other bodies' meshes. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
1 parent
e887e3a8e5
commit
6f07e4c09c
7 files changed
+650
-6
No files matched your search
@@ -1193,6 +1193,9 @@ pub const HEX_PROJECT: &[Field] = &[
|
||||
json("operations", "Operations").help("O-grids, extrusions, connections, removals and merges applied in order, as JSON.").hint(r#"[{"op": "o_grid", "block": 0, "open": ["+z"]}]"#),
|
||||
json("faces", "Face targets").help("Block faces sent to chosen B-rep faces; other outer faces go to the nearest surfaces.").hint(r#"[{"block": 0, "face": "+x", "to": ["F1:+x"]}]"#),
|
||||
json("divisions", "Divisions").help("Element counts per block direction.").hint(r#"[{"block": 0, "direction": 1, "count": 8}]"#),
|
||||
json("fixed", "Fixed faces")
|
||||
.help("Faces of the body another body's mesh already covers: its element faces there are kept as they are and this mesh meets them node to node.")
|
||||
.hint(r#"["F3:partition:P:b"]"#),
|
||||
choice("smoothing", "Smoothing", &[("elliptic", "Elliptic"), ("elliptic-controlled", "Elliptic, controlled (Thomas–Middlecoff)"), ("laplace", "Laplace"), ("none", "None")])
|
||||
.help("How interior nodes are smoothed after projection, with the boundary held on the geometry.")
|
||||
.def("\"elliptic\""),
|
||||
|
||||
@@ -6,7 +6,7 @@ use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use fieldcraft_mesh::hex::BoundaryFace;
|
||||
use fieldcraft_mesh::join::{check_conforming, weld};
|
||||
use fieldcraft_mesh::project::{ProjectOptions, Smoothing, Topology, project_mesh};
|
||||
use fieldcraft_mesh::project::{FixedPatch, ProjectOptions, Smoothing, Topology, project_mesh};
|
||||
use fieldcraft_mesh::surface::BoundarySurface;
|
||||
use fieldcraft_mesh::tet::geometry::Tessellation;
|
||||
use fieldcraft_model::{ElementKind, GeometryRef};
|
||||
@@ -19,7 +19,7 @@ use crate::{CommandKind, CommandSpec, EngineError, Result, Session};
|
||||
pub const HEX_PROJECT: CommandSpec = CommandSpec {
|
||||
id: "mesh.hex.project",
|
||||
label: "Projection Blocking",
|
||||
params: r#"{"body": "Body1", "size": 2.0 (element size, mm; default 1/25 of the body's diagonal), "template": "grid" (default) | "sphere" | "butterfly" | "blocks", "center": [x, y, z], "radius": r, "axis": [0, 0, 1], "levels": [z0, z1, …] (sphere and butterfly: default from the body's bounding box; butterfly levels are distances along the axis from the centre), "inset": 0.55 (inner square or cube corner at this fraction of the radius), "grid": {"planes": {"x": [...], "y": [...], "z": [...]} or "cells": [ni, nj, nk] (even planes over the bounding box, default [1, 1, 1]), "carve": [{"i": [a, b], "j": [c, d], "k": [e, f]}] (block index ranges, inclusive, removed)}, "blocks": {"vertices": [[x, y, z], ...], "blocks": [[v1..v8], ...] (0-based vertices, hex8 order)}, "operations": [{"op": "o_grid", "block": b, "open": [faces], "inset": 0.5 or [i1, i2, i3], "extrude": {"offset": [dx, dy, dz], "layers": 1, "faces": "all" (the open face's butterfly: a round branch) | "inner" (its inner face: a square stub)}} | {"op": "extrude", "faces": [[block, face], ...], "offset": [dx, dy, dz], "layers": 1} | {"op": "connect", "from": [block, face], "to": [block, face], "layers": 1} (blocks bridging two faces: closes a handle) | {"op": "remove", "blocks": [...]} (carve; later blocks renumber) | {"op": "merge", "distance": d} | {"op": "split", "block": b, "direction": 1|2|3 or "x"|"y"|"z", "at": [fractions]} (cuts the block and the whole sheet of blocks the cut runs through, so the topology stays conforming; fractions from the block's side-0 face, or from the low end along an axis; the block keeps its first slab, the others are appended)] (applied in order; blocks are 0-based, new blocks appended, the O-grid's inner block keeping the old index; a face is its hex8 number 1-6 or the direction it faces, "+x" … "-z"), "preview": true (return the topology without meshing), "faces": [{"block": b, "face": 1-6, "to": ["face names"]}] (targets set by hand; other outer block faces go to the smooth group of B-rep faces nearest them), "divisions": [{"block": b, "direction": 1|2|3, "count": n}], "smoothing": "elliptic" (default) | "elliptic-controlled" (Thomas–Middlecoff) | "laplace" | "none", "iterations": 100, "min_quality": 0.2 (smallest scaled Jacobian accepted), "max_arc_angle": 30 (degrees: the most a curved face may turn across one element; the divisions grow to keep it, 0 for no limit, at most 90), "merge": distance (join nodes of other bodies' meshes within it; default 1e-6 of the diagonal, 0 never), "tolerance": chordal tolerance of the tessellation} — replaces the body's mesh with an all-hex mesh of the block topology: outer block faces projected onto their B-rep faces, edges between faces with different targets onto the curve where those meet (tangent-plane iteration), vertices on three onto their point, nodes placed by transfinite interpolation and smoothed with the boundary held on the geometry. Elements are tagged with the body and boundary faces with their B-rep faces. Reports blocks, divisions, targets and quality before and after smoothing; a corner that flattens on a curved face is an error that says to use an O-grid or template there"#,
|
||||
params: r#"{"body": "Body1", "size": 2.0 (element size, mm; default 1/25 of the body's diagonal), "template": "grid" (default) | "sphere" | "butterfly" | "blocks", "center": [x, y, z], "radius": r, "axis": [0, 0, 1], "levels": [z0, z1, …] (sphere and butterfly: default from the body's bounding box; butterfly levels are distances along the axis from the centre), "inset": 0.55 (inner square or cube corner at this fraction of the radius), "grid": {"planes": {"x": [...], "y": [...], "z": [...]} or "cells": [ni, nj, nk] (even planes over the bounding box, default [1, 1, 1]), "carve": [{"i": [a, b], "j": [c, d], "k": [e, f]}] (block index ranges, inclusive, removed)}, "blocks": {"vertices": [[x, y, z], ...], "blocks": [[v1..v8], ...] (0-based vertices, hex8 order)}, "operations": [{"op": "o_grid", "block": b, "open": [faces], "inset": 0.5 or [i1, i2, i3], "extrude": {"offset": [dx, dy, dz], "layers": 1, "faces": "all" (the open face's butterfly: a round branch) | "inner" (its inner face: a square stub)}} | {"op": "extrude", "faces": [[block, face], ...], "offset": [dx, dy, dz], "layers": 1} | {"op": "connect", "from": [block, face], "to": [block, face], "layers": 1} (blocks bridging two faces: closes a handle) | {"op": "remove", "blocks": [...]} (carve; later blocks renumber) | {"op": "merge", "distance": d} | {"op": "split", "block": b, "direction": 1|2|3 or "x"|"y"|"z", "at": [fractions]} (cuts the block and the whole sheet of blocks the cut runs through, so the topology stays conforming; fractions from the block's side-0 face, or from the low end along an axis; the block keeps its first slab, the others are appended)] (applied in order; blocks are 0-based, new blocks appended, the O-grid's inner block keeping the old index; a face is its hex8 number 1-6 or the direction it faces, "+x" … "-z"), "preview": true (return the topology without meshing), "faces": [{"block": b, "face": 1-6, "to": ["face names"]}] (targets set by hand; other outer block faces go to the smooth group of B-rep faces nearest them), "divisions": [{"block": b, "direction": 1|2|3, "count": n}], "smoothing": "elliptic" (default) | "elliptic-controlled" (Thomas–Middlecoff) | "laplace" | "none", "iterations": 100, "min_quality": 0.2 (smallest scaled Jacobian accepted), "max_arc_angle": 30 (degrees: the most a curved face may turn across one element; the divisions grow to keep it, 0 for no limit, at most 90), "fixed": ["face names"] (faces of the body another body's mesh already covers: its element faces there are taken as given, their nodes held and the block divisions there taken from them, so the two meshes meet node to node), "merge": distance (join nodes of other bodies' meshes within it; default 1e-6 of the diagonal, 0 never), "tolerance": chordal tolerance of the tessellation} — replaces the body's mesh with an all-hex mesh of the block topology: outer block faces projected onto their B-rep faces, edges between faces with different targets onto the curve where those meet (tangent-plane iteration), vertices on three onto their point, nodes placed by transfinite interpolation and smoothed with the boundary held on the geometry. Elements are tagged with the body and boundary faces with their B-rep faces. Reports blocks, divisions, targets and quality before and after smoothing; a corner that flattens on a curved face is an error that says to use an O-grid or template there"#,
|
||||
form: Some(super::forms::HEX_PROJECT),
|
||||
kind: CommandKind::Model,
|
||||
requires: &[Requirement::Geometry],
|
||||
@@ -242,6 +242,8 @@ struct ProjectParams {
|
||||
min_quality: Option<f64>,
|
||||
max_arc_angle: Option<f64>,
|
||||
merge: Option<f64>,
|
||||
#[serde(default)]
|
||||
fixed: Vec<String>,
|
||||
// Read from the raw parameters by the surface preparation (mesh.tet.generate's options).
|
||||
#[allow(dead_code)]
|
||||
weld: Option<f64>,
|
||||
@@ -475,6 +477,7 @@ fn hex_project(s: &mut Session, raw: &Value) -> Result<Value> {
|
||||
bounded(&p.operations, "operations")?;
|
||||
bounded(&p.faces, "faces")?;
|
||||
bounded(&p.divisions, "divisions")?;
|
||||
bounded(&p.fixed, "fixed")?;
|
||||
let b = open_body(s, &p.body, p.tolerance, raw)?;
|
||||
let diag = b.input.diagonal;
|
||||
let size = element_size(p.size, diag)?;
|
||||
@@ -514,6 +517,7 @@ fn hex_project(s: &mut Session, raw: &Value) -> Result<Value> {
|
||||
if !(0.0..=90.0).contains(&max_arc_angle) {
|
||||
return Err(EngineError::invalid("max_arc_angle", "from 0 to 90 degrees"));
|
||||
}
|
||||
let fixed = if p.fixed.is_empty() { Vec::new() } else { vec![fixed_patch(s, &b, &p.body, &p.fixed, size)?] };
|
||||
let opts = ProjectOptions {
|
||||
size,
|
||||
max_arc_angle: max_arc_angle.to_radians(),
|
||||
@@ -523,11 +527,63 @@ fn hex_project(s: &mut Session, raw: &Value) -> Result<Value> {
|
||||
iterations,
|
||||
max_elements: fieldcraft_model::MAX_ELEMENTS / 8,
|
||||
min_quality,
|
||||
fixed,
|
||||
};
|
||||
let merge = merge_distance(p.merge, diag)?;
|
||||
project_body(s, &p.body, b, &t, &opts, merge)
|
||||
}
|
||||
|
||||
/// Fraction of the element size within which a node of another mesh lies on a face.
|
||||
const ON_FACE: f64 = 0.05;
|
||||
|
||||
/// The quads other bodies' meshes have on faces `names` of body `body` (opened as `b`), as one
|
||||
/// fixed patch: the element faces of other bodies, tagged as on their boundary, whose nodes all
|
||||
/// lie on one of those faces (within [`ON_FACE`] of the element size `size`).
|
||||
pub(crate) fn fixed_patch(s: &Session, b: &Body, body: &str, names: &[String], size: f64) -> Result<FixedPatch> {
|
||||
use fieldcraft_mesh::project::Geometry;
|
||||
let faces: Vec<u32> = names.iter().map(|n| face_index(b, body, n)).collect::<Result<_>>()?;
|
||||
let tol = (ON_FACE * size).max(1e-9 * b.diagonal());
|
||||
let c = s.model.classification();
|
||||
let mut on: BTreeMap<u32, Option<usize>> = BTreeMap::new();
|
||||
let mut index: BTreeMap<u32, u32> = BTreeMap::new();
|
||||
let mut seen: BTreeSet<[u32; 4]> = BTreeSet::new();
|
||||
let mut patch = FixedPatch { faces: faces.clone(), ..FixedPatch::default() };
|
||||
for (&element, tags) in c.elements() {
|
||||
let theirs = tags.region.and_then(|r| c.entity(r)).is_some_and(|g| g.body != body);
|
||||
if !theirs {
|
||||
continue;
|
||||
}
|
||||
for &face in tags.faces.keys() {
|
||||
let Some(labels) = s.model.face_nodes(fieldcraft_model::ElementFace { element, face }) else { continue };
|
||||
let Ok(quad) = <[u32; 4]>::try_from(labels) else { continue };
|
||||
let mut lies = true;
|
||||
for l in quad {
|
||||
let hit = *on.entry(l).or_insert_with(|| {
|
||||
let x = s.model.node(l)?.xyz;
|
||||
faces.iter().position(|&f| b.target.project(f, x).is_some_and(|q| (0..3).map(|k| (q[k] - x[k]).powi(2)).sum::<f64>().sqrt() <= tol))
|
||||
});
|
||||
lies &= hit.is_some();
|
||||
}
|
||||
let mut key = quad;
|
||||
key.sort_unstable();
|
||||
if !lies || !seen.insert(key) {
|
||||
continue;
|
||||
}
|
||||
let q = quad.map(|l| {
|
||||
*index.entry(l).or_insert_with(|| {
|
||||
patch.nodes.push(s.model.node(l).map_or([0.0; 3], |n| n.xyz));
|
||||
patch.nodes.len() as u32 - 1
|
||||
})
|
||||
});
|
||||
patch.quads.push(q);
|
||||
}
|
||||
}
|
||||
if patch.quads.is_empty() {
|
||||
return Err(EngineError::Params(format!("`fixed`: no other body's mesh has element faces on faces {names:?} of `{body}`; mesh the neighbour first")));
|
||||
}
|
||||
Ok(patch)
|
||||
}
|
||||
|
||||
/// Mesh body `name` (opened as `b`) with the block topology `t` projected onto it, replace its
|
||||
/// mesh, and reply what `mesh.hex.project` replies. The plan's projection mesher (plan.rs) takes
|
||||
/// the same path.
|
||||
@@ -547,6 +603,7 @@ pub(crate) fn project_body(s: &mut Session, name: &str, b: Body, t: &Topology, o
|
||||
"min_scaled_jacobian": round3(r.min_scaled_jacobian),
|
||||
"mean_scaled_jacobian": round3(r.mean_scaled_jacobian),
|
||||
"smoothing": {"iterations": r.smoothing.iterations, "refused": r.smoothing.refused},
|
||||
"fixed_nodes": r.fixed_nodes,
|
||||
"max_gap": r.max_gap,
|
||||
"volume": r.volume,
|
||||
"body_volume": r.surface_volume,
|
||||
|
||||
@@ -256,3 +256,43 @@ fn thin_enclosure_layers() {
|
||||
assert!(sj(&r) > 0.99, "{r}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Two discs stacked as two bodies: the lower meshed fine, the upper meshed against it with its
|
||||
/// bottom face fixed (#490) takes the lower's top quads as given — the same divisions though its
|
||||
/// own size is coarser, its nodes there the lower's (joined, not copied), all hex and valid.
|
||||
#[test]
|
||||
fn a_fixed_face_takes_the_neighbours_mesh_as_given() {
|
||||
let (_d, mut s) = session(json!([
|
||||
{"command": "solid.cylinder", "params": {"radius": 10, "height": 2, "body_name": "Low"}},
|
||||
{"command": "solid.cylinder", "params": {"base": [0, 0, 2], "radius": 10, "height": 2, "body_name": "High"}}
|
||||
]));
|
||||
s.execute("mesh.hex.project", &json!({"body": "Low", "template": "butterfly", "size": 0.75})).unwrap();
|
||||
let nodes_before = s.model.nodes().len();
|
||||
let top_quads = |s: &Session, body: &str, z: f64| {
|
||||
let c = s.model.classification();
|
||||
s.model
|
||||
.body_elements(body)
|
||||
.into_iter()
|
||||
.flat_map(|e| c.elements().get(&e).map(|t| t.faces.keys().map(|&f| (e, f)).collect::<Vec<_>>()).unwrap_or_default())
|
||||
.filter(|&(element, face)| {
|
||||
s.model.face_nodes(fieldcraft_model::ElementFace { element, face }).is_some_and(|ns| ns.iter().all(|n| (s.model.node(*n).unwrap().xyz[2] - z).abs() < 1e-9))
|
||||
})
|
||||
.count()
|
||||
};
|
||||
let shared = top_quads(&s, "Low", 2.0);
|
||||
let r = s.execute("mesh.hex.project", &json!({"body": "High", "template": "butterfly", "size": 1.5, "fixed": ["F2:bottom"]})).unwrap();
|
||||
assert!(sj(&r) >= 0.5, "{r}");
|
||||
// Every node of the lower's top joined: the upper adds only its own.
|
||||
let on_face = r["fixed_nodes"].as_u64().unwrap() as usize;
|
||||
assert!(on_face > 0 && r["joined"].as_u64().unwrap() as usize == on_face, "{r}");
|
||||
assert_eq!(s.model.nodes().len(), nodes_before + r["nodes"].as_u64().unwrap() as usize);
|
||||
assert_eq!(top_quads(&s, "High", 4.0), shared, "{r}");
|
||||
let q = s.execute("mesh.quality", &json!({})).unwrap();
|
||||
assert_eq!(q["valid"].as_u64(), Some(s.model.elements().len() as u64), "{q}");
|
||||
// A face no other mesh covers, or not the body's, is an error that changes nothing.
|
||||
let before = s.model.clone();
|
||||
for bad in [json!(["F2:top"]), json!(["nope"])] {
|
||||
assert!(s.execute("mesh.hex.project", &json!({"body": "High", "template": "butterfly", "size": 1.5, "fixed": bad})).is_err(), "{bad}");
|
||||
}
|
||||
assert!(s.model == before);
|
||||
}
|
||||
@@ -0,0 +1,422 @@
|
||||
//! Fixed boundary: an already-meshed patch of the body's surface taken as given.
|
||||
//!
|
||||
//! A neighbour meshed first (a corner cell mirrored onto its image, a cell next to an existing
|
||||
//! mesh) leaves quads on the face the two bodies share. The projection mesh of the second body
|
||||
//! keeps them: the block faces on that face take their divisions from the patch, their nodes are
|
||||
//! the patch's nodes (held through smoothing), and the two meshes meet node to node.
|
||||
//!
|
||||
//! The match is topological. Each block vertex on the patch goes to the nearest patch node; each
|
||||
//! block edge on it must run along one grid line of the patch from its first vertex's node to its
|
||||
//! second's (a straight walk: at every node the line goes on through the one neighbour that shares
|
||||
//! no quad with the edge it came in on); each block face is then filled quad by quad from two of
|
||||
//! its edges and must close on the other two. Every patch quad belongs to exactly one block face.
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet, HashMap};
|
||||
|
||||
use super::lattice::face_key;
|
||||
use super::topology::{CORNERS, Topology, corner_at};
|
||||
use crate::vec3::{P, dist};
|
||||
use crate::{Error, Result};
|
||||
|
||||
/// Most patches one projection takes.
|
||||
pub const MAX_PATCHES: usize = 64;
|
||||
|
||||
/// An already-meshed patch of the body's boundary, taken as given.
|
||||
#[derive(Debug, Clone, PartialEq, Default)]
|
||||
pub struct FixedPatch {
|
||||
/// The B-rep faces of the body the patch covers.
|
||||
pub faces: Vec<u32>,
|
||||
pub nodes: Vec<P>,
|
||||
/// Its quads, by node index (either orientation).
|
||||
pub quads: Vec<[u32; 4]>,
|
||||
}
|
||||
|
||||
/// The block faces on the patches, and the patch nodes of their grids.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub(crate) struct Fixed {
|
||||
/// Per topology vertex on a patch, its node's position.
|
||||
pub vertex: BTreeMap<u32, P>,
|
||||
/// Per block edge on a patch (ascending vertex pair), its divisions.
|
||||
pub edge_divisions: BTreeMap<[u32; 2], usize>,
|
||||
/// Per block face on a patch, `(block, d, s)` and the positions of its grid: index
|
||||
/// `i + (n1 + 1) * j`, `i` along the first other direction of `d`, `j` along the second.
|
||||
pub faces: Vec<((usize, usize, usize), Vec<P>)>,
|
||||
/// Patch nodes taken.
|
||||
pub nodes: usize,
|
||||
}
|
||||
|
||||
/// One patch's quad mesh: the quads round each node and each node's neighbours.
|
||||
struct Grid<'a> {
|
||||
patch: &'a FixedPatch,
|
||||
quads_at: Vec<Vec<u32>>,
|
||||
nbrs: Vec<BTreeSet<u32>>,
|
||||
}
|
||||
|
||||
impl<'a> Grid<'a> {
|
||||
fn new(patch: &'a FixedPatch) -> Result<Grid<'a>> {
|
||||
let n = patch.nodes.len();
|
||||
let mut quads_at = vec![Vec::new(); n];
|
||||
let mut nbrs = vec![BTreeSet::new(); n];
|
||||
for (k, q) in patch.quads.iter().enumerate() {
|
||||
if q.iter().any(|&v| v as usize >= n) || q.iter().collect::<BTreeSet<_>>().len() != 4 {
|
||||
return Err(Error::Invalid(format!("fixed patch quad {k} names a node it does not have, or one twice")));
|
||||
}
|
||||
for i in 0..4 {
|
||||
let (a, b) = (q[i], q[(i + 1) % 4]);
|
||||
quads_at[a as usize].push(k as u32);
|
||||
nbrs[a as usize].insert(b);
|
||||
nbrs[b as usize].insert(a);
|
||||
}
|
||||
}
|
||||
Ok(Grid { patch, quads_at, nbrs })
|
||||
}
|
||||
|
||||
fn quad(&self, k: u32) -> [u32; 4] {
|
||||
self.patch.quads.get(k as usize).copied().unwrap_or([0; 4])
|
||||
}
|
||||
|
||||
/// The node after `p` on the grid line coming from `q`: the one neighbour of `p` in no quad
|
||||
/// with the edge `q`–`p`.
|
||||
fn straight_on(&self, q: u32, p: u32) -> Option<u32> {
|
||||
let beside: BTreeSet<u32> = self
|
||||
.quads_at
|
||||
.get(p as usize)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|&k| self.quad(k))
|
||||
.filter(|quad| quad.contains(&q))
|
||||
.flatten()
|
||||
.collect();
|
||||
let mut on = self.nbrs.get(p as usize)?.iter().filter(|r| **r != q && !beside.contains(r));
|
||||
let r = on.next()?;
|
||||
on.next().is_none().then_some(*r)
|
||||
}
|
||||
|
||||
/// The grid line from `a` to `b`: one of `a`'s neighbours, walked straight on, reaches `b`.
|
||||
fn line(&self, a: u32, b: u32) -> Option<Vec<u32>> {
|
||||
let cap = self.patch.nodes.len();
|
||||
let mut found: Option<Vec<u32>> = None;
|
||||
for &first in self.nbrs.get(a as usize)? {
|
||||
let mut line = vec![a, first];
|
||||
while line.len() <= cap {
|
||||
let (Some(&p), Some(&q)) = (line.last(), line.get(line.len().wrapping_sub(2))) else { break };
|
||||
if p == b {
|
||||
break;
|
||||
}
|
||||
match self.straight_on(q, p) {
|
||||
Some(r) => line.push(r),
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
if line.last() == Some(&b) && found.as_ref().is_none_or(|f| line.len() < f.len()) {
|
||||
found = Some(line);
|
||||
}
|
||||
}
|
||||
found
|
||||
}
|
||||
|
||||
/// The nodes no block face can hold inside: on the patch's boundary, those in other than two
|
||||
/// quads (its corners); inside it, those in other than four.
|
||||
fn singular(&self) -> Vec<u32> {
|
||||
let mut edge_quads: HashMap<[u32; 2], usize> = HashMap::new();
|
||||
for q in &self.patch.quads {
|
||||
for i in 0..4 {
|
||||
let (a, b) = (q[i], q[(i + 1) % 4]);
|
||||
*edge_quads.entry([a.min(b), a.max(b)]).or_insert(0) += 1;
|
||||
}
|
||||
}
|
||||
let mut on_boundary = vec![false; self.patch.nodes.len()];
|
||||
for (e, n) in &edge_quads {
|
||||
if *n == 1 {
|
||||
for v in e {
|
||||
if let Some(slot) = on_boundary.get_mut(*v as usize) {
|
||||
*slot = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(0..self.patch.nodes.len() as u32)
|
||||
.filter(|&v| {
|
||||
let quads = self.quads_at.get(v as usize).map_or(0, Vec::len);
|
||||
quads > 0 && quads != if on_boundary.get(v as usize).copied().unwrap_or(false) { 2 } else { 4 }
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The nodes of the grid lines from `a`, each walked straight on to its end (at most the
|
||||
/// patch's node count per line).
|
||||
fn lines_from(&self, a: u32) -> Vec<Vec<u32>> {
|
||||
let cap = self.patch.nodes.len();
|
||||
let mut out = Vec::new();
|
||||
for &first in self.nbrs.get(a as usize).into_iter().flatten() {
|
||||
let mut line = vec![a, first];
|
||||
while line.len() <= cap {
|
||||
let (Some(&p), Some(&q)) = (line.last(), line.get(line.len().wrapping_sub(2))) else { break };
|
||||
match self.straight_on(q, p) {
|
||||
Some(r) if r != a => line.push(r),
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
out.push(line);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The patch node of each block vertex in `vertices` (positions `vpos`, joined by the block
|
||||
/// edges `edges`). A node no block face can hold inside is a block vertex: each goes to the
|
||||
/// nearest vertex. Then, out from the vertices placed, a vertex joined to a placed one goes
|
||||
/// to the node nearest it on the grid lines from the placed one's node. Any left go to their
|
||||
/// nearest node.
|
||||
fn match_vertices(&self, vertices: &[u32], edges: &BTreeSet<[u32; 2]>, vpos: &[P]) -> BTreeMap<u32, u32> {
|
||||
let at = |v: u32| vpos.get(v as usize).copied().unwrap_or_default();
|
||||
let node = |n: u32| self.patch.nodes.get(n as usize).copied().unwrap_or_default();
|
||||
let mut node_of: BTreeMap<u32, u32> = BTreeMap::new();
|
||||
// Singular nodes to vertices, nearest pairs first.
|
||||
let mut pairs: Vec<(f64, u32, u32)> =
|
||||
self.singular().into_iter().flat_map(|n| vertices.iter().map(move |&v| (n, v))).map(|(n, v)| (dist(node(n), at(v)), v, n)).collect();
|
||||
pairs.sort_by(|a, b| a.0.total_cmp(&b.0));
|
||||
let mut taken: BTreeSet<u32> = BTreeSet::new();
|
||||
for (_, v, n) in pairs {
|
||||
if !node_of.contains_key(&v) && !taken.contains(&n) {
|
||||
node_of.insert(v, n);
|
||||
taken.insert(n);
|
||||
}
|
||||
}
|
||||
loop {
|
||||
let mut grew = false;
|
||||
for &[a, b] in edges {
|
||||
let (from, to) = match (node_of.get(&a), node_of.get(&b)) {
|
||||
(Some(&na), None) => (na, b),
|
||||
(None, Some(&nb)) => (nb, a),
|
||||
_ => continue,
|
||||
};
|
||||
let best = self
|
||||
.lines_from(from)
|
||||
.into_iter()
|
||||
.flat_map(|l| l.into_iter().skip(1))
|
||||
.filter(|n| !taken.contains(n))
|
||||
.min_by(|x, y| dist(node(*x), at(to)).total_cmp(&dist(node(*y), at(to))));
|
||||
if let Some(n) = best {
|
||||
node_of.insert(to, n);
|
||||
taken.insert(n);
|
||||
grew = true;
|
||||
}
|
||||
}
|
||||
if !grew {
|
||||
break;
|
||||
}
|
||||
}
|
||||
for &v in vertices {
|
||||
if !node_of.contains_key(&v) {
|
||||
let nearest = (0..self.patch.nodes.len() as u32).filter(|n| !taken.contains(n)).min_by(|x, y| dist(node(*x), at(v)).total_cmp(&dist(node(*y), at(v))));
|
||||
if let Some(n) = nearest {
|
||||
node_of.insert(v, n);
|
||||
taken.insert(n);
|
||||
}
|
||||
}
|
||||
}
|
||||
node_of
|
||||
}
|
||||
|
||||
/// The fourth corner of the quad holding `a`, `b` and `c` (`a` the corner between them), and
|
||||
/// the quad.
|
||||
fn complete(&self, a: u32, b: u32, c: u32) -> Option<(u32, u32)> {
|
||||
let k = *self.quads_at.get(a as usize)?.iter().find(|&&k| {
|
||||
let q = self.quad(k);
|
||||
q.contains(&b) && q.contains(&c)
|
||||
})?;
|
||||
let d = self.quad(k).into_iter().find(|v| ![a, b, c].contains(v))?;
|
||||
Some((d, k))
|
||||
}
|
||||
}
|
||||
|
||||
/// The block-local corner of face `(d, s)` at `(i, j)` (0 or 1) along the face's two directions.
|
||||
fn face_corner(block: &[u32; 8], d: usize, s: usize, i: usize, j: usize) -> u32 {
|
||||
let (e1, e2) = super::other(d);
|
||||
let mut bits = [0; 3];
|
||||
bits[d] = s;
|
||||
bits[e1] = i;
|
||||
bits[e2] = j;
|
||||
block[corner_at(bits)]
|
||||
}
|
||||
|
||||
/// Match the block faces whose targets lie on the patches: `outer` lists the outer block faces
|
||||
/// `(block, d, s)`, `target_of` gives a face's target set of B-rep faces, `vpos` the projected
|
||||
/// vertex positions.
|
||||
pub(crate) fn match_patches(
|
||||
t: &Topology,
|
||||
outer: &[(usize, usize, usize)],
|
||||
target_of: &dyn Fn(&[u32; 4]) -> Option<Vec<u32>>,
|
||||
patches: &[FixedPatch],
|
||||
vpos: &[P],
|
||||
) -> Result<Fixed> {
|
||||
if patches.len() > MAX_PATCHES {
|
||||
return Err(Error::Invalid(format!("at most {MAX_PATCHES} fixed patches")));
|
||||
}
|
||||
let mut fixed = Fixed::default();
|
||||
for (pi, patch) in patches.iter().enumerate() {
|
||||
let grid = Grid::new(patch)?;
|
||||
let on: BTreeSet<u32> = patch.faces.iter().copied().collect();
|
||||
// The block faces on this patch.
|
||||
let mut faces: Vec<(usize, usize, usize)> = Vec::new();
|
||||
for &(b, d, s) in outer {
|
||||
let Some(block) = t.blocks.get(b) else { continue };
|
||||
let Some(set) = target_of(&face_key(block, d, s)) else { continue };
|
||||
let inside = set.iter().filter(|f| on.contains(f)).count();
|
||||
if inside == 0 {
|
||||
continue;
|
||||
}
|
||||
if inside < set.len() {
|
||||
return Err(Error::Invalid(format!(
|
||||
"fixed patch {pi}: face {} of block {b} projects onto B-rep faces {set:?}, only some of them fixed; fix all of its smooth group or none",
|
||||
super::topology::face_number(d, s)
|
||||
)));
|
||||
}
|
||||
faces.push((b, d, s));
|
||||
}
|
||||
if faces.is_empty() {
|
||||
return Err(Error::Invalid(format!("fixed patch {pi}: no block face projects onto its faces {:?}", patch.faces)));
|
||||
}
|
||||
// Block vertices to patch nodes.
|
||||
let mut on_face: Vec<u32> = Vec::new();
|
||||
let mut block_edges: BTreeSet<[u32; 2]> = BTreeSet::new();
|
||||
for &(b, d, s) in &faces {
|
||||
let block = t.blocks.get(b).copied().unwrap_or([0; 8]);
|
||||
let cyc = [(0, 0), (1, 0), (1, 1), (0, 1)].map(|(i, j)| face_corner(&block, d, s, i, j));
|
||||
for k in 0..4 {
|
||||
on_face.push(cyc[k]);
|
||||
block_edges.insert([cyc[k].min(cyc[(k + 1) % 4]), cyc[k].max(cyc[(k + 1) % 4])]);
|
||||
}
|
||||
}
|
||||
on_face.sort_unstable();
|
||||
on_face.dedup();
|
||||
let node_of = grid.match_vertices(&on_face, &block_edges, vpos);
|
||||
let taken: BTreeSet<u32> = node_of.values().copied().collect();
|
||||
if taken.len() != node_of.len() {
|
||||
return Err(Error::Failed(format!(
|
||||
"fixed patch {pi}: two block vertices fall on one patch node; the blocks there do not match the patch's mesh"
|
||||
)));
|
||||
}
|
||||
// Block edges along grid lines.
|
||||
let mut lines: BTreeMap<[u32; 2], Vec<u32>> = BTreeMap::new();
|
||||
for &(b, d, s) in &faces {
|
||||
let block = t.blocks.get(b).copied().unwrap_or([0; 8]);
|
||||
let cyc = [(0, 0), (1, 0), (1, 1), (0, 1)].map(|(i, j)| face_corner(&block, d, s, i, j));
|
||||
for k in 0..4 {
|
||||
let (a, c) = (cyc[k].min(cyc[(k + 1) % 4]), cyc[k].max(cyc[(k + 1) % 4]));
|
||||
if lines.contains_key(&[a, c]) {
|
||||
continue;
|
||||
}
|
||||
let (Some(&na), Some(&nc)) = (node_of.get(&a), node_of.get(&c)) else { continue };
|
||||
let line = grid.line(na, nc).ok_or_else(|| {
|
||||
Error::Failed(format!(
|
||||
"fixed patch {pi}: the block edge from vertex {a} to {c} (face {} of block {b}) does not follow a grid line of the patch; the blocks there do not match the patch's mesh",
|
||||
super::topology::face_number(d, s)
|
||||
))
|
||||
})?;
|
||||
lines.insert([a, c], line);
|
||||
}
|
||||
}
|
||||
// Each face filled quad by quad from its i and j edges at the 0 corner.
|
||||
let mut used: BTreeSet<u32> = BTreeSet::new();
|
||||
for &(b, d, s) in &faces {
|
||||
let block = t.blocks.get(b).copied().unwrap_or([0; 8]);
|
||||
let corner = |i, j| face_corner(&block, d, s, i, j);
|
||||
// The edge from corner (i0, j0) to (i1, j1), in that order.
|
||||
let edge = |from: u32, to: u32| -> Vec<u32> {
|
||||
let line = lines.get(&[from.min(to), from.max(to)]).cloned().unwrap_or_default();
|
||||
if from < to { line } else { line.into_iter().rev().collect() }
|
||||
};
|
||||
let (bottom, left) = (edge(corner(0, 0), corner(1, 0)), edge(corner(0, 0), corner(0, 1)));
|
||||
let (top, right) = (edge(corner(0, 1), corner(1, 1)), edge(corner(1, 0), corner(1, 1)));
|
||||
let (n1, n2) = (bottom.len().saturating_sub(1), left.len().saturating_sub(1));
|
||||
let mismatch = || {
|
||||
Error::Failed(format!(
|
||||
"fixed patch {pi}: face {} of block {b} does not close on the patch's mesh ({n1} × {n2} divisions from two of its edges); the blocks there do not match the patch's mesh",
|
||||
super::topology::face_number(d, s)
|
||||
))
|
||||
};
|
||||
if n1 == 0 || n2 == 0 || top.len() != n1 + 1 || right.len() != n2 + 1 {
|
||||
return Err(mismatch());
|
||||
}
|
||||
let mut g: Vec<u32> = vec![u32::MAX; (n1 + 1) * (n2 + 1)];
|
||||
let at = |i: usize, j: usize| i + (n1 + 1) * j;
|
||||
for (i, &v) in bottom.iter().enumerate() {
|
||||
g[at(i, 0)] = v;
|
||||
}
|
||||
for (j, &v) in left.iter().enumerate() {
|
||||
g[at(0, j)] = v;
|
||||
}
|
||||
for j in 0..n2 {
|
||||
for i in 0..n1 {
|
||||
let (a, x, y) = (g[at(i, j)], g[at(i + 1, j)], g[at(i, j + 1)]);
|
||||
let (dn, k) = grid.complete(a, x, y).ok_or_else(mismatch)?;
|
||||
if !used.insert(k) {
|
||||
return Err(mismatch());
|
||||
}
|
||||
g[at(i + 1, j + 1)] = dn;
|
||||
}
|
||||
}
|
||||
let closes = (0..=n1).all(|i| g[at(i, n2)] == top[i]) && (0..=n2).all(|j| g[at(n1, j)] == right[j]);
|
||||
if !closes {
|
||||
return Err(mismatch());
|
||||
}
|
||||
for (pair, line) in [([corner(0, 0), corner(1, 0)], &bottom), ([corner(0, 0), corner(0, 1)], &left)] {
|
||||
fixed.edge_divisions.insert([pair[0].min(pair[1]), pair[0].max(pair[1])], line.len() - 1);
|
||||
}
|
||||
for (pair, line) in [([corner(0, 1), corner(1, 1)], &top), ([corner(1, 0), corner(1, 1)], &right)] {
|
||||
fixed.edge_divisions.insert([pair[0].min(pair[1]), pair[0].max(pair[1])], line.len() - 1);
|
||||
}
|
||||
let positions = g.iter().map(|&v| patch.nodes.get(v as usize).copied().unwrap_or_default()).collect();
|
||||
fixed.faces.push(((b, d, s), positions));
|
||||
}
|
||||
if used.len() != patch.quads.len() {
|
||||
return Err(Error::Failed(format!(
|
||||
"fixed patch {pi}: the block faces on it cover {} of its {} quads; the patch reaches beyond the faces it names, or the blocks do not match its mesh",
|
||||
used.len(),
|
||||
patch.quads.len()
|
||||
)));
|
||||
}
|
||||
let mut nodes: BTreeSet<u32> = BTreeSet::new();
|
||||
for k in &used {
|
||||
nodes.extend(grid.quad(*k));
|
||||
}
|
||||
fixed.nodes += nodes.len();
|
||||
for (v, n) in node_of {
|
||||
if let Some(p) = patch.nodes.get(n as usize) {
|
||||
fixed.vertex.insert(v, *p);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(fixed)
|
||||
}
|
||||
|
||||
/// The class divisions the patches set: per class, the count every fixed edge of it has. Edges of
|
||||
/// one class with different counts on the patches are an error.
|
||||
pub(crate) fn class_divisions(t: &Topology, classes: &[[usize; 3]], fixed: &Fixed) -> Result<HashMap<usize, usize>> {
|
||||
let mut out: HashMap<usize, usize> = HashMap::new();
|
||||
for (block, cls) in t.blocks.iter().zip(classes) {
|
||||
for (d, &class) in cls.iter().enumerate() {
|
||||
for (k, bits) in CORNERS.iter().enumerate() {
|
||||
if bits[d] != 0 {
|
||||
continue;
|
||||
}
|
||||
let mut e = *bits;
|
||||
e[d] = 1;
|
||||
let (a, b) = (block[k], block[corner_at(e)]);
|
||||
let Some(&n) = fixed.edge_divisions.get(&[a.min(b), a.max(b)]) else { continue };
|
||||
match out.get(&class) {
|
||||
Some(&m) if m != n => {
|
||||
return Err(Error::Failed(format!(
|
||||
"the fixed patches give parallel block edges {m} and {n} divisions (the edge from vertex {a} to {b}); a structured block mesh cannot meet both"
|
||||
)));
|
||||
}
|
||||
_ => {
|
||||
out.insert(class, n);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
@@ -17,6 +17,7 @@
|
||||
//! index space, carved, projected face → surface, edge → curve, vertex → point, then smoothed);
|
||||
//! the algorithms are the papers cited in each module.
|
||||
|
||||
pub mod fixed;
|
||||
mod lattice;
|
||||
pub mod smooth;
|
||||
pub mod target;
|
||||
@@ -29,6 +30,7 @@ use std::collections::{BTreeMap, BTreeSet, HashMap};
|
||||
|
||||
use fieldcraft_model::ElementKind;
|
||||
|
||||
pub use fixed::FixedPatch;
|
||||
pub use smooth::{QUALITY_FLOOR, SmoothReport, Smoothing};
|
||||
pub use target::Geometry;
|
||||
pub use topology::Topology;
|
||||
@@ -89,6 +91,9 @@ pub struct ProjectOptions {
|
||||
/// The smallest scaled Jacobian accepted: a mesh with a worse element is an error that
|
||||
/// says where a template is missing (0 accepts any valid mesh).
|
||||
pub min_quality: f64,
|
||||
/// Already-meshed patches of the boundary taken as given ([`fixed`]): the block faces on
|
||||
/// them take their divisions and nodes from the patch, held through smoothing.
|
||||
pub fixed: Vec<FixedPatch>,
|
||||
}
|
||||
|
||||
impl Default for ProjectOptions {
|
||||
@@ -102,6 +107,7 @@ impl Default for ProjectOptions {
|
||||
iterations: 100,
|
||||
max_elements: 2_000_000,
|
||||
min_quality: DEFAULT_MIN_QUALITY,
|
||||
fixed: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -125,6 +131,8 @@ pub struct ProjectReport {
|
||||
pub targets: Vec<Vec<u32>>,
|
||||
/// Divisions per block.
|
||||
pub divisions: Vec<[usize; 3]>,
|
||||
/// Nodes taken from the fixed patches, held where they are.
|
||||
pub fixed_nodes: usize,
|
||||
}
|
||||
|
||||
/// An all-hex mesh of a projected topology.
|
||||
@@ -383,6 +391,18 @@ pub fn project_mesh(t: &Topology, surface: &BoundarySurface, g: &dyn Geometry, o
|
||||
for (v, p) in vpos.iter_mut().enumerate() {
|
||||
*p = place(g, &targets, vertex_c(v as u32), *p, tol);
|
||||
}
|
||||
// Vertices on fixed patches go to their patch nodes.
|
||||
let fixed = if opts.fixed.is_empty() {
|
||||
fixed::Fixed::default()
|
||||
} else {
|
||||
let target_of = |k: &[u32; 4]| face_target.get(k).and_then(|&i| targets.get(i)).cloned();
|
||||
fixed::match_patches(t, &outer, &target_of, &opts.fixed, &vpos)?
|
||||
};
|
||||
for (&v, &p) in &fixed.vertex {
|
||||
if let Some(slot) = vpos.get_mut(v as usize) {
|
||||
*slot = p;
|
||||
}
|
||||
}
|
||||
// Inner vertices follow: the projected vertices' moves blended through the block edges
|
||||
// (a bounded Jacobi solve of Laplace's equation for the displacement), so a block the
|
||||
// projection stretches does not leave its inner corners behind.
|
||||
@@ -535,6 +555,14 @@ pub fn project_mesh(t: &Topology, surface: &BoundarySurface, g: &dyn Geometry, o
|
||||
*slot = n;
|
||||
}
|
||||
}
|
||||
for (class, n) in fixed::class_divisions(t, &classes, &fixed)? {
|
||||
if let Some(slot) = class_n.get_mut(class) {
|
||||
if opts.divisions.iter().any(|&(b, d, _)| classes.get(b).and_then(|c| c.get(d)) == Some(&class)) && *slot != n {
|
||||
return Err(Error::Invalid(format!("divisions set by hand ({}) differ from the fixed patch's ({n})", *slot)));
|
||||
}
|
||||
*slot = n;
|
||||
}
|
||||
}
|
||||
let divisions: Vec<[usize; 3]> = classes.iter().map(|c| c.map(|k| class_n.get(k).copied().unwrap_or(1))).collect();
|
||||
let lat = build(t, &divisions, opts.max_elements)?;
|
||||
|
||||
@@ -552,8 +580,30 @@ pub fn project_mesh(t: &Topology, surface: &BoundarySurface, g: &dyn Geometry, o
|
||||
let n = lat.entity.len();
|
||||
let mut nodes = vec![[0.0; 3]; n];
|
||||
let mut placed = vec![false; n];
|
||||
// Nodes on fixed patches: placed and held.
|
||||
let mut held = vec![false; n];
|
||||
for ((b, d, s), grid) in &fixed.faces {
|
||||
let (Some(nd), Some(bn)) = (lat.divisions.get(*b), lat.block_nodes.get(*b)) else { continue };
|
||||
let (e1, e2) = other(*d);
|
||||
for j in 0..=nd[e2] {
|
||||
for i in 0..=nd[e1] {
|
||||
let mut at = [0; 3];
|
||||
at[*d] = s * nd[*d];
|
||||
at[e1] = i;
|
||||
at[e2] = j;
|
||||
let (Some(&id), Some(&p)) = (bn.get(crate::quad::lattice_index(*nd, at)), grid.get(i + (nd[e1] + 1) * j)) else {
|
||||
return Err(Error::Failed("a fixed patch's grid does not fit its block face".into()));
|
||||
};
|
||||
if let (Some(slot), Some(h), Some(pl)) = (nodes.get_mut(id as usize), held.get_mut(id as usize), placed.get_mut(id as usize)) {
|
||||
*slot = p;
|
||||
*h = true;
|
||||
*pl = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (i, e) in lat.entity.iter().enumerate() {
|
||||
if let Entity::Vertex(v) = e {
|
||||
if let (Entity::Vertex(v), false) = (e, held[i]) {
|
||||
nodes[i] = vpos.get(*v as usize).copied().unwrap_or_default();
|
||||
placed[i] = true;
|
||||
}
|
||||
@@ -655,7 +705,7 @@ pub fn project_mesh(t: &Topology, surface: &BoundarySurface, g: &dyn Geometry, o
|
||||
}
|
||||
let boundary_quads: Vec<[u32; 4]> = quads.iter().map(|q| q.0).collect();
|
||||
let smoothing = smooth::smooth(
|
||||
smooth::Mesh { nodes: &mut nodes, hexes: &lat.hexes, constraint: &constraint, boundary: &boundary_quads },
|
||||
smooth::Mesh { nodes: &mut nodes, hexes: &lat.hexes, constraint: &constraint, held: &held, boundary: &boundary_quads },
|
||||
g,
|
||||
&targets,
|
||||
opts.smoothing,
|
||||
@@ -709,6 +759,7 @@ pub fn project_mesh(t: &Topology, surface: &BoundarySurface, g: &dyn Geometry, o
|
||||
surface_volume,
|
||||
targets: targets.clone(),
|
||||
divisions: lat.divisions.clone(),
|
||||
fixed_nodes: held.iter().filter(|h| **h).count(),
|
||||
};
|
||||
// Where the worst element is, for the error messages.
|
||||
let worst = || {
|
||||
|
||||
@@ -287,6 +287,8 @@ pub(crate) struct Mesh<'a> {
|
||||
pub nodes: &'a mut Vec<P>,
|
||||
pub hexes: &'a [[u32; 8]],
|
||||
pub constraint: &'a [Constraint],
|
||||
/// Nodes held where they are (a fixed patch's).
|
||||
pub held: &'a [bool],
|
||||
/// Boundary quads (outward), for the neighbours of boundary nodes.
|
||||
pub boundary: &'a [[u32; 4]],
|
||||
}
|
||||
@@ -403,6 +405,7 @@ fn pass(m: &mut Mesh, g: &dyn Geometry, targets: &[Vec<u32>], method: Smoothing,
|
||||
}
|
||||
}
|
||||
let cons = |v: u32| m.constraint.get(v as usize).copied().unwrap_or(Constraint::Free);
|
||||
let is_held = |v: u32| m.held.get(v as usize).copied().unwrap_or(false);
|
||||
let mut stencils: Vec<Option<Stencil>> = vec![None; n];
|
||||
if matches!(method, Smoothing::Elliptic | Smoothing::EllipticControlled) {
|
||||
for v in 0..n as u32 {
|
||||
@@ -445,6 +448,9 @@ fn pass(m: &mut Mesh, g: &dyn Geometry, targets: &[Vec<u32>], method: Smoothing,
|
||||
report.iterations = it + 1;
|
||||
let mut moved = 0.0f64;
|
||||
for v in 0..n as u32 {
|
||||
if is_held(v) {
|
||||
continue;
|
||||
}
|
||||
let c = cons(v);
|
||||
let p = at(m.nodes, v);
|
||||
let mean = |nbrs: &mut dyn Iterator<Item = u32>| -> Option<P> {
|
||||
@@ -531,7 +537,7 @@ fn pass(m: &mut Mesh, g: &dyn Geometry, targets: &[Vec<u32>], method: Smoothing,
|
||||
let mut improved = false;
|
||||
for &v in &poor {
|
||||
let c = cons(v);
|
||||
if matches!(c, Constraint::Corner(..)) {
|
||||
if matches!(c, Constraint::Corner(..)) || is_held(v) {
|
||||
continue;
|
||||
}
|
||||
let p = at(m.nodes, v);
|
||||
@@ -577,7 +583,7 @@ fn pass(m: &mut Mesh, g: &dyn Geometry, targets: &[Vec<u32>], method: Smoothing,
|
||||
let mut moved = false;
|
||||
for v in 0..n as u32 {
|
||||
let c = cons(v);
|
||||
if !matches!(c, Constraint::Surface(_)) {
|
||||
if !matches!(c, Constraint::Surface(_)) || is_held(v) {
|
||||
continue;
|
||||
}
|
||||
let p = at(m.nodes, v);
|
||||
|
||||
@@ -273,3 +273,68 @@ fn hostile_topologies_are_errors() {
|
||||
let mut g3 = Topology::grid([&[0.0, 1.0], &[0.0, 1.0], &[0.0, 1.0]]).unwrap();
|
||||
assert!(g3.carve([1, 1, 1], &[[[0, 0], [0, 0], [0, 0]]]).is_err());
|
||||
}
|
||||
|
||||
/// The quads of `m` on B-rep face `face`, as a patch over its own nodes.
|
||||
fn patch_of(m: &ProjectedMesh, face: u32) -> FixedPatch {
|
||||
let mut index: std::collections::BTreeMap<u32, u32> = std::collections::BTreeMap::new();
|
||||
let mut patch = FixedPatch { faces: vec![face], ..FixedPatch::default() };
|
||||
for b in m.boundary.iter().filter(|b| b.brep_face == face) {
|
||||
let hex = m.hexes[b.element as usize];
|
||||
let local = ElementKind::Hex8.faces()[b.face as usize - 1];
|
||||
let q = [0, 1, 2, 3].map(|i| {
|
||||
let v = hex[local[i]];
|
||||
*index.entry(v).or_insert_with(|| {
|
||||
patch.nodes.push(m.nodes[v as usize]);
|
||||
patch.nodes.len() as u32 - 1
|
||||
})
|
||||
});
|
||||
patch.quads.push(q);
|
||||
}
|
||||
patch
|
||||
}
|
||||
|
||||
/// A fixed patch is taken as given (#490): a disc meshed against the top quads of another disc's
|
||||
/// mesh (moved down onto its bottom face, as the face two stacked bodies share) keeps those nodes
|
||||
/// exactly and their divisions, though its own size asks for coarser elements, and smooths the
|
||||
/// rest around them.
|
||||
#[test]
|
||||
fn a_fixed_patch_sets_the_nodes_and_divisions_of_the_faces_on_it() {
|
||||
let (r, h) = (10.0, 2.0);
|
||||
let (s, t) = cylinder(r, h, 128);
|
||||
let topo = Topology::butterfly([0.0; 3], [0.0, 0.0, 1.0], r, &[0.0, h], 0.55).unwrap();
|
||||
let below = project_mesh(&topo, &s, &t, &options(0.75)).unwrap();
|
||||
let mut patch = patch_of(&below, 1);
|
||||
for p in &mut patch.nodes {
|
||||
p[2] -= h;
|
||||
}
|
||||
patch.faces = vec![0];
|
||||
let m = project_mesh(&topo, &s, &t, &ProjectOptions { fixed: vec![patch.clone()], ..options(1.5) }).unwrap();
|
||||
assert_eq!(m.report.fixed_nodes, patch.nodes.len());
|
||||
assert!(m.report.min_scaled_jacobian >= 0.5, "{:?}", m.report);
|
||||
let bottom = patch_of(&m, 0);
|
||||
assert_eq!(bottom.quads.len(), patch.quads.len());
|
||||
for p in &patch.nodes {
|
||||
assert!(bottom.nodes.iter().any(|q| dist(*p, *q) == 0.0), "{p:?} is not a node of the mesh");
|
||||
}
|
||||
// The top is meshed at the patch's divisions, not the coarser size's.
|
||||
assert_eq!(patch_of(&m, 1).quads.len(), patch.quads.len());
|
||||
}
|
||||
|
||||
/// A patch the blocks do not match is an error that names the patch, not a mesh that leaves a
|
||||
/// gap: here half the bottom's quads, and a patch on a face no block lies on.
|
||||
#[test]
|
||||
fn a_fixed_patch_the_blocks_do_not_match_is_an_error() {
|
||||
let (r, h) = (10.0, 2.0);
|
||||
let (s, t) = cylinder(r, h, 128);
|
||||
let topo = Topology::butterfly([0.0; 3], [0.0, 0.0, 1.0], r, &[0.0, h], 0.55).unwrap();
|
||||
let m = project_mesh(&topo, &s, &t, &options(1.0)).unwrap();
|
||||
let mut half = patch_of(&m, 0);
|
||||
half.quads.retain(|q| q.iter().all(|&v| half.nodes[v as usize][0] <= 1e-9));
|
||||
let e = project_mesh(&topo, &s, &t, &ProjectOptions { fixed: vec![half], ..options(1.0) }).unwrap_err().to_string();
|
||||
assert!(e.contains("fixed patch 0"), "{e}");
|
||||
let elsewhere = FixedPatch { faces: vec![7], ..patch_of(&m, 0) };
|
||||
let e = project_mesh(&topo, &s, &t, &ProjectOptions { fixed: vec![elsewhere], ..options(1.0) }).unwrap_err().to_string();
|
||||
assert!(e.contains("fixed patch 0"), "{e}");
|
||||
let broken = FixedPatch { quads: vec![[0, 0, 1, 2]], ..patch_of(&m, 0) };
|
||||
assert!(project_mesh(&topo, &s, &t, &ProjectOptions { fixed: vec![broken], ..options(1.0) }).is_err());
|
||||
}
|
||||
Reference in new issue
Block a user