Preferences: Body check, once per feature by default

The Design page's Body check section sets the validation preference
(each_feature; each_step keeps the old checks after every step; off is
experimental), applied to the session like the hole and surface extrude
preferences, to the sessions a design is opened in off the window thread,
and kept in step with diagnostics.validation. ui.prefs refuses an unknown
mode; a saved unknown one reads as the default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jyviko
2026-10-30 09:36:06 -04:00
committed by jyviko
parent 1a93a39770
commit 50501fe14a
6 changed files with 127 additions and 1 deletions
@@ -0,0 +1,65 @@
{
"ar": {
"cmd.diagnostics_validation": "التحقق",
"prefs.validation": "التحقق من الأجسام",
"prefs.validation.each_feature": "مرة لكل ميزة",
"prefs.validation.each_step": "بعد كل خطوة",
"prefs.validation.label": "فحص SMLib الكامل",
"prefs.validation.off": "متوقف (تجريبي)",
"prefs.validation_note": "مرة لكل ميزة: تُفحص الأجسام التي تنتجها الميزة مرة واحدة قبل الاحتفاظ بها، وتفشل الميزة إذا رُفض أحدها. بعد كل خطوة: يُفحص ناتج كل عملية سحب ودمج وتقويس، كما في السابق. متوقف: لا فحص، فقد يُحتفظ بجسم معيب."
},
"de": {
"cmd.diagnostics_validation": "Prüfung",
"prefs.validation": "Körperprüfung",
"prefs.validation.each_feature": "Einmal je Feature",
"prefs.validation.each_step": "Nach jedem Schritt",
"prefs.validation.label": "Vollständige SMLib-Prüfung",
"prefs.validation.off": "Aus (experimentell)",
"prefs.validation_note": "Einmal je Feature: Die Körper, die ein Feature erzeugt, werden einmal geprüft, bevor sie übernommen werden; wird einer abgelehnt, schlägt das Feature fehl. Nach jedem Schritt: Das Ergebnis jedes Sweeps, jeder Boolesche Operation und jeder Rundung wird geprüft, wie bisher. Aus: keine Prüfung, ein fehlerhafter Körper kann bleiben."
},
"en": {
"cmd.diagnostics_validation": "Validation",
"prefs.validation": "Body check",
"prefs.validation.each_feature": "Once per feature",
"prefs.validation.each_step": "After every step",
"prefs.validation.label": "SMLib's full check",
"prefs.validation.off": "Off (experimental)",
"prefs.validation_note": "Once per feature: the bodies a feature makes are checked once before they are kept, and the feature fails if one is refused. After every step: the result of every sweep, boolean and blend is checked, as before. Off: no check, so a faulty body can be kept."
},
"he": {
"cmd.diagnostics_validation": "אימות",
"prefs.validation": "בדיקת גופים",
"prefs.validation.each_feature": "פעם אחת לכל תכונה",
"prefs.validation.each_step": "אחרי כל שלב",
"prefs.validation.label": "הבדיקה המלאה של SMLib",
"prefs.validation.off": "כבוי (ניסיוני)",
"prefs.validation_note": "פעם אחת לכל תכונה: הגופים שתכונה יוצרת נבדקים פעם אחת לפני שהם נשמרים, והתכונה נכשלת אם אחד מהם נדחה. אחרי כל שלב: התוצאה של כל סחיפה, פעולה בוליאנית ועיגול נבדקת, כמו קודם. כבוי: אין בדיקה, ולכן גוף פגום עלול להישמר."
},
"ja": {
"cmd.diagnostics_validation": "検証",
"prefs.validation": "ボディのチェック",
"prefs.validation.each_feature": "フィーチャーごとに 1 回",
"prefs.validation.each_step": "各ステップの後",
"prefs.validation.label": "SMLib の完全チェック",
"prefs.validation.off": "オフ (試験的)",
"prefs.validation_note": "フィーチャーごとに 1 回: フィーチャーが作るボディを保持する前に 1 回チェックし、拒否されたものがあればフィーチャーは失敗します。各ステップの後: これまでどおり、スイープ、ブーリアン、ブレンドのたびに結果をチェックします。オフ: チェックしないため、不正なボディが残ることがあります。"
},
"ko": {
"cmd.diagnostics_validation": "검증",
"prefs.validation": "바디 검사",
"prefs.validation.each_feature": "피처마다 한 번",
"prefs.validation.each_step": "모든 단계 후",
"prefs.validation.label": "SMLib 전체 검사",
"prefs.validation.off": "끄기 (실험적)",
"prefs.validation_note": "피처마다 한 번: 피처가 만든 바디를 유지하기 전에 한 번 검사하며, 하나라도 거부되면 피처가 실패합니다. 모든 단계 후: 이전처럼 스윕, 불리언, 블렌드마다 결과를 검사합니다. 끄기: 검사하지 않으므로 결함 있는 바디가 남을 수 있습니다."
},
"zh": {
"cmd.diagnostics_validation": "验证",
"prefs.validation": "实体检查",
"prefs.validation.each_feature": "每个特征一次",
"prefs.validation.each_step": "每一步之后",
"prefs.validation.label": "SMLib 完整检查",
"prefs.validation.off": "关闭(实验性)",
"prefs.validation_note": "每个特征一次:特征生成的实体在保留前检查一次,若有实体被拒绝,该特征失败。每一步之后:与以前一样,每次扫掠、布尔运算和倒圆后都检查结果。关闭:不检查,因此可能保留有缺陷的实体。"
}
}
+1
View File
@@ -160,6 +160,7 @@ fn swap_in(app: &mut SolveApp, mut slot: DocSlot) -> DocSlot {
app.session.auto_project = out.session.auto_project;
app.session.metric_tap_drill_major_less_pitch = out.session.metric_tap_drill_major_less_pitch;
app.session.surface_extrude_symmetric_per_side = out.session.surface_extrude_symmetric_per_side;
app.session.set_validation(out.session.validation());
// So does the host's pre-command check: every tab is held to it.
app.session.set_command_check(out.session.command_check());
// Views cache by revision: the incoming design must not look unchanged.
+4 -1
View File
@@ -408,8 +408,10 @@ fn start(app: &mut SolveApp) {
let before = if insert { crate::open_meshing::session_world(&app.session).bodies.iter().map(|b| b.name.clone()).collect() } else { Vec::new() };
let update = request.kind == Kind::UpdateLinks;
let mut scratch = if insert || update || request.kind == Kind::ExportDxf { app.session.scratch() } else { Session::default() };
// A design opened or recovered here is run through the host's pre-command check too.
// A design opened or recovered here is run through the host's pre-command check too, and
// evaluated under the user's validation mode.
scratch.set_command_check(app.session.command_check());
scratch.set_validation(app.preferences.validation);
if insert {
scratch.selection = app.session.selection.clone();
}
@@ -537,6 +539,7 @@ fn start_staged(app: &mut SolveApp) {
let before = if insert { crate::open_meshing::session_world(&app.session).bodies.iter().map(|b| b.name.clone()).collect() } else { Vec::new() };
let mut session = if insert { app.session.scratch() } else { Session::default() };
session.set_command_check(app.session.command_check());
session.set_validation(app.preferences.validation);
if insert {
session.selection = app.session.selection.clone();
}
+3
View File
@@ -982,6 +982,7 @@ impl SolveApp {
// next frame applies them.
self.session.metric_tap_drill_major_less_pitch = self.preferences.metric_tap_drill_major_less_pitch;
self.session.surface_extrude_symmetric_per_side = self.preferences.surface_extrude_symmetric_per_side;
self.session.set_validation(self.preferences.validation);
let t0 = now_ms();
let r = self.session.execute(id, &params).map_err(|e| e.to_string());
self.big_operation |= now_ms() - t0 >= recovery_ui::BIG_OPERATION_MS;
@@ -1010,6 +1011,8 @@ impl SolveApp {
self.ui.visual_style = index;
}
}
// The command and the preference are one setting.
"diagnostics.validation" => self.preferences.validation = self.session.validation(),
"view.grid" => self.ui.show_grid = v.get("on").and_then(Value::as_bool).unwrap_or(!self.ui.show_grid),
"view.ground_shadow" => self.ui.ground_shadow = v.get("on").and_then(Value::as_bool).unwrap_or(!self.ui.ground_shadow),
"view.navigation_bar" => {
+35
View File
@@ -641,6 +641,10 @@ preference_settings! {
/// extrudes; off by default: the distance is the total, split evenly around the start plane.
/// Saved designs keep the reading they stored either way.
surface_extrude_symmetric_per_side: bool = false, PAGE_DESIGN => check_id("prefs.surface_extrude_symmetric_per_side");
/// When SMLib's full check runs while features are computed (`diagnostics.validation`):
/// `each_feature` (the default) checks the bodies each feature keeps once; `each_step`, as
/// before, checks after every sweep, boolean and blend; `off` (experimental) never does.
validation: solidcraft_engine::kernel::Validation = solidcraft_engine::kernel::Validation::EachFeature, PAGE_DESIGN;
/// Reopen the saved designs that were open when the app was last closed (#5542).
reopen_designs: bool = true, PAGE_GENERAL;
/// Those designs' files, in tab order, as the last quit left them.
@@ -1130,6 +1134,8 @@ pub fn apply(app: &mut SolveApp, ctx: &egui::Context) {
// symmetric reading.
app.session.metric_tap_drill_major_less_pitch = app.preferences.metric_tap_drill_major_less_pitch;
app.session.surface_extrude_symmetric_per_side = app.preferences.surface_extrude_symmetric_per_side;
// The features computed from now on are checked as the preference says.
app.session.set_validation(app.preferences.validation);
// The profiler records while the preference is on (switched when it changes; at start-up only
// when on, so SOLIDCRAFT_PROFILE=1 is not undone by the default).
let profile = app.preferences.profile_operations;
@@ -2341,6 +2347,11 @@ pub fn set(app: &mut SolveApp, p: &serde_json::Value) -> Result<serde_json::Valu
{
return Err("`viewport_theme` must be theme, light or dark".into());
}
if let Some(v) = p.get("validation")
&& v.as_str().and_then(solidcraft_engine::kernel::Validation::parse).is_none()
{
return Err("`validation` must be each_step, each_feature or off".into());
}
let new: Prefs = migrate(serde_json::from_value(cur).map_err(|e| e.to_string())?);
if !UNITS.contains(&new.default_units.as_str()) {
return Err(format!("units must be one of {}", UNITS.join(", ")));
@@ -3843,6 +3854,30 @@ mod tests {
assert_eq!(app.preferences.display_quality, "normal");
}
/// The validation mode is once per feature by default; set by name or by the command, the
/// two stay one setting, the session computes under it, and Restore Defaults on Design brings
/// each_feature back. A saved unknown mode reads as the default.
#[test]
fn validation_defaults_to_each_feature_and_follows_the_command() {
use serde_json::json;
use solidcraft_engine::kernel::Validation;
let mut app = crate::SolveApp::new(solidcraft_engine::Session::default(), crate::Services::default());
assert_eq!(app.preferences.validation, Validation::EachFeature);
assert_eq!(super::lenient(&json!({"validation": "sometimes"})).validation, Validation::EachFeature);
assert!(super::set(&mut app, &json!({"validation": "never"})).unwrap_err().contains("each_step, each_feature or off"));
let out = super::set(&mut app, &json!({"validation": "each_step"})).unwrap();
assert_eq!(out["validation"], "each_step");
assert_eq!(super::lenient(&out).validation, Validation::EachStep);
// A command runs under the preference.
app.run("diagnostics.validation", json!({})).unwrap();
assert_eq!(app.session.validation(), Validation::EachStep);
// The command sets the preference too.
app.run("diagnostics.validation", json!({"mode": "off"})).unwrap();
assert_eq!((app.preferences.validation, app.session.validation()), (Validation::Off, Validation::Off));
super::defaults(&mut app, super::PAGE_DESIGN);
assert_eq!(app.preferences.validation, Validation::EachFeature);
}
#[test]
fn sketch_halo_is_off_by_default_saved_and_restored() {
use serde_json::json;
+19
View File
@@ -44,6 +44,25 @@ pub(super) fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
let check = setting::surface_extrude_symmetric_per_side(app, ui);
crate::scenario::publish_handle("prefs:surface_extrude_symmetric_per_side", check.rect.center());
});
// When SMLib's full check runs while features are computed (`diagnostics.validation`).
ui_kit::settings_section(ui, "validation", app.tr("prefs.validation"), |ui| {
use solidcraft_engine::kernel::Validation;
let label = |v: &Validation| match v {
Validation::EachStep => "prefs.validation.each_step",
Validation::EachFeature => "prefs.validation.each_feature",
Validation::Off => "prefs.validation.off",
};
let labels: Vec<String> = Validation::ALL.iter().map(|v| app.tr(label(v)).to_string()).collect();
let options: Vec<&str> = labels.iter().map(String::as_str).collect();
let mut selected = Validation::ALL.iter().position(|v| *v == app.preferences.validation).unwrap_or(1);
ui_kit::row(ui, app.tr("prefs.validation.label"), |ui| {
ui_kit::segmented(ui, &mut selected, &options, ui_kit::Segments { intrinsic: true, ..Default::default() });
});
if let Some(v) = Validation::ALL.get(selected) {
app.preferences.validation = *v;
}
ui_kit::text(ui, app.tr("prefs.validation_note"), ui_kit::Text::Hint("validation"));
});
ui_kit::settings_section(ui, "orientation", app.tr("prefs.orientation"), |ui| {
ui.horizontal_wrapped(|ui| {
for (id, label, y_up) in [("z", "Z up", false), ("y", "Y up", true)] {