diff --git a/crates/geom/src/bspline.rs b/crates/geom/src/bspline.rs index 252a695e..8d36ae40 100644 --- a/crates/geom/src/bspline.rs +++ b/crates/geom/src/bspline.rs @@ -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 { + pub degree: usize, + pub knots: Vec, + pub ctrl: Vec<[f64; N]>, +} + +impl CurveN { + /// 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> { + 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::() / 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 = (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"); + } +} diff --git a/crates/geom/src/tess.rs b/crates/geom/src/tess.rs index 3bd91aae..94d23b94 100644 --- a/crates/geom/src/tess.rs +++ b/crates/geom/src/tess.rs @@ -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; 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; } diff --git a/crates/gpu/src/lib.rs b/crates/gpu/src/lib.rs index c723f088..130c2b35 100644 --- a/crates/gpu/src/lib.rs +++ b/crates/gpu/src/lib.rs @@ -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, + /// Per plan (in the order given), its vertices and indices. + pub parts: Vec, 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, + pub indices: std::ops::Range, +} + /// 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 { - 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 { - 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 { + 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 = 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, c: &EvalCurve) -> Result<(u32, u32)> { Ok((knots, ctrl)) } -fn pack(plan: &TessPlan) -> Result { - 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 { + let mut surfaces = Vec::with_capacity(plans.iter().map(|p| p.surfaces.len()).sum::() * SURFACE_WORDS); let mut data: Vec = 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 { } 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::() * 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, diff --git a/crates/gpu/src/tests.rs b/crates/gpu/src/tests.rs index 993c03d0..4254c0c7 100644 --- a/crates/gpu/src/tests.rs +++ b/crates/gpu/src/tests.rs @@ -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 = 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 = 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] diff --git a/crates/kernel/src/tess/edges.rs b/crates/kernel/src/tess/edges.rs index 1955c7b6..11d620a8 100644 --- a/crates/kernel/src/tess/edges.rs +++ b/crates/kernel/src/tess/edges.rs @@ -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, + /// 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>)>, +} + +impl Nodes { + fn plain(points: Vec) -> 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>> { + 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 { +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 { 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 { + 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 = Vec::with_capacity(initial as usize + 1); + let mut ends: Vec = 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 { struct Seg { a: f64, b: f64, - pa: Vec3, - pb: Vec3, - mid: Option, + pa: Pt, + pb: Pt, + mid: Option, depth: u32, } - let mut out: Vec = vec![front]; + let Some(&first) = ends.first() else { return fallback() }; + let mut out: Vec = vec![(front, first.1)]; let mut stack: Vec = 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 { 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 { } } 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 = 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"); + } } diff --git a/crates/kernel/src/tess/mod.rs b/crates/kernel/src/tess/mod.rs index 6516d4ee..10e18f5a 100644 --- a/crates/kernel/src/tess/mod.rs +++ b/crates/kernel/src/tess/mod.rs @@ -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) -> 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> = 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> { - 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) -> 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::new(); + let mut known: Vec>> = 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) -> 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>) { +fn relax_crowded_faces(faces: &[&mt::Face], jobs: &[(&mt::Edge, f64)], rules: &size::Rules, nodes: &mut HashMap) { use truck_meshalgo::tessellation::Deflection; let Some(turn) = rules.turn else { return }; let h_of: HashMap = 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 = 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 = boundaries .iter() .filter(|ids| ids.iter().map(segments).sum::() > Deflection::MAX_ANGLE_BOUNDARY) @@ -184,6 +196,7 @@ fn plan_face( fi: usize, s: &EvalSurface, loops3: &[Vec], + known: &[Vec>], rules: &size::Rules, size: (f64, [f64; 2]), stats: &mut PlanStats, @@ -197,11 +210,17 @@ fn plan_face( let mut fixed: Vec = Vec::new(); let mut uvs: Vec<[f64; 2]> = Vec::new(); let mut ranges: Vec> = 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]); diff --git a/crates/kernel/src/tess/ssi.rs b/crates/kernel/src/tess/ssi.rs new file mode 100644 index 00000000..c69a36c5 --- /dev/null +++ b/crates/kernel/src/tess/ssi.rs @@ -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 { + 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::()); + 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 = (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 = 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"); + } +} diff --git a/crates/kernel/src/tess/uv.rs b/crates/kernel/src/tess/uv.rs index b495aedd..4e15853f 100644 --- a/crates/kernel/src/tess/uv.rs +++ b/crates/kernel/src/tess/uv.rs @@ -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]) { diff --git a/crates/ui-egui/src/gpu.rs b/crates/ui-egui/src/gpu.rs index b605e6ef..75b64842 100644 --- a/crates/ui-egui/src/gpu.rs +++ b/crates/ui-egui/src/gpu.rs @@ -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, colors: wgpu::Buffer, @@ -206,9 +206,9 @@ struct Batches { xray: Option, under: Option, planned: Vec, - /// 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, Arc)>, + /// 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>, Arc)>, } 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 { - 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::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::>(); + 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 = 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 }); }