diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 4f968382..8d71e64d 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -78,6 +78,10 @@ pub enum Kind { Sweep { operation: usize, }, + /// Physical material (and with it the appearance) of bodies. + Material { + index: usize, + }, /// Section Analysis: cut the view by a plane, moved along its normal. Section { offset: String, @@ -202,6 +206,7 @@ impl Dialog { "MirrorCommand" => { Dialog::new(Kind::Mirror, vec![SelInput::new("Bodies", BODIES, true), SelInput::new("Mirror plane", PLANES | PLANAR_FACES, false)]) } + "PhysicalMaterialCommand" => Dialog::new(Kind::Material { index: 1 }, vec![SelInput::new("Bodies", BODIES, true)]), "FusionHalfSectionViewCommand" => { Dialog::new(Kind::Section { offset: "0 mm".into(), flip: false }, vec![SelInput::new("Plane", PLANES | PLANAR_FACES, false)]) } @@ -489,6 +494,7 @@ fn title(k: &Kind) -> &'static str { Kind::Draft { .. } => "DRAFT", Kind::Mirror => "MIRROR", Kind::Measure { .. } => "MEASURE", + Kind::Material { .. } => "PHYSICAL MATERIAL", Kind::Section { .. } => "SECTION ANALYSIS", Kind::PatternRect { .. } => "RECTANGULAR PATTERN", Kind::PatternCirc { .. } => "CIRCULAR PATTERN", @@ -749,6 +755,12 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) { } Kind::Mirror => {} Kind::Measure { result, .. } => measure_rows(ui, result.as_ref()), + Kind::Material { index } => { + let names: Vec<&str> = solvecraft_engine::doc::MATERIALS.iter().map(|(n, _)| *n).collect(); + ui.label("Material"); + combo(ui, "mat", &names, index); + ui.end_row(); + } Kind::Section { offset, flip } => { ui.label("Distance"); enter |= field(ui, offset); @@ -902,7 +914,8 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) { let (enter_free, esc) = ctx.input(|i| (i.key_pressed(egui::Key::Enter), i.key_pressed(egui::Key::Escape))); let nothing_focused = ctx.memory(|m| m.focused().is_none()); let canvas_enter = enter_free && ctx.memory(|m| m.had_focus_last_frame(egui::Id::new("sc_canvas_value"))); - if d.previews() && (enter || canvas_enter || (enter_free && nothing_focused)) { + let enter_applies = !matches!(d.kind, Kind::Sketch | Kind::Params { .. } | Kind::Measure { .. } | Kind::ConfirmDelete { .. }); + if enter_applies && (enter || canvas_enter || (enter_free && nothing_focused)) { ok = true; } if esc { @@ -1212,6 +1225,11 @@ fn dialog_commands(app: &SolveApp, d: &Dialog) -> Result, S json!({"sketch": sketch, "profiles": idx, "path_sketch": path_sketch, "path": path, "operation": OPS.get(*operation).copied().unwrap_or("new")}), ) } + Kind::Material { index } => { + need(0, "bodies")?; + let name = solvecraft_engine::doc::MATERIALS.get(*index).map(|(n, _)| *n).unwrap_or("Default"); + ("PhysicalMaterialCommand", json!({"bodies": body_names(0), "material": name})) + } Kind::Section { offset, flip } => { need(0, "a plane or planar face")?; let plane = match sels(d, 0).first() { diff --git a/crates/ui-egui/src/theme.rs b/crates/ui-egui/src/theme.rs index e4f02fbc..d0d2f41e 100644 --- a/crates/ui-egui/src/theme.rs +++ b/crates/ui-egui/src/theme.rs @@ -239,6 +239,26 @@ impl Tokens { } } +/// How a physical material looks (the default appearance follows the material). +pub fn material_color(name: &str) -> Option { + Some(match name { + "Steel" => Color32::from_rgb(158, 164, 172), + "Stainless Steel" => Color32::from_rgb(186, 191, 198), + "Aluminum" => Color32::from_rgb(204, 208, 214), + "Brass" => Color32::from_rgb(208, 172, 88), + "Copper" => Color32::from_rgb(204, 122, 80), + "Titanium" => Color32::from_rgb(150, 148, 158), + "Cast Iron" => Color32::from_rgb(104, 104, 110), + "ABS Plastic" => Color32::from_rgb(222, 222, 216), + "PLA" => Color32::from_rgb(110, 160, 222), + "Nylon" => Color32::from_rgb(236, 230, 214), + "Polycarbonate" => Color32::from_rgb(196, 216, 232), + "Oak" => Color32::from_rgb(190, 146, 92), + "Glass" => Color32::from_rgb(170, 208, 218), + _ => return None, + }) +} + /// Switch themes (the visuals follow on the next [`apply`]). pub fn set_dark(dark: bool) { DARK.store(dark, Ordering::Relaxed); diff --git a/crates/ui-egui/src/viewport.rs b/crates/ui-egui/src/viewport.rs index 4a70f6dd..2c4a2ace 100644 --- a/crates/ui-egui/src/viewport.rs +++ b/crates/ui-egui/src/viewport.rs @@ -195,7 +195,7 @@ fn build_scene(app: &SolveApp) -> GpuScene { if app.ui.hidden_bodies.contains(&b.name) || app.preview.replaced.contains(&b.name) { continue; } - let col = c4(tk.body); + let col = c4(s.doc.materials.get(&b.name).and_then(|m| crate::theme::material_color(m)).unwrap_or(tk.body)); let m = b.mesh(); for t in &m.triangles { for k in t { @@ -663,8 +663,8 @@ fn build_highlight(app: &SolveApp) -> GpuScene { // Bodies made by the timeline item under the cursor. if let Some(fid) = app.viewport.hover_feature { let k = t.hover_face_lift; - let c = (t.body.r(), t.body.g(), t.body.b()); for b in st.bodies.iter().filter(|b| b.feature == fid && !app.ui.hidden_bodies.contains(&b.name)) { + let c = body_rgb(app, &b.name); let m = b.mesh(); for f in 0..b.body.face_count() { face_tris(&mut sc, &m, f, [c.0.saturating_add(k), c.1.saturating_add(k), c.2.saturating_add(k), 255], true); @@ -682,7 +682,7 @@ fn build_highlight(app: &SolveApp) -> GpuScene { Some(Hit::Face { body, .. }) if app.ui.pick_bodies && app.dialog.is_none() && app.tool.is_none() => { if let Some(b) = st.body(body) { let k = t.hover_face_lift; - let c = (t.body.r(), t.body.g(), t.body.b()); + let c = body_rgb(app, body); let m = b.mesh(); for f in 0..b.body.face_count() { face_tris(&mut sc, &m, f, [c.0.saturating_add(k), c.1.saturating_add(k), c.2.saturating_add(k), 255], true); @@ -695,7 +695,7 @@ fn build_highlight(app: &SolveApp) -> GpuScene { { if let Some(b) = st.body(body) { let k = t.hover_face_lift; - let c = (t.body.r(), t.body.g(), t.body.b()); + let c = body_rgb(app, body); face_tris(&mut sc, &b.mesh(), *index, [c.0.saturating_add(k), c.1.saturating_add(k), c.2.saturating_add(k), 255], true); let m = b.mesh(); for e in m.face_edges(u32::try_from(*index).unwrap_or(u32::MAX)) { @@ -727,6 +727,12 @@ fn build_highlight(app: &SolveApp) -> GpuScene { sc } +/// A body's display colour (its material's, else the theme's) as RGB. +fn body_rgb(app: &SolveApp, name: &str) -> (u8, u8, u8) { + let c = app.session.doc.materials.get(name).and_then(|m| crate::theme::material_color(m)).unwrap_or(Tokens::get().body); + (c.r(), c.g(), c.b()) +} + fn polyline_mid(pts: &[Vec3]) -> Vec3 { let len: f64 = pts.windows(2).map(|w| w[0].dist(w[1])).sum(); let mut acc = 0.0;