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

+8 -180
View File
@@ -168,9 +168,7 @@ pub struct Pipeline {
/// it back into the base set (issue #316).
text_preview_vbuf: Option<wgpu::Buffer>,
text_preview_vcount: u32,
/// Per-frame DISPSILH silhouette line list — rebuilt every prepare() from
/// the mesh sets' curved-face generators and the current view direction, so
/// the outline tracks the camera. Reuses the mesh vertex format / pipeline.
/// Per-frame silhouette line list from the kernel mesh and current view.
silhouette_vbuf: Option<wgpu::Buffer>,
silhouette_vcount: u32,
/// Last requested render size (the full viewport rect, in pixels). The
@@ -2290,35 +2288,17 @@ impl Pipeline {
// The math lives outside the GPU method so it can be unit-tested; see the
// module test below.
/// Rebuild the per-frame DISPSILH silhouette line list from the mesh sets'
/// curved-face generators and the current eye. For each cone/cylinder face
/// the silhouette runs at the two angles where the surface turns edge-on to
/// the view — `θ = φ ± acos(-tanα·(view·axis) / |view⊥|)`, which reduces to
/// `φ ± π/2` for a cylinder. Segments are uploaded in the mesh vertex format
/// so they draw through the existing wireframe pipeline.
/// Rebuild view-dependent silhouette lines through the kernel.
pub fn upload_silhouettes(
&mut self,
device: &wgpu::Device,
sets: &[crate::scene::model::mesh_model::MeshLodSet],
view_dir: glam::Vec3,
) {
// Silhouettes follow the view *angle* only — a single parallel direction
// for the whole scene, not the eye-to-surface vector — so the outline
// stays put under pan and doesn't foreshorten. This is the orthographic
// silhouette a CAD wireframe expects.
let view = glam::DVec3::new(view_dir.x as f64, view_dir.y as f64, view_dir.z as f64)
.normalize_or(glam::DVec3::NEG_Z);
use crate::scene::model::mesh_model::CurvedGen;
use crate::scene::pipeline::mesh_gpu::MeshVertex;
let mut verts: Vec<MeshVertex> = Vec::new();
let d3 = |a: [f32; 3]| glam::DVec3::new(a[0] as f64, a[1] as f64, a[2] as f64);
let lo = |c: [f32; 3], l: [f32; 3]| {
glam::DVec3::new(
c[0] as f64 + l[0] as f64,
c[1] as f64 + l[1] as f64,
c[2] as f64 + l[2] as f64,
)
};
for set in sets {
let color = set.lods.first().map(|m| m.color).unwrap_or([0.0, 0.0, 0.0, 1.0]);
let mk = |w: glam::DVec3| -> MeshVertex {
@@ -2346,141 +2326,12 @@ impl Pipeline {
uv_normal: [0.0; 2],
}
};
for g in &set.curved_gens {
match g {
CurvedGen::Cone {
base, base_low, axis, u_dir, v_dir, radius, tan_a,
h_max, theta_min, theta_span, full,
} => {
let base = lo(*base, *base_low);
let (axis, u, v) = (d3(*axis), d3(*u_dir), d3(*v_dir));
let Some((t0, t1)) =
silhouette_thetas(view.dot(u), view.dot(v), view.dot(axis), *tan_a as f64)
else {
continue;
};
let r0 = *radius as f64;
let r1 = *radius as f64 + *h_max as f64 * *tan_a as f64;
for theta in [t0, t1] {
if !full {
let off = (theta - *theta_min as f64).rem_euclid(std::f64::consts::TAU);
if off > *theta_span as f64 {
continue;
}
}
let (c, s) = (theta.cos(), theta.sin());
let radial = u * c + v * s;
verts.push(mk(base + radial * r0));
verts.push(mk(base + radial * r1 + axis * *h_max as f64));
}
}
CurvedGen::Sphere {
center, center_low, pole, u_dir, v_dir, radius,
theta_min, theta_span, full, phi_min, phi_max,
} => {
let c = lo(*center, *center_low);
let (pole, u, v) = (d3(*pole), d3(*u_dir), d3(*v_dir));
let r = *radius as f64;
// Great circle in the plane perpendicular to the view.
let mut e1 = view.cross(pole);
if e1.length_squared() < 1e-12 {
e1 = view.cross(u);
}
let e1 = e1.normalize();
let e2 = view.cross(e1).normalize();
const N: usize = 64;
let mut prev: Option<glam::DVec3> = None;
for i in 0..=N {
let a = std::f64::consts::TAU * (i as f64 / N as f64);
let dir = e1 * a.cos() + e2 * a.sin();
// Keep only the arc that lies on the actual face.
// `full` is a *longitude* wrap flag: a dish cap sits
// on the pole and so covers every longitude while
// still ending at its seam, so the colatitude test
// always applies. A whole ball reports phi 0..π and
// passes it regardless.
let phi = dir.dot(pole).clamp(-1.0, 1.0).acos();
let in_phi =
phi >= *phi_min as f64 && phi <= *phi_max as f64;
let in_theta = *full || {
let th = dir.dot(v).atan2(dir.dot(u));
let toff = (th - *theta_min as f64).rem_euclid(std::f64::consts::TAU);
toff <= *theta_span as f64
};
let on_face = in_phi && in_theta;
let p = if on_face { Some(c + dir * r) } else { None };
if let (Some(a), Some(b)) = (prev, p) {
verts.push(mk(a));
verts.push(mk(b));
}
prev = p;
}
}
CurvedGen::Torus {
center, center_low, axis, u_dir, v_dir, major, minor,
phi_min, phi_span, full, theta_min, theta_span, theta_full,
} => {
let ctr = lo(*center, *center_low);
let (axis, u, v) = (d3(*axis), d3(*u_dir), d3(*v_dir));
let (major, minor) = (*major as f64, *minor as f64);
// True silhouette: at each revolution angle the tube is a
// circle; the two points where its normal turns edge-on
// trace two curves around the ring. Sample the revolution
// and connect consecutive edge-on points.
const N: usize = 72;
let span = if *full { std::f64::consts::TAU } else { *phi_span as f64 };
let mut prev: [Option<glam::DVec3>; 2] = [None, None];
for i in 0..=N {
let phi = *phi_min as f64 + span * (i as f64 / N as f64);
let radial = u * phi.cos() + v * phi.sin();
let ring = ctr + radial * major;
let (rv, av) = (radial.dot(view), axis.dot(view));
if rv.abs() < 1e-9 && av.abs() < 1e-9 {
prev = [None, None];
continue;
}
// tube normal(θ) = radial·cosθ + axis·sinθ; ⟂ view at
// θ = atan2(-rv, av) and +π.
let th = (-rv).atan2(av);
let cur = [th, th + std::f64::consts::PI];
for k in 0..2 {
let t = cur[k];
let theta_offset =
(t - *theta_min as f64).rem_euclid(std::f64::consts::TAU);
let on_face = *theta_full || theta_offset <= *theta_span as f64;
let p = on_face.then(|| {
ring + (radial * t.cos() + axis * t.sin()) * minor
});
if let (Some(pp), Some(p)) = (prev[k], p) {
verts.push(mk(pp));
verts.push(mk(p));
}
prev[k] = p;
}
}
}
}
}
if set.curved_gens.is_empty() || !set.complete {
let best = set.stored_silhouettes.iter().max_by(|left, right| {
let score = |silhouette: &crate::scene::model::mesh_model::StoredSilhouette| {
let direction = d3(silhouette.view_direction)
.normalize_or(glam::DVec3::NEG_Z);
direction.dot(view).abs()
};
score(left)
.partial_cmp(&score(right))
.unwrap_or(std::cmp::Ordering::Equal)
});
if let Some(silhouette) = best {
for (index, high) in silhouette.edge_verts.iter().copied().enumerate() {
let low = silhouette
.edge_verts_low
.get(index)
.copied()
.unwrap_or([0.0; 3]);
verts.push(mk(lo(high, low)));
}
for generator in &set.curved_gens {
for point in acadrust::kernel::brep::mesh::silhouette(
&generator.source,
[view.x, view.y, view.z],
) {
verts.push(mk(glam::DVec3::from_array(point)));
}
}
}
@@ -4678,26 +4529,3 @@ impl iced::widget::shader::Pipeline for MultiPipeline {
}
}
}
/// The two silhouette angles of a cone/cylinder face for a view direction,
/// expressed in the face's `(u, v, axis)` frame via the view's components on
/// each: `du = view·u`, `dv = view·v`, `da = view·axis`. `tan_a` is the cone
/// taper (0 for a cylinder).
///
/// The outward normal is edge-on to the view where `du·cosθ + dv·sinθ =
/// -tanα·da`, i.e. `θ = φ ± acos(-tanα·da / |view⊥|)` with `φ = atan2(dv, du)`.
/// `None` when the view runs down the axis (no outline) or the whole cone faces
/// toward/away (`|arg| > 1`).
fn silhouette_thetas(du: f64, dv: f64, da: f64, tan_a: f64) -> Option<(f64, f64)> {
let r_perp = (du * du + dv * dv).sqrt();
if r_perp < 1e-6 {
return None;
}
let arg = -tan_a * da / r_perp;
if arg.abs() > 1.0 {
return None;
}
let phi = dv.atan2(du);
let delta = arg.acos();
Some((phi + delta, phi - delta))
}