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 <DuBearte@users.noreply.github.com>
This commit is contained in:
DuBearteandDuBearte authored and GitHub committed 2026-10-15 01:19:59 -04:00
1 parent 1ec33fcdc7
commit 7391e8ed75
3 files changed
+61 -5

No files matched your search

+6
View File
@@ -200,6 +200,12 @@ fn fs_tri(i: TOut) -> @location(0) vec4<f32> {
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<f32> {
return shade(i);
}
fn shade(i: TOut) -> vec4<f32> {
// A zero normal marks flat colour (selection fills, translucent planes).
if (length(i.n) < 0.5) {
+30
View File
@@ -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<u8>,
/// 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<u8>,
pub guide_lines: Vec<u8>,
/// Opaque bodies drawn from their plans on the GPU, like `tris`.
pub planned: Vec<PlannedBody>,
/// 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<Batch>,
xray: Option<Batch>,
under: Option<Batch>,
guide_trans: Option<Batch>,
guide_lines: Option<Batch>,
planned: Vec<PlannedDraw>,
/// 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);
+25 -5
View File
@@ -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();