Section Analysis visuals: the cut faces are hatched at 45 degrees in the cap colour, and each body's outline along the cut is drawn

Part of #51 (per-body cap colours and the cut in snapshots to follow)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01RjGw4gA5ZvKrU8MQP6sBgT
This commit is contained in:
bherbruckandClaude Opus 5.5 committed 2026-10-10 07:11:40 +00:00
1 parent 794a394b28
commit 535bd5dfff
2 files changed
+56 -1

No files matched your search

+3 -1
View File
@@ -328,7 +328,9 @@ fn fs_solid(i: TOut, @builtin(front_facing) front: bool) -> @location(0) vec4<f3
let fw = fwidth(ndv);
if (clipped(i.wp)) { discard; }
if (!front && length(u.clip.xyz) > 0.5) {
return out_color(u.cap);
// The cut face: the cap colour, hatched at 45 degrees on screen.
let h = fract((i.pos.x + i.pos.y) / 9.0);
return out_color(vec4<f32>(select(u.cap.rgb, u.cap.rgb * 0.62, h < 0.22), u.cap.a));
}
if (u.ana.x > 0.5 && length(i.n) > 0.5) {
return analysis(i, k);
+53
View File
@@ -165,6 +165,8 @@ fn scene_key(app: &SolveApp) -> u64 {
app.ui.show_grid.hash(&mut h);
app.ui.visual_style.hash(&mut h);
app.ui.ground_shadow.hash(&mut h);
// The section plane (its cut outlines are part of the model scene).
section_plane(app).map(|(o, n)| [o.x, o.y, o.z, n.x, n.y, n.z].map(f64::to_bits)).hash(&mut h);
app.cam.up.hash(&mut h);
(app.cam.pitch > 0.0).hash(&mut h);
app.ui.show_sketches.hash(&mut h);
@@ -281,6 +283,15 @@ fn build_scene(app: &SolveApp) -> GpuScene {
if app.ui.ground_shadow && app.ui.visual_style != 2 && app.cam.pitch > 0.0 {
ground_shadow(&mut sc, app, &st);
}
// Section: the outline of each body where the plane cuts it.
if let Some((o, n)) = section_plane(app) {
let col = c4(tk.text.gamma_multiply(0.9));
for b in st.bodies.iter().filter(|b| !app.ui.hidden_bodies.contains(&b.name)) {
for (a, c) in section_segments(&b.mesh(), o, n) {
sc.line(a.to_f32(), c.to_f32(), col, 1.8, false);
}
}
}
for sid in visible_sketches(app) {
let Some(ss) = st.sketch(sid) else { continue };
let active = s.active_sketch == Some(sid);
@@ -365,6 +376,28 @@ fn ground_shadow(sc: &mut GpuScene, app: &SolveApp, st: &solvecraft_engine::doc:
}
}
/// Where the plane through `o` with normal `n` cuts a mesh: one segment per crossed triangle,
/// nudged a hair to the kept side (so the clip leaves them drawn).
pub(crate) fn section_segments(m: &solvecraft_engine::geom::Mesh, o: Vec3, n: Vec3) -> Vec<(Vec3, Vec3)> {
let eps = 1e-4 * (1.0 + m.bounds().diagonal());
let mut out = Vec::new();
for t in &m.triangles {
let Some(p) = m.tri(t) else { continue };
let d = p.map(|q| n.dot(q - o));
let mut pts = Vec::with_capacity(2);
for (i, j) in [(0, 1), (1, 2), (2, 0)] {
if (d[i] < 0.0) != (d[j] < 0.0) {
let s = d[i] / (d[i] - d[j]);
pts.push(p[i] + (p[j] - p[i]) * s - n * eps);
}
}
if let [a, b] = pts[..] {
out.push((a, b));
}
}
out
}
/// The drawn pieces of a polyline in a dash pattern (on, off, on, off… lengths).
pub(crate) fn dashes(pts: &[Vec3], pattern: &[f64]) -> Vec<(Vec3, Vec3)> {
let mut out = Vec::new();
@@ -2191,6 +2224,26 @@ fn view_menu(app: &mut SolveApp, ui: &mut egui::Ui, button: Rect) -> bool {
mod tests {
use super::*;
#[test]
fn section_outlines_follow_the_cut() {
let mut s = solvecraft_engine::Session::default();
s.execute("solid.box", &json!({"length": 40, "width": 30, "height": 20})).unwrap();
let st = s.model.state();
let m = st.bodies[0].mesh();
let (o, n) = (Vec3::new(0.0, 15.0, 0.0), Vec3::new(0.0, -1.0, 0.0));
let segs = section_segments(&m, o, n);
assert!(segs.len() >= 4, "{segs:?}");
// On the plane (a hair to the kept side) and around the 40 x 20 cut.
let mut len = 0.0;
for (a, b) in &segs {
assert!((a.y - 15.0).abs() < 0.01 && (b.y - 15.0).abs() < 0.01);
len += a.dist(*b);
}
assert!((len - 120.0).abs() < 0.5, "outline length {len}");
// A plane that misses the body cuts nothing.
assert!(section_segments(&m, Vec3::new(0.0, 99.0, 0.0), n).is_empty());
}
#[test]
fn selection_shows_where_the_model_hides_it() {
let mut s = solvecraft_engine::Session::default();