* Preferences ▸ Graphics ▸ Display quality: Coarse, Normal, Fine, Ultra (#1287) A viewing preference that sets how finely the viewport tessellates curved faces and edges: chord tolerance relative to each body's size (3e-3, 1e-3, 3e-4, 1e-4) and the most a facet may turn (30, 20, 12, 7 degrees). Normal is the tolerance the viewport has always used. Bodies keep a display plan per level, so switching is a re-plan the first time and instant after; the reference mesh the model reads (picks, holes, measuring) and exports are unchanged. Away from Normal the model scene builds edges, and faces on the CPU path, from the plan at that level, so silhouettes and edges get finer with the faces and a highlight still lies on the displayed triangles. The preference is saved with the others, set over the control channel (`ui.prefs` `display_quality`), labelled in en/de/ja/ko/zh. `solidcraft-cli bench-open --display LEVEL [--edit NAME EXPR]` times the plans at a level and after a parameter edit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Highlighted edges follow the display quality too Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
47517578a8
commit
9eddb1cbd3
11 files changed
+298
-22
No files matched your search
@@ -248,6 +248,9 @@ pub struct Prefs {
|
||||
pub space_mouse_speed: f32,
|
||||
/// Anti-aliasing samples (applies on restart).
|
||||
pub msaa: u32,
|
||||
/// How finely the viewport tessellates curved faces and edges: `coarse`, `normal`, `fine` or
|
||||
/// `ultra` (#1287). Only the display plan the viewport draws; never the design or exports.
|
||||
pub display_quality: String,
|
||||
/// Decimals shown for lengths and angles.
|
||||
pub length_decimals: u8,
|
||||
pub angle_decimals: u8,
|
||||
@@ -327,6 +330,7 @@ impl Default for Prefs {
|
||||
space_mouse_enabled: true,
|
||||
space_mouse_speed: 1.0,
|
||||
msaa: 4,
|
||||
display_quality: "normal".into(),
|
||||
length_decimals: 2,
|
||||
angle_decimals: 1,
|
||||
theme: "dark".into(),
|
||||
@@ -467,6 +471,7 @@ fn defaults(app: &mut SolveApp, page: usize) {
|
||||
PAGE_GRAPHICS => {
|
||||
let msaa = p.msaa != d.msaa;
|
||||
p.msaa = d.msaa;
|
||||
p.display_quality = d.display_quality;
|
||||
p.color_vision = d.color_vision;
|
||||
app.ui.visual_style = 0;
|
||||
app.ui.show_grid = true;
|
||||
@@ -484,6 +489,10 @@ fn defaults(app: &mut SolveApp, page: usize) {
|
||||
/// Settings that follow the preferences every frame: the theme when it follows the system, and
|
||||
/// the interface scale.
|
||||
pub fn apply(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
// The viewport's display quality (an unknown saved value draws at Normal).
|
||||
solidcraft_engine::doc::display_quality::set_current(
|
||||
solidcraft_engine::doc::display_quality::DisplayQuality::parse(&app.preferences.display_quality).unwrap_or_default(),
|
||||
);
|
||||
// The view's up axis follows the preference (set in the dialog, by ui.prefs or loaded).
|
||||
if app.prefs_window.applied_up.as_deref() != Some(app.preferences.up_axis.as_str()) {
|
||||
up_axis_changed(app);
|
||||
@@ -1094,6 +1103,21 @@ fn page(app: &mut SolveApp, ui: &mut egui::Ui, t: &Tokens) {
|
||||
});
|
||||
ui.end_row();
|
||||
note(ui, app.tr("settings.msaa_applies_on_restart"));
|
||||
ui.label(app.tr("ui.display_quality"));
|
||||
{
|
||||
use solidcraft_engine::doc::display_quality::DisplayQuality;
|
||||
let label = |q: DisplayQuality| crate::i18n::tr(&format!("ui.display_quality_{}", q.id())).to_string();
|
||||
let current = DisplayQuality::parse(&app.preferences.display_quality).unwrap_or_default();
|
||||
egui::ComboBox::from_id_salt("pref_display_quality").selected_text(label(current)).show_ui(ui, |ui| {
|
||||
for q in DisplayQuality::ALL {
|
||||
if ui.selectable_label(q == current, label(q)).clicked() {
|
||||
app.preferences.display_quality = q.id().to_string();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
ui.end_row();
|
||||
note(ui, app.tr("settings.display_quality_note"));
|
||||
ui.label(crate::i18n::tr_label("Renderer"));
|
||||
ui.label(match (&app.viewport.gpu, &app.help.gpu) {
|
||||
(Some(_), Some(g)) => format!("GPU: {g}"),
|
||||
@@ -1305,6 +1329,9 @@ pub fn set(app: &mut SolveApp, p: &serde_json::Value) -> Result<serde_json::Valu
|
||||
if !(0.5..=2.5).contains(&new.ui_scale) || !(0.25..=4.0).contains(&new.look_at_speed) {
|
||||
return Err("`ui_scale` must be 0.5…2.5 and `look_at_speed` 0.25…4".into());
|
||||
}
|
||||
if solidcraft_engine::doc::display_quality::DisplayQuality::parse(&new.display_quality).is_none() {
|
||||
return Err("`display_quality` must be coarse, normal, fine or ultra".into());
|
||||
}
|
||||
if MiddleDoubleClick::parse(&new.middle_double_click).is_none() {
|
||||
return Err("`middle_double_click` must be home, fit or off".into());
|
||||
}
|
||||
@@ -1325,6 +1352,22 @@ pub fn set(app: &mut SolveApp, p: &serde_json::Value) -> Result<serde_json::Valu
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
/// Display quality (#1287) is a preference like any other: set by name, checked, saved with
|
||||
/// the others, back to Normal with Restore Defaults.
|
||||
#[test]
|
||||
fn display_quality_is_set_checked_and_restored() {
|
||||
use serde_json::json;
|
||||
let mut app = crate::SolveApp::new(solidcraft_engine::Session::default(), crate::Services::default());
|
||||
assert_eq!(app.preferences.display_quality, "normal");
|
||||
let out = super::set(&mut app, &json!({"display_quality": "fine"})).unwrap();
|
||||
assert_eq!(out["display_quality"], "fine");
|
||||
assert!(super::set(&mut app, &json!({"display_quality": "silky"})).unwrap_err().contains("coarse, normal, fine or ultra"));
|
||||
assert_eq!(app.preferences.display_quality, "fine");
|
||||
assert_eq!(super::lenient(&json!({"display_quality": "ultra"})).display_quality, "ultra");
|
||||
super::defaults(&mut app, super::PAGE_GRAPHICS);
|
||||
assert_eq!(app.preferences.display_quality, "normal");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_value_this_version_does_not_understand_keeps_the_other_preferences() {
|
||||
use serde_json::json;
|
||||
|
||||
@@ -266,6 +266,7 @@ fn scene_key(app: &SolveApp) -> u64 {
|
||||
app.ui.show_grid.hash(&mut h);
|
||||
app.ui.visual_style.hash(&mut h);
|
||||
app.ui.ground_shadow.hash(&mut h);
|
||||
solidcraft_engine::doc::display_quality::current().hash(&mut h);
|
||||
// The section plane (its cut outlines are part of the model scene).
|
||||
section_plane(app).map(|(o, n)| [o.x, o.y, o.z, n.x, n.y, n.z].map(f64::to_bits)).hash(&mut h);
|
||||
app.cam.up.hash(&mut h);
|
||||
@@ -347,7 +348,7 @@ fn drawn_plan(
|
||||
}
|
||||
|
||||
/// The mesh whose triangles the model scene shows for `b`'s faces: the plan's (evaluated on the
|
||||
/// CPU, the same triangles the GPU draws) or the reference mesh. A face highlighted over the model
|
||||
/// CPU, the same triangles the GPU draws) or [`drawn_mesh`]. A face highlighted over the model
|
||||
/// must use the same triangles: another triangulation of the same face fights it at equal depth
|
||||
/// and shimmers.
|
||||
fn shown_mesh(
|
||||
@@ -362,7 +363,16 @@ fn shown_mesh(
|
||||
.then(|| drawn_plan(app, b, true))
|
||||
.flatten()
|
||||
.and_then(|_| b.plan_mesh())
|
||||
.unwrap_or_else(|| b.mesh())
|
||||
.unwrap_or_else(|| drawn_mesh(b))
|
||||
}
|
||||
|
||||
/// The mesh the model scene builds `b` from on the CPU (its edges always, its faces when they
|
||||
/// are not drawn from the plan on the GPU): the reference mesh at Normal display quality, else
|
||||
/// the plan's mesh at the chosen quality (finer or coarser faces and edges alike), falling back
|
||||
/// to the reference mesh when the body has no plan.
|
||||
fn drawn_mesh(b: &solidcraft_engine::doc::ModelBody) -> std::sync::Arc<solidcraft_engine::geom::Mesh> {
|
||||
use solidcraft_engine::doc::display_quality::{DisplayQuality, current};
|
||||
if current() == DisplayQuality::Normal { b.mesh() } else { b.plan_mesh().unwrap_or_else(|| b.mesh()) }
|
||||
}
|
||||
|
||||
/// A model scene block kept by the viewport: its bytes and the meshes they were built from
|
||||
@@ -513,7 +523,7 @@ fn build_scene(app: &SolveApp) -> GpuScene {
|
||||
.collect();
|
||||
sc.planned.push(crate::gpu::PlannedBody { plan, face_colors });
|
||||
}
|
||||
let body = BodyDraw { mesh: b.mesh(), col, faded, opacity, face_looks, on_gpu };
|
||||
let body = BodyDraw { mesh: drawn_mesh(b), col, faded, opacity, face_looks, on_gpu };
|
||||
let key = body.key(shaded, edges, hidden_edges, edge_col, hidden_col);
|
||||
key.hash(&mut run_key);
|
||||
run.push(body);
|
||||
@@ -1665,7 +1675,7 @@ fn build_highlight(app: &SolveApp) -> GpuScene {
|
||||
// A live preview stands in for the body: its old faces would fight with it.
|
||||
if let Some(b) = st.body(body).filter(|_| app.session.body_visible(body) && !app.preview.replaced.contains(body)) {
|
||||
face_tris(&mut sc, &shown(b), *index, c4(t.sel_face), false);
|
||||
hidden_face_outline(&mut sc, &b.mesh(), *index, t.sel_edge);
|
||||
hidden_face_outline(&mut sc, &drawn_mesh(b), *index, t.sel_edge);
|
||||
}
|
||||
}
|
||||
Sel::Body { name } => {
|
||||
@@ -1681,7 +1691,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)
|
||||
&& let Some(e) = edge_of(&drawn_mesh(b), *index, *point)
|
||||
{
|
||||
edge_lines(&mut sc, e, t.sel_edge, t.sel_edge_rim, 3.0);
|
||||
}
|
||||
@@ -1745,7 +1755,7 @@ fn build_highlight(app: &SolveApp) -> GpuScene {
|
||||
}
|
||||
let k = t.hover_face_lift;
|
||||
let c = body_rgb(app, &b.name);
|
||||
let m = b.mesh();
|
||||
let m = drawn_mesh(b);
|
||||
let lit = shown(b);
|
||||
for &f in &faces {
|
||||
if !sel.iter().any(|x| {
|
||||
@@ -1780,7 +1790,7 @@ fn build_highlight(app: &SolveApp) -> GpuScene {
|
||||
if let Some(b) = st.body(body) {
|
||||
let k = t.hover_face_lift;
|
||||
let c = body_rgb(app, body);
|
||||
let m = b.mesh();
|
||||
let m = drawn_mesh(b);
|
||||
let patch = m.surface_patch(*index);
|
||||
let lit = shown(b);
|
||||
for &f in &patch {
|
||||
@@ -1797,7 +1807,8 @@ fn build_highlight(app: &SolveApp) -> GpuScene {
|
||||
}
|
||||
Some(Hit::Edge { body, index, mid }) => {
|
||||
if let Some(b) = st.body(body) {
|
||||
let m = b.mesh();
|
||||
// Lit over the drawn edges (finer away from Normal display quality).
|
||||
let m = drawn_mesh(b);
|
||||
for i in m.circular_rim(*index) {
|
||||
let selected = sel.iter().any(|x| matches!(x, Sel::Edge { body: b, index: j, .. } if b == body && *j == i));
|
||||
if !selected && let Some(e) = if i == *index { edge_of(&m, i, *mid) } else { m.edges.get(i) } {
|
||||
|
||||
Reference in new issue
Block a user