Preferences: keep the older per-side reading of a symmetric surface extrude

Design defaults gains a Surfaces section with one setting,
surface_extrude_symmetric_per_side (off by default). When it is on, new
symmetric surface extrudes go their distance each way, as they did
before they matched solid extrudes. The app hands it to the session
before every command and each frame, and it stays with the user across
tabs and new designs, like the metric tap drill rule.

The Surface Extrude dialog now sends a feature's stored convention only
when the feature is already symmetric, so a one-sided one switched to
Symmetric takes the new reading, as in the solid Extrude dialog.
Labels in en, ar, he, ja, zh, de and ko.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jyviko
2026-10-29 17:50:34 -04:00
committed by Irakli Kourtis
parent 9008d13f31
commit de5c338e78
6 changed files with 78 additions and 3 deletions
@@ -0,0 +1,30 @@
{
"en": {
"prefs.surface_extrude_symmetric_per_side": "Symmetric surface extrudes go the distance each way (10 mm: 20 mm in all), as before, not the total like solid extrudes",
"prefs.surfaces": "Surfaces"
},
"ar": {
"prefs.surface_extrude_symmetric_per_side": "يمتد بثق السطح المتماثل بالمسافة في كل جانب كما كان من قبل (10 مم: 20 مم إجمالًا)، لا بالمسافة الكلية كبثق المجسم",
"prefs.surfaces": "الأسطح"
},
"he": {
"prefs.surface_extrude_symmetric_per_side": "שליפת משטח סימטרית נשלפת את המרחק לכל צד כמו קודם (10 מ\"מ: 20 מ\"מ בסך הכול), ולא את המרחק הכולל כמו שליפת גוף",
"prefs.surfaces": "משטחים"
},
"ja": {
"prefs.surface_extrude_symmetric_per_side": "対称のサーフェス押し出しを、ソリッドの押し出しのような合計ではなく、以前のように両側にそれぞれその距離だけ押し出す(10 mm: 合計 20 mm)",
"prefs.surfaces": "サーフェス"
},
"zh": {
"prefs.surface_extrude_symmetric_per_side": "对称曲面拉伸像以前一样向两侧各拉伸该距离(10 mm:共 20 mm),而非像实体拉伸那样取总距离",
"prefs.surfaces": "曲面"
},
"de": {
"prefs.surface_extrude_symmetric_per_side": "Symmetrische Flächen-Extrusionen wie bisher um den Abstand nach jeder Seite (10 mm: insgesamt 20 mm), nicht um die Gesamtlänge wie Volumen-Extrusionen",
"prefs.surfaces": "Flächen"
},
"ko": {
"prefs.surface_extrude_symmetric_per_side": "대칭 서피스 돌출을 솔리드 돌출처럼 전체 거리가 아니라 이전처럼 양쪽으로 각각 그 거리만큼 돌출 (10 mm: 총 20 mm)",
"prefs.surfaces": "서피스"
}
}
+5 -1
View File
@@ -937,8 +937,12 @@ pub fn for_feature(app: &SolveApp, kind: &FeatureKind, frame: &Mat) -> Option<(K
if let Some(input) = ins.first_mut() {
input.items = sels;
}
// A symmetric extrude keeps the reading it was made with; a one-sided one made
// symmetric here takes a new one's (timeline.redefine fills it in either way).
let mut extra = Map::new();
extra.insert("convention".into(), json!(extent.convention));
if extent.direction == Direction::Symmetric {
extra.insert("convention".into(), json!(extent.convention));
}
return Some((Kind::Surface(k), ins, extra));
}
FeatureKind::Patch { sketch, edges, crown, continuity, points, .. } => {
+1
View File
@@ -159,6 +159,7 @@ fn swap_in(app: &mut SolveApp, mut slot: DocSlot) -> DocSlot {
// Preferences the session holds go with the user, not the tab (#680).
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;
// 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.
+3 -1
View File
@@ -978,8 +978,10 @@ impl SolveApp {
if matches!(id, "file.save_mesh" | "file.send_to_slicer") {
solidcraft_engine::slicers::set_chosen_programs(&self.preferences.custom_slicers);
}
// The preference the hole command reads, current even before the next frame applies it.
// The preferences the hole and surface extrude commands read, current even before the
// 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;
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;
+35 -1
View File
@@ -637,6 +637,10 @@ preference_settings! {
/// the tap maker's drills; off by default: the tap maker's drill (M8: 6.8 mm). Saved designs
/// keep the drill they stored either way.
metric_tap_drill_major_less_pitch: bool = false, PAGE_DESIGN => check_id("prefs.metric_tap_drill_major_less_pitch");
/// New symmetric surface extrudes go their distance each way, as before they matched solid
/// 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");
/// 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.
@@ -1122,8 +1126,10 @@ fn defaults(app: &mut SolveApp, page: usize) {
/// the interface scale.
pub fn apply(app: &mut SolveApp, ctx: &egui::Context) {
apply_network(&app.preferences);
// The hole command reads the metric tapped drill rule from the session.
// The hole command reads the metric tapped drill rule from the session, Surface Extrude its
// 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 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;
@@ -2484,6 +2490,34 @@ mod tests {
assert!(h.app.session.metric_tap_drill_major_less_pitch);
}
/// New symmetric surface extrudes read their distance as the total, as solid extrudes do; the
/// preference brings back the distance on each side they took before. It reaches the command
/// whether set through the control channel or the Preferences page, and survives a new design.
#[test]
fn the_surface_extrude_preference_keeps_the_distance_on_each_side() {
use serde_json::json;
let height = |h: &mut crate::scenario::Harness, name: &str| {
h.app.run("sketch.create", json!({"plane":"XY"})).unwrap();
h.app.run("sketch.rectangle.two_point", json!({"p0":[0,0],"p1":[10,10]})).unwrap();
h.app.run("sketch.finish", json!({})).unwrap();
h.app.run("surface.extrude", json!({"distance":"10 mm","direction":"symmetric","body_name":name})).unwrap();
let st = h.app.session.world_state().unwrap();
let b = st.body(name).unwrap().mesh().bounds();
b.max.z - b.min.z
};
let mut h = crate::scenario::Harness::new();
h.frames(1);
assert!(!h.app.preferences.surface_extrude_symmetric_per_side, "off by default");
assert!((height(&mut h, "Total") - 10.0).abs() < 1e-7);
let mut h = crate::scenario::Harness::new();
h.call("ui.prefs", json!({"surface_extrude_symmetric_per_side": true}));
h.frames(2);
assert!(h.app.session.surface_extrude_symmetric_per_side);
assert!((height(&mut h, "Each") - 20.0).abs() < 1e-7);
h.app.run("file.new", json!({})).unwrap();
assert!(h.app.session.surface_extrude_symmetric_per_side);
}
#[test]
fn the_ghost_position_defaults_round_trips_and_reads_tolerantly() {
use super::SpaceMouseGhost::{LeftViewport as Current, LeftWindow as Left, RightViewport as Right};
+4
View File
@@ -40,6 +40,10 @@ pub(super) fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
let check = setting::metric_tap_drill_major_less_pitch(app, ui);
crate::scenario::publish_handle("prefs:metric_tap_drill_major_less_pitch", check.rect.center());
});
ui_kit::settings_section(ui, "surfaces", app.tr("prefs.surfaces"), |ui| {
let check = setting::surface_extrude_symmetric_per_side(app, ui);
crate::scenario::publish_handle("prefs:surface_extrude_symmetric_per_side", check.rect.center());
});
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)] {