* 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
@@ -4,7 +4,7 @@
|
||||
//! solidcraft-cli run <script.json|design.solidcraft|part.step|mesh.3mf> [--in design|part.step|mesh.3mf] [--out FILE]... [--save FILE] [--quiet] [--results] [--json-out FILE] [--allow-errors]
|
||||
//! solidcraft-cli eval <script.json|design.solidcraft|part.step> [--json-out FILE] (alias: inspect) measurements as JSON
|
||||
//! solidcraft-cli script <design.solidcraft> [--out script.json] the design's history as a command script
|
||||
//! solidcraft-cli bench-open <design.solidcraft | part.step> [--parse-only] loading timings as JSON lines
|
||||
//! solidcraft-cli bench-open <design.solidcraft | part.step> [--parse-only] [--display LEVEL [--edit NAME EXPR]] loading timings as JSON lines
|
||||
//! solidcraft-cli bench-agent [--scale F] [--save F] [--script-out F] a ~1,300-body design built command by command, timed
|
||||
//! solidcraft-cli snapshot <script|design> --out shot.png [--width W] [--height H] [--view iso|front|top|…]
|
||||
//! solidcraft-cli exec <command> [json-params] run one command on an empty design
|
||||
@@ -45,7 +45,7 @@ const USAGE: &str = "usage:
|
||||
as plain UTF-8: a shell's > can re-encode it, e.g. Windows PowerShell adds a byte-order mark)
|
||||
solidcraft-cli script <design.solidcraft|script.json> [--out script.json]
|
||||
(the design's history as a command script that eval and run replay)
|
||||
solidcraft-cli bench-open <design.solidcraft | part.step> [--parse-only]
|
||||
solidcraft-cli bench-open <design.solidcraft | part.step> [--parse-only] [--display LEVEL [--edit NAME EXPR]]
|
||||
solidcraft-cli bench-agent [--scale F] [--save design.solidcraft] [--script-out script.json]
|
||||
solidcraft-cli snapshot <script|design> --out shot.png [--width W] [--height H] [--view iso|front|back|top|bottom|left|right]
|
||||
solidcraft-cli render <script|design> --out image.png [--setup NAME] [--width W] [--height H] [--transparent] [--overwrite] [--timeout SECONDS]
|
||||
@@ -500,5 +500,37 @@ pub(crate) fn cmd_bench_open(args: &[String]) -> Result<(), String> {
|
||||
st.warm_meshes(|| true);
|
||||
let triangles: usize = st.bodies.iter().map(|b| b.mesh().triangles.len()).sum();
|
||||
println!("{}", json!({"step": "mesh", "ms": t.elapsed().as_secs_f64() * 1000.0, "bodies": st.bodies.len(), "triangles": triangles}));
|
||||
// `--display LEVEL`: the display plans the viewport draws at that display quality, made as
|
||||
// the GPU viewport makes them; `--edit NAME EXPR` then changes a parameter and times the
|
||||
// evaluation and the plans of the bodies it changed.
|
||||
let flag = |name: &str| args.iter().position(|a| a == name);
|
||||
let Some(level) = flag("--display").and_then(|i| args.get(i + 1)) else { return Ok(()) };
|
||||
let q = solidcraft_engine::doc::display_quality::DisplayQuality::parse(level).ok_or("--display: coarse, normal, fine or ultra")?;
|
||||
let plans = |st: &solidcraft_engine::doc::ModelState| {
|
||||
let t = std::time::Instant::now();
|
||||
solidcraft_engine::kernel::for_each_on_threads(&st.bodies, 16, |b| {
|
||||
let _ = b.plan_mesh_at(q);
|
||||
});
|
||||
let triangles: usize = st.bodies.iter().filter_map(|b| b.plan_mesh_at(q)).map(|m| m.triangles.len()).sum();
|
||||
(t.elapsed().as_secs_f64() * 1000.0, triangles)
|
||||
};
|
||||
let (ms, triangles) = plans(&st);
|
||||
println!("{}", json!({"step": "display", "quality": q.id(), "ms": ms, "triangles": triangles}));
|
||||
if let Some(i) = flag("--edit") {
|
||||
let (Some(name), Some(expr)) = (args.get(i + 1), args.get(i + 2)) else { return Err("--edit NAME EXPR".into()) };
|
||||
let mut s = s;
|
||||
let t = std::time::Instant::now();
|
||||
s.execute("parameters.change", &json!({"name": name, "expression": expr})).map_err(|e| e.to_string())?;
|
||||
let eval_ms = t.elapsed().as_secs_f64() * 1000.0;
|
||||
let st = s.world_state();
|
||||
let t = std::time::Instant::now();
|
||||
st.warm_meshes(|| true);
|
||||
let mesh_ms = t.elapsed().as_secs_f64() * 1000.0;
|
||||
let (ms, triangles) = plans(&st);
|
||||
println!(
|
||||
"{}",
|
||||
json!({"step": "edit", "quality": q.id(), "evaluate_ms": eval_ms, "mesh_ms": mesh_ms, "display_ms": ms, "triangles": triangles})
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -49,6 +49,12 @@
|
||||
"ui.animation": "Animation",
|
||||
"ui.animation_speed": "Animationsgeschwindigkeit",
|
||||
"ui.anti_aliasing": "Kantenglättung",
|
||||
"ui.display_quality": "Darstellungsqualität",
|
||||
"ui.display_quality_coarse": "Grob",
|
||||
"ui.display_quality_normal": "Normal",
|
||||
"ui.display_quality_fine": "Fein",
|
||||
"ui.display_quality_ultra": "Ultra",
|
||||
"settings.display_quality_note": "Feinere Stufen zeigen gekrümmte Flächen und Kanten glatter und brauchen länger zum Vernetzen. Nur die Ansicht ändert sich, nie das Design oder Exporte.",
|
||||
"ui.apply": "Anwenden",
|
||||
"ui.arc": "Kreisbogen",
|
||||
"ui.assemble": "Zusammenbau",
|
||||
|
||||
@@ -327,6 +327,12 @@
|
||||
"ui.animation": "Animation",
|
||||
"ui.animation_speed": "Animation speed",
|
||||
"ui.anti_aliasing": "Anti-aliasing",
|
||||
"ui.display_quality": "Display quality",
|
||||
"ui.display_quality_coarse": "Coarse",
|
||||
"ui.display_quality_normal": "Normal",
|
||||
"ui.display_quality_fine": "Fine",
|
||||
"ui.display_quality_ultra": "Ultra",
|
||||
"settings.display_quality_note": "Finer levels draw curved faces and edges smoother and take longer to mesh. Only the viewport changes, never the design or exports.",
|
||||
"ui.apply": "Apply",
|
||||
"ui.arc": "Arc",
|
||||
"ui.assemble": "Assemble",
|
||||
|
||||
@@ -327,6 +327,12 @@
|
||||
"ui.animation": "アニメーション",
|
||||
"ui.animation_speed": "アニメーション速度",
|
||||
"ui.anti_aliasing": "アンチエイリアス",
|
||||
"ui.display_quality": "表示品質",
|
||||
"ui.display_quality_coarse": "粗い",
|
||||
"ui.display_quality_normal": "標準",
|
||||
"ui.display_quality_fine": "細かい",
|
||||
"ui.display_quality_ultra": "最高",
|
||||
"settings.display_quality_note": "細かいほど曲面とエッジが滑らかに表示されますが、メッシュの生成に時間がかかります。変わるのはビューポートだけで、設計やエクスポートは変わりません。",
|
||||
"ui.apply": "適用",
|
||||
"ui.arc": "円弧",
|
||||
"ui.assemble": "アセンブリ",
|
||||
|
||||
@@ -49,6 +49,12 @@
|
||||
"ui.animation": "애니메이션",
|
||||
"ui.animation_speed": "애니메이션 속도",
|
||||
"ui.anti_aliasing": "안티앨리어싱",
|
||||
"ui.display_quality": "표시 품질",
|
||||
"ui.display_quality_coarse": "거침",
|
||||
"ui.display_quality_normal": "보통",
|
||||
"ui.display_quality_fine": "세밀",
|
||||
"ui.display_quality_ultra": "최고",
|
||||
"settings.display_quality_note": "세밀할수록 곡면과 모서리가 더 매끄럽게 표시되지만 메시 생성 시간이 늘어납니다. 뷰포트만 바뀌며 설계나 내보내기는 바뀌지 않습니다.",
|
||||
"ui.apply": "적용",
|
||||
"ui.arc": "호",
|
||||
"ui.assemble": "조립",
|
||||
|
||||
@@ -326,6 +326,12 @@
|
||||
"ui.animation": "动画",
|
||||
"ui.animation_speed": "动画速度",
|
||||
"ui.anti_aliasing": "抗锯齿",
|
||||
"ui.display_quality": "显示质量",
|
||||
"ui.display_quality_coarse": "粗糙",
|
||||
"ui.display_quality_normal": "标准",
|
||||
"ui.display_quality_fine": "精细",
|
||||
"ui.display_quality_ultra": "极高",
|
||||
"settings.display_quality_note": "级别越精细,曲面和边显示越平滑,但网格生成耗时更长。只影响视口,不影响设计或导出。",
|
||||
"ui.apply": "应用",
|
||||
"ui.arc": "圆弧",
|
||||
"ui.assemble": "装配",
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
//! How finely the viewport tessellates curved faces and edges (Preferences ▸ Graphics ▸ Display
|
||||
//! quality). A viewing preference, not design data: it only picks the tolerances of the display
|
||||
//! plan ([`crate::ModelBody::display_plan`]) the viewport draws. The reference mesh the model
|
||||
//! reads (picks, holes, measuring) and exports keep their own tolerances.
|
||||
|
||||
use std::sync::atomic::{AtomicU8, Ordering};
|
||||
|
||||
/// A display quality level.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
|
||||
pub enum DisplayQuality {
|
||||
Coarse,
|
||||
#[default]
|
||||
Normal,
|
||||
Fine,
|
||||
Ultra,
|
||||
}
|
||||
|
||||
impl DisplayQuality {
|
||||
pub const ALL: [DisplayQuality; 4] = [DisplayQuality::Coarse, DisplayQuality::Normal, DisplayQuality::Fine, DisplayQuality::Ultra];
|
||||
|
||||
/// The id kept in the settings file.
|
||||
pub fn id(self) -> &'static str {
|
||||
match self {
|
||||
DisplayQuality::Coarse => "coarse",
|
||||
DisplayQuality::Normal => "normal",
|
||||
DisplayQuality::Fine => "fine",
|
||||
DisplayQuality::Ultra => "ultra",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(id: &str) -> Option<DisplayQuality> {
|
||||
Self::ALL.into_iter().find(|q| q.id() == id)
|
||||
}
|
||||
|
||||
/// Position in [`DisplayQuality::ALL`] (indexes the per-level display caches).
|
||||
pub fn index(self) -> usize {
|
||||
self as usize
|
||||
}
|
||||
|
||||
/// Chord tolerance as a fraction of the body's size: the largest gap allowed between a
|
||||
/// facet and its surface.
|
||||
pub fn chord_fraction(self) -> f64 {
|
||||
match self {
|
||||
DisplayQuality::Coarse => 3e-3,
|
||||
DisplayQuality::Normal => 1e-3,
|
||||
DisplayQuality::Fine => 3e-4,
|
||||
DisplayQuality::Ultra => 1e-4,
|
||||
}
|
||||
}
|
||||
|
||||
/// Smallest chord tolerance (mm), so a tiny body does not mesh down to nothing.
|
||||
pub fn min_chord(self) -> f64 {
|
||||
match self {
|
||||
DisplayQuality::Coarse => 3e-3,
|
||||
DisplayQuality::Normal => 1e-3,
|
||||
DisplayQuality::Fine => 3e-4,
|
||||
DisplayQuality::Ultra => 1e-4,
|
||||
}
|
||||
}
|
||||
|
||||
/// Most a facet of a curved face or edge may turn (radians), so a small radius on a large
|
||||
/// body still looks round.
|
||||
pub fn angle(self) -> f64 {
|
||||
let deg: f64 = match self {
|
||||
DisplayQuality::Coarse => 30.0,
|
||||
DisplayQuality::Normal => 20.0,
|
||||
DisplayQuality::Fine => 12.0,
|
||||
DisplayQuality::Ultra => 7.0,
|
||||
};
|
||||
deg.to_radians()
|
||||
}
|
||||
|
||||
/// The chord tolerance (mm) for a body of this size.
|
||||
pub fn chord(self, size: f64) -> f64 {
|
||||
let size = if size.is_finite() && size > 0.0 { size } else { 0.0 };
|
||||
(size * self.chord_fraction()).max(self.min_chord())
|
||||
}
|
||||
}
|
||||
|
||||
static CURRENT: AtomicU8 = AtomicU8::new(1);
|
||||
|
||||
/// The level the viewport draws at (process-wide; set from the preferences).
|
||||
pub fn current() -> DisplayQuality {
|
||||
DisplayQuality::ALL.get(CURRENT.load(Ordering::Relaxed) as usize).copied().unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Set the level the viewport draws at. Bodies keep a display plan per level, so switching back
|
||||
/// is instant and nothing is re-evaluated.
|
||||
pub fn set_current(q: DisplayQuality) {
|
||||
CURRENT.store(q.index() as u8, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn normal_is_the_application_tolerance_and_levels_get_finer() {
|
||||
let n = DisplayQuality::Normal;
|
||||
assert_eq!(n.chord(200.0), 0.2);
|
||||
assert_eq!(n.chord(0.5), 1e-3);
|
||||
assert!((n.angle() - solidcraft_kernel::DISPLAY_ANGLE).abs() < 1e-12);
|
||||
for w in DisplayQuality::ALL.windows(2) {
|
||||
assert!(w[1].chord(100.0) < w[0].chord(100.0) && w[1].angle() < w[0].angle());
|
||||
}
|
||||
for q in DisplayQuality::ALL {
|
||||
assert_eq!(DisplayQuality::parse(q.id()), Some(q));
|
||||
assert_eq!(DisplayQuality::ALL[q.index()], q);
|
||||
}
|
||||
}
|
||||
|
||||
/// Each level keeps its own plan: finer levels draw a round body with more triangles and
|
||||
/// closer to the surface, and the reference mesh the model reads never changes.
|
||||
#[test]
|
||||
fn each_level_has_its_own_plan_and_the_reference_mesh_stays() {
|
||||
use solidcraft_geom::Vec3;
|
||||
let body = solidcraft_kernel::cylinder(Vec3::ZERO, Vec3::Z, 20.0, 50.0).unwrap();
|
||||
let mb = crate::ModelBody::new("Body1".into(), body, 1);
|
||||
let reference = mb.mesh();
|
||||
let mut last = 0;
|
||||
for q in DisplayQuality::ALL {
|
||||
let mesh = mb.plan_mesh_at(q).unwrap();
|
||||
let r = |v: Vec3| (v.x * v.x + v.y * v.y).sqrt();
|
||||
// The worst chord's sag on the side wall, from its triangle centres.
|
||||
let sag = mesh
|
||||
.triangles
|
||||
.iter()
|
||||
.map(|t| t.map(|i| mesh.positions[i as usize]))
|
||||
.filter(|t| t.iter().all(|p| (r(*p) - 20.0).abs() < 1e-9))
|
||||
.map(|[a, b, c]| 20.0 - r((a + b + c) * (1.0 / 3.0)))
|
||||
.fold(0.0, f64::max);
|
||||
assert!(sag <= q.chord(mb.body.size()) * 1.01, "{q:?}: sag {sag}");
|
||||
assert!(mesh.triangles.len() > last, "{q:?}: {} triangles", mesh.triangles.len());
|
||||
last = mesh.triangles.len();
|
||||
assert!(std::sync::Arc::ptr_eq(&mesh, &mb.plan_mesh_at(q).unwrap()), "made once per level");
|
||||
}
|
||||
assert!(std::sync::Arc::ptr_eq(&reference, &mb.mesh()));
|
||||
assert_eq!(mb.display_plan_at(DisplayQuality::Normal).unwrap().vertex_count(), {
|
||||
let plan = mb.body.display_plan_angular((mb.body.size() * 1e-3).max(1e-3), solidcraft_kernel::DISPLAY_ANGLE).unwrap();
|
||||
plan.vertex_count()
|
||||
});
|
||||
}
|
||||
}
|
||||
+28
-12
@@ -30,10 +30,10 @@ pub struct ModelBody {
|
||||
mesh: Arc<OnceLock<Arc<Mesh>>>,
|
||||
/// The display plan (none when the body has no plan: a mesh body, or one the planner
|
||||
/// refuses), shared like the mesh. A GPU viewport draws it.
|
||||
plan: Arc<OnceLock<Option<Arc<TessPlan>>>>,
|
||||
plan: Arc<[OnceLock<Option<Arc<TessPlan>>>; 4]>,
|
||||
/// The plan evaluated on the CPU (none without a plan), shared like the mesh: the triangles a
|
||||
/// GPU viewport draws, so a highlight over a face lies exactly on it.
|
||||
plan_mesh: Arc<OnceLock<Option<Arc<Mesh>>>>,
|
||||
plan_mesh: Arc<[OnceLock<Option<Arc<Mesh>>>; 4]>,
|
||||
/// Identity of this body's content for the evaluation cache: the key of the evaluation
|
||||
/// that produced it (kept while later features pass it through unchanged).
|
||||
key: u64,
|
||||
@@ -61,8 +61,8 @@ impl ModelBody {
|
||||
body,
|
||||
feature,
|
||||
mesh: Arc::new(OnceLock::new()),
|
||||
plan: Arc::new(OnceLock::new()),
|
||||
plan_mesh: Arc::new(OnceLock::new()),
|
||||
plan: Arc::new(Default::default()),
|
||||
plan_mesh: Arc::new(Default::default()),
|
||||
key: 0,
|
||||
names: Default::default(),
|
||||
made: Default::default(),
|
||||
@@ -88,11 +88,19 @@ impl ModelBody {
|
||||
.clone()
|
||||
}
|
||||
|
||||
/// The display plan of this body (the same tolerance and angle as [`ModelBody::mesh`]), made
|
||||
/// once; `None` when the body has no plan (a mesh body, or one the planner refuses).
|
||||
/// The display plan of this body at the viewport's display quality
|
||||
/// ([`crate::display_quality::current`]; at Normal, the same tolerance and angle as
|
||||
/// [`ModelBody::mesh`]), made once per level; `None` when the body has no plan (a mesh body,
|
||||
/// or one the planner refuses).
|
||||
pub fn display_plan(&self) -> Option<Arc<TessPlan>> {
|
||||
self.display_plan_at(crate::display_quality::current())
|
||||
}
|
||||
|
||||
/// The display plan at a given display quality, made once per level.
|
||||
pub fn display_plan_at(&self, q: crate::display_quality::DisplayQuality) -> Option<Arc<TessPlan>> {
|
||||
self.plan
|
||||
.get_or_init(|| match self.body.display_plan_angular(self.display_tol(), kernel::DISPLAY_ANGLE) {
|
||||
.get(q.index())?
|
||||
.get_or_init(|| match self.body.display_plan_angular(q.chord(self.body.size()), q.angle()) {
|
||||
Ok(plan) => Some(Arc::new(plan)),
|
||||
Err(e) => {
|
||||
log::debug!("{}: no display plan ({e})", self.name);
|
||||
@@ -102,12 +110,20 @@ impl ModelBody {
|
||||
.clone()
|
||||
}
|
||||
|
||||
/// The display plan's mesh, evaluated on the CPU once: the same triangles a GPU viewport
|
||||
/// evaluates and draws for this body. Highlights over a face drawn from the plan use it, since
|
||||
/// the reference mesh ([`ModelBody::mesh`]) triangulates the face differently and the two
|
||||
/// would fight at equal depth. `None` when the body has no plan.
|
||||
/// The display plan's mesh, evaluated on the CPU once per level: the same triangles a GPU
|
||||
/// viewport evaluates and draws for this body. Highlights over a face drawn from the plan use
|
||||
/// it, since the reference mesh ([`ModelBody::mesh`]) triangulates the face differently and
|
||||
/// the two would fight at equal depth. `None` when the body has no plan.
|
||||
pub fn plan_mesh(&self) -> Option<Arc<Mesh>> {
|
||||
self.plan_mesh.get_or_init(|| self.display_plan().and_then(|plan| self.body.display_mesh_of(&plan).ok()).map(Arc::new)).clone()
|
||||
self.plan_mesh_at(crate::display_quality::current())
|
||||
}
|
||||
|
||||
/// The display plan's mesh at a given display quality.
|
||||
pub fn plan_mesh_at(&self, q: crate::display_quality::DisplayQuality) -> Option<Arc<Mesh>> {
|
||||
self.plan_mesh
|
||||
.get(q.index())?
|
||||
.get_or_init(|| self.display_plan_at(q).and_then(|plan| self.body.display_mesh_of(&plan).ok()).map(Arc::new))
|
||||
.clone()
|
||||
}
|
||||
|
||||
/// The display mesh's chord tolerance: 1/1000 of the body's size.
|
||||
|
||||
@@ -16,6 +16,7 @@ mod clipboard;
|
||||
pub use clipboard::{clone_expression, remap_topology};
|
||||
pub mod config;
|
||||
pub mod construct;
|
||||
pub mod display_quality;
|
||||
mod document;
|
||||
mod eval;
|
||||
pub mod expr;
|
||||
|
||||
@@ -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