Merge remote-tracking branch 'origin/main' into fix/model-render-section

# Conflicts:
#	crates/ui-egui/src/viewport.rs
This commit is contained in:
CypherNaugh_0x committed 2026-10-11 13:12:20 +02:00
commit 5a2174899f
186 files changed
+29730 -1578

No files matched your search

+57 -14
View File
@@ -15,6 +15,10 @@ use crate::selection::{BoxSel, origin_quads, origin_size, ray_quad};
use crate::theme::Tokens;
use crate::{SolveApp, icons};
#[cfg(test)]
#[path = "viewport_appearance_tests.rs"]
mod appearance_tests;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum NavMode {
Orbit,
@@ -214,11 +218,14 @@ fn scene_key(app: &SolveApp) -> u64 {
(app.cam.pitch > 0.0).hash(&mut h);
app.ui.show_sketches.hash(&mut h);
app.session.visibility.hidden_bodies.hash(&mut h);
app.session.visibility.hidden_occurrences.hash(&mut h);
app.session.visibility.hidden_sketches.hash(&mut h);
app.session.visibility.shown_sketches.hash(&mut h);
app.ui.hidden_profiles.hash(&mut h);
app.session.active_component.hash(&mut h);
app.session.active_occurrence_path.hash(&mut h);
app.preview.replaced.hash(&mut h);
app.tree.redefine.hash(&mut h);
crate::theme::is_dark().hash(&mut h);
app.session.active_sketch.hash(&mut h);
let (minor, _) = grid_step(app.cam.half_height());
@@ -264,12 +271,17 @@ fn build_scene(app: &SolveApp) -> GpuScene {
sc.under_line(w3(c + Vec3::new(-ext, t, 0.0)), w3(c + Vec3::new(ext, t, 0.0)), col, w);
}
}
let active_bodies = s.active_occurrence_path.as_ref().and_then(|p| s.occurrence_bodies(p, true).ok());
for b in &st.bodies {
if app.session.visibility.hidden_bodies.contains(&b.name) || app.preview.replaced.contains(&b.name) {
if !app.session.body_visible(&b.name) || app.preview.replaced.contains(&b.name) {
continue;
}
// With a component active, bodies outside it are drawn faded.
let faded = s.active_component != 0 && !s.doc.component_within(s.doc.body_component(&b.name, b.feature), s.active_component);
let faded = if let Some(names) = &active_bodies {
!names.contains(&b.name)
} else {
s.active_component != 0 && !s.doc.component_within(b.component, s.active_component)
};
let base = body_color(&s.doc, b).unwrap_or(tk.body);
let col = if faded { c4(base.gamma_multiply(0.35)) } else { c4(base) };
let m = b.mesh();
@@ -283,7 +295,7 @@ fn build_scene(app: &SolveApp) -> GpuScene {
let hidden_edges = app.ui.visual_style == 3;
let hidden_col = c4(tk.text.gamma_multiply(0.35));
// Appearances: the body's opacity, and faces with looks of their own.
let opacity = s.doc.body_look(&b.name, b.feature).map_or(1.0, |l| l.opacity);
let opacity = s.doc.model_body_opacity(b);
let face_looks: std::collections::HashMap<usize, solvecraft_engine::doc::appearance::Look> =
if shaded { s.doc.face_colors(b).into_iter().collect() } else { Default::default() };
for (ti, t) in m.triangles.iter().enumerate().filter(|_| shaded) {
@@ -329,7 +341,7 @@ fn build_scene(app: &SolveApp) -> GpuScene {
// Section: the outline of each body where the plane cuts it.
if let Some((o, n)) = section_plane(app) {
let col = c4(tk.text.gamma_multiply(0.9));
for b in st.bodies.iter().filter(|b| !app.session.visibility.hidden_bodies.contains(&b.name)) {
for b in st.bodies.iter().filter(|b| app.session.body_visible(&b.name)) {
for (a, c) in solvecraft_engine::view::section_segments(&b.mesh(), o, n) {
sc.line(a.to_f32(), c.to_f32(), col, 1.8, false);
}
@@ -363,6 +375,12 @@ fn build_scene(app: &SolveApp) -> GpuScene {
}
}
}
let guide_unit = grid_step(app.cam.half_height()).0;
for points in crate::sketch_attachment_ui::guide_lines(app) {
for (a, b) in dashes(&points, &[guide_unit * 0.25, guide_unit * 0.15]) {
sc.line(a.to_f32(), b.to_f32(), c4(tk.warning), 1.5, true);
}
}
sc
}
@@ -370,7 +388,7 @@ fn build_scene(app: &SolveApp) -> GpuScene {
fn ground_shadow(sc: &mut GpuScene, app: &SolveApp, st: &solvecraft_engine::doc::ModelState) {
// The bodies' box in the camera's Z-up frame (the ground is its XY plane).
let mut b = solvecraft_engine::geom::Aabb3::EMPTY;
for x in st.bodies.iter().filter(|x| !app.session.visibility.hidden_bodies.contains(&x.name)) {
for x in st.bodies.iter().filter(|x| app.session.body_visible(&x.name)) {
let bb = x.mesh().bounds();
if bb.is_empty() {
continue;
@@ -561,6 +579,9 @@ pub fn model_bounds(app: &SolveApp) -> solvecraft_engine::geom::Aabb3 {
}
}
}
for point in crate::sketch_attachment_ui::guide_lines(app).into_iter().flatten() {
b.add(point);
}
b
}
@@ -610,6 +631,7 @@ fn pick_cached(app: &mut SolveApp, proj: &Proj, pos: Pos2) -> Vec<Hit> {
app.session.active_sketch.hash(&mut h);
app.dialog.as_ref().and_then(|d| d.active_input()).map(|i| i.accept).hash(&mut h);
app.session.visibility.hidden_bodies.hash(&mut h);
app.session.visibility.hidden_occurrences.hash(&mut h);
app.ui.perspective.hash(&mut h);
(proj.rect.width().to_bits(), proj.rect.height().to_bits()).hash(&mut h);
let key = h.finish();
@@ -685,7 +707,7 @@ pub fn pick(app: &SolveApp, proj: &Proj, pos: Pos2) -> Vec<Hit> {
// Nearest face along the ray (hides what is behind it).
let mut bestf: Option<(f64, Hit)> = None;
for b in &st.bodies {
if app.session.visibility.hidden_bodies.contains(&b.name) {
if !app.session.body_visible(&b.name) {
continue;
}
// Bodies whose box the ray misses (or enters behind the best hit so far) are skipped.
@@ -711,7 +733,7 @@ pub fn pick(app: &SolveApp, proj: &Proj, pos: Pos2) -> Vec<Hit> {
let mut bestv: Option<(f32, Hit)> = None;
let mut beste: Option<(f32, Hit)> = None;
for b in &st.bodies {
if app.session.visibility.hidden_bodies.contains(&b.name) {
if !app.session.body_visible(&b.name) {
continue;
}
// Only bodies whose outline on screen is near the cursor can have an edge under it.
@@ -852,7 +874,7 @@ fn feature_geometry(app: &SolveApp, id: u64) -> Vec<Sel> {
let prefix = format!("F{id}:");
let mut out = Vec::new();
for b in &st.bodies {
if app.session.visibility.hidden_bodies.contains(&b.name) || app.preview.replaced.contains(&b.name) {
if !app.session.body_visible(&b.name) || app.preview.replaced.contains(&b.name) {
continue;
}
if b.feature == id {
@@ -878,6 +900,14 @@ fn feature_geometry(app: &SolveApp, id: u64) -> Vec<Sel> {
fn displayed_selection(app: &SolveApp, item: &Sel) -> Vec<Sel> {
match item {
Sel::Feature { id } => feature_geometry(app, *id),
Sel::Occurrence { path } => app
.session
.occurrence_bodies(path, true)
.unwrap_or_default()
.into_iter()
.filter(|name| app.session.body_visible(name))
.map(|name| Sel::Body { name })
.collect(),
_ => grouped_selection(&app.session, item),
}
}
@@ -1048,13 +1078,18 @@ fn hidden_face_outline(sc: &mut GpuScene, m: &solvecraft_engine::geom::Mesh, fac
}
}
fn profile_fill(sc: &mut GpuScene, app: &SolveApp, sketch: u64, index: usize, fill: Color32, edge: Color32) {
pub(crate) fn profile_fill(sc: &mut GpuScene, app: &SolveApp, sketch: u64, index: usize, fill: Color32, edge: Color32) {
let st = app.session.world_state();
let Some(ss) = st.sketch(sketch) else { return };
let Some(p) = ss.profiles.get(index) else { return };
let plane = match crate::dialogs::emboss_profile_plane(app, sketch) {
Ok(Some(plane)) => plane,
Ok(None) => ss.plane,
Err(_) => return,
};
// Lift toward the eye so a profile on a face shows over it.
let lift = app.cam.back() * (app.cam.half_height() * 0.002);
let w = |q: solvecraft_engine::geom::Vec2| (ss.plane.to_world(q) + lift).to_f32();
let w = |q: solvecraft_engine::geom::Vec2| (plane.to_world(q) + lift).to_f32();
let col = c4(fill);
// In the translucent pass (after the grid and edges) so it covers them.
for [a, b, c] in p.region.triangulate(0.02) {
@@ -1098,6 +1133,7 @@ fn highlight_key(app: &SolveApp) -> u64 {
app.origin_visible().hash(&mut h);
app.session.visibility.hidden_origin.hash(&mut h);
app.session.visibility.hidden_bodies.hash(&mut h);
app.session.visibility.hidden_occurrences.hash(&mut h);
app.cam.half_height().to_bits().hash(&mut h);
app.cam.back().x.to_bits().hash(&mut h);
h.finish()
@@ -1180,14 +1216,14 @@ fn build_highlight(app: &SolveApp) -> GpuScene {
match x {
Sel::Face { body, index, .. } => {
// A live preview stands in for the body: its old faces would fight with it.
if let Some(b) = st.body(body).filter(|_| !app.preview.replaced.contains(body)) {
if let Some(b) = st.body(body).filter(|_| app.session.body_visible(body) && !app.preview.replaced.contains(body)) {
face_tris(&mut sc, &b.mesh(), *index, c4(t.sel_face), false);
hidden_face_outline(&mut sc, &b.mesh(), *index, t.sel_edge);
}
}
Sel::Body { name } => {
if let Some(b) = st.body(name)
&& !app.session.visibility.hidden_bodies.contains(name)
&& app.session.body_visible(name)
{
let m = b.mesh();
for f in 0..b.body.face_count() {
@@ -1197,6 +1233,7 @@ fn build_highlight(app: &SolveApp) -> GpuScene {
}
Sel::Edge { body, index, point } => {
if let Some(b) = st.body(body)
&& app.session.body_visible(body)
&& let Some(e) = edge_of(&b.mesh(), *index, *point)
{
edge_lines(&mut sc, e, t.sel_edge, t.sel_edge_rim, 3.0);
@@ -1231,7 +1268,7 @@ fn build_highlight(app: &SolveApp) -> GpuScene {
// Timeline modifiers light only their resulting faces; browser rows light whole bodies.
let mut hovered = app.viewport.hover_feature.map(|id| feature_geometry(app, id)).unwrap_or_default();
hovered.extend(app.viewport.hover_bodies.iter().map(|name| Sel::Body { name: name.clone() }));
for b in st.bodies.iter().filter(|b| !app.session.visibility.hidden_bodies.contains(&b.name) && !app.preview.replaced.contains(&b.name)) {
for b in st.bodies.iter().filter(|b| app.session.body_visible(&b.name) && !app.preview.replaced.contains(&b.name)) {
let whole = hovered.iter().any(|x| matches!(x, Sel::Body { name } if name == &b.name));
let faces: Vec<_> = if whole {
(0..b.body.face_count()).collect()
@@ -1746,6 +1783,12 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
crate::drag_snap::show(app, &painter, &proj);
hover_highlight(app, &painter, &proj);
points_2d(app, &painter, &proj);
if let Some((plane, sketch)) = crate::sketch_attachment_ui::lost_guide(app)
&& let Some((low, high)) = sketch.bounds()
&& let Some(at) = proj.to_screen(plane.to_world(solvecraft_engine::geom::Vec2::new((low.x + high.x) * 0.5, high.y)))
{
painter.text(at - vec2(0.0, 12.0), Align2::CENTER_BOTTOM, "Saved sketch plane — attachment lost", FontId::proportional(12.0), t.warning);
}
if let Some(bx) = app.viewport.boxsel {
let col = if bx.crossing { t.box_crossing } else { t.box_window };
painter.rect_filled(bx.rect, 0.0, col.gamma_multiply(0.12));
@@ -1914,7 +1957,7 @@ fn box_select(app: &mut SolveApp, proj: &Proj, bx: BoxSel, add: bool) {
} else {
let accept = app.dialog.as_ref().and_then(|d| d.active_input()).map(|i| i.accept).unwrap_or(BODIES);
for b in &st.bodies {
if app.session.visibility.hidden_bodies.contains(&b.name) {
if !app.session.body_visible(&b.name) {
continue;
}
let m = b.mesh();