From 7391e8ed75add86e8dbef5e37589743a4028f0ff Mon Sep 17 00:00:00 2001 From: DuBearte <130190089+DuBearte@users.noreply.github.com> Date: Thu, 15 Oct 2026 01:19:59 -0400 Subject: [PATCH] Section: leave the origin and construction planes and axes whole (#1286) Section Analysis clipped the origin and construction planes and axes along with the model. They now draw from their own unclipped lists, with the same depth and blending; bodies, edges, sketches and highlights are clipped as before. Fixes #1229. Co-authored-by: DuBearte --- assets/shaders/viewport.wgsl | 6 ++++++ crates/ui-egui/src/gpu.rs | 30 ++++++++++++++++++++++++++++++ crates/ui-egui/src/viewport.rs | 30 +++++++++++++++++++++++++----- 3 files changed, 61 insertions(+), 5 deletions(-) diff --git a/assets/shaders/viewport.wgsl b/assets/shaders/viewport.wgsl index 97c3c479..49479153 100644 --- a/assets/shaders/viewport.wgsl +++ b/assets/shaders/viewport.wgsl @@ -200,6 +200,12 @@ fn fs_tri(i: TOut) -> @location(0) vec4 { return shade(i); } +/// Reference planes: shaded like `fs_tri`, but never cut by the section plane. +@fragment +fn fs_tri_all(i: TOut) -> @location(0) vec4 { + return shade(i); +} + fn shade(i: TOut) -> vec4 { // A zero normal marks flat colour (selection fills, translucent planes). if (length(i.n) < 0.5) { diff --git a/crates/ui-egui/src/gpu.rs b/crates/ui-egui/src/gpu.rs index 2ea1ce3a..dfab108f 100644 --- a/crates/ui-egui/src/gpu.rs +++ b/crates/ui-egui/src/gpu.rs @@ -56,6 +56,10 @@ pub struct GpuScene { /// Lines drawn first, under everything (the ground grid: the model hides it, it never /// hides the model). pub under: Vec, + /// Origin and construction planes (translucent, like `trans`) and their axes (lines, like + /// `lines`): reference geometry, never cut away by a section (#1229). + pub guide_trans: Vec, + pub guide_lines: Vec, /// Opaque bodies drawn from their plans on the GPU, like `tris`. pub planned: Vec, /// The model's bodies, a few at a time, drawn before the loose geometry above (`tris`, @@ -157,6 +161,21 @@ impl GpuScene { self.under.extend_from_slice(&c); self.under.extend_from_slice(&width.to_le_bytes()); } + /// A translucent triangle of a plane a section does not cut (see [`GpuScene::guide_trans`]). + pub fn guide_tri(&mut self, p: [f32; 3], c: [u8; 4]) { + for v in p.iter().chain(&[0.0; 3]) { + self.guide_trans.extend_from_slice(&v.to_le_bytes()); + } + self.guide_trans.extend_from_slice(&c); + } + /// A depth-tested line a section does not cut (see [`GpuScene::guide_lines`]). + pub fn guide_line(&mut self, a: [f32; 3], b: [f32; 3], c: [u8; 4], width: f32) { + for v in a.iter().chain(&b) { + self.guide_lines.extend_from_slice(&v.to_le_bytes()); + } + self.guide_lines.extend_from_slice(&c); + self.guide_lines.extend_from_slice(&width.to_le_bytes()); + } pub fn line(&mut self, a: [f32; 3], b: [f32; 3], c: [u8; 4], width: f32, on_top: bool) { let out = if on_top { &mut self.overlay } else { &mut self.lines }; for v in a.iter().chain(&b) { @@ -291,6 +310,8 @@ struct Batches { ghost: Option, xray: Option, under: Option, + guide_trans: Option, + guide_lines: Option, planned: Vec, /// 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. @@ -341,6 +362,8 @@ impl Batches { self.ghost = upload(device, "sc_ghost", &sc.ghost, TRI_SIZE); self.xray = upload(device, "sc_xray", &sc.xray, TRI_SIZE); self.under = upload(device, "sc_under", &sc.under, LINE_SIZE); + self.guide_trans = upload(device, "sc_guide_trans", &sc.guide_trans, TRI_SIZE); + self.guide_lines = upload(device, "sc_guide_lines", &sc.guide_lines, LINE_SIZE); self.key = Some(key); } } @@ -428,6 +451,9 @@ struct Resources { /// Lines behind the model (depth test >). line_hidden: wgpu::RenderPipeline, overlay: wgpu::RenderPipeline, + /// Reference planes and axes: as `trans` and `line`, but not cut by a section. + guide_trans: wgpu::RenderPipeline, + guide_line: wgpu::RenderPipeline, uniform: wgpu::Buffer, bind: wgpu::BindGroup, linear_out: bool, @@ -764,6 +790,8 @@ impl Resources { line: pipeline("sc_lines", "vs_line", "fs_line", line_layout(), depth(wgpu::CompareFunction::LessEqual, false), alpha), line_hidden: pipeline("sc_lines_hidden", "vs_line", "fs_line", line_layout(), depth(wgpu::CompareFunction::Greater, false), alpha), overlay: pipeline("sc_overlay", "vs_line", "fs_line_all", line_layout(), depth(wgpu::CompareFunction::Always, false), alpha), + guide_trans: pipeline("sc_guide_trans", "vs_tri", "fs_tri_all", tri_layout(), depth(wgpu::CompareFunction::LessEqual, false), alpha), + guide_line: pipeline("sc_guide_lines", "vs_line", "fs_line_all", line_layout(), depth(wgpu::CompareFunction::LessEqual, false), alpha), uniform, bind, linear_out: t.format.is_srgb(), @@ -1038,6 +1066,7 @@ impl egui_wgpu::CallbackTrait for ViewportCallback { lines(pass, &res.line_hidden, &h.hidden); lines(pass, &res.line, &pv.lines); lines(pass, &res.line, &h.lines); + lines(pass, &res.guide_line, &h.guide_lines); for (id, q) in res.quads.iter().filter(|_| !self.product) { if let Some((_, bind)) = res.textures.get(id) { pass.set_pipeline(&res.image); @@ -1054,6 +1083,7 @@ impl egui_wgpu::CallbackTrait for ViewportCallback { tris(pass, &res.trans, &m.trans); tris(pass, &res.trans, &sk.trans); tris(pass, &res.trans, &h.trans); + tris(pass, &res.guide_trans, &h.guide_trans); tris(pass, &res.xray, &pv.xray); lines(pass, &res.overlay, &m.overlays); lines(pass, &res.overlay, &sk.overlays); diff --git a/crates/ui-egui/src/viewport.rs b/crates/ui-egui/src/viewport.rs index 5eb34bbc..0cfa5572 100644 --- a/crates/ui-egui/src/viewport.rs +++ b/crates/ui-egui/src/viewport.rs @@ -1544,15 +1544,16 @@ pub(crate) fn profile_fill(sc: &mut GpuScene, app: &SolveApp, sketch: u64, index } } +/// An origin or construction plane: reference geometry, which a section does not cut (#1229). fn quad(sc: &mut GpuScene, q: &[Vec3; 4], fill: Color32, edge: Color32) { let col = c4(fill); for i in [0, 1, 2, 0, 2, 3] { if let Some(p) = q.get(i) { - sc.trans_tri(p.to_f32(), [0.0; 3], col); + sc.guide_tri(p.to_f32(), col); } } for i in 0..4 { - sc.line(q[i].to_f32(), q[(i + 1) % 4].to_f32(), c4(edge), 1.2, false); + sc.guide_line(q[i].to_f32(), q[(i + 1) % 4].to_f32(), c4(edge), 1.2); } } @@ -1628,11 +1629,11 @@ fn build_highlight(app: &SolveApp) -> GpuScene { }; let on = sel.iter().any(|x| matches!(x, Sel::Axis { name: n } if n == name)) || matches!(hover, Some(Hit::Axis { name: n }) if n == name); let w = if on { 4.0 } else { 2.0 }; - sc.line([0.0; 3], (dir * size).to_f32(), c4(col), w, false); + sc.guide_line([0.0; 3], (dir * size).to_f32(), c4(col), w); let dash = size * 0.08; let mut a = size + dash; while a < size * AXIS_LEN { - sc.line((dir * a).to_f32(), (dir * (a + dash)).to_f32(), c4(col), w * 0.75, false); + sc.guide_line((dir * a).to_f32(), (dir * (a + dash)).to_f32(), c4(col), w * 0.75); a += dash * 2.0; } } @@ -1647,7 +1648,7 @@ fn build_highlight(app: &SolveApp) -> GpuScene { let mut s = 0.0; while s < len { let e = (s + dash).min(len); - sc.line(a.lerp(b, s / len).to_f32(), a.lerp(b, e / len).to_f32(), col, w, false); + sc.guide_line(a.lerp(b, s / len).to_f32(), a.lerp(b, e / len).to_f32(), col, w); s += dash * 1.6; } } @@ -4590,6 +4591,25 @@ mod tests { assert_eq!(d.items().len(), 1, "the other half toggles the same wall"); } + #[test] + fn a_section_leaves_the_origin_planes_and_axes_whole() { + // #1229: the origin planes and axes are reference geometry; the section plane cuts the + // model, not them, so they go to the lists the GPU draws without the section clip. + let mut h = crate::scenario::Harness::new(); + h.app.run("solid.box", json!({"length":20,"width":20,"height":20,"corner":[-10,-10,-10]})).unwrap(); + h.app.run("inspect.section", json!({"plane": "XZ", "offset": 0})).unwrap(); + h.app.ui.show_origin = true; + h.frames(2); + assert!(section_plane(&h.app).is_some()); + let sc = build_highlight(&h.app); + let planes = origin_planes(&h.app).len(); + assert!(planes > 0); + assert_eq!(sc.guide_trans.len(), planes * 6 * crate::gpu::TRI_SIZE); + assert!(sc.trans.is_empty(), "no origin plane is drawn where the section clips"); + // Each origin axis: a solid part and dashes beyond, plus each plane's four edges. + assert!(sc.guide_lines.len() >= (planes * 4 + 3) * crate::gpu::LINE_SIZE); + } + #[test] fn circular_rim_hover_click_and_dialog_pick_include_both_halves() { let mut app = cylinder_app();