From a0b4a69452e2ce0bb294d51634bd7235cdd71789 Mon Sep 17 00:00:00 2001 From: jyviko <2501648+jyviko@users.noreply.github.com> Date: Thu, 15 Oct 2026 01:04:40 -0400 Subject: [PATCH] Mesh: the swept cross-section's quad mesh as its own module The schemes, interval constraints, curve nodes and region meshes of a multi-level sweep's cross-section move out of layered::mesh into hex::section, with the curve placement (seeds, projection onto walls) passed in, so the cross-section can be meshed on its own. region::mesh_structured gives a scheme's mesh or None where it would fold, without falling back to the free mesher. No change in behaviour. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo --- crates/mesh/src/hex/layered.rs | 123 ++++------------------------- crates/mesh/src/hex/mod.rs | 1 + crates/mesh/src/hex/region.rs | 23 ++++-- crates/mesh/src/hex/section.rs | 139 +++++++++++++++++++++++++++++++++ 4 files changed, 174 insertions(+), 112 deletions(-) create mode 100644 crates/mesh/src/hex/section.rs diff --git a/crates/mesh/src/hex/layered.rs b/crates/mesh/src/hex/layered.rs index 955195c..fa0314a 100644 --- a/crates/mesh/src/hex/layered.rs +++ b/crates/mesh/src/hex/layered.rs @@ -18,8 +18,9 @@ //! region fills is decided by the inside test at its centre ([`super::geom::Inside`]). //! //! **Meshing.** Interval assignment ([`super::interval`]) gives every curve its count so that -//! four-sided regions are mapped (opposite sides equal) and the others have even loops for the -//! free all-quad mesher; each level interval gets its own layer count. Each region's quads are +//! every region's scheme fits ([`super::section`]: four-sided regions mapped with opposite sides +//! equal, three-sided ones the triangle primitive, rings and disks O-grids, the rest even loops +//! for the free all-quad mesher); each level interval gets its own layer count. Each region's quads are //! stacked through its intervals; shared curves and levels share nodes, so the hexahedra //! conform. Nodes of curves on analytic walls are projected onto them; in a revolution, the //! rotated nodes then lie exactly on every surface of revolution. @@ -31,7 +32,8 @@ use super::arrangement::{self, Arrangement, Q}; use super::brep::{Brep, Face}; use super::geom::{Inside, Surface, add, dist, scale, unit}; use super::interval::Intervals; -use super::region::{self, Scheme}; +use super::region::Scheme; +use super::section; use super::{Ctx, Raw}; use crate::predicates::Sign; use crate::seeds::EdgeSeed; @@ -435,27 +437,7 @@ pub(crate) fn mesh(ctx: &Ctx, prepared: &Prepared, refine: u32) -> Result { let goal = ideal_divisions(ctx.field, &curve3d(c))?; ia.var(goal * fine, 1); } - let oriented = |c: usize, fwd: bool| -> Vec { - let mut v: Vec = - arr.curves.get(c).map(|x| x.points.iter().map(|&p| arr.points.get(p).copied().unwrap_or([0.0; 2])).collect()).unwrap_or_default(); - if !fwd { - v.reverse(); - } - v - }; - let mut schemes: Vec = vec![Scheme::Free; arr.regions.len()]; - for (r, region) in arr.regions.iter().enumerate() { - if !used.get(r).copied().unwrap_or(false) { - continue; - } - let polylines: Vec>> = region.loops.iter().map(|lp| lp.iter().map(|&(c, f)| oriented(c, f)).collect()).collect(); - let scheme = region::classify(&polylines); - let vars: Vec> = region.loops.iter().map(|lp| lp.iter().map(|x| x.0).collect()).collect(); - region::constrain(&mut ia, &vars, scheme); - if let Some(slot) = schemes.get_mut(r) { - *slot = scheme; - } - } + let schemes = section::schemes(arr, used, &mut ia); let seeds = Seeds::of(ctx, frame, h_ref, intervals, setup.h_tol, &(0..arr.curves.len()).map(curve3d).collect::>())?; for (c, seed) in seeds.curves.iter().enumerate() { if let Some((seed, _)) = seed { @@ -483,85 +465,21 @@ pub(crate) fn mesh(ctx: &Ctx, prepared: &Prepared, refine: u32) -> Result { } layers.push(n); } - // 2D nodes: curve ends, then curve interiors (projected onto analytic walls). - let mut nodes2: Vec = Vec::new(); - let mut point_node: HashMap = HashMap::new(); - let mut curve_nodes: Vec> = Vec::with_capacity(arr.curves.len()); - for (c, curve) in arr.curves.iter().enumerate() { - let n = counts.get(c).copied().unwrap_or(1); + // The cross-section's nodes (curve interiors projected onto analytic walls) and quadrilaterals. + let place = |c: usize, n: usize| -> Result> { let pts3 = match seeds.curves.get(c).copied().flatten() { Some((seed, rev)) => crate::seeds::place(&curve3d(c), &seed.fractions(n, rev))?, None => divide_polyline(ctx.field, &curve3d(c), n)?, }; - let wall = curve.tags.iter().find_map(|&f| brep.faces.get(f).and_then(|x| x.surface)).filter(|s| !matches!(s, Surface::Plane { .. })); - let mut ids = Vec::with_capacity(pts3.len()); - let last = pts3.len().saturating_sub(1); - for (k, p) in pts3.iter().enumerate() { - let end = if k == 0 { - curve.points.first() - } else if k == last { - curve.points.last() - } else { - None - }; - let id = match end { - Some(&pid) => *point_node.entry(pid).or_insert_with(|| { - nodes2.push(arr.points.get(pid).copied().unwrap_or([0.0; 2])); - nodes2.len() as u32 - 1 - }), - None => { - let p = wall.and_then(|s| s.project(*p)).unwrap_or(*p); - nodes2.push(frame.coords(p).0); - nodes2.len() as u32 - 1 - } - }; - ids.push(id); - } - curve_nodes.push(ids); - } - // Quad meshes of the filled regions. - let mut region_quads: Vec> = vec![Vec::new(); arr.regions.len()]; - let quad_opts = &ctx.opts.quad; - for (r, region) in arr.regions.iter().enumerate() { - if !used.get(r).copied().unwrap_or(false) { - continue; - } - let mut loops_ids: Vec> = Vec::with_capacity(region.loops.len()); - let mut starts: Vec> = Vec::new(); - for lp in ®ion.loops { - let mut ids = Vec::new(); - let mut st = Vec::new(); - for &(c, fwd) in lp { - st.push(ids.len()); - let cn = curve_nodes.get(c).map(Vec::as_slice).unwrap_or(&[]); - let mut seq: Vec = cn.to_vec(); - if !fwd { - seq.reverse(); - } - seq.pop(); - ids.extend(seq); - } - loops_ids.push(ids); - starts.push(st); - } - let at = |i: u32| nodes2.get(i as usize).copied().unwrap_or([0.0; 2]); - let loops: Vec> = loops_ids.iter().map(|l| l.iter().map(|&i| at(i)).collect()).collect(); - let scheme = schemes.get(r).copied().unwrap_or(Scheme::Free); - let (mesh, used_scheme) = region::mesh(&loops, &starts, scheme, &plane_field, quad_opts).map_err(|e| region_error(r, e))?; - if let Some(slot) = schemes.get_mut(r) { - *slot = used_scheme; - } - let boundary: Vec = loops_ids.concat(); - let mut local: Vec = boundary.clone(); - for p in mesh.nodes.iter().skip(mesh.boundary_nodes) { - nodes2.push(*p); - local.push(nodes2.len() as u32 - 1); - } - let quads = region_quads.get_mut(r).ok_or_else(|| Error::Failed("region index".into()))?; - for q in &mesh.quads { - quads.push(q.map(|i| local.get(i as usize).copied().unwrap_or(0))); - } - } + let wall = arr + .curves + .get(c) + .and_then(|curve| curve.tags.iter().find_map(|&f| brep.faces.get(f).and_then(|x| x.surface))) + .filter(|s| !matches!(s, Surface::Plane { .. })); + Ok(pts3.iter().map(|p| frame.coords(wall.and_then(|s| s.project(*p)).unwrap_or(*p)).0).collect()) + }; + let section::Section { nodes: nodes2, quads: region_quads, schemes } = + section::mesh(arr, used, schemes, &counts, &place, &plane_field, &ctx.opts.quad)?; // Layer coordinates. let mut heights: Vec = Vec::new(); let mut first_layer: Vec = Vec::with_capacity(intervals.len()); @@ -885,13 +803,6 @@ fn cuts(arr: &Arrangement, used: &[bool], tol: f64) -> Vec<(Vec, usize)> { out } -fn region_error(r: usize, e: Error) -> Error { - match e { - Error::Limit(m) => Error::Limit(m), - other => Error::Failed(format!("cross-section region {r}: {other}")), - } -} - /// A point well inside a region: off the middle of one of its outer edges, inside the outer /// loop, outside the holes and clear of every boundary segment. fn interior_point(arr: &Arrangement, region: &arrangement::Region) -> Result { diff --git a/crates/mesh/src/hex/mod.rs b/crates/mesh/src/hex/mod.rs index f278f6f..8ca17d7 100644 --- a/crates/mesh/src/hex/mod.rs +++ b/crates/mesh/src/hex/mod.rs @@ -42,6 +42,7 @@ mod interval; mod layered; mod mapped; mod region; +mod section; mod sweep; #[cfg(test)] diff --git a/crates/mesh/src/hex/region.rs b/crates/mesh/src/hex/region.rs index 94853b6..abe006e 100644 --- a/crates/mesh/src/hex/region.rs +++ b/crates/mesh/src/hex/region.rs @@ -163,7 +163,22 @@ pub(crate) fn constrain(ia: &mut Intervals, loops: &[Vec], scheme: Scheme /// positions where its curve uses start. Returns the mesh (boundary nodes first, in the order of /// the loops) and the scheme used. pub(crate) fn mesh(loops: &[Vec], starts: &[Vec], scheme: Scheme, size: &dyn SizeField, opts: &QuadOptions) -> Result<(QuadMesh, Scheme)> { - let structured = match scheme { + match mesh_structured(loops, starts, scheme, size, opts)? { + Some(m) => Ok((m, scheme)), + None => Ok((free_quad_mesh(loops, size, opts)?, Scheme::Free)), + } +} + +/// The structured mesh of a region with its scheme, or `None` when the scheme is free or its +/// grid would fold. +pub(crate) fn mesh_structured( + loops: &[Vec], + starts: &[Vec], + scheme: Scheme, + size: &dyn SizeField, + opts: &QuadOptions, +) -> Result> { + Ok(match scheme { Scheme::Mapped(c) => { let s0 = starts.first(); let pos = c.map(|k| s0.and_then(|s| s.get(k)).copied().unwrap_or(0)); @@ -181,11 +196,7 @@ pub(crate) fn mesh(loops: &[Vec], starts: &[Vec], scheme: Scheme, size Scheme::Annulus => annulus(loops, size, opts)?, Scheme::Disk => disk(loops, size, opts)?, Scheme::Free => None, - }; - match structured { - Some(m) => Ok((m, scheme)), - None => Ok((free_quad_mesh(loops, size, opts)?, Scheme::Free)), - } + }) } /// The triangle primitive on one loop with corners at node positions `corners` (in loop order), diff --git a/crates/mesh/src/hex/section.rs b/crates/mesh/src/hex/section.rs new file mode 100644 index 0000000..cd1a6bb --- /dev/null +++ b/crates/mesh/src/hex/section.rs @@ -0,0 +1,139 @@ +//! The quadrilateral mesh of a swept cross-section: the scheme of every filled region of the +//! arrangement and its interval constraints, then, on the assigned counts, the nodes of every +//! curve (shared by the regions on either side) and each region's quadrilaterals. + +use std::collections::HashMap; + +use super::arrangement::{Arrangement, Q}; +use super::interval::Intervals; +use super::region::{self, Scheme}; +use crate::quad::QuadOptions; +use crate::size::SizeField; +use crate::{Error, Result}; + +/// A meshed cross-section: its nodes and, per region of the arrangement, its quadrilaterals and +/// the scheme that made them (free for regions not filled). +pub(crate) struct Section { + pub nodes: Vec, + pub quads: Vec>, + pub schemes: Vec, +} + +/// The scheme of each filled region (`used`), its constraints added to `ia` (one variable per +/// curve of the arrangement, by curve index). +pub(crate) fn schemes(arr: &Arrangement, used: &[bool], ia: &mut Intervals) -> Vec { + let oriented = |c: usize, fwd: bool| -> Vec { + let mut v: Vec = + arr.curves.get(c).map(|x| x.points.iter().map(|&p| arr.points.get(p).copied().unwrap_or([0.0; 2])).collect()).unwrap_or_default(); + if !fwd { + v.reverse(); + } + v + }; + let mut schemes: Vec = vec![Scheme::Free; arr.regions.len()]; + for (r, region) in arr.regions.iter().enumerate() { + if !used.get(r).copied().unwrap_or(false) { + continue; + } + let polylines: Vec>> = region.loops.iter().map(|lp| lp.iter().map(|&(c, f)| oriented(c, f)).collect()).collect(); + let scheme = region::classify(&polylines); + let vars: Vec> = region.loops.iter().map(|lp| lp.iter().map(|x| x.0).collect()).collect(); + region::constrain(ia, &vars, scheme); + if let Some(slot) = schemes.get_mut(r) { + *slot = scheme; + } + } + schemes +} + +/// Mesh the filled regions on the curve counts `counts`: `place(c, n)` gives the `n + 1` points +/// of curve `c` (its interior ones are used; its ends are the arrangement's points, shared). +/// The schemes come from [`schemes`]; a structured one whose grid would fold is meshed free. +pub(crate) fn mesh( + arr: &Arrangement, + used: &[bool], + mut schemes: Vec, + counts: &[usize], + place: &dyn Fn(usize, usize) -> Result>, + field: &dyn SizeField, + opts: &QuadOptions, +) -> Result
{ + // Nodes: curve ends, then curve interiors. + let mut nodes: Vec = Vec::new(); + let mut point_node: HashMap = HashMap::new(); + let mut curve_nodes: Vec> = Vec::with_capacity(arr.curves.len()); + for (c, curve) in arr.curves.iter().enumerate() { + let n = counts.get(c).copied().unwrap_or(1); + let pts = place(c, n)?; + let mut ids = Vec::with_capacity(pts.len()); + let last = pts.len().saturating_sub(1); + for (k, p) in pts.iter().enumerate() { + let end = if k == 0 { + curve.points.first() + } else if k == last { + curve.points.last() + } else { + None + }; + let id = match end { + Some(&pid) => *point_node.entry(pid).or_insert_with(|| { + nodes.push(arr.points.get(pid).copied().unwrap_or([0.0; 2])); + nodes.len() as u32 - 1 + }), + None => { + nodes.push(*p); + nodes.len() as u32 - 1 + } + }; + ids.push(id); + } + curve_nodes.push(ids); + } + let mut region_quads: Vec> = vec![Vec::new(); arr.regions.len()]; + for (r, region) in arr.regions.iter().enumerate() { + if !used.get(r).copied().unwrap_or(false) { + continue; + } + let mut loops_ids: Vec> = Vec::with_capacity(region.loops.len()); + let mut starts: Vec> = Vec::new(); + for lp in ®ion.loops { + let mut ids = Vec::new(); + let mut st = Vec::new(); + for &(c, fwd) in lp { + st.push(ids.len()); + let mut seq: Vec = curve_nodes.get(c).cloned().unwrap_or_default(); + if !fwd { + seq.reverse(); + } + seq.pop(); + ids.extend(seq); + } + loops_ids.push(ids); + starts.push(st); + } + let at = |i: u32| nodes.get(i as usize).copied().unwrap_or([0.0; 2]); + let loops: Vec> = loops_ids.iter().map(|l| l.iter().map(|&i| at(i)).collect()).collect(); + let scheme = schemes.get(r).copied().unwrap_or(Scheme::Free); + let (mesh, used_scheme) = region::mesh(&loops, &starts, scheme, field, opts).map_err(|e| region_error(r, e))?; + if let Some(slot) = schemes.get_mut(r) { + *slot = used_scheme; + } + let mut local: Vec = loops_ids.concat(); + for p in mesh.nodes.iter().skip(mesh.boundary_nodes) { + nodes.push(*p); + local.push(nodes.len() as u32 - 1); + } + let quads = region_quads.get_mut(r).ok_or_else(|| Error::Failed("region index".into()))?; + for q in &mesh.quads { + quads.push(q.map(|i| local.get(i as usize).copied().unwrap_or(0))); + } + } + Ok(Section { nodes, quads: region_quads, schemes }) +} + +fn region_error(r: usize, e: Error) -> Error { + match e { + Error::Limit(m) => Error::Limit(m), + other => Error::Failed(format!("cross-section region {r}: {other}")), + } +}