Section Analysis: cut faces in their body's colour (tinted toward the cap colour) and hatched; snapshots show the section too (the cut part removed, hatched cut faces)
Fixes #51 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01RjGw4gA5ZvKrU8MQP6sBgT
This commit is contained in:
1 parent
c90070c497
commit
74f70614cc
3 files changed
+43
-3
No files matched your search
@@ -121,7 +121,7 @@ pub fn sketch_point_visible(sk: &Sketch, i: usize) -> bool {
|
||||
/// The scene for a headless render.
|
||||
pub fn scene(s: &Session, cam: &Camera) -> Scene {
|
||||
let st = s.world_state();
|
||||
let mut sc = Scene { background: Some((colors::BG_TOP, colors::BG_BOTTOM)), ..Default::default() };
|
||||
let mut sc = Scene { background: Some((colors::BG_TOP, colors::BG_BOTTOM)), clip: s.section.map(|(o, n)| (n, n.dot(o))), ..Default::default() };
|
||||
let b = bounds(s);
|
||||
sc.radius = (b.diagonal() * 0.5).max(cam.half_height()).max(10.0) + b.center().dist(cam.target);
|
||||
// Grid on the ground plane (XY with Z up, XZ with Y up).
|
||||
@@ -293,4 +293,19 @@ mod tests {
|
||||
let dark = img.rgba.chunks(4).filter(|p| p[0] < 70 && p[1] < 70 && p[2] < 75).count();
|
||||
assert!(dark > 40, "silhouette pixels: {dark}");
|
||||
}
|
||||
|
||||
/// A section shows in snapshots: the cut part is gone and the cut face is drawn.
|
||||
#[test]
|
||||
fn snapshots_show_the_section() {
|
||||
let mut s = Session::default();
|
||||
s.execute("solid.box", &json!({"length": 40, "width": 30, "height": 20})).unwrap();
|
||||
let cam = home_camera(&s);
|
||||
let whole = solvecraft_render::render(&scene(&s, &cam), &cam, 200, 140).rgba;
|
||||
s.execute("inspect.section", &json!({"plane": {"origin": [0, 15, 0], "normal": [0, -1, 0]}})).unwrap();
|
||||
let sc = scene(&s, &cam);
|
||||
assert!(sc.clip.is_some());
|
||||
let cut = solvecraft_render::render(&sc, &cam, 200, 140).rgba;
|
||||
let changed = whole.chunks(4).zip(cut.chunks(4)).filter(|(a, b)| a != b).count();
|
||||
assert!(changed > 500, "{changed} pixels changed");
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,9 @@ pub struct Scene {
|
||||
pub background: Option<(Rgb, Rgb)>,
|
||||
/// Scene radius for clipping planes.
|
||||
pub radius: f64,
|
||||
/// A section cut: (normal, d); what lies where normal·p > d is cut away, and the inside
|
||||
/// shows as a hatched cap.
|
||||
pub clip: Option<(Vec3, f64)>,
|
||||
}
|
||||
|
||||
/// RGBA pixels plus depth.
|
||||
@@ -109,6 +112,9 @@ pub fn render(scene: &Scene, cam: &Camera, w: usize, h: usize) -> Canvas {
|
||||
for (ti, t) in m.triangles.iter().enumerate() {
|
||||
let face = m.tri_face.get(ti).copied();
|
||||
let base = sm.face_colors.iter().find(|(f, _)| Some(*f) == face).map_or(body, |(_, c)| c.f());
|
||||
let wp = t.map(|k| m.positions.get(k as usize).copied().unwrap_or_default());
|
||||
// Seen from inside (only where a section cuts the body open).
|
||||
let inside = (wp[1] - wp[0]).cross(wp[2] - wp[0]).dot(view_dir) < 0.0;
|
||||
let (Some(Some(a)), Some(Some(b)), Some(Some(cc))) = (pts.get(t[0] as usize), pts.get(t[1] as usize), pts.get(t[2] as usize)) else {
|
||||
continue;
|
||||
};
|
||||
@@ -134,6 +140,11 @@ pub fn render(scene: &Scene, cam: &Camera, w: usize, h: usize) -> Canvas {
|
||||
if w0 < -1e-9 || w1 < -1e-9 || w2 < -1e-9 {
|
||||
continue;
|
||||
}
|
||||
if let Some((cn, cd)) = scene.clip
|
||||
&& cn.dot(wp[0] * w0 + wp[1] * w1 + wp[2] * w2) > cd
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let z = (w0 * a[2] + w1 * b[2] + w2 * cc[2]) as f32;
|
||||
let i = y * sw + x;
|
||||
if c.depth.get(i).is_some_and(|d| z < *d) {
|
||||
@@ -141,6 +152,12 @@ pub fn render(scene: &Scene, cam: &Camera, w: usize, h: usize) -> Canvas {
|
||||
*d = z;
|
||||
}
|
||||
let n = (ns[0] * w0 + ns[1] * w1 + ns[2] * w2).normalized().unwrap_or(Vec3::Z);
|
||||
if scene.clip.is_some() && inside {
|
||||
// The cut: the body's colour, hatched at 45 degrees.
|
||||
let k = if ((x + y) / SS) % 9 < 2 { 0.5 } else { 0.8 };
|
||||
c.put(x, y, base.map(|v| v * k), 1.0);
|
||||
continue;
|
||||
}
|
||||
let mut col = shade(n, view_dir, base);
|
||||
let ndv = n.dot(view_dir).abs();
|
||||
if curved && ndv < 0.14 {
|
||||
@@ -165,6 +182,11 @@ pub fn render(scene: &Scene, cam: &Camera, w: usize, h: usize) -> Canvas {
|
||||
let steps = (len.ceil() as usize).clamp(1, 20_000);
|
||||
for k in 0..=steps {
|
||||
let t = k as f64 / steps as f64;
|
||||
if let Some((cn, cd)) = scene.clip
|
||||
&& cn.dot(*p + (*q - *p) * t) > cd + 1e-6
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let (x, y, z) = (a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t);
|
||||
let r = half.ceil() as i64;
|
||||
for dy in -r..=r {
|
||||
@@ -268,6 +290,7 @@ mod tests {
|
||||
lines: vec![],
|
||||
background: None,
|
||||
radius: 20.0,
|
||||
clip: None,
|
||||
};
|
||||
let mut cam = Camera { distance: 50.0, ..Default::default() };
|
||||
cam.set_view(StandardView::Front);
|
||||
|
||||
@@ -328,9 +328,11 @@ 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) {
|
||||
// The cut face: the cap colour, hatched at 45 degrees on screen.
|
||||
// The cut face in its body's colour (as Fusion's "from component"), tinted toward the
|
||||
// cap colour so it reads as a cut, hatched at 45 degrees on screen.
|
||||
let base = mix(i.c.rgb, u.cap.rgb, 0.35);
|
||||
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));
|
||||
return out_color(vec4<f32>(select(base, base * 0.6, h < 0.22), 1.0));
|
||||
}
|
||||
if (u.ana.x > 0.5 && length(i.n) > 0.5) {
|
||||
return analysis(i, k);
|
||||
|
||||
Reference in new issue
Block a user