forked from 1b-agent/fieldcraft-upstream
mesh: projection blocking — block topology, projection onto the B-rep, elliptic smoothing (#399)
A block topology (carved grid, butterfly and seven-block sphere templates, O-grid, extrude, connect) projected onto a body: outer block faces onto their smooth face groups, edges onto the curve of two targets and vertices onto the point of three by tangent-plane iteration, nodes by transfinite interpolation, then Winslow smoothing with the boundary on the geometry and a compass search on the worst elements. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
1 parent
8092c39892
commit
6efad8feeb
8 files changed
+2179
No files matched your search
@@ -0,0 +1,514 @@
|
||||
//! The block topology of the projection method: hexahedral blocks over shared vertices, built
|
||||
//! from a carved grid, from the round-feature templates (butterfly, seven-block sphere) and by the
|
||||
//! O-grid and extrude operations.
|
||||
//!
|
||||
//! A block's corners are in hex8 order (1-2-3-4 counter-clockwise seen from 5-6-7-8's side) and
|
||||
//! its parameter directions run 1→2, 1→4 and 1→5. Faces are numbered as hex8 faces: 1 = 1-2-3-4,
|
||||
//! 2 = 5-6-7-8, 3 = 1-2-6-5, 4 = 2-3-7-6, 5 = 3-4-8-7, 6 = 4-1-5-8. Positions are only a first
|
||||
//! guess: the projection moves every boundary vertex onto the geometry.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::vec3::{P, add, det, dist, scale, sub};
|
||||
use crate::{Error, Result};
|
||||
|
||||
/// Most blocks one topology holds.
|
||||
pub const MAX_BLOCKS: usize = 20_000;
|
||||
|
||||
/// Corner `c` of a block in parameter space (hex8 order).
|
||||
pub(crate) const CORNERS: [[usize; 3]; 8] = [[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0], [0, 0, 1], [1, 0, 1], [1, 1, 1], [0, 1, 1]];
|
||||
|
||||
/// The direction and side of each hex8 face (1-based face `f` at index `f - 1`).
|
||||
pub(crate) const FACE_SIDES: [(usize, usize); 6] = [(2, 0), (2, 1), (1, 0), (0, 1), (1, 1), (0, 0)];
|
||||
|
||||
/// The corner at parameter `c`.
|
||||
pub(crate) fn corner_at(c: [usize; 3]) -> usize {
|
||||
CORNERS.iter().position(|x| *x == c).unwrap_or(0)
|
||||
}
|
||||
|
||||
/// Hex8 face number (1-based) of direction `d`, side `s`.
|
||||
pub(crate) fn face_number(d: usize, s: usize) -> u8 {
|
||||
FACE_SIDES.iter().position(|x| *x == (d, s)).map_or(1, |i| i as u8 + 1)
|
||||
}
|
||||
|
||||
/// Blocks over shared vertices.
|
||||
#[derive(Debug, Clone, Default, PartialEq)]
|
||||
pub struct Topology {
|
||||
pub vertices: Vec<P>,
|
||||
pub blocks: Vec<[u32; 8]>,
|
||||
}
|
||||
|
||||
/// The trilinear map of a block's corners at parameter `u`.
|
||||
pub(crate) fn trilinear(c: &[P; 8], u: [f64; 3]) -> P {
|
||||
let mut x = [0.0; 3];
|
||||
for (k, bits) in CORNERS.iter().enumerate() {
|
||||
let w: f64 = (0..3).map(|d| if bits[d] == 1 { u[d] } else { 1.0 - u[d] }).product();
|
||||
x = add(x, scale(c[k], w));
|
||||
}
|
||||
x
|
||||
}
|
||||
|
||||
/// The smallest corner Jacobian determinant of a trilinear block (positive for a valid one).
|
||||
pub(crate) fn min_corner_det(c: &[P; 8]) -> f64 {
|
||||
let mut m = f64::INFINITY;
|
||||
for (k, bits) in CORNERS.iter().enumerate() {
|
||||
// The three edges leaving corner k along increasing parameters, signed to keep the
|
||||
// right-handed order.
|
||||
let along = |d: usize| {
|
||||
let mut b = *bits;
|
||||
b[d] = 1 - b[d];
|
||||
let e = sub(c[corner_at(b)], c[k]);
|
||||
if bits[d] == 1 { scale(e, -1.0) } else { e }
|
||||
};
|
||||
m = m.min(det(along(0), along(1), along(2)));
|
||||
}
|
||||
m
|
||||
}
|
||||
|
||||
impl Topology {
|
||||
/// The corner positions of block `b`.
|
||||
pub fn corners(&self, b: usize) -> Result<[P; 8]> {
|
||||
let block = self.blocks.get(b).ok_or_else(|| Error::Invalid(format!("there is no block {b}")))?;
|
||||
let mut out = [[0.0; 3]; 8];
|
||||
for (o, &v) in out.iter_mut().zip(block) {
|
||||
*o = *self.vertices.get(v as usize).ok_or_else(|| Error::Invalid(format!("block {b} uses vertex {v}, which does not exist")))?;
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Every block uses existing, distinct vertices, is right-handed and caps are kept.
|
||||
pub fn check(&self) -> Result<()> {
|
||||
if self.blocks.is_empty() {
|
||||
return Err(Error::Invalid("the topology has no blocks".into()));
|
||||
}
|
||||
if self.blocks.len() > MAX_BLOCKS {
|
||||
return Err(Error::Limit(format!("{} blocks; at most {MAX_BLOCKS}", self.blocks.len())));
|
||||
}
|
||||
if let Some(v) = self.vertices.iter().position(|v| !v.iter().all(|x| x.is_finite() && x.abs() <= fieldcraft_model::MAX_COORD)) {
|
||||
return Err(Error::Invalid(format!("vertex {v} is not a finite point")));
|
||||
}
|
||||
for (b, block) in self.blocks.iter().enumerate() {
|
||||
let mut sorted = *block;
|
||||
sorted.sort_unstable();
|
||||
if sorted.windows(2).any(|w| w[0] == w[1]) {
|
||||
return Err(Error::Invalid(format!("block {b} uses one vertex twice")));
|
||||
}
|
||||
if !(min_corner_det(&self.corners(b)?) > 0.0) {
|
||||
return Err(Error::Invalid(format!(
|
||||
"block {b} is inside out or folded at a corner: give its corners in hex8 order (1-2-3-4 counter-clockwise seen from 5-6-7-8)"
|
||||
)));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The bounding-box diagonal of the vertices.
|
||||
pub fn diagonal(&self) -> f64 {
|
||||
let (mut lo, mut hi) = ([f64::INFINITY; 3], [f64::NEG_INFINITY; 3]);
|
||||
for v in &self.vertices {
|
||||
for d in 0..3 {
|
||||
lo[d] = lo[d].min(v[d]);
|
||||
hi[d] = hi[d].max(v[d]);
|
||||
}
|
||||
}
|
||||
if self.vertices.is_empty() { 0.0 } else { dist(lo, hi) }
|
||||
}
|
||||
|
||||
/// A grid of blocks between the planes `planes[d]` (increasing coordinates along X, Y and Z),
|
||||
/// block `(i, j, k)` at index `(k·nj + j)·ni + i`.
|
||||
pub fn grid(planes: [&[f64]; 3]) -> Result<Topology> {
|
||||
for (d, p) in planes.iter().enumerate() {
|
||||
if p.len() < 2 || !p.iter().all(|x| x.is_finite()) || p.windows(2).any(|w| !(w[1] > w[0])) {
|
||||
return Err(Error::Invalid(format!("the grid planes along {} must be at least two increasing numbers", ["X", "Y", "Z"][d])));
|
||||
}
|
||||
}
|
||||
let n = planes.map(|p| p.len() - 1);
|
||||
if n.iter().try_fold(1usize, |a, &b| a.checked_mul(b)).is_none_or(|c| c > MAX_BLOCKS) {
|
||||
return Err(Error::Limit(format!("a grid of {n:?} blocks; at most {MAX_BLOCKS}")));
|
||||
}
|
||||
let vid = |i: usize, j: usize, k: usize| ((k * (n[1] + 1) + j) * (n[0] + 1) + i) as u32;
|
||||
let mut vertices = Vec::new();
|
||||
for z in planes[2] {
|
||||
for y in planes[1] {
|
||||
for x in planes[0] {
|
||||
vertices.push([*x, *y, *z]);
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut blocks = Vec::new();
|
||||
for k in 0..n[2] {
|
||||
for j in 0..n[1] {
|
||||
for i in 0..n[0] {
|
||||
blocks.push(CORNERS.map(|c| vid(i + c[0], j + c[1], k + c[2])));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Topology { vertices, blocks })
|
||||
}
|
||||
|
||||
/// Remove the blocks of a grid of `n` blocks per direction whose index lies in every range
|
||||
/// of `ranges` (inclusive, per direction), keeping the others in order; vertices no block
|
||||
/// uses are dropped.
|
||||
pub fn carve(&mut self, n: [usize; 3], ranges: &[[[usize; 2]; 3]]) -> Result<()> {
|
||||
if self.blocks.len() != n.iter().product::<usize>() {
|
||||
return Err(Error::Invalid("carving needs the grid's own block numbering".into()));
|
||||
}
|
||||
let mut keep = Vec::new();
|
||||
for (b, block) in self.blocks.iter().enumerate() {
|
||||
let at = [b % n[0], (b / n[0]) % n[1], b / (n[0] * n[1])];
|
||||
let gone = ranges.iter().any(|r| (0..3).all(|d| r[d][0] <= at[d] && at[d] <= r[d][1]));
|
||||
if !gone {
|
||||
keep.push(*block);
|
||||
}
|
||||
}
|
||||
self.blocks = keep;
|
||||
self.compact();
|
||||
if self.blocks.is_empty() { Err(Error::Invalid("carving removed every block".into())) } else { Ok(()) }
|
||||
}
|
||||
|
||||
/// Drop unused vertices and renumber.
|
||||
fn compact(&mut self) {
|
||||
let mut map: BTreeMap<u32, u32> = BTreeMap::new();
|
||||
let mut vertices = Vec::new();
|
||||
for block in &mut self.blocks {
|
||||
for v in block.iter_mut() {
|
||||
let next = map.len() as u32;
|
||||
let new = *map.entry(*v).or_insert_with(|| {
|
||||
vertices.push(self.vertices.get(*v as usize).copied().unwrap_or([0.0; 3]));
|
||||
next
|
||||
});
|
||||
*v = new;
|
||||
}
|
||||
}
|
||||
self.vertices = vertices;
|
||||
}
|
||||
|
||||
/// Join vertices closer than `tol` (the first one stays).
|
||||
pub fn merge(&mut self, tol: f64) {
|
||||
let mut map: Vec<u32> = (0..self.vertices.len() as u32).collect();
|
||||
// Sorted by X, so only a window is compared.
|
||||
let mut order: Vec<usize> = (0..self.vertices.len()).collect();
|
||||
order.sort_by(|&a, &b| self.vertices[a][0].total_cmp(&self.vertices[b][0]).then(a.cmp(&b)));
|
||||
for (oi, &a) in order.iter().enumerate() {
|
||||
if map[a] as usize != a {
|
||||
continue;
|
||||
}
|
||||
for &b in order.iter().skip(oi + 1) {
|
||||
if self.vertices[b][0] - self.vertices[a][0] > tol {
|
||||
break;
|
||||
}
|
||||
if map[b] as usize == b && dist(self.vertices[a], self.vertices[b]) <= tol {
|
||||
map[b] = a as u32;
|
||||
}
|
||||
}
|
||||
}
|
||||
for block in &mut self.blocks {
|
||||
for v in block.iter_mut() {
|
||||
*v = map.get(*v as usize).copied().unwrap_or(*v);
|
||||
}
|
||||
}
|
||||
self.compact();
|
||||
}
|
||||
|
||||
/// Replace block `b` by an O-grid: an inner block, scaled by `inset[d]` (0 to 1) about the
|
||||
/// block's centre along each of its directions (from the open face, along a direction with one
|
||||
/// open face), and one block between each face of the inner
|
||||
/// block and the block's own face, except across the `open` faces (hex8 face numbers), where
|
||||
/// the inner block reaches the face. Faces of `b` stay faces of the result, so neighbours
|
||||
/// still fit, except the open ones, which become the inner block's face ringed by the side
|
||||
/// blocks' faces (the butterfly pattern on that face). The inner block takes index `b`; the
|
||||
/// others are appended. Returns, per open face, the faces now on it: the inner block's first,
|
||||
/// then the ring's.
|
||||
pub fn o_grid(&mut self, b: usize, open: &[u8], inset: [f64; 3]) -> Result<Vec<Vec<(usize, u8)>>> {
|
||||
if !inset.iter().all(|i| *i > 0.0 && *i < 1.0) {
|
||||
return Err(Error::Invalid(format!("the O-grid inset must be between 0 and 1, got {inset:?}")));
|
||||
}
|
||||
if let Some(f) = open.iter().find(|f| !(1..=6).contains(*f)) {
|
||||
return Err(Error::Invalid(format!("face {f} is not a hex8 face (1 to 6)")));
|
||||
}
|
||||
let c = self.corners(b)?;
|
||||
let outer = self.blocks.get(b).copied().ok_or_else(|| Error::Invalid(format!("there is no block {b}")))?;
|
||||
let is_open = |d: usize, s: usize| open.contains(&face_number(d, s));
|
||||
// Each corner's inner copy: on an open face it stays on the face.
|
||||
let mut inner = [0u32; 8];
|
||||
for (k, bits) in CORNERS.iter().enumerate() {
|
||||
// Along a direction with one open face the inner block reaches `inset` of the way
|
||||
// across from it; with none it is centred.
|
||||
let u = [0, 1, 2].map(|d| {
|
||||
let b = bits[d] as f64;
|
||||
match (is_open(d, 0), is_open(d, 1)) {
|
||||
(true, true) => b,
|
||||
(true, false) => {
|
||||
if bits[d] == 0 {
|
||||
0.0
|
||||
} else {
|
||||
inset[d]
|
||||
}
|
||||
}
|
||||
(false, true) => {
|
||||
if bits[d] == 1 {
|
||||
1.0
|
||||
} else {
|
||||
1.0 - inset[d]
|
||||
}
|
||||
}
|
||||
(false, false) => 0.5 + inset[d] * (b - 0.5),
|
||||
}
|
||||
});
|
||||
inner[k] = self.vertices.len() as u32;
|
||||
self.vertices.push(trilinear(&c, u));
|
||||
}
|
||||
if let Some(slot) = self.blocks.get_mut(b) {
|
||||
*slot = inner;
|
||||
}
|
||||
let mut on_open: Vec<Vec<(usize, u8)>> = open.iter().map(|&f| vec![(b, f)]).collect();
|
||||
for (d, s) in FACE_SIDES {
|
||||
if is_open(d, s) {
|
||||
continue;
|
||||
}
|
||||
// Local directions: the face's two directions, then from the outer face inwards.
|
||||
let (e1, e2) = match d {
|
||||
0 => (1, 2),
|
||||
1 => (0, 2),
|
||||
_ => (0, 1),
|
||||
};
|
||||
let pick = |a: usize, bb: usize, w: usize| {
|
||||
let mut bits = [0; 3];
|
||||
bits[d] = s;
|
||||
bits[e1] = a;
|
||||
bits[e2] = bb;
|
||||
let k = corner_at(bits);
|
||||
if w == 0 { outer[k] } else { inner[k] }
|
||||
};
|
||||
let mut block = CORNERS.map(|q| pick(q[0], q[1], q[2]));
|
||||
let pos = |blk: &[u32; 8]| blk.map(|v| self.vertices.get(v as usize).copied().unwrap_or([0.0; 3]));
|
||||
let flipped = min_corner_det(&pos(&block)) < 0.0;
|
||||
if flipped {
|
||||
block = CORNERS.map(|q| pick(q[1], q[0], q[2]));
|
||||
}
|
||||
let index = self.blocks.len();
|
||||
self.blocks.push(block);
|
||||
// This block's face on each open face of the old block.
|
||||
for (list, &f) in on_open.iter_mut().zip(open) {
|
||||
let Some(&(od, os)) = FACE_SIDES.get(usize::from(f) - 1) else { continue };
|
||||
let local = if od == e1 {
|
||||
0
|
||||
} else if od == e2 {
|
||||
1
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
let local = if flipped { 1 - local } else { local };
|
||||
list.push((index, face_number(local, os)));
|
||||
}
|
||||
}
|
||||
Ok(on_open)
|
||||
}
|
||||
|
||||
/// The hex8 face of block `b` whose outward direction is nearest `dir`.
|
||||
pub fn face_towards(&self, b: usize, dir: P) -> Result<u8> {
|
||||
let c = self.corners(b)?;
|
||||
let centre = scale(c.iter().fold([0.0; 3], |a, p| add(a, *p)), 0.125);
|
||||
let mut best = (f64::NEG_INFINITY, 1u8);
|
||||
for (fi, (d, s)) in FACE_SIDES.iter().enumerate() {
|
||||
let fc = CORNERS.iter().zip(&c).filter(|(bits, _)| bits[*d] == *s).fold([0.0; 3], |a, (_, p)| add(a, *p));
|
||||
let out = sub(scale(fc, 0.25), centre);
|
||||
let l = crate::vec3::norm(out);
|
||||
let score = if l > 0.0 { crate::vec3::dot(out, dir) / l } else { f64::NEG_INFINITY };
|
||||
if score > best.0 {
|
||||
best = (score, fi as u8 + 1);
|
||||
}
|
||||
}
|
||||
Ok(best.1)
|
||||
}
|
||||
|
||||
/// The corners of a block face in cyclic order.
|
||||
fn face_quad(&self, b: usize, f: u8) -> Result<[u32; 4]> {
|
||||
let (d, s) = FACE_SIDES.get(usize::from(f).wrapping_sub(1)).copied().ok_or_else(|| Error::Invalid(format!("face {f} is not a hex8 face")))?;
|
||||
let block = self.blocks.get(b).copied().ok_or_else(|| Error::Invalid(format!("there is no block {b}")))?;
|
||||
let (e1, e2) = match d {
|
||||
0 => (1, 2),
|
||||
1 => (0, 2),
|
||||
_ => (0, 1),
|
||||
};
|
||||
Ok([[0, 0], [1, 0], [1, 1], [0, 1]].map(|[a, bb]| {
|
||||
let mut bits = [0; 3];
|
||||
bits[d] = s;
|
||||
bits[e1] = a;
|
||||
bits[e2] = bb;
|
||||
block[corner_at(bits)]
|
||||
}))
|
||||
}
|
||||
|
||||
/// Fill the space between two block faces with `layers` blocks, each face's corner matched
|
||||
/// to the nearest of the other's (over the eight ways of laying one quad on the other).
|
||||
/// Returns the new blocks.
|
||||
pub fn connect(&mut self, from: (usize, u8), to: (usize, u8), layers: usize) -> Result<Vec<usize>> {
|
||||
if !(1..=1000).contains(&layers) {
|
||||
return Err(Error::Invalid(format!("a connection has 1 to 1000 layers, got {layers}")));
|
||||
}
|
||||
let a = self.face_quad(from.0, from.1)?;
|
||||
let b = self.face_quad(to.0, to.1)?;
|
||||
let verts = self.vertices.clone();
|
||||
let at = |v: u32| verts.get(v as usize).copied().unwrap_or([0.0; 3]);
|
||||
let mut best: Option<(f64, [u32; 4])> = None;
|
||||
for rev in [false, true] {
|
||||
for shift in 0..4 {
|
||||
let m: [u32; 4] = [0, 1, 2, 3].map(|i| {
|
||||
let k = if rev { (4 + shift - i) % 4 } else { (shift + i) % 4 };
|
||||
b[k]
|
||||
});
|
||||
let cost: f64 = (0..4).map(|i| dist(at(a[i]), at(m[i]))).sum();
|
||||
if best.is_none_or(|x| cost < x.0) {
|
||||
best = Some((cost, m));
|
||||
}
|
||||
}
|
||||
}
|
||||
let (_, m) = best.ok_or_else(|| Error::Invalid("no way to match the faces".into()))?;
|
||||
// Intermediate layers of vertices, then the blocks.
|
||||
let mut rows: Vec<[u32; 4]> = vec![a];
|
||||
for l in 1..layers {
|
||||
let t = l as f64 / layers as f64;
|
||||
let pts = [0, 1, 2, 3].map(|i| crate::vec3::lerp(at(a[i]), at(m[i]), t));
|
||||
let row = pts.map(|p| {
|
||||
self.vertices.push(p);
|
||||
self.vertices.len() as u32 - 1
|
||||
});
|
||||
rows.push(row);
|
||||
}
|
||||
rows.push(m);
|
||||
let mut made = Vec::new();
|
||||
for w in rows.windows(2) {
|
||||
let (q, t) = (w[0], w[1]);
|
||||
let mut block = [q[0], q[1], q[2], q[3], t[0], t[1], t[2], t[3]];
|
||||
let pos = |blk: &[u32; 8]| blk.map(|v| self.vertices.get(v as usize).copied().unwrap_or([0.0; 3]));
|
||||
if min_corner_det(&pos(&block)) < 0.0 {
|
||||
block = [q[0], q[3], q[2], q[1], t[0], t[3], t[2], t[1]];
|
||||
}
|
||||
made.push(self.blocks.len());
|
||||
self.blocks.push(block);
|
||||
}
|
||||
Ok(made)
|
||||
}
|
||||
|
||||
/// Remove blocks (the others keep their order and are renumbered).
|
||||
pub fn remove(&mut self, blocks: &[usize]) -> Result<()> {
|
||||
if let Some(b) = blocks.iter().find(|b| **b >= self.blocks.len()) {
|
||||
return Err(Error::Invalid(format!("there is no block {b}")));
|
||||
}
|
||||
let mut k = 0;
|
||||
self.blocks.retain(|_| {
|
||||
k += 1;
|
||||
!blocks.contains(&(k - 1))
|
||||
});
|
||||
self.compact();
|
||||
if self.blocks.is_empty() { Err(Error::Invalid("every block was removed".into())) } else { Ok(()) }
|
||||
}
|
||||
|
||||
/// Extrude block faces `(block, hex8 face)` by `offset` in `layers` blocks each; the faces'
|
||||
/// shared vertices stay shared. Returns the new blocks' indices, the last layer's outer face
|
||||
/// is face 2 of the blocks made from it.
|
||||
pub fn extrude(&mut self, faces: &[(usize, u8)], offset: P, layers: usize) -> Result<Vec<usize>> {
|
||||
if !(1..=1000).contains(&layers) {
|
||||
return Err(Error::Invalid(format!("an extrusion has 1 to 1000 layers, got {layers}")));
|
||||
}
|
||||
if !offset.iter().all(|x| x.is_finite()) {
|
||||
return Err(Error::Invalid("the extrusion offset must be finite".into()));
|
||||
}
|
||||
let mut quads = Vec::new();
|
||||
for &(b, f) in faces {
|
||||
let (d, s) =
|
||||
FACE_SIDES.get(usize::from(f).wrapping_sub(1)).copied().ok_or_else(|| Error::Invalid(format!("face {f} is not a hex8 face")))?;
|
||||
let block = self.blocks.get(b).copied().ok_or_else(|| Error::Invalid(format!("there is no block {b}")))?;
|
||||
let (e1, e2) = match d {
|
||||
0 => (1, 2),
|
||||
1 => (0, 2),
|
||||
_ => (0, 1),
|
||||
};
|
||||
let q = [[0, 0], [1, 0], [1, 1], [0, 1]].map(|[a, bb]| {
|
||||
let mut bits = [0; 3];
|
||||
bits[d] = s;
|
||||
bits[e1] = a;
|
||||
bits[e2] = bb;
|
||||
block[corner_at(bits)]
|
||||
});
|
||||
quads.push(q);
|
||||
}
|
||||
let mut made = Vec::new();
|
||||
let mut layer_of: BTreeMap<u32, u32> = BTreeMap::new();
|
||||
let mut base: Vec<[u32; 4]> = quads;
|
||||
for _ in 0..layers {
|
||||
layer_of.clear();
|
||||
let mut next = Vec::new();
|
||||
for q in &base {
|
||||
let mut top = [0u32; 4];
|
||||
for (t, &v) in top.iter_mut().zip(q) {
|
||||
*t = match layer_of.get(&v) {
|
||||
Some(&w) => w,
|
||||
None => {
|
||||
let p = self.vertices.get(v as usize).copied().ok_or_else(|| Error::Invalid(format!("vertex {v} does not exist")))?;
|
||||
let w = self.vertices.len() as u32;
|
||||
self.vertices.push(add(p, scale(offset, 1.0 / layers as f64)));
|
||||
layer_of.insert(v, w);
|
||||
w
|
||||
}
|
||||
};
|
||||
}
|
||||
let mut block = [q[0], q[1], q[2], q[3], top[0], top[1], top[2], top[3]];
|
||||
let pos = |blk: &[u32; 8]| blk.map(|v| self.vertices.get(v as usize).copied().unwrap_or([0.0; 3]));
|
||||
if min_corner_det(&pos(&block)) < 0.0 {
|
||||
block = [q[0], q[3], q[2], q[1], top[0], top[3], top[2], top[1]];
|
||||
}
|
||||
made.push(self.blocks.len());
|
||||
self.blocks.push(block);
|
||||
next.push(top);
|
||||
}
|
||||
base = next;
|
||||
}
|
||||
Ok(made)
|
||||
}
|
||||
|
||||
/// The seven-block sphere: an inner cube with its corners at `inset · radius` from the
|
||||
/// centre and six blocks between the cube's faces and the sphere (the cube-to-sphere O-grid).
|
||||
pub fn sphere(center: P, radius: f64, inset: f64) -> Result<Topology> {
|
||||
if !(radius > 0.0 && radius.is_finite()) || !center.iter().all(|x| x.is_finite()) {
|
||||
return Err(Error::Invalid(format!("a sphere needs a finite centre and a positive radius, got {radius}")));
|
||||
}
|
||||
let h = radius / 3f64.sqrt();
|
||||
let mut t = Topology::grid([&[center[0] - h, center[0] + h], &[center[1] - h, center[1] + h], &[center[2] - h, center[2] + h]])?;
|
||||
t.o_grid(0, &[], [inset; 3])?;
|
||||
Ok(t)
|
||||
}
|
||||
|
||||
/// The butterfly (five-block O-grid of a disc) of radius `radius` about `center` in the plane
|
||||
/// normal to `axis`, stacked through `levels` (distances along `axis` from `center`,
|
||||
/// increasing): a centre square with its corners at `inset · radius` and four blocks around
|
||||
/// it out to the circle, per level.
|
||||
pub fn butterfly(center: P, axis: P, radius: f64, levels: &[f64], inset: f64) -> Result<Topology> {
|
||||
if !(radius > 0.0 && radius.is_finite()) || !center.iter().all(|x| x.is_finite()) {
|
||||
return Err(Error::Invalid(format!("a butterfly needs a finite centre and a positive radius, got {radius}")));
|
||||
}
|
||||
let z = crate::vec3::norm(axis);
|
||||
if !(z > 0.0 && z.is_finite()) {
|
||||
return Err(Error::Invalid("the butterfly's axis must be a nonzero direction".into()));
|
||||
}
|
||||
let w = scale(axis, 1.0 / z);
|
||||
let t0 = if w[0].abs() < 0.9 { [1.0, 0.0, 0.0] } else { [0.0, 1.0, 0.0] };
|
||||
let u = {
|
||||
let c = crate::vec3::cross(w, t0);
|
||||
scale(c, 1.0 / crate::vec3::norm(c))
|
||||
};
|
||||
let v = crate::vec3::cross(w, u);
|
||||
let h = radius / 2f64.sqrt();
|
||||
let grid = Topology::grid([&[-h, h], &[-h, h], levels])?;
|
||||
let mut t = Topology {
|
||||
vertices: grid.vertices.iter().map(|p| add(center, add(add(scale(u, p[0]), scale(v, p[1])), scale(w, p[2])))).collect(),
|
||||
blocks: grid.blocks,
|
||||
};
|
||||
for b in 0..levels.len() - 1 {
|
||||
t.o_grid(b, &[1, 2], [inset; 3])?;
|
||||
}
|
||||
t.merge(1e-9 * (radius + levels.iter().fold(0.0f64, |m, x| m.max(x.abs()))));
|
||||
Ok(t)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user