forked from 1b-agent/fieldcraft-upstream
When the grid layout does not fit a swept body (sides at an angle to the axes, root arcs where an arm meets its bosses), mesh.plan tries the ring-and-pillow layout (cmd/plan/pillow.rs): each boss is an annulus from its bore or pedal hole to its circle through every level, the arm a grid carved a margin inside the outline and the bosses, with a pillow layer of blocks to the wall each boundary face faces; a grid corner facing a boss and the outline gets a corner block reaching the kink where the outline leaves the boss's circle. The old crank test (frame cuts) becomes the heavy acceptance test at sizes 2.5, 3 and 4; the moved frame cut keeps a unit test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
1129 lines
48 KiB
Rust
1129 lines
48 KiB
Rust
//! The ring-and-pillow layout of a mesh plan (#455), for a body swept along the plan's direction
|
||
//! whose section the grid layout ([`super::blocks`]) does not fit: sides at an angle to the axes
|
||
//! and arcs where an arm runs into its bosses, as on a crank arm. The body is read in the local
|
||
//! frame of [`super::blocks`] (third axis along the sweep):
|
||
//! - **Levels.** The planar faces square to the sweep cut the layout into slabs. One section (the
|
||
//! slab with the most material) is the reference: its outline, and every other slab's section
|
||
//! is made of the same walls (a pocket's floor carves grid cells, a boss's top stands above the
|
||
//! arm on its ring alone).
|
||
//! - **Rings.** Each boss (a butterfly ring of the plan) is an annulus of blocks from the hole
|
||
//! inside it (the circle of a concentric O-grid ring, or a hole of the section inside the
|
||
//! circle: a bore, with a block corner at the middle of each of its corner fillets) out to the
|
||
//! boss's circle, through every level, so a boss's top stands on the blocks the arm joins.
|
||
//! - **Carved grid.** The rest of the section (the arm) is a grid of planes square to the in-plane
|
||
//! axes, carved to the cells a margin inside the outline and outside every boss: the planar faces
|
||
//! square to an axis (a pocket's sides, whose corner fillets the projection meets with the grid
|
||
//! corner), the planes a margin off each boss's circle, and planes every few element sizes
|
||
//! (closer across the arm than along it, so the carved boundary follows a taper).
|
||
//! - **Pillow.** A layer of blocks between the carved grid's boundary and the wall each boundary
|
||
//! face faces: the outline, or a boss's circle where the arm runs into it (there the pillow's outer
|
||
//! vertices are the annulus's). This is pillowing (Mitchell and Tautges 1995) between a carved grid
|
||
//! and the geometry, as the grid-based methods project their boundary layer (Schneiders 1996).
|
||
//! Each boundary vertex goes along the mean normal of its boundary faces to its wall, the
|
||
//! landings kept in order along the wall and apart: a corner whose landing would pass or crowd
|
||
//! a neighbour's goes between its neighbours' in proportion to its two faces, inside its fan. A
|
||
//! grid corner whose faces face a boss and the outline lands on each and gets a corner block
|
||
//! reaching the kink where the outline leaves the boss's circle: a junction that meets the
|
||
//! circle at an angle (an arm's root fillet that is not tangent to the boss).
|
||
//!
|
||
//! Each slab keeps the blocks whose middle lies inside the body (generalized winding number, as
|
||
//! [`super::blocks`] carves). A body this layout does not fit is reported with the reason; the plan
|
||
//! then keeps its frame cuts and swept cells.
|
||
//!
|
||
//! References: S. A. Mitchell, T. J. Tautges, *Pillowing doublets: refining a mesh to ensure that
|
||
//! faces share at most one edge*, Proc. 4th International Meshing Roundtable, 231–240, 1995;
|
||
//! R. Schneiders, *A grid-based algorithm for the generation of hexahedral element meshes*,
|
||
//! Engineering with Computers 12:168–177, 1996.
|
||
|
||
use std::collections::BTreeMap;
|
||
|
||
use fieldcraft_geometry::FaceSurface;
|
||
use fieldcraft_mesh::project::Topology;
|
||
use fieldcraft_model::RingKind;
|
||
|
||
use super::blocks::{Input, Layout, PERM, corner_det, winding};
|
||
|
||
type V2 = [f64; 2];
|
||
type V3 = [f64; 3];
|
||
|
||
/// Grid spacing along the carved region's longer extent, in element sizes.
|
||
const SPACING_ALONG: f64 = 2.5;
|
||
/// Grid spacing across it, in element sizes: the carved boundary steps with a taper.
|
||
const SPACING_ACROSS: f64 = 1.25;
|
||
/// The smallest distance of a kept grid cell's corner from a wall, in element sizes: the pillow's
|
||
/// least thickness.
|
||
const MARGIN: f64 = 0.6;
|
||
/// The largest arc of a boss's circle one annulus block spans where the boss is the outline (30°);
|
||
/// otherwise the arc the arm's landings on it span on average.
|
||
const MAX_OWN_ARC: f64 = std::f64::consts::PI / 6.0;
|
||
/// The smallest such arc (8°).
|
||
const MIN_OWN_ARC: f64 = std::f64::consts::PI / 22.5;
|
||
/// The least angle at which the outline leaves a boss's circle: a tangent junction leaves a sliver
|
||
/// no block corner takes.
|
||
const MIN_KINK: f64 = 10.0 * std::f64::consts::PI / 180.0;
|
||
/// How much further than the nearest wall a boundary face's normal may meet the wall it faces.
|
||
const FACING_REACH: f64 = 2.0;
|
||
/// The least width of a grid cell beside a plane that bounds a corner fillet, in fillet radii.
|
||
const FILLET_CLEARANCE: f64 = 0.7;
|
||
/// Most grid cells before carving.
|
||
const MAX_CELLS: usize = 20_000;
|
||
/// Most blocks of the layout.
|
||
const MAX_BLOCKS: usize = 20_000;
|
||
/// Most points of one section loop.
|
||
const MAX_LOOP: usize = 200_000;
|
||
/// The least distance between neighbouring landings, as a fraction of the boundary edge between
|
||
/// their vertices.
|
||
const LANDING_GAP: f64 = 0.5;
|
||
/// How far inside its fan a corner's landing stays: its faces' normals tilted toward each other
|
||
/// by atan(0.2), 11°.
|
||
const FAN_INSET: f64 = 0.2;
|
||
/// Rounds of the ordering of the landings.
|
||
const ORDER_ROUNDS: usize = 500;
|
||
/// How much the layout's quads may miss a slab's section by, as a fraction of its area.
|
||
const AREA_TOLERANCE: f64 = 0.03;
|
||
|
||
fn sub(a: V2, b: V2) -> V2 {
|
||
[a[0] - b[0], a[1] - b[1]]
|
||
}
|
||
|
||
fn add(a: V2, b: V2) -> V2 {
|
||
[a[0] + b[0], a[1] + b[1]]
|
||
}
|
||
|
||
fn scale(a: V2, s: f64) -> V2 {
|
||
[a[0] * s, a[1] * s]
|
||
}
|
||
|
||
fn dot(a: V2, b: V2) -> f64 {
|
||
a[0] * b[0] + a[1] * b[1]
|
||
}
|
||
|
||
fn cross(a: V2, b: V2) -> f64 {
|
||
a[0] * b[1] - a[1] * b[0]
|
||
}
|
||
|
||
fn norm(a: V2) -> f64 {
|
||
a[0].hypot(a[1])
|
||
}
|
||
|
||
fn unit(a: V2) -> Option<V2> {
|
||
let n = norm(a);
|
||
(n.is_finite() && n > 1e-300).then(|| scale(a, 1.0 / n))
|
||
}
|
||
|
||
/// The closest point to `p` on segment `a b`.
|
||
fn near_segment(p: V2, a: V2, b: V2) -> V2 {
|
||
let e = sub(b, a);
|
||
let l = dot(e, e);
|
||
let t = if l > 0.0 { (dot(sub(p, a), e) / l).clamp(0.0, 1.0) } else { 0.0 };
|
||
add(a, scale(e, t))
|
||
}
|
||
|
||
/// A closed loop of a section: its points, counterclockwise for an outline and clockwise for a
|
||
/// hole (material on the left), and the B-rep face of each segment `i → i + 1`.
|
||
#[derive(Debug, Clone)]
|
||
struct Loop {
|
||
pts: Vec<V2>,
|
||
face: Vec<u32>,
|
||
}
|
||
|
||
impl Loop {
|
||
fn segment(&self, i: usize) -> (V2, V2) {
|
||
let n = self.pts.len();
|
||
(self.pts.get(i % n).copied().unwrap_or_default(), self.pts.get((i + 1) % n).copied().unwrap_or_default())
|
||
}
|
||
|
||
fn area(&self) -> f64 {
|
||
(0..self.pts.len()).map(|i| self.segment(i)).map(|(a, b)| cross(a, b)).sum::<f64>() / 2.0
|
||
}
|
||
|
||
/// Even-odd point in polygon.
|
||
fn contains(&self, p: V2) -> bool {
|
||
let mut odd = false;
|
||
for i in 0..self.pts.len() {
|
||
let (a, b) = self.segment(i);
|
||
if (a[1] > p[1]) != (b[1] > p[1]) && p[0] < a[0] + (p[1] - a[1]) * (b[0] - a[0]) / (b[1] - a[1]) {
|
||
odd = !odd;
|
||
}
|
||
}
|
||
odd
|
||
}
|
||
|
||
fn distance(&self, p: V2) -> f64 {
|
||
(0..self.pts.len()).map(|i| self.segment(i)).map(|(a, b)| norm(sub(p, near_segment(p, a, b)))).fold(f64::INFINITY, f64::min)
|
||
}
|
||
|
||
/// The first point where the ray `p + t d` (t > `eps`) meets the loop: (t, point).
|
||
fn ray(&self, p: V2, d: V2, eps: f64) -> Option<(f64, V2)> {
|
||
let mut best: Option<(f64, V2)> = None;
|
||
for i in 0..self.pts.len() {
|
||
let (a, b) = self.segment(i);
|
||
let e = sub(b, a);
|
||
let den = cross(d, e);
|
||
if den.abs() < 1e-300 {
|
||
continue;
|
||
}
|
||
let w = sub(a, p);
|
||
let (t, u) = (cross(w, e) / den, cross(w, d) / den);
|
||
if t > eps && (-1e-12..=1.0 + 1e-12).contains(&u) && best.is_none_or(|x| t < x.0) {
|
||
best = Some((t, add(p, scale(d, t))));
|
||
}
|
||
}
|
||
best
|
||
}
|
||
|
||
/// The cumulative length at each point (the last entry the perimeter).
|
||
fn lengths(&self) -> Vec<f64> {
|
||
let mut out = Vec::with_capacity(self.pts.len() + 1);
|
||
out.push(0.0);
|
||
for i in 0..self.pts.len() {
|
||
let (a, b) = self.segment(i);
|
||
let last = out.last().copied().unwrap_or(0.0);
|
||
out.push(last + norm(sub(b, a)));
|
||
}
|
||
out
|
||
}
|
||
}
|
||
|
||
/// A boss of the plan in the local frame: its circle, the hole inside it (the inner loop of its
|
||
/// annulus) and the points of that hole that must be block corners (its corner fillets' middles).
|
||
#[derive(Debug, Clone)]
|
||
struct Boss {
|
||
c: V2,
|
||
r: f64,
|
||
inner: Loop,
|
||
corners: Vec<V2>,
|
||
}
|
||
|
||
/// A wall a boundary face of the carved grid faces.
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||
enum Wall {
|
||
Outline,
|
||
Boss(usize),
|
||
}
|
||
|
||
/// Slice the tessellation at local height `z`: the section's loops. Within a face the triangles
|
||
/// share points, so its crossings chain by mesh edge; the pieces of different faces chain by their
|
||
/// ends (each face carries its own copy of the points it shares, within `join`).
|
||
fn slice(inp: &Input, local: &dyn Fn(V3) -> V3, z: f64, join: f64) -> Result<Vec<Loop>, String> {
|
||
let m = inp.mesh;
|
||
// Segments as (start key, end key, start, end, face); a key is a mesh edge (its two points).
|
||
let mut segs: Vec<([u32; 2], [u32; 2], V2, V2, u32)> = Vec::new();
|
||
for (t, &f) in m.triangles.iter().zip(&m.tri_face) {
|
||
let Some(p) = t.iter().map(|&i| m.positions.get(i as usize).map(|q| local(*q))).collect::<Option<Vec<V3>>>() else { continue };
|
||
let (Some(&a), Some(&b), Some(&c)) = (p.first(), p.get(1), p.get(2)) else { continue };
|
||
let above = [a[2] > z, b[2] > z, c[2] > z];
|
||
if above.iter().all(|x| *x) || above.iter().all(|x| !*x) {
|
||
continue;
|
||
}
|
||
let mut cuts: Vec<([u32; 2], V2)> = Vec::with_capacity(2);
|
||
for (i, j) in [(0usize, 1usize), (1, 2), (2, 0)] {
|
||
if above[i] != above[j] {
|
||
let (pi, pj) = (p[i], p[j]);
|
||
let s = (z - pi[2]) / (pj[2] - pi[2]);
|
||
let key = [t[i].min(t[j]), t[i].max(t[j])];
|
||
cuts.push((key, [pi[0] + s * (pj[0] - pi[0]), pi[1] + s * (pj[1] - pi[1])]));
|
||
}
|
||
}
|
||
let [(ka, qa), (kb, qb)] = cuts.as_slice() else { continue };
|
||
// Material on the left: the segment runs along the outward normal turned +90°.
|
||
let n = {
|
||
let (u, v) = ([b[0] - a[0], b[1] - a[1], b[2] - a[2]], [c[0] - a[0], c[1] - a[1], c[2] - a[2]]);
|
||
[u[1] * v[2] - u[2] * v[1], u[2] * v[0] - u[0] * v[2]]
|
||
};
|
||
let along = [-n[1], n[0]];
|
||
if dot(sub(*qb, *qa), along) >= 0.0 {
|
||
segs.push((*ka, *kb, *qa, *qb, f));
|
||
} else {
|
||
segs.push((*kb, *ka, *qb, *qa, f));
|
||
}
|
||
}
|
||
if segs.len() > MAX_LOOP {
|
||
return Err(format!("the section at {z:.4} has {} segments", segs.len()));
|
||
}
|
||
// Pieces: chains within a face, by mesh edge.
|
||
let mut by_start: BTreeMap<(u32, [u32; 2]), usize> = BTreeMap::new();
|
||
for (i, s) in segs.iter().enumerate() {
|
||
by_start.insert((s.4, s.0), i);
|
||
}
|
||
let mut used = vec![false; segs.len()];
|
||
let mut pieces: Vec<Loop> = Vec::new();
|
||
// Start from segments no other segment of the face ends at, then from the rest (closed loops).
|
||
let mut ends: std::collections::BTreeSet<(u32, [u32; 2])> = std::collections::BTreeSet::new();
|
||
for s in &segs {
|
||
ends.insert((s.4, s.1));
|
||
}
|
||
let order: Vec<usize> = (0..segs.len()).filter(|&i| !ends.contains(&(segs[i].4, segs[i].0))).chain(0..segs.len()).collect();
|
||
for i in order {
|
||
if used.get(i).copied().unwrap_or(true) {
|
||
continue;
|
||
}
|
||
let mut piece = Loop { pts: Vec::new(), face: Vec::new() };
|
||
let mut k = i;
|
||
while let Some(s) = segs.get(k) {
|
||
if used[k] {
|
||
break;
|
||
}
|
||
used[k] = true;
|
||
piece.pts.push(s.2);
|
||
piece.face.push(s.4);
|
||
match by_start.get(&(s.4, s.1)) {
|
||
Some(&next) if !used[next] => k = next,
|
||
_ => {
|
||
piece.pts.push(s.3);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
pieces.push(piece);
|
||
}
|
||
// Loops: a piece's end joins the piece starting nearest it (within `join`).
|
||
let mut loops = Vec::new();
|
||
let mut taken = vec![false; pieces.len()];
|
||
for start in 0..pieces.len() {
|
||
if taken[start] {
|
||
continue;
|
||
}
|
||
taken[start] = true;
|
||
let mut lp = Loop { pts: Vec::new(), face: Vec::new() };
|
||
let mut cur = start;
|
||
for _ in 0..=pieces.len() {
|
||
let Some(p) = pieces.get(cur) else { break };
|
||
let n = p.face.len();
|
||
lp.pts.extend(p.pts.iter().take(n));
|
||
lp.face.extend(&p.face);
|
||
let end = p.pts.last().copied().unwrap_or_default();
|
||
let first = lp.pts.first().copied().unwrap_or_default();
|
||
if norm(sub(end, first)) <= join {
|
||
break;
|
||
}
|
||
let next = (0..pieces.len())
|
||
.filter(|&j| !taken[j])
|
||
.filter_map(|j| pieces.get(j).and_then(|q| q.pts.first()).map(|q| (norm(sub(*q, end)), j)))
|
||
.filter(|(d, _)| *d <= join)
|
||
.min_by(|a, b| a.0.total_cmp(&b.0));
|
||
match next {
|
||
Some((_, j)) => {
|
||
taken[j] = true;
|
||
cur = j;
|
||
}
|
||
None => return Err(format!("the section at {z:.4} does not close")),
|
||
}
|
||
}
|
||
if lp.pts.len() >= 3 {
|
||
loops.push(lp);
|
||
}
|
||
}
|
||
Ok(loops)
|
||
}
|
||
|
||
/// The grid planes over `[lo, hi]`: the fixed ones inside, and between them (and the ends) planes
|
||
/// `step` apart at most; a fixed plane bounding a corner fillet of radius `r` keeps the planes
|
||
/// beside it `r` away.
|
||
fn planes(lo: f64, hi: f64, fixed: &[(f64, f64)], step: f64, eps: f64) -> Vec<f64> {
|
||
let mut pts: Vec<(f64, f64)> = vec![(lo, 0.0), (hi, 0.0)];
|
||
pts.extend(fixed.iter().copied().filter(|(v, _)| *v > lo + eps && *v < hi - eps));
|
||
pts.sort_by(|a, b| a.0.total_cmp(&b.0));
|
||
pts.dedup_by(|a, b| {
|
||
if (a.0 - b.0).abs() < eps {
|
||
b.1 = b.1.max(a.1);
|
||
true
|
||
} else {
|
||
false
|
||
}
|
||
});
|
||
let mut out = Vec::new();
|
||
for w in pts.windows(2) {
|
||
let [(a, ra), (b, rb)] = [w[0], w[1]];
|
||
let len = b - a;
|
||
let mut n = (len / step).ceil().clamp(1.0, MAX_CELLS as f64) as usize;
|
||
// A filleted corner's grid corner moves 0.29 of the radius along each axis onto the
|
||
// fillet's middle: the cells beside it stay 0.7 of the radius wide.
|
||
while n > 1 && len / n as f64 + 1e-12 < FILLET_CLEARANCE * ra.max(rb) {
|
||
n -= 1;
|
||
}
|
||
out.extend((0..n).map(|k| a + len * k as f64 / n as f64));
|
||
}
|
||
out.push(hi);
|
||
out
|
||
}
|
||
|
||
/// The parameter of point `q` along the closed polyline with cumulative `lengths`, and the point
|
||
/// at a parameter.
|
||
struct Param<'a> {
|
||
lp: &'a Loop,
|
||
cum: Vec<f64>,
|
||
}
|
||
|
||
impl Param<'_> {
|
||
fn perimeter(&self) -> f64 {
|
||
self.cum.last().copied().unwrap_or(0.0)
|
||
}
|
||
|
||
fn of(&self, q: V2) -> f64 {
|
||
let mut best = (f64::INFINITY, 0.0);
|
||
for i in 0..self.lp.pts.len() {
|
||
let (a, b) = self.lp.segment(i);
|
||
let h = near_segment(q, a, b);
|
||
let d = norm(sub(q, h));
|
||
if d < best.0 {
|
||
best = (d, self.cum.get(i).copied().unwrap_or(0.0) + norm(sub(h, a)));
|
||
}
|
||
}
|
||
best.1
|
||
}
|
||
|
||
fn at(&self, s: f64) -> V2 {
|
||
let per = self.perimeter();
|
||
if !(per > 0.0) {
|
||
return self.lp.pts.first().copied().unwrap_or_default();
|
||
}
|
||
let s = s.rem_euclid(per);
|
||
let i = self.cum.partition_point(|c| *c <= s).saturating_sub(1).min(self.lp.pts.len().saturating_sub(1));
|
||
let (a, b) = self.lp.segment(i);
|
||
let (c0, c1) = (self.cum.get(i).copied().unwrap_or(0.0), self.cum.get(i + 1).copied().unwrap_or(per));
|
||
let t = if c1 > c0 { ((s - c0) / (c1 - c0)).clamp(0.0, 1.0) } else { 0.0 };
|
||
add(a, scale(sub(b, a), t))
|
||
}
|
||
}
|
||
|
||
/// The first point where the ray `p + t d` meets the circle about `c` of radius `r`.
|
||
fn ray_circle(p: V2, d: V2, c: V2, r: f64) -> Option<(f64, V2)> {
|
||
let w = sub(p, c);
|
||
let b = dot(w, d);
|
||
let disc = b * b - (dot(w, w) - r * r);
|
||
if disc < 0.0 {
|
||
return None;
|
||
}
|
||
[-b - disc.sqrt(), -b + disc.sqrt()].into_iter().find(|t| *t > 0.0).map(|t| (t, add(p, scale(d, t))))
|
||
}
|
||
|
||
/// Whether the quad's corners turn one way (convex, not degenerate).
|
||
fn convex(q: &[V2; 4]) -> bool {
|
||
let turns: Vec<f64> = (0..4).map(|t| cross(sub(q[(t + 1) % 4], q[t]), sub(q[(t + 2) % 4], q[(t + 1) % 4]))).collect();
|
||
turns.iter().all(|x| *x > 0.0) || turns.iter().all(|x| *x < 0.0)
|
||
}
|
||
|
||
/// Plan the ring-and-pillow block topology of the body.
|
||
// Several per-axis and per-loop arrays are indexed together.
|
||
#[allow(clippy::needless_range_loop)]
|
||
pub(crate) fn layout(inp: &Input) -> Result<Layout, String> {
|
||
let d = inp.direction;
|
||
let k = (0..3).find(|&k| (d[k].abs() - 1.0).abs() < 1e-9).ok_or("the sweep direction is not along an axis")?;
|
||
let perm = PERM[k];
|
||
let local = |p: V3| -> V3 { [p[perm[0]], p[perm[1]], p[perm[2]]] };
|
||
let world = |l: V3| -> V3 {
|
||
let mut w = [0.0; 3];
|
||
for a in 0..3 {
|
||
w[perm[a]] = l[a];
|
||
}
|
||
w
|
||
};
|
||
if inp.shell.is_some() {
|
||
return Err("a shelled body takes the grid layout's wall layers".into());
|
||
}
|
||
if inp.mesh.triangles.is_empty() {
|
||
return Err("the body has no tessellation".into());
|
||
}
|
||
let (mut lo, mut hi) = ([f64::INFINITY; 3], [f64::NEG_INFINITY; 3]);
|
||
for p in &inp.mesh.positions {
|
||
let q = local(*p);
|
||
for a in 0..3 {
|
||
lo[a] = lo[a].min(q[a]);
|
||
hi[a] = hi[a].max(q[a]);
|
||
}
|
||
}
|
||
let diag = (0..3).map(|a| (hi[a] - lo[a]).powi(2)).sum::<f64>().sqrt();
|
||
if !(diag.is_finite() && diag > 0.0 && inp.size.is_finite() && inp.size > 0.0) {
|
||
return Err("the body has no extent".into());
|
||
}
|
||
let eps = 1e-6 * diag;
|
||
let name = |f: usize| inp.shape.faces().get(f).map_or_else(|| format!("#{f}"), |x| x.name.clone());
|
||
let size = inp.size;
|
||
let margin = MARGIN * size;
|
||
|
||
// Levels: the planar faces square to the sweep; every other face along it.
|
||
let mut levels = vec![lo[2], hi[2]];
|
||
// Planar faces square to an in-plane axis: (axis, offset, face).
|
||
let mut sides: Vec<(usize, f64, usize)> = Vec::new();
|
||
for (f, s) in inp.surfaces.iter().enumerate() {
|
||
match s {
|
||
Some(FaceSurface::Plane { normal, point }) => {
|
||
let (n, q) = (local(*normal), local(*point));
|
||
if (n[2].abs() - 1.0).abs() < 1e-6 {
|
||
levels.push(q[2]);
|
||
} else if n[2].abs() > 1e-6 {
|
||
return Err(format!("face `{}` is a plane at an angle to the sweep", name(f)));
|
||
} else if let Some(a) = (0..2).find(|&a| (n[a].abs() - 1.0).abs() < 1e-6) {
|
||
sides.push((a, q[a], f));
|
||
}
|
||
}
|
||
Some(FaceSurface::Cylinder { axis, .. }) if (local(*axis)[2].abs() - 1.0).abs() < 1e-6 => {}
|
||
_ => return Err(format!("face `{}` is not prismatic along the sweep", name(f))),
|
||
}
|
||
}
|
||
levels.sort_by(f64::total_cmp);
|
||
levels.dedup_by(|a, b| (*a - *b).abs() < eps);
|
||
if levels.len() < 2 || levels.len() > 1001 {
|
||
return Err(format!("{} levels", levels.len()));
|
||
}
|
||
|
||
// The sections of the slabs, and the reference: the one with the most material.
|
||
let join = (4.0 * inp.mesh.weld_distance).max(1e-6 * diag);
|
||
let mut sections: Vec<Vec<Loop>> = Vec::with_capacity(levels.len() - 1);
|
||
for w in levels.windows(2) {
|
||
sections.push(slice(inp, &local, 0.5 * (w[0] + w[1]), join)?);
|
||
}
|
||
let area = |s: &[Loop]| s.iter().map(Loop::area).sum::<f64>();
|
||
let reference = (0..sections.len()).max_by(|a, b| area(§ions[*a]).total_cmp(&area(§ions[*b]))).ok_or("no slab")?;
|
||
let refs = sections.get(reference).ok_or("no reference section")?;
|
||
let outlines: Vec<&Loop> = refs.iter().filter(|l| l.area() > 0.0).collect();
|
||
let [outline] = outlines.as_slice() else {
|
||
return Err(format!("the widest section has {} outlines", outlines.len()));
|
||
};
|
||
let outline: &Loop = outline;
|
||
let holes: Vec<&Loop> = refs.iter().filter(|l| l.area() < 0.0).collect();
|
||
|
||
// The bosses: the plan's butterfly rings, each with the hole its annulus goes round.
|
||
let ring_at = |r: &fieldcraft_model::PlanRing| -> Result<V2, String> {
|
||
if (r.axis.iter().zip(&d).map(|(a, b)| a * b).sum::<f64>().abs() - 1.0).abs() > 1e-6 {
|
||
return Err(format!("the ring of F{} is not along the sweep", r.feature));
|
||
}
|
||
let c = local(r.center);
|
||
Ok([c[0], c[1]])
|
||
};
|
||
let mut bosses: Vec<Boss> = Vec::new();
|
||
for r in inp.rings.iter().filter(|r| r.kind == RingKind::Butterfly) {
|
||
let c = ring_at(r)?;
|
||
if bosses.iter().any(|b| norm(sub(b.c, c)) < eps) {
|
||
continue;
|
||
}
|
||
let inside: Vec<&&Loop> = holes.iter().filter(|h| h.pts.iter().all(|p| norm(sub(*p, c)) < r.radius - eps)).collect();
|
||
let [inner] = inside.as_slice() else {
|
||
return Err(format!("the boss of radius {:.4} holds {} holes (this layout rings a boss round one)", r.radius, inside.len()));
|
||
};
|
||
// Its corners: the middles of the runs of blend faces (corner fillets).
|
||
let mut corners = Vec::new();
|
||
let n = inner.pts.len();
|
||
let blend = |i: usize| inner.face.get(i % n).is_some_and(|f| inp.blends.contains(f));
|
||
if (0..n).any(|i| !blend(i)) {
|
||
let first = (0..n).find(|&i| !blend(i)).unwrap_or(0);
|
||
let mut i = first + 1;
|
||
while i <= first + n {
|
||
if blend(i) {
|
||
let start = i;
|
||
while blend(i) && i < start + n {
|
||
i += 1;
|
||
}
|
||
let run: Vec<V2> = (start..=i).map(|j| inner.pts.get(j % n).copied().unwrap_or_default()).collect();
|
||
let cum: Vec<f64> = run
|
||
.windows(2)
|
||
.scan(0.0, |acc, w| {
|
||
*acc += norm(sub(w[1], w[0]));
|
||
Some(*acc)
|
||
})
|
||
.collect();
|
||
let half = cum.last().copied().unwrap_or(0.0) / 2.0;
|
||
let j = cum.partition_point(|c| *c < half);
|
||
corners.push(run.get(j + 1).copied().unwrap_or_default());
|
||
}
|
||
i += 1;
|
||
}
|
||
}
|
||
bosses.push(Boss { c, r: r.radius, inner: (**inner).clone(), corners });
|
||
}
|
||
if bosses.is_empty() {
|
||
return Err("no boss to ring".into());
|
||
}
|
||
for r in inp.rings.iter().filter(|r| r.kind == RingKind::OGrid) {
|
||
let c = ring_at(r)?;
|
||
if !bosses.iter().any(|b| norm(sub(b.c, c)) < 1e-6 * b.r.max(1.0) && r.radius < b.r) {
|
||
return Err(format!("the hole of radius {:.4} is not inside a boss", r.radius));
|
||
}
|
||
}
|
||
for h in &holes {
|
||
if !bosses.iter().any(|b| b.inner.pts.first() == h.pts.first()) {
|
||
return Err("a hole of the section is not inside a boss".into());
|
||
}
|
||
}
|
||
let on_boss = |b: &Boss, p: V2| (norm(sub(p, b.c)) - b.r).abs() < 1e-4 * b.r.max(1.0) + join;
|
||
// Each boss's circle runs along the outline (its own side).
|
||
for b in &bosses {
|
||
if !outline.pts.iter().any(|p| on_boss(b, *p)) {
|
||
return Err(format!("the boss of radius {:.4} is not on the outline", b.r));
|
||
}
|
||
}
|
||
|
||
// The kinks: where the outline leaves a boss's circle, at an angle.
|
||
let n_out = outline.pts.len();
|
||
// A segment of the outline on a boss: on a cylindrical face about the boss's axis, of its radius.
|
||
let seg_on = |bi: usize, i: usize| {
|
||
let (Some(b), Some(&f)) = (bosses.get(bi), outline.face.get(i)) else { return false };
|
||
match inp.surfaces.get(f as usize) {
|
||
Some(Some(FaceSurface::Cylinder { origin, radius, .. })) => {
|
||
let o = local(*origin);
|
||
(radius - b.r).abs() < 1e-6 * b.r.max(1.0) && norm(sub([o[0], o[1]], b.c)) < 1e-6 * b.r.max(1.0)
|
||
}
|
||
_ => false,
|
||
}
|
||
};
|
||
let mut kinks: Vec<Vec<V2>> = vec![Vec::new(); bosses.len()];
|
||
for (bi, b) in bosses.iter().enumerate() {
|
||
for i in 0..n_out {
|
||
let prev = (i + n_out - 1) % n_out;
|
||
if seg_on(bi, prev) == seg_on(bi, i) {
|
||
continue;
|
||
}
|
||
let p = outline.pts.get(i).copied().unwrap_or_default();
|
||
// The outline's direction off the circle against the circle's tangent there.
|
||
let (a0, a1) = if seg_on(bi, i) { outline.segment(prev) } else { outline.segment(i) };
|
||
let dir = unit(sub(a1, a0)).ok_or("a degenerate outline segment")?;
|
||
let radial = unit(sub(p, b.c)).ok_or("a kink at a boss's centre")?;
|
||
let angle = dot(dir, radial).abs().clamp(0.0, 1.0).asin();
|
||
if angle < MIN_KINK {
|
||
return Err(format!("the outline meets the boss of radius {:.4} tangentially (at {:.1}°)", b.r, angle.to_degrees()));
|
||
}
|
||
kinks[bi].push(p);
|
||
}
|
||
}
|
||
|
||
// The carved region's extent: the outline outside the bosses.
|
||
let (mut rlo, mut rhi) = ([f64::INFINITY; 2], [f64::NEG_INFINITY; 2]);
|
||
for p in &outline.pts {
|
||
if bosses.iter().all(|b| norm(sub(*p, b.c)) > b.r + eps) {
|
||
for a in 0..2 {
|
||
rlo[a] = rlo[a].min(p[a]);
|
||
rhi[a] = rhi[a].max(p[a]);
|
||
}
|
||
}
|
||
}
|
||
if !(0..2).all(|a| rhi[a] - rlo[a] > 2.0 * margin) {
|
||
return Err("no arm between the bosses".into());
|
||
}
|
||
let along = if rhi[0] - rlo[0] >= rhi[1] - rlo[1] { 0 } else { 1 };
|
||
// Fixed planes: the planar faces square to an in-plane axis in the region (not a boss's
|
||
// hole), each with the largest corner fillet it bounds, and the planes a margin off each
|
||
// boss's circle.
|
||
let fillet_of = |a: usize, v: f64| -> f64 {
|
||
inp.blends
|
||
.iter()
|
||
.filter_map(|&f| match inp.surfaces.get(f as usize) {
|
||
Some(Some(FaceSurface::Cylinder { origin, radius, .. })) => {
|
||
let o = local(*origin);
|
||
((o[a] - v).abs() - radius).abs().lt(&(1e-4 * radius.max(1.0))).then_some(*radius)
|
||
}
|
||
_ => None,
|
||
})
|
||
.fold(0.0, f64::max)
|
||
};
|
||
let mut fixed: [Vec<(f64, f64)>; 2] = [Vec::new(), Vec::new()];
|
||
for &(a, v, f) in &sides {
|
||
let pts: Vec<V2> = inp
|
||
.mesh
|
||
.triangles
|
||
.iter()
|
||
.zip(&inp.mesh.tri_face)
|
||
.filter(|(_, tf)| **tf as usize == f)
|
||
.flat_map(|(t, _)| t.iter().filter_map(|&i| inp.mesh.positions.get(i as usize).map(|p| local(*p))))
|
||
.map(|p| [p[0], p[1]])
|
||
.collect();
|
||
if pts.iter().all(|p| bosses.iter().any(|b| norm(sub(*p, b.c)) < b.r)) {
|
||
continue;
|
||
}
|
||
fixed[a].push((v, fillet_of(a, v)));
|
||
}
|
||
// Along the arm, where it runs into the bosses.
|
||
for b in &bosses {
|
||
for s in [-1.0, 1.0] {
|
||
fixed[along].push((b.c[along] + s * (b.r + margin), 0.0));
|
||
}
|
||
}
|
||
let spacing = |a: usize| if a == along { SPACING_ALONG * size } else { SPACING_ACROSS * size };
|
||
let gp: [Vec<f64>; 2] = std::array::from_fn(|a| planes(rlo[a], rhi[a], &fixed[a], spacing(a), eps));
|
||
let (nu, nv) = (gp[0].len() - 1, gp[1].len() - 1);
|
||
if nu * nv > MAX_CELLS {
|
||
return Err(format!("a grid of {nu}×{nv} cells is too large for one layout"));
|
||
}
|
||
let gpt = |i: usize, j: usize| -> V2 { [gp[0].get(i).copied().unwrap_or_default(), gp[1].get(j).copied().unwrap_or_default()] };
|
||
|
||
// The kept cells: every corner inside the outline, `margin` off it and off every boss.
|
||
let clear = |p: V2| outline.contains(p) && outline.distance(p) >= margin - eps && bosses.iter().all(|b| norm(sub(p, b.c)) >= b.r + margin - eps);
|
||
let corner_ok: Vec<Vec<bool>> = (0..=nu).map(|i| (0..=nv).map(|j| clear(gpt(i, j))).collect()).collect();
|
||
let ok = |i: usize, j: usize| corner_ok.get(i).and_then(|c| c.get(j)).copied().unwrap_or(false);
|
||
let mut kept: std::collections::BTreeSet<(usize, usize)> = std::collections::BTreeSet::new();
|
||
for i in 0..nu {
|
||
for j in 0..nv {
|
||
if ok(i, j) && ok(i + 1, j) && ok(i, j + 1) && ok(i + 1, j + 1) {
|
||
kept.insert((i, j));
|
||
}
|
||
}
|
||
}
|
||
// Two cells meeting at a corner alone: the one with fewer neighbours goes.
|
||
for _ in 0..=nu * nv {
|
||
let mut changed = false;
|
||
for i in 1..nu {
|
||
for j in 1..nv {
|
||
let q = [kept.contains(&(i - 1, j - 1)), kept.contains(&(i, j - 1)), kept.contains(&(i, j)), kept.contains(&(i - 1, j))];
|
||
let pair = match q {
|
||
[true, false, true, false] => [(i - 1, j - 1), (i, j)],
|
||
[false, true, false, true] => [(i, j - 1), (i - 1, j)],
|
||
_ => continue,
|
||
};
|
||
let neighbours = |c: (usize, usize)| {
|
||
[(1i64, 0i64), (-1, 0), (0, 1), (0, -1)]
|
||
.iter()
|
||
.filter(|(a, b)| kept.contains(&((c.0 as i64 + a) as usize, (c.1 as i64 + b) as usize)))
|
||
.count()
|
||
};
|
||
let gone = if neighbours(pair[0]) <= neighbours(pair[1]) { pair[0] } else { pair[1] };
|
||
kept.remove(&gone);
|
||
changed = true;
|
||
}
|
||
}
|
||
if !changed {
|
||
break;
|
||
}
|
||
}
|
||
if kept.is_empty() {
|
||
return Err("the arm has no grid cell a margin inside its walls".into());
|
||
}
|
||
|
||
// The carved boundary: one loop of grid edges, the cells on its left.
|
||
let mut next: BTreeMap<(usize, usize), (usize, usize)> = BTreeMap::new();
|
||
for &(i, j) in &kept {
|
||
let sides = [
|
||
((i, j), (i + 1, j), j.checked_sub(1).map(|jj| (i, jj))),
|
||
((i + 1, j), (i + 1, j + 1), Some((i + 1, j))),
|
||
((i + 1, j + 1), (i, j + 1), Some((i, j + 1))),
|
||
((i, j + 1), (i, j), i.checked_sub(1).map(|ii| (ii, j))),
|
||
];
|
||
for (a, b, nb) in sides {
|
||
if nb.is_none_or(|c| !kept.contains(&c)) && next.insert(a, b).is_some() {
|
||
return Err("the carved grid pinches".into());
|
||
}
|
||
}
|
||
}
|
||
let start = next.keys().next().copied().ok_or("no carved boundary")?;
|
||
let mut ring: Vec<(usize, usize)> = vec![start];
|
||
for _ in 0..next.len() {
|
||
let Some(&b) = ring.last().and_then(|v| next.get(v)) else { break };
|
||
if b == start {
|
||
break;
|
||
}
|
||
ring.push(b);
|
||
}
|
||
if ring.len() != next.len() {
|
||
return Err("the carved grid has more than one boundary".into());
|
||
}
|
||
let n = ring.len();
|
||
let pts: Vec<V2> = ring.iter().map(|&(i, j)| gpt(i, j)).collect();
|
||
let at = |k: usize| pts.get(k % n).copied().unwrap_or_default();
|
||
let normal = |k: usize| -> V2 {
|
||
let e = unit(sub(at(k + 1), at(k))).unwrap_or([1.0, 0.0]);
|
||
[e[1], -e[0]]
|
||
};
|
||
|
||
// A boss's circle as a wall: where the arm runs into it (its points inside the outline).
|
||
let internal = |q: V2| outline.contains(q) && outline.distance(q) > join;
|
||
let boss_distance = |bi: usize, p: V2| -> f64 {
|
||
let Some(b) = bosses.get(bi) else { return f64::INFINITY };
|
||
let q = add(b.c, scale(unit(sub(p, b.c)).unwrap_or([1.0, 0.0]), b.r));
|
||
if internal(q) { (norm(sub(p, b.c)) - b.r).abs() } else { f64::INFINITY }
|
||
};
|
||
let wall_distance = |w: Wall, p: V2| match w {
|
||
Wall::Outline => outline.distance(p),
|
||
Wall::Boss(bi) => boss_distance(bi, p),
|
||
};
|
||
// The wall each boundary edge faces: the first its normal meets (a boss's circle where the
|
||
// arm runs into it, or the outline), unless that is much further than the nearest wall (a
|
||
// step of the carved boundary facing a slanted side): then the nearest.
|
||
let walls: Vec<Wall> = (0..n)
|
||
.map(|k| {
|
||
let mid = scale(add(at(k), at(k + 1)), 0.5);
|
||
let nk = normal(k);
|
||
let mut candidates: Vec<(Wall, f64)> =
|
||
(0..bosses.len()).map(Wall::Boss).chain([Wall::Outline]).map(|w| (w, wall_distance(w, mid))).collect();
|
||
candidates.sort_by(|a, b| a.1.total_cmp(&b.1));
|
||
let nearest = candidates.first().copied().unwrap_or((Wall::Outline, 0.0));
|
||
let mut first: Option<(Wall, f64)> = outline.ray(mid, nk, eps).map(|h| (Wall::Outline, h.0));
|
||
for (bi, b) in bosses.iter().enumerate() {
|
||
if let Some((t, q)) = ray_circle(mid, nk, b.c, b.r)
|
||
&& internal(q)
|
||
&& first.is_none_or(|f| t < f.1)
|
||
{
|
||
first = Some((Wall::Boss(bi), t));
|
||
}
|
||
}
|
||
match first {
|
||
Some((w, t)) if t <= FACING_REACH * nearest.1 => w,
|
||
_ => nearest.0,
|
||
}
|
||
})
|
||
.collect();
|
||
let wall = |k: usize| walls.get(k % n).copied().unwrap_or(Wall::Outline);
|
||
|
||
// The landings: parameters along the walls (arc length on the outline, angle on a boss).
|
||
let along_outline = Param { lp: outline, cum: outline.lengths() };
|
||
let param = |w: Wall, p: V2| -> f64 {
|
||
match w {
|
||
Wall::Outline => along_outline.of(p),
|
||
Wall::Boss(b) => bosses.get(b).map_or(0.0, |b| (p[1] - b.c[1]).atan2(p[0] - b.c[0])),
|
||
}
|
||
};
|
||
let point = |w: Wall, s: f64| -> V2 {
|
||
match w {
|
||
Wall::Outline => along_outline.at(s),
|
||
Wall::Boss(b) => bosses.get(b).map_or([0.0; 2], |b| add(b.c, [b.r * s.cos(), b.r * s.sin()])),
|
||
}
|
||
};
|
||
let period = |w: Wall| match w {
|
||
Wall::Outline => along_outline.perimeter(),
|
||
Wall::Boss(_) => std::f64::consts::TAU,
|
||
};
|
||
// A vertex's landing on wall `w` along `dir`; the nearest point of the wall when the ray
|
||
// misses it or meets it much further than that.
|
||
let hit = |w: Wall, p: V2, dir: V2| -> Option<V2> {
|
||
let ray = match w {
|
||
Wall::Outline => outline.ray(p, dir, eps).map(|h| h.1),
|
||
Wall::Boss(b) => bosses.get(b).and_then(|b| ray_circle(p, dir, b.c, b.r)).map(|h| h.1),
|
||
};
|
||
let reach = FACING_REACH * wall_distance(w, p);
|
||
match ray {
|
||
Some(q) if norm(sub(q, p)) <= reach => Some(q),
|
||
_ => match w {
|
||
Wall::Outline => Some(along_outline.at(along_outline.of(p))),
|
||
Wall::Boss(b) => bosses.get(b).map(|b| add(b.c, scale(unit(sub(p, b.c)).unwrap_or([1.0, 0.0]), b.r))),
|
||
},
|
||
}
|
||
};
|
||
let junction: Vec<bool> = (0..n).map(|k| wall(k + n - 1) != wall(k)).collect();
|
||
// Per vertex: its landing's parameter on the wall of the edge before it and after it (one
|
||
// landing, unless a junction).
|
||
let (mut pin, mut pout) = (vec![0.0; n], vec![0.0; n]);
|
||
let mut fixed_landing = vec![false; n];
|
||
for k in 0..n {
|
||
let (w0, w1) = (wall(k + n - 1), wall(k));
|
||
let v = at(k);
|
||
if junction[k] {
|
||
let a = hit(w0, v, normal(k + n - 1)).ok_or("a corner's ray misses its wall")?;
|
||
let b = hit(w1, v, normal(k)).ok_or("a corner's ray misses its wall")?;
|
||
pin[k] = param(w0, a);
|
||
pout[k] = param(w1, b);
|
||
fixed_landing[k] = true;
|
||
} else {
|
||
let (n0, n1) = (normal(k + n - 1), normal(k));
|
||
let dir = unit(add(n0, n1)).unwrap_or(n1);
|
||
let q = hit(w1, v, dir).ok_or("a boundary vertex's ray misses its wall")?;
|
||
pin[k] = param(w1, q);
|
||
pout[k] = pin[k];
|
||
fixed_landing[k] = cross(n0, n1).abs() < 1e-12;
|
||
}
|
||
}
|
||
let unwrap = |x: f64, near: f64, w: Wall| {
|
||
let per = period(w);
|
||
x - per * ((x - near) / per).round()
|
||
};
|
||
// A corner's landing stays inside its corner's fan: between the rays along its two faces'
|
||
// normals, each tilted toward the other by atan(FAN_INSET).
|
||
let fan: Vec<Option<(f64, f64)>> = (0..n)
|
||
.map(|k| {
|
||
if fixed_landing[k] {
|
||
return None;
|
||
}
|
||
let (n0, n1) = (normal(k + n - 1), normal(k));
|
||
let w = wall(k);
|
||
let turn = |a: V2, b: V2| unit(add(a, scale(b, FAN_INSET)));
|
||
let (d0, d1) = (turn(n0, n1)?, turn(n1, n0)?);
|
||
let ray = |dir: V2| match w {
|
||
Wall::Outline => outline.ray(at(k), dir, eps).map(|h| h.1),
|
||
Wall::Boss(b) => bosses.get(b).and_then(|b| ray_circle(at(k), dir, b.c, b.r)).map(|h| h.1),
|
||
};
|
||
let (p0, p1) = (unwrap(param(w, ray(d0)?), pout[k], w), unwrap(param(w, ray(d1)?), pout[k], w));
|
||
Some((p0.min(p1), p0.max(p1)))
|
||
})
|
||
.collect();
|
||
// A corner's landing goes between its neighbours' in proportion to its two boundary edges'
|
||
// lengths (within its fan) when it passes a neighbour's, or comes within half an edge's
|
||
// length of it (a step of the carved boundary, whose corners' bisectors land almost on the
|
||
// next vertex's).
|
||
let length_of = |w: Wall| match w {
|
||
Wall::Outline => 1.0,
|
||
Wall::Boss(b) => bosses.get(b).map_or(1.0, |b| b.r),
|
||
};
|
||
for _ in 0..ORDER_ROUNDS {
|
||
let mut changed = false;
|
||
for k in 0..n {
|
||
if fixed_landing[k] {
|
||
continue;
|
||
}
|
||
let w = wall(k);
|
||
let a = unwrap(pout[(k + n - 1) % n], pout[k], w);
|
||
let b = unwrap(pin[(k + 1) % n], pout[k], w);
|
||
let (la, lb) = (norm(sub(at(k), at(k + n - 1))), norm(sub(at(k + 1), at(k))));
|
||
let scale_w = length_of(w);
|
||
let cramped = (pout[k] - a).abs() * scale_w < LANDING_GAP * la || (b - pout[k]).abs() * scale_w < LANDING_GAP * lb;
|
||
let mut target = a + (b - a) * la / (la + lb).max(1e-300);
|
||
if let Some(Some((lo, hi))) = fan.get(k) {
|
||
let (lo, hi) = (unwrap(*lo, target, w), unwrap(*hi, target, w));
|
||
target = target.clamp(lo.min(hi), lo.max(hi));
|
||
}
|
||
if (!(a.min(b) < pout[k] && pout[k] < a.max(b)) || cramped) && (target - pout[k]).abs() * scale_w > eps {
|
||
pout[k] = target;
|
||
pin[k] = target;
|
||
changed = true;
|
||
}
|
||
}
|
||
if !changed {
|
||
break;
|
||
}
|
||
}
|
||
|
||
// The 2D vertices and quads.
|
||
let mut v2: Vec<V2> = Vec::new();
|
||
let mut grid_id: BTreeMap<(usize, usize), usize> = BTreeMap::new();
|
||
let mut gid = |i: usize, j: usize, v2: &mut Vec<V2>| -> usize {
|
||
*grid_id.entry((i, j)).or_insert_with(|| {
|
||
v2.push(gpt(i, j));
|
||
v2.len() - 1
|
||
})
|
||
};
|
||
let mut quads: Vec<[usize; 4]> = Vec::new();
|
||
for &(i, j) in &kept {
|
||
let q = [gid(i, j, &mut v2), gid(i + 1, j, &mut v2), gid(i + 1, j + 1, &mut v2), gid(i, j + 1, &mut v2)];
|
||
quads.push(q);
|
||
}
|
||
let base: Vec<usize> = ring.iter().map(|&(i, j)| gid(i, j, &mut v2)).collect();
|
||
// Landings: one point per vertex, two at a junction.
|
||
let mut land_in = vec![0usize; n];
|
||
let mut land_out = vec![0usize; n];
|
||
for k in 0..n {
|
||
let w0 = wall(k + n - 1);
|
||
v2.push(point(w0, pin[k]));
|
||
land_in[k] = v2.len() - 1;
|
||
if junction[k] {
|
||
v2.push(point(wall(k), pout[k]));
|
||
land_out[k] = v2.len() - 1;
|
||
} else {
|
||
land_out[k] = land_in[k];
|
||
}
|
||
}
|
||
let mut checked: Vec<[usize; 4]> = Vec::new();
|
||
for k in 0..n {
|
||
checked.push([base[k], base[(k + 1) % n], land_in[(k + 1) % n], land_out[k]]);
|
||
}
|
||
// Corner blocks: a junction's two landings and the kink between them.
|
||
let mut kink_id: BTreeMap<(usize, usize), usize> = BTreeMap::new();
|
||
let mut junction_kink: Vec<Option<usize>> = vec![None; n];
|
||
for k in 0..n {
|
||
if !junction[k] {
|
||
continue;
|
||
}
|
||
let bi = match (wall(k + n - 1), wall(k)) {
|
||
(Wall::Boss(b), _) | (_, Wall::Boss(b)) => b,
|
||
_ => return Err("a corner between two walls neither of which is a boss".into()),
|
||
};
|
||
let v = at(k);
|
||
let (ki, _) = kinks
|
||
.get(bi)
|
||
.into_iter()
|
||
.flatten()
|
||
.enumerate()
|
||
.map(|(i, p)| (i, norm(sub(*p, v))))
|
||
.min_by(|a, b| a.1.total_cmp(&b.1))
|
||
.ok_or("the outline does not leave a boss the arm runs into")?;
|
||
let id = *kink_id.entry((bi, ki)).or_insert_with(|| {
|
||
v2.push(kinks.get(bi).and_then(|kk| kk.get(ki)).copied().unwrap_or_default());
|
||
v2.len() - 1
|
||
});
|
||
junction_kink[k] = Some(id);
|
||
checked.push([base[k], land_in[k], id, land_out[k]]);
|
||
}
|
||
quads.extend(checked.iter().copied());
|
||
|
||
// The annuli: per boss, the landings on its circle in order, the kinks, and its own side.
|
||
let mut rings_made = 0;
|
||
for (bi, b) in bosses.iter().enumerate() {
|
||
let edges: Vec<usize> = (0..n).filter(|&k| wall(k) == Wall::Boss(bi)).collect();
|
||
let starts: Vec<usize> = edges.iter().copied().filter(|&k| wall(k + n - 1) != Wall::Boss(bi)).collect();
|
||
let [ja] = starts.as_slice() else {
|
||
return Err(format!("the arm meets the boss of radius {:.4} in {} places", b.r, starts.len()));
|
||
};
|
||
let mut run = vec![*ja];
|
||
while run.len() < n && wall(run.last().copied().unwrap_or(0) + 1) == Wall::Boss(bi) {
|
||
run.push((run.last().copied().unwrap_or(0) + 1) % n);
|
||
}
|
||
let jb = (run.last().copied().unwrap_or(0) + 1) % n;
|
||
let (Some(ka), Some(kb)) = (junction_kink[*ja], junction_kink[jb]) else {
|
||
return Err(format!("the arm's run along the boss of radius {:.4} has no kinks at its ends", b.r));
|
||
};
|
||
// Counterclockwise round the boss: the run backwards, then the kink at its start, the
|
||
// boss's own side and the kink at its end.
|
||
let mut outer: Vec<usize> = std::iter::once(land_out[*ja]).chain(run.iter().skip(1).map(|&k| land_out[k])).chain([land_in[jb]]).collect();
|
||
outer.reverse();
|
||
outer.push(ka);
|
||
let ang = |p: V2| (p[1] - b.c[1]).atan2(p[0] - b.c[0]);
|
||
let mut angles: Vec<f64> = Vec::with_capacity(outer.len());
|
||
for &id in &outer {
|
||
let mut x = ang(v2.get(id).copied().unwrap_or_default());
|
||
if let Some(&last) = angles.last() {
|
||
while x <= last {
|
||
x += std::f64::consts::TAU;
|
||
}
|
||
}
|
||
angles.push(x);
|
||
}
|
||
let first = angles.first().copied().unwrap_or(0.0);
|
||
let last = angles.last().copied().unwrap_or(0.0);
|
||
let mut end = ang(v2.get(kb).copied().unwrap_or_default());
|
||
while end <= last {
|
||
end += std::f64::consts::TAU;
|
||
}
|
||
if end - first >= std::f64::consts::TAU - 1e-9 || angles.len() < 3 {
|
||
return Err(format!("the arm's landings round the boss of radius {:.4} are out of order", b.r));
|
||
}
|
||
// The own side in steps of the run's mean step (within bounds).
|
||
let step = ((angles[angles.len() - 2] - first) / (angles.len() - 2) as f64).clamp(MIN_OWN_ARC, MAX_OWN_ARC);
|
||
let span = end - last;
|
||
let count = ((span / step).round() as usize).max(1);
|
||
for t in 1..count {
|
||
let x = last + span * t as f64 / count as f64;
|
||
v2.push(add(b.c, [b.r * x.cos(), b.r * x.sin()]));
|
||
outer.push(v2.len() - 1);
|
||
angles.push(x);
|
||
}
|
||
outer.push(kb);
|
||
angles.push(end);
|
||
// The inner loop: the hole's point on each outer point's ray, the corners on the nearest.
|
||
let mut inner: Vec<V2> = Vec::with_capacity(angles.len());
|
||
for &x in &angles {
|
||
let h = b.inner.ray(b.c, [x.cos(), x.sin()], 0.0).ok_or("a ray from a boss's centre misses its hole")?;
|
||
inner.push(h.1);
|
||
}
|
||
let wrap = |x: f64| (x + std::f64::consts::PI).rem_euclid(std::f64::consts::TAU) - std::f64::consts::PI;
|
||
let mut snapped = vec![false; inner.len()];
|
||
for c in &b.corners {
|
||
let ca = ang(*c);
|
||
let best = (0..angles.len()).min_by(|p, q| wrap(angles[*p] - ca).abs().total_cmp(&wrap(angles[*q] - ca).abs())).ok_or("an empty ring")?;
|
||
if snapped[best] {
|
||
return Err(format!("the ring round the hole in the boss of radius {:.4} has too few blocks for its corners", b.r));
|
||
}
|
||
snapped[best] = true;
|
||
inner[best] = *c;
|
||
}
|
||
let first_inner = v2.len();
|
||
v2.extend(&inner);
|
||
let m = outer.len();
|
||
for t in 0..m {
|
||
let q = [first_inner + t, first_inner + (t + 1) % m, outer[(t + 1) % m], outer[t]];
|
||
checked.push(q);
|
||
quads.push(q);
|
||
}
|
||
rings_made += 1;
|
||
}
|
||
for q in &checked {
|
||
let pts = q.map(|i| v2.get(i).copied().unwrap_or_default());
|
||
if !convex(&pts) {
|
||
return Err(format!(
|
||
"a block of the layout folds near ({:.4}, {:.4})",
|
||
0.25 * pts.iter().map(|p| p[0]).sum::<f64>(),
|
||
0.25 * pts.iter().map(|p| p[1]).sum::<f64>()
|
||
));
|
||
}
|
||
}
|
||
|
||
// The slabs: each keeps the blocks whose middle lies inside the body.
|
||
let mut t = Topology { vertices: Vec::new(), blocks: Vec::new() };
|
||
let mut v3: BTreeMap<(usize, usize), u32> = BTreeMap::new();
|
||
let quad_area = |q: &[usize; 4]| {
|
||
let p = q.map(|i| v2.get(i).copied().unwrap_or_default());
|
||
0.5 * (cross(p[0], p[1]) + cross(p[1], p[2]) + cross(p[2], p[3]) + cross(p[3], p[0])).abs()
|
||
};
|
||
for (s, w) in levels.windows(2).enumerate() {
|
||
let (z0, z1) = (w[0], w[1]);
|
||
let zm = 0.5 * (z0 + z1);
|
||
let mut covered = 0.0;
|
||
for q in &quads {
|
||
let p = q.map(|i| v2.get(i).copied().unwrap_or_default());
|
||
let c = scale(p.iter().fold([0.0; 2], |a, x| add(a, *x)), 0.25);
|
||
if winding(inp.mesh, world([c[0], c[1], zm])) < 0.5 {
|
||
continue;
|
||
}
|
||
covered += quad_area(q);
|
||
let mut b = [0u32; 8];
|
||
for (h, (lvl, z)) in [(s, z0), (s + 1, z1)].into_iter().enumerate() {
|
||
for (c, &id) in q.iter().enumerate() {
|
||
let key = (id, lvl);
|
||
let vid = match v3.get(&key) {
|
||
Some(&x) => x,
|
||
None => {
|
||
let pt = v2.get(id).copied().unwrap_or_default();
|
||
t.vertices.push(world([pt[0], pt[1], z]));
|
||
let x = (t.vertices.len() - 1) as u32;
|
||
v3.insert(key, x);
|
||
x
|
||
}
|
||
};
|
||
b[4 * h + c] = vid;
|
||
}
|
||
}
|
||
let pos = b.map(|v| t.vertices.get(v as usize).copied().unwrap_or_default());
|
||
if corner_det(&pos) < 0.0 {
|
||
b = [b[0], b[3], b[2], b[1], b[4], b[7], b[6], b[5]];
|
||
}
|
||
t.blocks.push(b);
|
||
if t.blocks.len() > MAX_BLOCKS {
|
||
return Err(format!("more than {MAX_BLOCKS} blocks: too many for one layout"));
|
||
}
|
||
}
|
||
// The blocks cover the slab's section: no wall of it is left out of the layout.
|
||
let section = sections.get(s).map_or(0.0, |l| area(l));
|
||
if (covered - section).abs() > AREA_TOLERANCE * section.abs() {
|
||
return Err(format!("the layout covers {covered:.1} of the {section:.1} mm² section between {z0:.4} and {z1:.4}"));
|
||
}
|
||
}
|
||
t.check().map_err(|e| format!("the layout's blocks do not fit: {e}"))?;
|
||
Ok(Layout {
|
||
topology: t,
|
||
rings: rings_made,
|
||
layers: Vec::new(),
|
||
planes: [gp[0].len(), gp[1].len(), levels.len()],
|
||
frames: Vec::new(),
|
||
pillow: Some(margin),
|
||
})
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
#[test]
|
||
fn the_planes_keep_clear_of_a_filleted_corner() {
|
||
let p = planes(0.0, 20.0, &[(10.0, 4.5 / FILLET_CLEARANCE)], 2.0, 1e-9);
|
||
assert!(p.windows(2).all(|w| w[1] > w[0]), "{p:?}");
|
||
let i = p.iter().position(|x| (x - 10.0).abs() < 1e-12).unwrap();
|
||
assert!(p[i] - p[i - 1] >= 4.5 && p[i + 1] - p[i] >= 4.5, "{p:?}");
|
||
assert!(planes(0.0, 20.0, &[], 2.0, 1e-9).windows(2).all(|w| (w[1] - w[0] - 2.0).abs() < 1e-12));
|
||
}
|
||
|
||
#[test]
|
||
fn a_box_slices_into_its_rectangle() {
|
||
let shape = fieldcraft_geometry::block([0.0; 3], [4.0, 3.0, 2.0]).unwrap();
|
||
let mesh = shape.tessellate(0.01).unwrap();
|
||
let surfaces = shape.face_surfaces().unwrap();
|
||
let inp = Input {
|
||
shape: &shape,
|
||
surfaces: &surfaces,
|
||
mesh: &mesh,
|
||
direction: [0.0, 0.0, 1.0],
|
||
rings: &[],
|
||
blends: Vec::new(),
|
||
shell: None,
|
||
size: 1.0,
|
||
};
|
||
let loops = slice(&inp, &|p| p, 1.0, 1e-6).unwrap();
|
||
assert_eq!(loops.len(), 1, "{loops:?}");
|
||
assert!((loops[0].area() - 12.0).abs() < 1e-9, "{}", loops[0].area());
|
||
assert!(loops[0].contains([2.0, 1.5]) && !loops[0].contains([5.0, 1.5]));
|
||
}
|
||
}
|