Display tessellation plans (4/4): one GPU dispatch for the scene, intersection edges by Newton (#806)
Packs the scene's planned bodies into one GPU upload and dispatch, and evaluates intersection edges by seeded Newton refinement from a fitted B-spline through exactly solved leader points, falling back to the previous evaluation near tangency. Intersection nodes carry their faces' parameters. This changes speed, not what the app draws. Part 4 of 4 of #303. Co-authored-by: jyviko <2501648+jyviko@users.noreply.github.com>
This commit is contained in:
1 parent
c1c813e572
commit
587cf865b8
9 files changed
+612
-106
No files matched your search
@@ -854,3 +854,122 @@ pub(crate) mod tests {
|
||||
assert_eq!(local_basis(&[], 0.5, Side::Right, 1), vec![0.0, 0.0]);
|
||||
}
|
||||
}
|
||||
|
||||
/// A non-rational B-spline curve whose control points have `N` coordinates: a point in space
|
||||
/// together with other values that vary along it (an intersection curve with its parameters on
|
||||
/// both surfaces).
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct CurveN<const N: usize> {
|
||||
pub degree: usize,
|
||||
pub knots: Vec<f64>,
|
||||
pub ctrl: Vec<[f64; N]>,
|
||||
}
|
||||
|
||||
impl<const N: usize> CurveN<N> {
|
||||
/// The curve of degree `degree` through `points` at the increasing parameters `params`
|
||||
/// (global interpolation, The NURBS Book A9.1: clamped, interior knots by averaging the
|
||||
/// parameters).
|
||||
pub fn interpolate(params: &[f64], points: &[[f64; N]], degree: usize) -> SplineResult<CurveN<N>> {
|
||||
let m = points.len();
|
||||
if params.len() != m || degree == 0 || degree > MAX_SPLINE_DEGREE || m <= degree {
|
||||
return invalid(format!("{m} points, {} parameters, degree {degree}", params.len()));
|
||||
}
|
||||
if m > MAX_KNOTS || params.windows(2).any(|w| !(w[1] > w[0])) || params.iter().any(|t| !t.is_finite()) {
|
||||
return invalid("the parameters must be finite and increasing");
|
||||
}
|
||||
let (first, last) = (params[0], params[m - 1]);
|
||||
let mut knots = vec![first; degree + 1];
|
||||
for j in 1..m - degree {
|
||||
knots.push(params[j..j + degree].iter().sum::<f64>() / degree as f64);
|
||||
}
|
||||
knots.extend(std::iter::repeat_n(last, degree + 1));
|
||||
// N · P = Q, dense with partial pivoting (the matrix is banded; m is small here).
|
||||
let mut a = vec![vec![0.0f64; m]; m];
|
||||
for (row, &t) in a.iter_mut().zip(params) {
|
||||
let span = crate::tess::span_of(&knots, degree, m, t);
|
||||
let Some(b) = crate::tess::basis_ders(&knots, span, degree, t) else { return invalid("basis") };
|
||||
for (x, n) in row.iter_mut().skip(span - degree).zip(&b[0][..=degree]) {
|
||||
*x = *n;
|
||||
}
|
||||
}
|
||||
let mut q: Vec<[f64; N]> = points.to_vec();
|
||||
for col in 0..m {
|
||||
let Some(piv) = (col..m).max_by(|&x, &y| a[x][col].abs().total_cmp(&a[y][col].abs())) else { return invalid("empty") };
|
||||
if !(a[piv][col].abs() > 1e-14) {
|
||||
return invalid("singular interpolation system");
|
||||
}
|
||||
a.swap(col, piv);
|
||||
q.swap(col, piv);
|
||||
let (pr, pq) = (a[col].clone(), q[col]);
|
||||
for r in col + 1..m {
|
||||
let f = a[r][col] / pr[col];
|
||||
if f != 0.0 {
|
||||
for (x, p) in a[r].iter_mut().zip(&pr).skip(col) {
|
||||
*x -= f * p;
|
||||
}
|
||||
for (x, p) in q[r].iter_mut().zip(pq) {
|
||||
*x -= f * p;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut ctrl = vec![[0.0; N]; m];
|
||||
for r in (0..m).rev() {
|
||||
let mut v = q[r];
|
||||
for (c, x) in ctrl.iter().enumerate().skip(r + 1) {
|
||||
for (vi, xi) in v.iter_mut().zip(x) {
|
||||
*vi -= a[r][c] * xi;
|
||||
}
|
||||
}
|
||||
ctrl[r] = v.map(|x| x / a[r][r]);
|
||||
}
|
||||
if ctrl.iter().flatten().any(|x| !x.is_finite()) {
|
||||
return invalid("interpolation did not give finite control points");
|
||||
}
|
||||
Ok(CurveN { degree, knots, ctrl })
|
||||
}
|
||||
|
||||
/// The point and first derivative at `t` (clamped to the curve's range).
|
||||
pub fn eval(&self, t: f64) -> Option<([f64; N], [f64; N])> {
|
||||
let (p, n) = (self.degree, self.ctrl.len());
|
||||
if n <= p || self.knots.len() != n + p + 1 || !t.is_finite() {
|
||||
return None;
|
||||
}
|
||||
let t = t.clamp(*self.knots.get(p)?, *self.knots.get(n)?);
|
||||
let span = crate::tess::span_of(&self.knots, p, n, t);
|
||||
let b = crate::tess::basis_ders(&self.knots, span, p, t)?;
|
||||
let (mut x, mut dx) = ([0.0; N], [0.0; N]);
|
||||
let ctrl = self.ctrl.get(span - p..=span)?;
|
||||
for ((c, n), dn) in ctrl.iter().zip(&b[0]).zip(&b[1]) {
|
||||
for ((xk, dk), ck) in x.iter_mut().zip(dx.iter_mut()).zip(c) {
|
||||
*xk += n * ck;
|
||||
*dk += dn * ck;
|
||||
}
|
||||
}
|
||||
Some((x, dx))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod curve_n_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn interpolates_its_points_and_their_slope() {
|
||||
// A helix with a fourth coordinate rising linearly: the cubic passes through every
|
||||
// point and follows the curve between them.
|
||||
let at = |t: f64| [t.cos(), t.sin(), 0.3 * t, 2.0 * t];
|
||||
let params: Vec<f64> = (0..=24).map(|k| f64::from(k) * 0.25).collect();
|
||||
let points: Vec<[f64; 4]> = params.iter().map(|&t| at(t)).collect();
|
||||
let Ok(c) = CurveN::interpolate(¶ms, &points, 3) else { panic!("interpolates") };
|
||||
for (t, p) in params.iter().zip(&points) {
|
||||
let Some((q, _)) = c.eval(*t) else { panic!("evaluates") };
|
||||
assert!(q.iter().zip(p).all(|(a, b)| (a - b).abs() < 1e-9), "{t}");
|
||||
}
|
||||
let Some((q, d)) = c.eval(2.6) else { panic!("evaluates") };
|
||||
let w = at(2.6);
|
||||
assert!(q.iter().zip(w).all(|(a, b)| (a - b).abs() < 1e-3), "{q:?} vs {w:?}");
|
||||
assert!((d[3] - 2.0).abs() < 1e-6 && (d[2] - 0.3).abs() < 1e-3, "{d:?}");
|
||||
assert!(CurveN::interpolate(&[0.0, 0.0, 1.0, 2.0], &points[..4], 3).is_err(), "repeated parameter");
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ pub const MAX_EVAL_KNOTS: usize = 1 << 12;
|
||||
/// Most control points in one evaluable surface.
|
||||
pub const MAX_EVAL_POINTS: usize = 1 << 18;
|
||||
/// Basis slots for the highest evaluable degree.
|
||||
const W: usize = MAX_SPLINE_DEGREE + 1;
|
||||
pub(crate) const W: usize = MAX_SPLINE_DEGREE + 1;
|
||||
|
||||
/// A rational B-spline curve: homogeneous control points `[x·w, y·w, z·w, w]`. Outside its
|
||||
/// domain it continues as the polynomial of the end span (a line extends for ever).
|
||||
@@ -241,7 +241,7 @@ fn nurbs_derivs(degree: [usize; 2], knots: &[Vec<f64>; 2], count: [usize; 2], ct
|
||||
|
||||
/// The knot span used at `t`: the last non-empty span starting at or before `t`, clamped to
|
||||
/// the valid spans `p ≤ k < n` so parameters just outside the domain extend the end spans.
|
||||
fn span_of(knots: &[f64], p: usize, n: usize, t: f64) -> usize {
|
||||
pub(crate) fn span_of(knots: &[f64], p: usize, n: usize, t: f64) -> usize {
|
||||
let mut k = p;
|
||||
for i in p..n {
|
||||
if let (Some(&a), Some(&b)) = (knots.get(i), knots.get(i + 1))
|
||||
@@ -259,7 +259,7 @@ fn span_of(knots: &[f64], p: usize, n: usize, t: f64) -> usize {
|
||||
|
||||
/// Non-zero basis functions of degree `p` on span `span` and their first derivatives
|
||||
/// (NURBS Book A2.3 with n = 1). `ders[0][j]` is `N_{span−p+j}`, `ders[1][j]` its derivative.
|
||||
fn basis_ders(knots: &[f64], span: usize, p: usize, t: f64) -> Option<[[f64; W]; 2]> {
|
||||
pub(crate) fn basis_ders(knots: &[f64], span: usize, p: usize, t: f64) -> Option<[[f64; W]; 2]> {
|
||||
if p == 0 || p >= W {
|
||||
return None;
|
||||
}
|
||||
|
||||
+70
-41
@@ -50,13 +50,22 @@ pub struct GpuMesh {
|
||||
pub indices: wgpu::Buffer,
|
||||
pub index_count: u32,
|
||||
pub vertex_count: u32,
|
||||
/// Each vertex's face (the plan's face numbering), for colouring by face.
|
||||
/// Each vertex's face (its plan's face numbering), for colouring by face.
|
||||
pub vertex_face: Vec<u32>,
|
||||
/// Per plan (in the order given), its vertices and indices.
|
||||
pub parts: Vec<Part>,
|
||||
bind: wgpu::BindGroup,
|
||||
// The inputs, kept until the evaluation has run.
|
||||
_inputs: [wgpu::Buffer; 4],
|
||||
}
|
||||
|
||||
/// One plan's share of a [`GpuMesh`]: its vertices and its entries in the index buffer.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Part {
|
||||
pub vertices: std::ops::Range<u32>,
|
||||
pub indices: std::ops::Range<u32>,
|
||||
}
|
||||
|
||||
/// The compute pipeline, ready to evaluate plans on one device.
|
||||
pub struct TessGpu {
|
||||
adapter: String,
|
||||
@@ -144,7 +153,7 @@ impl TessGpu {
|
||||
/// and normals in f32, fixed edge nodes exact). Blocks until the GPU is done (native).
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn evaluate(&self, plan: &TessPlan) -> Result<Mesh> {
|
||||
let packed = pack(plan)?;
|
||||
let packed = pack(&[plan])?;
|
||||
let mut mesh = Mesh::default();
|
||||
let mut base = 0usize;
|
||||
for chunk in packed.vertices.chunks(MAX_BATCH * VERTEX_WORDS) {
|
||||
@@ -186,9 +195,16 @@ impl TessGpu {
|
||||
/// [`TessGpu::record`]. Refused (so the caller draws its CPU mesh) for a plan with more than
|
||||
/// [`MAX_BATCH`] vertices or a face without an evaluable surface.
|
||||
pub fn prepare(&self, plan: &TessPlan) -> Result<GpuMesh> {
|
||||
let packed = pack(plan)?;
|
||||
self.prepare_many(&[plan])
|
||||
}
|
||||
|
||||
/// Like [`TessGpu::prepare`] for several plans at once: one upload, one buffer and one
|
||||
/// dispatch for all of them (a viewport's whole scene), each plan's vertices and triangles in
|
||||
/// [`GpuMesh::parts`]. Refused past [`MAX_BATCH`] vertices in all.
|
||||
pub fn prepare_many(&self, plans: &[&TessPlan]) -> Result<GpuMesh> {
|
||||
let packed = pack(plans)?;
|
||||
if packed.count > MAX_BATCH {
|
||||
return Err(GpuError::Unsupported(format!("a plan of {} vertices on the GPU", packed.count)));
|
||||
return Err(GpuError::Unsupported(format!("{} vertices in one dispatch", packed.count)));
|
||||
}
|
||||
let d = &self.device;
|
||||
let count = word(packed.count)?;
|
||||
@@ -211,14 +227,19 @@ impl TessGpu {
|
||||
});
|
||||
let mut tri: Vec<u32> = Vec::new();
|
||||
let mut vertex_face = Vec::with_capacity(packed.count);
|
||||
let mut parts = Vec::with_capacity(plans.len());
|
||||
let mut offset = 0u32;
|
||||
for f in &plan.faces {
|
||||
let n = word(f.uv.len())?;
|
||||
for t in f.triangles.iter().filter(|t| t.iter().all(|&i| i < n)) {
|
||||
tri.extend([offset + t[0], offset + t[1], offset + t[2]]);
|
||||
for plan in plans {
|
||||
let (v0, i0) = (offset, word(tri.len())?);
|
||||
for f in &plan.faces {
|
||||
let n = word(f.uv.len())?;
|
||||
for t in f.triangles.iter().filter(|t| t.iter().all(|&i| i < n)) {
|
||||
tri.extend([offset + t[0], offset + t[1], offset + t[2]]);
|
||||
}
|
||||
vertex_face.extend(std::iter::repeat_n(f.face, f.uv.len()));
|
||||
offset += n;
|
||||
}
|
||||
vertex_face.extend(std::iter::repeat_n(f.face, f.uv.len()));
|
||||
offset += n;
|
||||
parts.push(Part { vertices: v0..offset, indices: i0..word(tri.len())? });
|
||||
}
|
||||
let indices = init("tess mesh indices", &words(&tri), wgpu::BufferUsages::INDEX);
|
||||
let bind = d.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
@@ -238,6 +259,7 @@ impl TessGpu {
|
||||
index_count: word(tri.len())?,
|
||||
vertex_count: count,
|
||||
vertex_face,
|
||||
parts,
|
||||
bind,
|
||||
_inputs: [params, surfaces, data, verts],
|
||||
})
|
||||
@@ -348,10 +370,12 @@ fn push_curve(data: &mut Vec<f32>, c: &EvalCurve) -> Result<(u32, u32)> {
|
||||
Ok((knots, ctrl))
|
||||
}
|
||||
|
||||
fn pack(plan: &TessPlan) -> Result<Packed> {
|
||||
let mut surfaces = Vec::with_capacity(plan.surfaces.len() * SURFACE_WORDS);
|
||||
/// Several plans as one: their surfaces one after another, each face's surface index offset by
|
||||
/// the surfaces of the plans before it.
|
||||
fn pack(plans: &[&TessPlan]) -> Result<Packed> {
|
||||
let mut surfaces = Vec::with_capacity(plans.iter().map(|p| p.surfaces.len()).sum::<usize>() * SURFACE_WORDS);
|
||||
let mut data: Vec<f32> = Vec::new();
|
||||
for s in &plan.surfaces {
|
||||
for s in plans.iter().flat_map(|p| &p.surfaces) {
|
||||
let mut head = [0u32; SURFACE_WORDS];
|
||||
match s {
|
||||
EvalSurface::Plane { origin, du, dv } => {
|
||||
@@ -394,39 +418,44 @@ fn pack(plan: &TessPlan) -> Result<Packed> {
|
||||
}
|
||||
surfaces.extend(head);
|
||||
}
|
||||
let mut vertices = Vec::with_capacity(plan.vertex_count() * VERTEX_WORDS);
|
||||
let mut vertices = Vec::with_capacity(plans.iter().map(|p| p.vertex_count()).sum::<usize>() * VERTEX_WORDS);
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let mut fixed = Vec::new();
|
||||
let mut count = 0usize;
|
||||
for f in &plan.faces {
|
||||
let Some(surface) = f.surface else {
|
||||
return Err(GpuError::Unsupported("a face without an evaluable surface".into()));
|
||||
};
|
||||
if surface as usize >= plan.surfaces.len() {
|
||||
return Err(GpuError::Failed("surface index out of range".into()));
|
||||
}
|
||||
for (k, uv) in f.uv.iter().enumerate() {
|
||||
let pinned = f.fixed.get(k).copied();
|
||||
let flags = u32::from(f.flip) | if pinned.is_some() { 2 } else { 0 };
|
||||
let p = pinned.unwrap_or(Vec3::ZERO);
|
||||
vertices.extend([
|
||||
(uv[0] as f32).to_bits(),
|
||||
(uv[1] as f32).to_bits(),
|
||||
surface,
|
||||
flags,
|
||||
(p.x as f32).to_bits(),
|
||||
(p.y as f32).to_bits(),
|
||||
(p.z as f32).to_bits(),
|
||||
0,
|
||||
]);
|
||||
if let Some(p) = pinned {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fixed.push((count, p));
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let _ = p;
|
||||
let mut base = 0u32;
|
||||
for plan in plans {
|
||||
for f in &plan.faces {
|
||||
let Some(surface) = f.surface else {
|
||||
return Err(GpuError::Unsupported("a face without an evaluable surface".into()));
|
||||
};
|
||||
if surface as usize >= plan.surfaces.len() {
|
||||
return Err(GpuError::Failed("surface index out of range".into()));
|
||||
}
|
||||
let surface = base + surface;
|
||||
for (k, uv) in f.uv.iter().enumerate() {
|
||||
let pinned = f.fixed.get(k).copied();
|
||||
let flags = u32::from(f.flip) | if pinned.is_some() { 2 } else { 0 };
|
||||
let p = pinned.unwrap_or(Vec3::ZERO);
|
||||
vertices.extend([
|
||||
(uv[0] as f32).to_bits(),
|
||||
(uv[1] as f32).to_bits(),
|
||||
surface,
|
||||
flags,
|
||||
(p.x as f32).to_bits(),
|
||||
(p.y as f32).to_bits(),
|
||||
(p.z as f32).to_bits(),
|
||||
0,
|
||||
]);
|
||||
if let Some(p) = pinned {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fixed.push((count, p));
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let _ = p;
|
||||
}
|
||||
count += 1;
|
||||
}
|
||||
count += 1;
|
||||
}
|
||||
base += word(plan.surfaces.len())?;
|
||||
}
|
||||
Ok(Packed {
|
||||
surfaces,
|
||||
|
||||
@@ -112,6 +112,43 @@ fn a_resident_mesh_matches_the_cpu() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Several bodies in one buffer and one dispatch (a viewport's scene): each plan's part holds
|
||||
/// exactly its own vertices and triangles, evaluated as the CPU evaluates that plan alone.
|
||||
#[test]
|
||||
fn a_scene_in_one_dispatch_matches_each_body_on_the_cpu() {
|
||||
let Some(gpu) = gpu() else { return };
|
||||
let plans: Vec<TessPlan> = bodies().iter().map(|(_, b)| ok(b.display_plan_angular(0.01, std::f64::consts::PI / 9.0))).collect();
|
||||
let refs: Vec<&TessPlan> = plans.iter().collect();
|
||||
let mesh = ok(gpu.prepare_many(&refs));
|
||||
assert_eq!(mesh.parts.len(), plans.len());
|
||||
let d = &gpu.device;
|
||||
let size = u64::from(mesh.vertex_count) * VERTEX_STRIDE;
|
||||
let dst = d.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("test read"),
|
||||
size,
|
||||
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
let mut enc = d.create_command_encoder(&Default::default());
|
||||
gpu.record(&mut enc, &mesh);
|
||||
enc.copy_buffer_to_buffer(&mesh.vertices, 0, &dst, 0, size);
|
||||
gpu.queue.submit([enc.finish()]);
|
||||
let slice = dst.slice(..);
|
||||
slice.map_async(wgpu::MapMode::Read, |_| {});
|
||||
ok(d.poll(wgpu::PollType::wait_indefinitely()));
|
||||
let out: Vec<f32> = ok(slice.get_mapped_range()).as_chunks::<4>().0.iter().map(|b| f32::from_le_bytes(*b)).collect();
|
||||
for ((plan, part), (name, _)) in plans.iter().zip(&mesh.parts).zip(bodies()) {
|
||||
let cpu = plan.evaluate();
|
||||
assert_eq!((part.vertices.end - part.vertices.start) as usize, cpu.positions.len(), "{name}");
|
||||
assert_eq!((part.indices.end - part.indices.start) as usize, cpu.triangles.len() * 3, "{name}");
|
||||
for (k, p) in cpu.positions.iter().enumerate() {
|
||||
let i = part.vertices.start as usize + k;
|
||||
let q = Vec3::new(f64::from(out[8 * i]), f64::from(out[8 * i + 1]), f64::from(out[8 * i + 2]));
|
||||
assert!(q.dist(*p) < 20.0 * 1e-5, "{name}: vertex {k}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Display meshing of a 400-hole plate (#303's benchmark): reference mesher, plan, CPU and GPU
|
||||
/// evaluation. `cargo test --release -p solidcraft-gpu bench_plate -- --ignored --nocapture`.
|
||||
#[test]
|
||||
|
||||
@@ -5,6 +5,8 @@ use solidcraft_geom::Vec3;
|
||||
use truck_meshalgo::prelude::*;
|
||||
use truck_modeling as mt;
|
||||
|
||||
use solidcraft_geom::tess::EvalSurface;
|
||||
|
||||
use super::size::Rules;
|
||||
use crate::body::from_p3;
|
||||
|
||||
@@ -13,11 +15,33 @@ pub(crate) const MAX_EDGE_NODES: usize = 20_000;
|
||||
/// Deepest bisection of one edge segment.
|
||||
const MAX_DEPTH: u32 = 24;
|
||||
|
||||
/// An edge's nodes, from its absolute front to its absolute back.
|
||||
pub(crate) struct Nodes {
|
||||
pub points: Vec<Vec3>,
|
||||
/// For an intersection edge: its two surfaces and, per node, both surfaces' parameters
|
||||
/// (u1, v1, u2, v2) where Newton placed the node (`None` where truck's evaluation did).
|
||||
pub on: Option<([EvalSurface; 2], Vec<Option<[f64; 4]>>)>,
|
||||
}
|
||||
|
||||
impl Nodes {
|
||||
fn plain(points: Vec<Vec3>) -> Nodes {
|
||||
Nodes { points, on: None }
|
||||
}
|
||||
|
||||
/// The parameters of each node on `surface`, where the edge knows them: `None` unless
|
||||
/// `surface` is one of the edge's two surfaces.
|
||||
pub(crate) fn params_on(&self, surface: &EvalSurface) -> Option<Vec<Option<[f64; 2]>>> {
|
||||
let (surfaces, params) = self.on.as_ref()?;
|
||||
let side = surfaces.iter().position(|s| s == surface)?;
|
||||
Some(params.iter().map(|q| q.map(|q| [q[2 * side], q[2 * side + 1]])).collect())
|
||||
}
|
||||
}
|
||||
|
||||
/// Nodes of `edge` from its absolute front to its absolute back: every chord within the
|
||||
/// tolerance of the curve (checked at the quarter points, so an S-bend in one segment is not
|
||||
/// missed), at most `h` long, and within the rules' facet angle. The end nodes are the
|
||||
/// vertices' own points, so edges meeting at a vertex meet exactly.
|
||||
pub(crate) fn sample_edge(edge: &mt::Edge, rules: &Rules, h: f64) -> Vec<Vec3> {
|
||||
pub(crate) fn sample_edge(edge: &mt::Edge, rules: &Rules, h: f64) -> Nodes {
|
||||
let tol = rules.tol;
|
||||
let front = from_p3(edge.absolute_front().point());
|
||||
let back = from_p3(edge.absolute_back().point());
|
||||
@@ -32,29 +56,35 @@ pub(crate) fn sample_edge(edge: &mt::Edge, rules: &Rules, h: f64) -> Vec<Vec3> {
|
||||
if let Some(p) = pts.last_mut() {
|
||||
*p = back;
|
||||
}
|
||||
return pts;
|
||||
return Nodes::plain(pts);
|
||||
}
|
||||
let (t0, t1) = curve.range_tuple();
|
||||
if !(t0.is_finite() && t1.is_finite() && t1 > t0) {
|
||||
return vec![front, back];
|
||||
return Nodes::plain(vec![front, back]);
|
||||
}
|
||||
// Our evaluator where the curve has a form it knows (no allocation per point); truck's
|
||||
// for intersection curves.
|
||||
// Our evaluators: the curve's own form where it has one (no allocation per point); for an
|
||||
// intersection curve, Newton on both surfaces from seeded parameters (see `ssi`), which
|
||||
// also gives each node's parameters on both faces; truck's evaluation only where that does
|
||||
// not converge.
|
||||
let fast = super::surface::eval_curve(&curve);
|
||||
let at = |t: f64| {
|
||||
match &fast {
|
||||
Some(c) => c.eval(t).map(|(p, _)| p),
|
||||
None => Some(from_p3(curve.subs(t))),
|
||||
let ssi = if fast.is_none() { super::ssi::Intersection::new(&curve) } else { None };
|
||||
type Pt = (Vec3, Option<[f64; 4]>);
|
||||
let at = |t: f64| -> Option<Pt> {
|
||||
match (&fast, &ssi) {
|
||||
(Some(c), _) => c.eval(t).map(|(p, _)| (p, None)),
|
||||
(None, Some(ix)) => ix.eval(t).map(|(p, q)| (p, Some(q))).or_else(|| Some((from_p3(curve.subs(t)), None))),
|
||||
(None, None) => Some((from_p3(curve.subs(t)), None)),
|
||||
}
|
||||
.filter(|p| p.is_finite())
|
||||
.filter(|(p, _)| p.is_finite())
|
||||
};
|
||||
let fallback = || Nodes::plain(vec![front, back]);
|
||||
// A closed curve's chord between its ends is empty: start it from a few segments. An open
|
||||
// one starts whole, so an arc the tolerance leaves flat stays one segment.
|
||||
let initial: u32 = if edge.absolute_front().id() == edge.absolute_back().id() { 4 } else { 1 };
|
||||
let t_at = |i: u32| t0 + (t1 - t0) * f64::from(i) / f64::from(initial);
|
||||
let mut ends: Vec<Vec3> = Vec::with_capacity(initial as usize + 1);
|
||||
let mut ends: Vec<Pt> = Vec::with_capacity(initial as usize + 1);
|
||||
for i in 0..=initial {
|
||||
let Some(p) = at(t_at(i)) else { return vec![front, back] };
|
||||
let Some(p) = at(t_at(i)) else { return fallback() };
|
||||
ends.push(p);
|
||||
}
|
||||
// Depth-first bisection, segment by segment, keeping the output ordered. Each segment
|
||||
@@ -63,16 +93,17 @@ pub(crate) fn sample_edge(edge: &mt::Edge, rules: &Rules, h: f64) -> Vec<Vec3> {
|
||||
struct Seg {
|
||||
a: f64,
|
||||
b: f64,
|
||||
pa: Vec3,
|
||||
pb: Vec3,
|
||||
mid: Option<Vec3>,
|
||||
pa: Pt,
|
||||
pb: Pt,
|
||||
mid: Option<Pt>,
|
||||
depth: u32,
|
||||
}
|
||||
let mut out: Vec<Vec3> = vec![front];
|
||||
let Some(&first) = ends.first() else { return fallback() };
|
||||
let mut out: Vec<Pt> = vec![(front, first.1)];
|
||||
let mut stack: Vec<Seg> = Vec::with_capacity(64);
|
||||
for i in (0..initial).rev() {
|
||||
let (Some(pa), Some(pb)) = (<[Vec3]>::get(&ends, i as usize).copied(), <[Vec3]>::get(&ends, i as usize + 1).copied()) else {
|
||||
return vec![front, back];
|
||||
let (Some(pa), Some(pb)) = (<[Pt]>::get(&ends, i as usize).copied(), <[Pt]>::get(&ends, i as usize + 1).copied()) else {
|
||||
return fallback();
|
||||
};
|
||||
stack.push(Seg { a: t_at(i), b: t_at(i + 1), pa, pb, mid: None, depth: 0 });
|
||||
}
|
||||
@@ -86,11 +117,11 @@ pub(crate) fn sample_edge(edge: &mt::Edge, rules: &Rules, h: f64) -> Vec<Vec3> {
|
||||
let (q1, q3) = (at(a + 0.25 * (b - a)), at(a + 0.75 * (b - a)));
|
||||
let done = sg.depth >= MAX_DEPTH || out.len() + stack.len() >= MAX_EDGE_NODES;
|
||||
let fine = || {
|
||||
let sag = [q1, pm, q3].iter().flatten().map(|q| q.dist_to_segment(pa, pb)).fold(0.0, f64::max);
|
||||
let short = !(pa.dist(pb) > h * (1.0 + 1e-9));
|
||||
let sag = [q1, pm, q3].iter().flatten().map(|q| q.0.dist_to_segment(pa.0, pb.0)).fold(0.0, f64::max);
|
||||
let short = !(pa.0.dist(pb.0) > h * (1.0 + 1e-9));
|
||||
// The two half chords of an arc turning θ differ by θ/2.
|
||||
let straight = match (rules.turn, pm) {
|
||||
(Some(t), Some(q)) => !t.exceeds(2.0 * turn(q - pa, pb - q), sag),
|
||||
(Some(t), Some(q)) => !t.exceeds(2.0 * turn(q.0 - pa.0, pb.0 - q.0), sag),
|
||||
_ => true,
|
||||
};
|
||||
short && straight && sag <= tol
|
||||
@@ -104,12 +135,13 @@ pub(crate) fn sample_edge(edge: &mt::Edge, rules: &Rules, h: f64) -> Vec<Vec3> {
|
||||
}
|
||||
}
|
||||
if out.len() < 2 {
|
||||
return vec![front, back];
|
||||
return fallback();
|
||||
}
|
||||
if let Some(p) = out.last_mut() {
|
||||
*p = back;
|
||||
p.0 = back;
|
||||
}
|
||||
out
|
||||
let on = ssi.map(|ix| (ix.surfaces().clone(), out.iter().map(|p| p.1).collect()));
|
||||
Nodes { points: out.into_iter().map(|p| p.0).collect(), on }
|
||||
}
|
||||
|
||||
/// Angle (radians) between two directions; zero when either is empty.
|
||||
@@ -130,7 +162,7 @@ mod tests {
|
||||
let Ok(cyl) = crate::cylinder(Vec3::ZERO, Vec3::Z, 10.0, 5.0) else { panic!("cylinder") };
|
||||
for tol in [0.1, 0.01, 0.001] {
|
||||
for e in cyl.solid.edge_iter() {
|
||||
let pts = sample_edge(&e, &Rules { tol, max_len: f64::INFINITY, turn: None }, f64::INFINITY);
|
||||
let pts = sample_edge(&e, &Rules { tol, max_len: f64::INFINITY, turn: None }, f64::INFINITY).points;
|
||||
assert!(pts.len() >= 2);
|
||||
assert_eq!(pts.first().copied(), Some(from_p3(e.absolute_front().point())));
|
||||
assert_eq!(pts.last().copied(), Some(from_p3(e.absolute_back().point())));
|
||||
@@ -152,7 +184,7 @@ mod tests {
|
||||
fn edges_are_split_to_the_side_length_and_turn() {
|
||||
let Ok(cyl) = crate::cylinder(Vec3::ZERO, Vec3::Z, 10.0, 5.0) else { panic!("cylinder") };
|
||||
for e in cyl.solid.edge_iter() {
|
||||
let pts = sample_edge(&e, &Rules { tol: 1.0, max_len: f64::INFINITY, turn: None }, 1.2);
|
||||
let pts = sample_edge(&e, &Rules { tol: 1.0, max_len: f64::INFINITY, turn: None }, 1.2).points;
|
||||
for w in pts.windows(2) {
|
||||
assert!(w[0].dist(w[1]) <= 1.2 + 1e-9, "side {}", w[0].dist(w[1]));
|
||||
}
|
||||
@@ -173,8 +205,36 @@ mod tests {
|
||||
.solid
|
||||
.edge_iter()
|
||||
.filter(|e| seen.insert(e.id()) && !matches!(e.curve(), mt::Curve::Line(_)))
|
||||
.map(|e| sample_edge(&e, &thirty, f64::INFINITY).len() - 1)
|
||||
.map(|e| sample_edge(&e, &thirty, f64::INFINITY).points.len() - 1)
|
||||
.sum();
|
||||
assert!(sides >= 24, "{sides}");
|
||||
}
|
||||
|
||||
/// An intersection edge's nodes carry their parameters on both faces: each face on one of
|
||||
/// the edge's surfaces gets them, and the surface there is the node.
|
||||
#[test]
|
||||
fn intersection_nodes_carry_their_parameters_on_both_faces() {
|
||||
let Ok(pin) = crate::cylinder(Vec3::new(5.0, 5.0, -1.0), Vec3::Z, 2.0, 6.0) else { panic!("pin") };
|
||||
let Ok(cross) = crate::cylinder(Vec3::new(5.0, -1.0, 2.0), Vec3::Y, 1.0, 12.0) else { panic!("cross") };
|
||||
let Ok(Some(body)) = crate::boolean(&pin, &cross, crate::BoolOp::Cut) else { panic!("cut") };
|
||||
let rules = Rules { tol: 0.01, max_len: f64::INFINITY, turn: None };
|
||||
let surfaces: Vec<EvalSurface> = body.solid.face_iter().filter_map(|f| super::super::surface::eval_surface(&f.surface())).collect();
|
||||
let (mut edges, mut checked) = (0, 0);
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
for e in body.solid.edge_iter().filter(|e| seen.insert(e.id()) && matches!(e.curve(), mt::Curve::IntersectionCurve(_))) {
|
||||
let n = sample_edge(&e, &rules, f64::INFINITY);
|
||||
let on: Vec<_> = surfaces.iter().filter_map(|s| n.params_on(s).map(|q| (s, q))).collect();
|
||||
assert_eq!(on.len(), 2, "both faces of an intersection edge know its parameters");
|
||||
edges += 1;
|
||||
for (s, q) in on {
|
||||
for (p, uv) in n.points.iter().zip(q).skip(1).take(n.points.len().saturating_sub(2)) {
|
||||
let Some(uv) = uv else { continue };
|
||||
let Some((at, _, _)) = s.derivs(uv[0], uv[1]) else { panic!("evaluates") };
|
||||
assert!(at.dist(*p) < 1e-7, "{at:?} vs {p:?}");
|
||||
checked += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(edges > 0 && checked > 20, "{edges} edges, {checked} nodes");
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ pub(crate) mod coons;
|
||||
pub(crate) mod edges;
|
||||
pub(crate) mod grid;
|
||||
pub(crate) mod size;
|
||||
pub(crate) mod ssi;
|
||||
pub(crate) mod surface;
|
||||
pub(crate) mod uv;
|
||||
|
||||
@@ -83,13 +84,13 @@ pub(crate) fn plan(solid: &Solid, tol: f64, turn: Option<size::Turn>) -> Result<
|
||||
let sampled = crate::par::map("edge nodes", &jobs, solved >= PARALLEL_CURVES || edges.len() >= PARALLEL_EDGES, |(e, h)| {
|
||||
Ok(edges::sample_edge(e, &rules, *h))
|
||||
});
|
||||
let mut nodes: HashMap<_, Vec<Vec3>> = HashMap::with_capacity(edges.len());
|
||||
for (e, pts) in edges.iter().zip(sampled) {
|
||||
nodes.insert(e.id(), pts?);
|
||||
let mut nodes: HashMap<_, edges::Nodes> = HashMap::with_capacity(edges.len());
|
||||
for (e, n) in edges.iter().zip(sampled) {
|
||||
nodes.insert(e.id(), n?);
|
||||
}
|
||||
relax_crowded_faces(&faces, &jobs, &rules, &mut nodes);
|
||||
let oriented = |e: &mt::Edge| -> Option<Vec<Vec3>> {
|
||||
let mut pts = nodes.get(&e.id())?.clone();
|
||||
let mut pts = nodes.get(&e.id())?.points.clone();
|
||||
if !e.orientation() {
|
||||
pts.reverse();
|
||||
}
|
||||
@@ -102,21 +103,32 @@ pub(crate) fn plan(solid: &Solid, tol: f64, turn: Option<size::Turn>) -> Result<
|
||||
Some((Some(eval), Some(size))) => (eval.clone(), *size),
|
||||
_ => return Err(unsupported(format!("face {fi}: surface not evaluable"))),
|
||||
};
|
||||
// Boundary nodes, loop by loop (each edge's last node is the next edge's first).
|
||||
// Boundary nodes, loop by loop (each edge's last node is the next edge's first), with
|
||||
// their parameters on this face where the edge knows them (an intersection edge on
|
||||
// this face's surface).
|
||||
let mut loops3: Vec<Vec<Vec3>> = Vec::new();
|
||||
let mut known: Vec<Vec<Option<[f64; 2]>>> = Vec::new();
|
||||
for wire in face.absolute_boundaries() {
|
||||
let mut pts = Vec::new();
|
||||
let (mut pts, mut uvs) = (Vec::new(), Vec::new());
|
||||
for e in wire.edge_iter() {
|
||||
let mut ep = oriented(e).ok_or_else(|| unsupported("edge without nodes"))?;
|
||||
let n = nodes.get(&e.id()).ok_or_else(|| unsupported("edge without nodes"))?;
|
||||
let (mut ep, mut eq) = (n.points.clone(), n.params_on(&eval).unwrap_or_else(|| vec![None; n.points.len()]));
|
||||
if !e.orientation() {
|
||||
ep.reverse();
|
||||
eq.reverse();
|
||||
}
|
||||
ep.pop();
|
||||
eq.pop();
|
||||
pts.extend(ep);
|
||||
uvs.extend(eq);
|
||||
}
|
||||
if pts.len() >= 2 {
|
||||
loops3.push(pts);
|
||||
known.push(uvs);
|
||||
}
|
||||
}
|
||||
let mut stats = PlanStats::default();
|
||||
let fp = plan_face(fi, &eval, &loops3, &rules, size, &mut stats)?;
|
||||
let fp = plan_face(fi, &eval, &loops3, &known, &rules, size, &mut stats)?;
|
||||
let fp = FacePlan {
|
||||
flip: !face.orientation(),
|
||||
triangles: if face.orientation() { fp.triangles } else { fp.triangles.into_iter().map(|[a, b, c]| [a, c, b]).collect() },
|
||||
@@ -146,7 +158,7 @@ pub(crate) fn plan(solid: &Solid, tol: f64, turn: Option<size::Turn>) -> Result<
|
||||
/// holes is not worth a hundred times the work to draw each hole round). An edge keeps one set
|
||||
/// of nodes for both its faces, at the coarser of their levels, so every rim of a crowded face
|
||||
/// gets the same share whatever the order of the faces.
|
||||
fn relax_crowded_faces(faces: &[&mt::Face], jobs: &[(&mt::Edge, f64)], rules: &size::Rules, nodes: &mut HashMap<mt::EdgeID, Vec<Vec3>>) {
|
||||
fn relax_crowded_faces(faces: &[&mt::Face], jobs: &[(&mt::Edge, f64)], rules: &size::Rules, nodes: &mut HashMap<mt::EdgeID, edges::Nodes>) {
|
||||
use truck_meshalgo::tessellation::Deflection;
|
||||
let Some(turn) = rules.turn else { return };
|
||||
let h_of: HashMap<mt::EdgeID, (&mt::Edge, f64)> = jobs.iter().map(|(e, h)| (e.id(), (*e, *h))).collect();
|
||||
@@ -154,7 +166,7 @@ fn relax_crowded_faces(faces: &[&mt::Face], jobs: &[(&mt::Edge, f64)], rules: &s
|
||||
faces.iter().map(|f| f.absolute_boundaries().iter().flat_map(|w| w.edge_iter().map(|e| e.id())).collect()).collect();
|
||||
let mut level: HashMap<mt::EdgeID, u8> = HashMap::new();
|
||||
for l in 1..=Deflection::MAX_RELAX {
|
||||
let segments = |id: &mt::EdgeID| nodes.get(id).map_or(0, |n| n.len().saturating_sub(1));
|
||||
let segments = |id: &mt::EdgeID| nodes.get(id).map_or(0, |n| n.points.len().saturating_sub(1));
|
||||
let mut crowded: Vec<mt::EdgeID> = boundaries
|
||||
.iter()
|
||||
.filter(|ids| ids.iter().map(segments).sum::<usize>() > Deflection::MAX_ANGLE_BOUNDARY)
|
||||
@@ -184,6 +196,7 @@ fn plan_face(
|
||||
fi: usize,
|
||||
s: &EvalSurface,
|
||||
loops3: &[Vec<Vec3>],
|
||||
known: &[Vec<Option<[f64; 2]>>],
|
||||
rules: &size::Rules,
|
||||
size: (f64, [f64; 2]),
|
||||
stats: &mut PlanStats,
|
||||
@@ -197,11 +210,17 @@ fn plan_face(
|
||||
let mut fixed: Vec<Vec3> = Vec::new();
|
||||
let mut uvs: Vec<[f64; 2]> = Vec::new();
|
||||
let mut ranges: Vec<std::ops::Range<usize>> = Vec::new();
|
||||
for pts in loops3 {
|
||||
for (li, pts) in loops3.iter().enumerate() {
|
||||
let mut luv: Vec<[f64; 2]> = Vec::with_capacity(pts.len());
|
||||
let mut prev: Option<[f64; 2]> = None;
|
||||
for p in pts {
|
||||
let mut q = uv::project(s, &dom, *p, prev, accept).ok_or_else(|| fail("boundary does not lie on the surface"))?;
|
||||
for (k, p) in pts.iter().enumerate() {
|
||||
// Known parameters (an intersection edge's), else a projection seeded by the
|
||||
// previous node.
|
||||
let given = known.get(li).and_then(|l| l.get(k)).copied().flatten();
|
||||
let mut q = match given {
|
||||
Some(q) => q,
|
||||
None => uv::project(s, &dom, *p, prev, accept).ok_or_else(|| fail("boundary does not lie on the surface"))?,
|
||||
};
|
||||
if let Some(pr) = prev {
|
||||
for d in 0..2 {
|
||||
q[d] = uv::unwrap(q[d], pr[d], dom.period[d]);
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
//! Points on a surface–surface intersection edge, on our own surface evaluators.
|
||||
//!
|
||||
//! truck stores an intersection edge as a leader curve (a polyline or B-spline near the
|
||||
//! intersection) and its two surfaces, and finds each point by projecting the leader point onto
|
||||
//! each surface in turn from no starting guess: a nearest-point search per surface per point.
|
||||
//! Here the intersection is solved exactly at a set of leader parameters, and a cubic B-spline
|
||||
//! is fitted through those points together with both surfaces' parameters there (a smooth
|
||||
//! leader with its curves on both surfaces). Each point is then refined from the B-spline by
|
||||
//! Newton's method on the four unknowns (u1, v1, u2, v2): S1(u1, v1) = S2(u2, v2), held on the
|
||||
//! plane through the B-spline point normal to the B-spline (Barnhill & Kersey, "A marching
|
||||
//! method for parametric surface/surface intersection", CAGD 7, 1990; Patrikalakis & Maekawa,
|
||||
//! "Shape Interrogation for CAD/CAM", 2002, ch. 5). Every point is independent once seeded. Where the surfaces are close to
|
||||
//! tangent along the edge (the system becomes singular) or Newton does not converge, the caller
|
||||
//! uses truck's evaluation.
|
||||
|
||||
use solidcraft_geom::Vec3;
|
||||
use solidcraft_geom::bspline::CurveN;
|
||||
use solidcraft_geom::tess::EvalSurface;
|
||||
use truck_meshalgo::prelude::*;
|
||||
use truck_modeling as mt;
|
||||
|
||||
use super::uv::{self, Domain};
|
||||
use crate::body::from_p3;
|
||||
|
||||
/// Leader parameters at which both surfaces' parameters are found up front.
|
||||
const SEEDS: usize = 32;
|
||||
/// Newton iterations per point (two or three suffice from a seed).
|
||||
const MAX_ITER: usize = 8;
|
||||
/// Below this sine of the angle between the surface normals the intersection is treated as
|
||||
/// near-tangent: points there are left to truck.
|
||||
const MIN_SIN: f64 = 0.05;
|
||||
|
||||
/// An intersection edge ready for point evaluation.
|
||||
pub(crate) struct Intersection {
|
||||
surfaces: [EvalSurface; 2],
|
||||
domains: [Domain; 2],
|
||||
/// The smooth leader over the curve's parameter: the intersection point (x, y, z) and both
|
||||
/// surfaces' parameters (u1, v1, u2, v2), periodic directions unwrapped along it.
|
||||
leader: CurveN<7>,
|
||||
/// Convergence tolerance (mm).
|
||||
eps: f64,
|
||||
}
|
||||
|
||||
impl Intersection {
|
||||
/// `None` unless `curve` is an intersection curve whose surfaces have evaluators and whose
|
||||
/// leader projects onto both.
|
||||
pub(crate) fn new(curve: &mt::Curve) -> Option<Self> {
|
||||
let mt::Curve::IntersectionCurve(ic) = curve else { return None };
|
||||
let surfaces = [super::surface::eval_surface(ic.surface0())?, super::surface::eval_surface(ic.surface1())?];
|
||||
let domains = [uv::domain(&surfaces[0]), uv::domain(&surfaces[1])];
|
||||
let (t0, t1) = curve.range_tuple();
|
||||
if !(t0.is_finite() && t1.is_finite() && t1 > t0) {
|
||||
return None;
|
||||
}
|
||||
let leader = |t: f64| match ic.leader() {
|
||||
mt::Leader::Polyline(l) => (from_p3(l.subs(t)), from_v3(l.der(t))),
|
||||
mt::Leader::BSpline(l) => (from_p3(l.subs(t)), from_v3(l.der(t))),
|
||||
};
|
||||
// The leader lies within the curve's tolerance of both surfaces.
|
||||
let accept = (ic.tolerance() * 100.0).max(1e-6);
|
||||
let scale = (0..=SEEDS).map(|k| leader(t0 + (t1 - t0) * k as f64 / SEEDS as f64).0.len()).fold(1.0, f64::max);
|
||||
let mut it = Intersection { surfaces, domains, leader: CurveN { degree: 1, knots: Vec::new(), ctrl: Vec::new() }, eps: scale * 1e-10 };
|
||||
let (mut params, mut points) = (Vec::with_capacity(SEEDS + 1), Vec::with_capacity(SEEDS + 1));
|
||||
let mut prev: Option<[f64; 4]> = None;
|
||||
for k in 0..=SEEDS {
|
||||
let t = t0 + (t1 - t0) * k as f64 / SEEDS as f64;
|
||||
let (p, d) = leader(t);
|
||||
let mut q = [0.0; 4];
|
||||
for (i, (s, dom)) in it.surfaces.iter().zip(&it.domains).enumerate() {
|
||||
let hint = prev.map(|w| [w[2 * i], w[2 * i + 1]]);
|
||||
let mut uv = uv::project(s, dom, p, hint, accept)?;
|
||||
if let Some(h) = hint {
|
||||
for d in 0..2 {
|
||||
uv[d] = uv::unwrap(uv[d], h[d], dom.period[d]);
|
||||
}
|
||||
}
|
||||
q[2 * i] = uv[0];
|
||||
q[2 * i + 1] = uv[1];
|
||||
}
|
||||
// Exactly on the intersection where Newton converges (near-tangent seeds keep the
|
||||
// leader point and its projections).
|
||||
let (p, q) = d.normalized().and_then(|dir| it.newton(p, dir, q)).unwrap_or((p, q));
|
||||
params.push(t);
|
||||
points.push([p.x, p.y, p.z, q[0], q[1], q[2], q[3]]);
|
||||
prev = Some(q);
|
||||
}
|
||||
it.leader = CurveN::interpolate(¶ms, &points, 3).ok()?;
|
||||
Some(it)
|
||||
}
|
||||
|
||||
/// The point at curve parameter `t` and both surfaces' parameters there, or `None`
|
||||
/// (near-tangent, or Newton failed).
|
||||
pub(crate) fn eval(&self, t: f64) -> Option<(Vec3, [f64; 4])> {
|
||||
let (x, dx) = self.leader.eval(t)?;
|
||||
let dir = Vec3::new(dx[0], dx[1], dx[2]).normalized()?;
|
||||
self.newton(Vec3::new(x[0], x[1], x[2]), dir, [x[3], x[4], x[5], x[6]])
|
||||
}
|
||||
|
||||
/// The two surfaces, in the order of the parameters [`Intersection::eval`] gives.
|
||||
pub(crate) fn surfaces(&self) -> &[EvalSurface; 2] {
|
||||
&self.surfaces
|
||||
}
|
||||
|
||||
/// Newton from the parameters `x` on S1 = S2, held on the plane through `l` normal to `dir`:
|
||||
/// the point and its parameters, or `None` (near-tangent, singular, or not converged).
|
||||
fn newton(&self, l: Vec3, dir: Vec3, mut x: [f64; 4]) -> Option<(Vec3, [f64; 4])> {
|
||||
for _ in 0..MAX_ITER {
|
||||
let (Some((p1, u1, v1)), Some((p2, u2, v2))) = (self.surfaces[0].derivs(x[0], x[1]), self.surfaces[1].derivs(x[2], x[3])) else {
|
||||
return None;
|
||||
};
|
||||
let gap = p1 - p2;
|
||||
let plane = dir.dot(p1 - l);
|
||||
let n = u1.cross(v1).normalized()?.cross(u2.cross(v2).normalized()?);
|
||||
if n.len() < MIN_SIN {
|
||||
return None;
|
||||
}
|
||||
if gap.len() <= self.eps && plane.abs() <= self.eps {
|
||||
return Some(((p1 + p2) * 0.5, x));
|
||||
}
|
||||
// J · dx = −F, F = (S1 − S2, dir · (S1 − L)).
|
||||
let j = [[u1.x, v1.x, -u2.x, -v2.x], [u1.y, v1.y, -u2.y, -v2.y], [u1.z, v1.z, -u2.z, -v2.z], [dir.dot(u1), dir.dot(v1), 0.0, 0.0]];
|
||||
let dx = solve4(j, [-gap.x, -gap.y, -gap.z, -plane])?;
|
||||
for (xi, d) in x.iter_mut().zip(dx) {
|
||||
*xi += d;
|
||||
}
|
||||
for i in 0..2 {
|
||||
let [u, v] = self.domains[i].clamp([x[2 * i], x[2 * i + 1]]);
|
||||
x[2 * i] = u;
|
||||
x[2 * i + 1] = v;
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn from_v3(v: mt::Vector3) -> Vec3 {
|
||||
Vec3::new(v.x, v.y, v.z)
|
||||
}
|
||||
|
||||
/// `a · x = b` for a 4 × 4 system by Gaussian elimination with partial pivoting; `None` when
|
||||
/// singular.
|
||||
fn solve4(mut a: [[f64; 4]; 4], mut b: [f64; 4]) -> Option<[f64; 4]> {
|
||||
let scale = a.iter().flatten().fold(0.0f64, |m, x| m.max(x.abs()));
|
||||
if !(scale > 0.0) {
|
||||
return None;
|
||||
}
|
||||
for c in 0..4 {
|
||||
let p = (c..4).max_by(|&i, &j| a[i][c].abs().total_cmp(&a[j][c].abs()))?;
|
||||
if !(a[p][c].abs() > 1e-12 * scale) {
|
||||
return None;
|
||||
}
|
||||
a.swap(c, p);
|
||||
b.swap(c, p);
|
||||
let pivot = a[c];
|
||||
for r in c + 1..4 {
|
||||
let f = a[r][c] / pivot[c];
|
||||
for (x, p) in a[r].iter_mut().zip(pivot).skip(c) {
|
||||
*x -= f * p;
|
||||
}
|
||||
b[r] -= f * b[c];
|
||||
}
|
||||
}
|
||||
let mut x = [0.0; 4];
|
||||
for c in (0..4).rev() {
|
||||
let s: f64 = (c + 1..4).map(|k| a[c][k] * x[k]).sum();
|
||||
x[c] = (b[c] - s) / a[c][c];
|
||||
}
|
||||
x.iter().all(|v| v.is_finite()).then_some(x)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn solves_a_four_by_four_system() {
|
||||
let a = [[2.0, 1.0, 0.0, 0.0], [1.0, 3.0, 1.0, 0.0], [0.0, 1.0, 4.0, 1.0], [0.0, 0.0, 1.0, 5.0]];
|
||||
let x = [1.0, -2.0, 0.5, 3.0];
|
||||
let b = [0, 1, 2, 3].map(|r| (0..4).map(|c| a[r][c] * x[c]).sum::<f64>());
|
||||
let Some(got) = solve4(a, b) else { panic!("solves") };
|
||||
for (g, w) in got.iter().zip(x) {
|
||||
assert!((g - w).abs() < 1e-12);
|
||||
}
|
||||
assert!(solve4([[1.0; 4]; 4], [1.0; 4]).is_none(), "singular");
|
||||
}
|
||||
|
||||
/// Points on the intersection edges of a boolean (a cross hole: two cylinders) lie on both
|
||||
/// surfaces, on truck's curve (the parameterisation follows our smooth leader, so the same
|
||||
/// parameter may sit at a slightly different place along it), and advance along the curve.
|
||||
#[test]
|
||||
fn intersection_points_lie_on_both_surfaces_and_on_truck_s_curve() {
|
||||
let Ok(pin) = crate::cylinder(Vec3::new(5.0, 5.0, -1.0), Vec3::Z, 2.0, 6.0) else { panic!("pin") };
|
||||
let Ok(cross) = crate::cylinder(Vec3::new(5.0, -1.0, 2.0), Vec3::Y, 1.0, 12.0) else { panic!("cross") };
|
||||
let Ok(Some(body)) = crate::boolean(&pin, &cross, crate::BoolOp::Cut) else { panic!("cut") };
|
||||
let mut checked = 0;
|
||||
for e in body.solid.edge_iter() {
|
||||
let curve = e.curve();
|
||||
let Some(ix) = Intersection::new(&curve) else { continue };
|
||||
let (t0, t1) = curve.range_tuple();
|
||||
let dense: Vec<Vec3> = (0..=1000).map(|k| from_p3(curve.subs(t0 + (t1 - t0) * f64::from(k) / 1000.0))).collect();
|
||||
let near = |p: Vec3| dense.windows(2).map(|w| p.dist_to_segment(w[0], w[1])).fold(f64::MAX, f64::min);
|
||||
let mut last: Option<Vec3> = None;
|
||||
for k in 0..=50 {
|
||||
let t = t0 + (t1 - t0) * f64::from(k) / 50.0;
|
||||
let Some((p, _)) = ix.eval(t) else { continue };
|
||||
for (s, dom) in ix.surfaces.iter().zip(&ix.domains) {
|
||||
assert!(uv::project(s, dom, p, None, 1e-8).is_some(), "t {t}: {p:?} off a surface");
|
||||
}
|
||||
assert!(near(p) < 1e-4, "t {t}: {p:?} is {} from truck's curve", near(p));
|
||||
if let Some(q) = last {
|
||||
assert!(p.dist(q) > 0.0, "t {t}: the points advance");
|
||||
}
|
||||
last = Some(p);
|
||||
checked += 1;
|
||||
}
|
||||
}
|
||||
assert!(checked > 100, "{checked} points checked");
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,7 @@ pub(crate) fn domain(s: &EvalSurface) -> Domain {
|
||||
}
|
||||
|
||||
impl Domain {
|
||||
fn clamp(&self, uv: [f64; 2]) -> [f64; 2] {
|
||||
pub(crate) fn clamp(&self, uv: [f64; 2]) -> [f64; 2] {
|
||||
let mut out = uv;
|
||||
for (d, x) in out.iter_mut().enumerate() {
|
||||
if let (Some((a, b)), None) = (self.range[d], self.period[d]) {
|
||||
|
||||
+44
-21
@@ -184,7 +184,7 @@ struct Batch {
|
||||
count: u32,
|
||||
}
|
||||
|
||||
/// A planned body on the GPU: its evaluated mesh and a colour per vertex.
|
||||
/// A chunk of planned bodies on the GPU: their evaluated mesh and a colour per vertex.
|
||||
struct PlannedDraw {
|
||||
mesh: Arc<solidcraft_gpu::GpuMesh>,
|
||||
colors: wgpu::Buffer,
|
||||
@@ -206,9 +206,9 @@ struct Batches {
|
||||
xray: Option<Batch>,
|
||||
under: Option<Batch>,
|
||||
planned: Vec<PlannedDraw>,
|
||||
/// Evaluated plans by the plan's address, holding the plan so the address stays its own:
|
||||
/// a body whose plan is unchanged is not evaluated again when the scene is rebuilt.
|
||||
resident: std::collections::HashMap<usize, (Arc<solidcraft_engine::geom::tess::TessPlan>, Arc<solidcraft_gpu::GpuMesh>)>,
|
||||
/// Evaluated chunks by their plans' addresses, holding the plans so the addresses stay
|
||||
/// theirs: a chunk whose plans are unchanged is not evaluated again when the scene is rebuilt.
|
||||
resident: std::collections::HashMap<Vec<usize>, (Vec<Arc<solidcraft_engine::geom::tess::TessPlan>>, Arc<solidcraft_gpu::GpuMesh>)>,
|
||||
}
|
||||
|
||||
impl Batches {
|
||||
@@ -238,6 +238,9 @@ impl Batches {
|
||||
}
|
||||
}
|
||||
|
||||
/// The planned bodies on the GPU: in chunks of at most `MAX_BATCH` vertices (one for any
|
||||
/// ordinary scene), each uploaded and evaluated in one dispatch and drawn in one call. A
|
||||
/// chunk whose plans are unchanged is not evaluated again; colours are rebuilt each time.
|
||||
fn planned(
|
||||
&mut self,
|
||||
device: &wgpu::Device,
|
||||
@@ -245,27 +248,47 @@ impl Batches {
|
||||
encoder: &mut wgpu::CommandEncoder,
|
||||
bodies: &[PlannedBody],
|
||||
) -> Vec<PlannedDraw> {
|
||||
let ptr = |b: &PlannedBody| Arc::as_ptr(&b.plan) as usize;
|
||||
self.resident.retain(|k, _| bodies.iter().any(|b| ptr(b) == *k));
|
||||
let mut out = Vec::with_capacity(bodies.len());
|
||||
let mut chunks: Vec<Vec<&PlannedBody>> = Vec::new();
|
||||
let mut filled = 0usize;
|
||||
for b in bodies {
|
||||
let mesh = match self.resident.get(&ptr(b)) {
|
||||
let n = b.plan.vertex_count();
|
||||
match chunks.last_mut() {
|
||||
Some(c) if filled + n <= solidcraft_gpu::MAX_BATCH => c.push(b),
|
||||
_ => {
|
||||
chunks.push(vec![b]);
|
||||
filled = 0;
|
||||
}
|
||||
}
|
||||
filled += n;
|
||||
}
|
||||
let key = |c: &[&PlannedBody]| c.iter().map(|b| Arc::as_ptr(&b.plan) as usize).collect::<Vec<usize>>();
|
||||
self.resident.retain(|k, _| chunks.iter().any(|c| key(c) == *k));
|
||||
let mut out = Vec::with_capacity(chunks.len());
|
||||
for chunk in &chunks {
|
||||
let mesh = match self.resident.get(&key(chunk)) {
|
||||
Some((_, m)) => m.clone(),
|
||||
None => match tess.prepare(&b.plan) {
|
||||
Ok(m) => {
|
||||
tess.record(encoder, &m);
|
||||
let m = Arc::new(m);
|
||||
self.resident.insert(ptr(b), (b.plan.clone(), m.clone()));
|
||||
m
|
||||
None => {
|
||||
let plans: Vec<&solidcraft_engine::geom::tess::TessPlan> = chunk.iter().map(|b| &*b.plan).collect();
|
||||
match tess.prepare_many(&plans) {
|
||||
Ok(m) => {
|
||||
tess.record(encoder, &m);
|
||||
let m = Arc::new(m);
|
||||
self.resident.insert(key(chunk), (chunk.iter().map(|b| b.plan.clone()).collect(), m.clone()));
|
||||
m
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("bodies are not drawn on the GPU: {e}");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("a body is not drawn on the GPU: {e}");
|
||||
continue;
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
let last = b.face_colors.last().copied().unwrap_or([200, 200, 200, 255]);
|
||||
let bytes: Vec<u8> = mesh.vertex_face.iter().flat_map(|f| b.face_colors.get(*f as usize).copied().unwrap_or(last)).collect();
|
||||
let mut bytes = Vec::with_capacity(mesh.vertex_face.len() * 4);
|
||||
for (part, b) in mesh.parts.iter().zip(chunk) {
|
||||
let last = b.face_colors.last().copied().unwrap_or([200, 200, 200, 255]);
|
||||
let faces = mesh.vertex_face.get(part.vertices.start as usize..part.vertices.end as usize).unwrap_or_default();
|
||||
bytes.extend(faces.iter().flat_map(|f| b.face_colors.get(*f as usize).copied().unwrap_or(last)));
|
||||
}
|
||||
if let Some(colors) = upload(device, "sc_planned_colors", &bytes, 4) {
|
||||
out.push(PlannedDraw { mesh, colors: colors.buffer });
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user