diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 7ab92060..8d70d940 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -820,13 +820,17 @@ fn hint(inp: &SelInput) -> &'static str { /// Width of the label column; longer labels are cut with "…". const LABEL_W: f32 = 92.0; +/// The label column grows to fit its labels up to this, then long labels wrap. +const LABEL_MAX_W: f32 = 150.0; -/// A row label in the dialog's label column. +/// A row label in the dialog's label column (the grid sizes the column to its widest label). pub(crate) fn row_label(ui: &mut egui::Ui, text: &str) { ROW.with(|r| *r.borrow_mut() = text.to_string()); - ui.allocate_ui_with_layout(vec2(LABEL_W, 22.0), egui::Layout::left_to_right(egui::Align::Center), |ui| { - ui.set_width(LABEL_W); - ui.add(egui::Label::new(text).truncate()).on_hover_text(text); + let full = egui::WidgetText::from(text).into_galley(ui, Some(egui::TextWrapMode::Extend), f32::INFINITY, egui::TextStyle::Body).size().x; + let w = full.clamp(LABEL_W, LABEL_MAX_W); + ui.allocate_ui_with_layout(vec2(w, 22.0), egui::Layout::left_to_right(egui::Align::Center), |ui| { + ui.set_width(w); + ui.add(egui::Label::new(text).wrap()); }); } diff --git a/crates/ui-egui/src/preview.rs b/crates/ui-egui/src/preview.rs index 146cc719..e9ef862e 100644 --- a/crates/ui-egui/src/preview.rs +++ b/crates/ui-egui/src/preview.rs @@ -270,12 +270,19 @@ pub fn build(before: &ModelState, after: &ModelState, c: Colors) -> Built { } let nf = am.tri_face.iter().max().map_or(0, |m| *m as usize + 1); let added: Vec = (0..nf).map(|f| old.as_ref().is_none_or(|bm| face_is_new(&am, f, bm))).collect(); + // Material removed from a body (a hole, a pocket): its new faces also show through the + // body in the cut colour, so the cut is seen from any side. + let cut = old.as_ref().is_some_and(|bm| am.measure().volume < bm.measure().volume - 1e-6); for (t, f) in am.triangles.iter().zip(&am.tri_face) { - let col = if added.get(*f as usize).copied().unwrap_or(true) { c.added } else { c.body }; + let new = added.get(*f as usize).copied().unwrap_or(true); + let col = if new { c.added } else { c.body }; for k in t { let i = *k as usize; if let (Some(p), Some(n)) = (am.positions.get(i), am.normals.get(i)) { sc.tri(p.to_f32(), n.to_f32(), col); + if new && cut { + sc.xray_tri(p.to_f32(), n.to_f32(), c.removed); + } } } } @@ -364,6 +371,19 @@ mod tests { bytes.chunks(stride).filter(|v| v.get(24..28) == Some(&col[..])).count() } + #[test] + fn a_hole_shows_its_cut_through_the_body() { + let mut s = solvecraft_engine::Session::default(); + s.execute("solid.box", &json!({"length": 40, "width": 30, "height": 20})).unwrap(); + let p = s.preview(&[("solid.hole".to_string(), json!({"position": [20, 15, 20], "diameter": 5, "depth": 10}))]).unwrap(); + let b = build(&p.before, &p.after, colors()); + // The hole's walls are drawn see-through in the cut colour (seen from any side). + assert!(count(&b.scene.xray, 28, colors().removed) > 0); + // A feature that only adds material shows no cut. + let p = s.preview(&[("solid.box".to_string(), json!({"length": 10, "width": 10, "height": 30, "operation": "join"}))]).unwrap(); + assert_eq!(count(&build(&p.before, &p.after, colors()).scene.xray, 28, colors().removed), 0); + } + fn plate() -> Session { let mut s = Session::default(); for (id, p) in [