A refused face is ringed in the error colour while the window points at it

While a feature dialog says that a part's face can't be used for modelling, and while the
pointer is over that part's "needs repair" row in the browser, the face is outlined round its
edges in the theme's error colour, over the shading and dimmed where the model hides it, like
the selection's outline; it goes when the message or the hover does. Show face now frames the
face without selecting it (a selection in a dialog becomes its pick and cleared the message).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5Lzv2n8fyKqD61NCHSF
This commit is contained in:
jyviko
2026-10-28 15:40:00 -04:00
committed by Irakli Kourtis
parent c15c394433
commit 090fcec6ca
5 changed files with 66 additions and 9 deletions
+8
View File
@@ -2387,6 +2387,14 @@ fn entry_row(
..Default::default()
},
);
// A part that needs repair, under the pointer: its refused face is ringed in the view.
if r.hovered
&& folder == "bodies"
&& let Some(Sel::Body { name }) = &e.sel
&& let Some(face) = app.session.model.state().body(&e.key).and_then(|b| b.body.smlib_refused()).and_then(|r| r.face)
{
crate::refused_part::ring(app, name, face);
}
if r.fold
&& let Some(key) = pattern_key
{
+3
View File
@@ -2598,6 +2598,9 @@ fn error_rows(app: &mut SolveApp, ctx: &egui::Context, d: &mut Dialog, ui: &mut
// An operation on a part the kernel refuses: the part, what is wrong with it, and what
// to do, instead of the kernel's words and an issue report.
if let Some(part) = refused_operand(app, d) {
if let Some(face) = part.face {
crate::refused_part::ring(app, &part.body, face);
}
let operation =
dialog_commands(app, d).ok().and_then(|c| c.iter().find_map(|(_, p)| p.get("operation").and_then(Value::as_str).map(str::to_string)));
ui.vertical(|ui| {
+10
View File
@@ -1925,6 +1925,15 @@ impl SolveApp {
self.pending_fit = request;
}
/// Frame `bounds` as Fit frames the selection, leaving the selection as it is.
pub(crate) fn frame_bounds(&mut self, bounds: solidcraft_engine::geom::Aabb3) {
let from = self.cam;
let to = self.fitted_bounds(from, &bounds);
self.pending_fit = None;
self.animate_to(to);
self.pending_fit = Some((from, bounds, true));
}
/// Move the viewport to the camera a command asked for (`view.apply`): animated like a view
/// change, or at once (control requests, so a screenshot right after shows it).
pub fn follow_view_request(&mut self, animate: bool) {
@@ -1976,6 +1985,7 @@ impl SolveApp {
/// Per-frame logic before layout.
pub fn logic(&mut self, ctx: &egui::Context) {
frame_watch::begin(self);
refused_part::next_frame(self, ctx);
file_picker::tick(self, ctx);
dialogs_save_mesh::tick_wait(self, ctx);
easyprint_ui::tick(self, ctx);
+22 -9
View File
@@ -86,6 +86,22 @@ fn why_key(part: &RefusedPart) -> &'static str {
}
}
/// Ring `body`'s refused `face` in the viewport while this is called each frame.
pub(crate) fn ring(app: &mut SolveApp, body: &str, face: usize) {
if !app.viewport.refused_ring_next.iter().any(|(b, f)| b == body && *f == face) {
app.viewport.refused_ring_next.push((body.to_string(), face));
}
}
/// The faces asked for last frame are the ones ringed now; a change is drawn at once.
pub(crate) fn next_frame(app: &mut SolveApp, ctx: &egui::Context) {
let next = std::mem::take(&mut app.viewport.refused_ring_next);
if next != app.viewport.refused_ring {
app.viewport.refused_ring = next;
ctx.request_repaint();
}
}
/// Repair the part (the command says when it cannot yet).
pub(crate) fn repair(app: &mut SolveApp, body: &str) {
if let Err(e) = app.run(REPAIR, json!({ "body": body })) {
@@ -93,20 +109,17 @@ pub(crate) fn repair(app: &mut SolveApp, body: &str) {
}
}
/// Select the face the kernel refuses and frame it.
/// Frame the face the kernel refuses (ringed while the message shows). The selection is left
/// as it is: in a dialog it would become the dialog's pick.
pub(crate) fn show_face(app: &mut SolveApp, part: &RefusedPart) {
let Some(face) = part.face else { return };
let world = crate::open_meshing::session_world(&app.session);
let Some(b) = world.body(&part.body) else { return };
let mesh = b.mesh();
let point = mesh.tri_face.iter().zip(&mesh.triangles).find(|(f, _)| **f as usize == face).and_then(|(_, t)| {
let [a, b, c] = t.map(|i| mesh.positions.get(i as usize).copied());
Some((a? + b? + c?) / 3.0)
});
let Some(point) = point else { return };
let item = json!({"type": "face", "body": part.body, "index": face, "point": [point.x, point.y, point.z]});
if app.run("select.set", json!({ "items": [item] })).is_ok() {
let _ = app.run("view.fit", json!({}));
let corners = mesh.tri_face.iter().zip(&mesh.triangles).filter(|(f, _)| **f as usize == face).flat_map(|(_, t)| t.iter());
let bounds: solidcraft_engine::geom::Aabb3 = corners.filter_map(|i| mesh.positions.get(*i as usize).copied()).collect();
if !bounds.is_empty() {
app.frame_bounds(bounds);
}
}
+23
View File
@@ -50,6 +50,12 @@ pub enum NavMode {
#[derive(Default)]
pub struct ViewportState {
/// The faces the modelling kernel refuses that the window points at (a failed operation's
/// message, a "needs repair" row under the pointer): ringed in the error colour. What this
/// frame asks for ([`crate::refused_part::ring`]) is drawn from the next one on, so the ring
/// goes when nothing asks for it any more.
pub(crate) refused_ring: Vec<(String, usize)>,
pub(crate) refused_ring_next: Vec<(String, usize)>,
/// A drag on the handles of the drawing being placed (Insert SVG / DXF).
pub(crate) insert_drag: Option<crate::insert_handles::Drag>,
/// A drag on the handles of the Transform window's box.
@@ -3030,6 +3036,7 @@ fn highlight_geometry_key(app: &SolveApp) -> u64 {
crate::theme_files::color_key(&Tokens::get()).hash(&mut h);
app.preview.effective_replaced().hash(&mut h);
app.preview.rolled_back.hash(&mut h);
app.viewport.refused_ring.hash(&mut h);
app.origin_visible().hash(&mut h);
app.picking_plane().hash(&mut h);
app.preferences.reference_geometry_through_bodies.hash(&mut h);
@@ -3340,6 +3347,22 @@ pub(crate) fn build_highlight(app: &SolveApp) -> GpuScene {
sc.line((p - d * r).to_f32(), (p + d * r).to_f32(), construction, 2.0, true);
}
}
// A face the modelling kernel refuses, while the window points at it: a ring round its edges
// in the error colour, over the shading (and dimmed where the model hides it), like the
// selection's outline.
for (body, face) in &app.viewport.refused_ring {
let Some(b) = st.body(body).filter(|_| app.session.body_visible(body)) else { continue };
let m = drawn_mesh(app, b);
let hidden = sc.color(c4(t.error.gamma_multiply(0.45)));
for (ei, e) in m.edges.iter().enumerate() {
if m.edge_faces.get(ei).is_some_and(|f| f.iter().any(|x| *x as usize == *face)) && m.edge_pickable(ei) {
edge_lines(&mut sc, e, t.error, t.error.gamma_multiply(0.5), 3.0, &edge_normals(&m, e));
for p in e.windows(2) {
sc.hidden_line(p[0].to_f32(), p[1].to_f32(), hidden, 1.4);
}
}
}
}
// Reference widgets follow zoom; the often much larger world-space feedback does not.
let projected = sel.iter().any(|s| matches!(s, Sel::Profile { .. } | Sel::SketchCurve { .. } | Sel::ExternalSketchCurve { .. }))
|| matches!(hover, Some(Hit::Profile { .. }));