refactor(tess): move solid sampling to kernel

Remove local face, edge, spline, sweep, loft, and silhouette
samplers. Meshes and overlays share the kernel tolerance and edge
schedule.
This commit is contained in:
Hakan Seven committed 2026-08-10 10:18:53 +03:00
1 parent 60f5bca0d4
commit dc998fe46b
15 files changed
+410 -3938

No files matched your search

+68 -224
View File
@@ -14,7 +14,7 @@
use acadrust::kernel::brep::{self, Body};
use acadrust::kernel::geom2d::Curve;
use acadrust::kernel::space::{PlanarCurve, Plane, Vec3};
use acadrust::kernel::space::{PlanarCurve, Plane};
use acadrust::EntityType;
use crate::entities::curve::entity_curve;
@@ -47,7 +47,7 @@ pub fn profile_of(entity: &EntityType) -> Option<Profile> {
// both the fewest pieces a chain may have and the fewest that leave
// each one unambiguous about which way round it goes.
Curve::Circle(circle) => quarters(circle.centre, circle.radius),
other => split_evenly(other, 4),
_ => return None,
};
(pieces.len() >= 3).then_some(Profile {
plane: planar.plane,
@@ -72,21 +72,6 @@ fn quarters(centre: [f64; 2], radius: f64) -> Vec<Curve> {
.collect()
}
/// Any other closed curve as `count` straight pieces between points on it.
///
/// The honest fallback: an ellipse or a spline has no analytic sweep, so the
/// kernel would refuse the exact form anyway. Chords at least say plainly
/// what they are.
fn split_evenly(curve: &Curve, count: usize) -> Vec<Curve> {
use acadrust::kernel::geom2d::Line;
(0..count)
.map(|step| Curve::Line(Line {
start: curve.point_at(step as f64 / count as f64),
end: curve.point_at((step + 1) as f64 / count as f64),
}))
.collect()
}
/// EXTRUDE: drag the profile `height` along its own plane's normal.
///
/// `None` for a profile that does not close, encloses nothing, or holds a
@@ -123,226 +108,85 @@ pub fn revolved(
)
}
// ── SWEEP and LOFT ──────────────────────────────────────────────────────────
//
// Neither keeps a B-rep — both have only ever produced a mesh — so both are
// built from point lists rather than from topology. A profile becomes the
// points its own curve tessellates to, which is the same source EXTRUDE and
// REVOLVE read, so a circle stays round here too.
use crate::entities::curve::curve_points;
use crate::scene::model::mesh_model::{MeshLodSet, MeshModel};
/// The points a profile entity traces, and whether it closes.
fn outline(entity: &EntityType) -> Option<(Vec<[f64; 3]>, bool)> {
let planar = entity_curve(entity)?;
let closed = planar.curve.is_closed();
let mut points = curve_points(&planar);
// A closed curve tessellates back to its own start. Carrying the repeat
// would put a zero-width quad in every strip below.
if closed && points.len() > 1 {
let first = points[0];
let last = points[points.len() - 1];
if Vec3::from(first).distance(Vec3::from(last)) < 1e-9 {
points.pop();
}
}
(points.len() >= 2).then_some((points, closed))
}
/// SWEEP: drag a profile along a path.
///
/// The path contributes its direction and length, not its shape — which is
/// what SWEEP has always done here, and what makes it an extrusion along an
/// arbitrary vector rather than along a curve.
/// SWEEP through the kernel's tolerance-driven mesh API.
pub fn swept(profile: &EntityType, path: &EntityType, color: [f32; 4]) -> Option<MeshLodSet> {
let (points, closed) = outline(profile)?;
let along = {
let track = curve_points(&entity_curve(path)?);
let (from, to) = (Vec3::from(*track.first()?), Vec3::from(*track.last()?));
to - from
};
if along.length() < 1e-12 {
return None;
}
let moved: Vec<[f64; 3]> = points
.iter()
.map(|point| (Vec3::from(*point) + along).to_array())
.collect();
let mut mesh = Ribbon::default();
mesh.band(&points, &moved, closed);
if closed {
// An open profile sweeps into a sheet with nothing to cap.
mesh.cap(&points, true);
mesh.cap(&moved, false);
}
mesh.finish(color)
let tolerance = crate::scene::convert::curve_tol::current_curve_tol();
let surface = brep::mesh::sweep_surface(
&entity_curve(profile)?,
&entity_curve(path)?,
tolerance,
)?;
mesh_set(surface, color, tolerance)
}
/// LOFT: rule a surface through a run of profiles.
///
/// Consecutive profiles are joined by a band each, and the two ends are
/// capped when they close. Profiles with different point counts are resampled
/// onto the finer of the two, so a circle lofted to a square does not twist.
/// LOFT through the kernel's tolerance-driven mesh API.
pub fn lofted(profiles: &[EntityType], color: [f32; 4]) -> Option<MeshLodSet> {
let sections: Vec<(Vec<[f64; 3]>, bool)> = profiles.iter().filter_map(outline).collect();
if sections.len() < 2 {
let curves: Vec<PlanarCurve> = profiles.iter().filter_map(entity_curve).collect();
let tolerance = crate::scene::convert::curve_tol::current_curve_tol();
mesh_set(brep::mesh::loft_surface(&curves, tolerance)?, color, tolerance)
}
fn mesh_set(
surface: brep::mesh::SurfaceMesh,
color: [f32; 4],
tolerance: f64,
) -> Option<MeshLodSet> {
if surface.mesh.is_empty() {
return None;
}
let mut mesh = Ribbon::default();
for pair in sections.windows(2) {
let count = pair[0].0.len().max(pair[1].0.len());
let closed = pair[0].1 && pair[1].1;
let lower = resampled(&pair[0].0, count, pair[0].1);
let upper = resampled(&pair[1].0, count, pair[1].1);
mesh.band(&lower, &upper, closed);
let mut verts = Vec::with_capacity(surface.mesh.positions.len());
let mut verts_low = Vec::with_capacity(surface.mesh.positions.len());
for point in &surface.mesh.positions {
push_point(&mut verts, &mut verts_low, *point);
}
if sections.first()?.1 {
mesh.cap(&sections.first()?.0, true);
}
if sections.last()?.1 {
mesh.cap(&sections.last()?.0, false);
}
mesh.finish(color)
}
/// A ring walked in `count` even steps along its own length.
///
/// Even by distance rather than by index: two profiles given at different
/// densities line up where they are, so a band between them does not twist
/// wherever one of them happened to be sampled more finely.
fn resampled(points: &[[f64; 3]], count: usize, closed: bool) -> Vec<[f64; 3]> {
let mut ring: Vec<Vec3> = points.iter().map(|point| Vec3::from(*point)).collect();
if closed {
ring.push(ring[0]);
}
let mut walked = vec![0.0];
for pair in ring.windows(2) {
walked.push(walked[walked.len() - 1] + pair[0].distance(pair[1]));
}
let total = *walked.last().unwrap_or(&0.0);
if total <= 0.0 {
return points.to_vec();
}
let steps = if closed { count } else { count.max(2) - 1 };
(0..if closed { count } else { count.max(2) })
.map(|step| {
let want = total * step as f64 / steps as f64;
let at = walked
.iter()
.rposition(|reached| *reached <= want)
.unwrap_or(0)
.min(ring.len() - 2);
let span = walked[at + 1] - walked[at];
let along = if span > 0.0 { (want - walked[at]) / span } else { 0.0 };
ring[at].lerp(ring[at + 1], along).to_array()
})
.collect()
}
/// Triangles being gathered from bands and caps.
#[derive(Default)]
struct Ribbon {
positions: Vec<[f64; 3]>,
normals: Vec<[f64; 3]>,
triangles: Vec<[u32; 3]>,
}
impl Ribbon {
/// A strip of quads between two rings of the same length.
fn band(&mut self, lower: &[[f64; 3]], upper: &[[f64; 3]], closed: bool) {
let count = lower.len().min(upper.len());
if count < 2 {
return;
}
let spans = if closed { count } else { count - 1 };
for step in 0..spans {
let next = (step + 1) % count;
self.quad(lower[step], lower[next], upper[next], upper[step]);
}
}
/// A flat lid over a closed ring, fanned from its middle.
///
/// A fan rather than a proper triangulation: a lofted section can be
/// concave and a fan would then cover ground outside it, but every
/// profile these commands accept is a single closed curve, and the middle
/// of one is inside it.
fn cap(&mut self, ring: &[[f64; 3]], downward: bool) {
if ring.len() < 3 {
return;
}
let mut middle = Vec3::new(0.0, 0.0, 0.0);
for point in ring {
middle = middle + Vec3::from(*point);
}
let middle = (middle / ring.len() as f64).to_array();
for step in 0..ring.len() {
let next = (step + 1) % ring.len();
if downward {
self.triangle(middle, ring[next], ring[step]);
} else {
self.triangle(middle, ring[step], ring[next]);
let silhouette = surface.silhouette_source(tolerance);
let mut set = MeshLodSet::from_single(MeshModel {
name: String::new(),
verts,
verts_low,
normals: surface
.mesh
.normals
.iter()
.map(|normal| [normal[0] as f32, normal[1] as f32, normal[2] as f32])
.collect(),
indices: surface
.mesh
.triangles
.iter()
.flatten()
.map(|index| *index as u32)
.collect(),
triangle_material_handles: Vec::new(),
triangle_colors: Vec::new(),
color,
selected: false,
});
{
let (high, low) = (&mut set.edge_verts, &mut set.edge_verts_low);
for edge in surface.edges {
for segment in edge.windows(2) {
for point in segment {
push_point(high, low, *point);
}
}
}
}
set.curved_gens
.push(super::mesh_model::CurvedGen { source: silhouette });
Some(set)
}
fn quad(&mut self, a: [f64; 3], b: [f64; 3], c: [f64; 3], d: [f64; 3]) {
self.triangle(a, b, c);
self.triangle(a, c, d);
}
fn triangle(&mut self, a: [f64; 3], b: [f64; 3], c: [f64; 3]) {
let Some(normal) = (Vec3::from(b) - Vec3::from(a))
.cross(Vec3::from(c) - Vec3::from(a))
.normalize()
else {
// Collapsed: no normal, and nothing to draw.
return;
};
let base = self.positions.len() as u32;
for corner in [a, b, c] {
self.positions.push(corner);
self.normals.push(normal.to_array());
}
self.triangles.push([base, base + 1, base + 2]);
}
/// The gathered triangles as the renderer's mesh, or `None` for none.
fn finish(self, color: [f32; 4]) -> Option<MeshLodSet> {
if self.triangles.is_empty() {
return None;
}
// The renderer holds each position as a coarse float plus a fine
// correction, so a profile at survey coordinates keeps its last
// millimetres instead of losing them to f32.
let mut verts = Vec::with_capacity(self.positions.len());
let mut verts_low = Vec::with_capacity(self.positions.len());
for point in &self.positions {
let high = [point[0] as f32, point[1] as f32, point[2] as f32];
verts.push(high);
verts_low.push([
(point[0] - high[0] as f64) as f32,
(point[1] - high[1] as f64) as f32,
(point[2] - high[2] as f64) as f32,
]);
}
Some(MeshLodSet::from_single(MeshModel {
name: String::new(),
verts,
verts_low,
normals: self
.normals
.iter()
.map(|n| [n[0] as f32, n[1] as f32, n[2] as f32])
.collect(),
indices: self.triangles.iter().flatten().copied().collect(),
triangle_material_handles: Vec::new(),
triangle_colors: Vec::new(),
color,
selected: false,
}))
}
fn push_point(high: &mut Vec<[f32; 3]>, low: &mut Vec<[f32; 3]>, point: [f64; 3]) {
let coarse = [point[0] as f32, point[1] as f32, point[2] as f32];
high.push(coarse);
low.push([
(point[0] - coarse[0] as f64) as f32,
(point[1] - coarse[1] as f64) as f32,
(point[2] - coarse[2] as f64) as f32,
]);
}
#[cfg(test)]