Preferences: symmetric surface extrudes read only the total distance

The Design preference surface_extrude_symmetric_per_side, which gave new
symmetric surface extrudes their distance on each side, is removed with
its Surfaces section, its session field and its labels. A symmetric
surface.extrude given no convention splits the total around the start
plane, as solid.extrude does; timeline.edit and timeline.redefine give a
one-sided extrude made symmetric the same. default_extrude_convention,
which only existed to read the preference, goes too.

An explicit `convention: legacy_per_side` and the marker saved designs
carry are unchanged. A saved preferences file that carries the key loads
as before (the prefs test now covers both removed keys).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jyviko
2026-10-30 15:46:20 -04:00
committed by jyviko
parent 2ae53d387f
commit a71450aebd
11 changed files with 14 additions and 131 deletions
@@ -1,30 +0,0 @@
{
"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": "서피스"
}
}
+2 -3
View File
@@ -1051,14 +1051,13 @@ fn edit_feature(s: &mut Session, p: &Value) -> Result<Value> {
nf.id = f.id;
// A one-sided extrude made symmetric takes the convention a new one would: its old marker
// never read a symmetric distance.
let maker = if matches!(f.kind, FeatureKind::SurfaceExtrude { .. }) { "surface.extrude" } else { "solid.extrude" };
if let (FeatureKind::Extrude { extent: old, .. }, FeatureKind::Extrude { extent: new, .. })
| (FeatureKind::SurfaceExtrude { extent: old, .. }, FeatureKind::SurfaceExtrude { extent: new, .. }) = (&f.kind, &mut nf.kind)
&& old.direction != solidcraft_doc::Direction::Symmetric
&& new.direction == solidcraft_doc::Direction::Symmetric
&& set.get("extent").and_then(|e| e.get("convention")).is_none()
{
new.convention = super::features::default_extrude_convention(s, maker);
new.convention = solidcraft_doc::ExtrudeConvention::TotalDistance;
}
if std::mem::discriminant(&nf.kind) != std::mem::discriminant(&f.kind) {
return Err(bad(cmd, "the feature type cannot change"));
@@ -1419,7 +1418,7 @@ fn redefine(s: &mut Session, p: &Value) -> Result<Value> {
let convention = if extent.direction == solidcraft_doc::Direction::Symmetric {
extent.convention
} else {
super::features::default_extrude_convention(s, cid)
solidcraft_doc::ExtrudeConvention::TotalDistance
};
parameters.entry("convention").or_insert_with(|| json!(convention));
}
@@ -193,34 +193,6 @@ fn older_symmetric_surface_extrudes_keep_the_distance_on_each_side() {
}
}
/// The preference brings back the per-side reading for new surface extrudes only: solid
/// extrudes and an explicit `convention` are unchanged by it.
#[test]
fn the_per_side_preference_keeps_the_older_surface_extrude_reading() {
let mut s = fixture(json!({}));
s.surface_extrude_symmetric_per_side = true;
run(&mut s, "surface.extrude", json!({"sketch":"Square","distance":"10 mm","direction":"symmetric","name":"Sheet","body_name":"Sheet"}));
assert_z(&s, "Sheet", -10.0, 10.0);
let FeatureKind::SurfaceExtrude { extent, .. } = &s.doc.find_feature("Sheet").unwrap().kind else { panic!("surface extrude") };
assert_eq!(extent.convention, ExtrudeConvention::LegacyPerSide);
run(
&mut s,
"surface.extrude",
json!({"sketch":"Square","distance":"10 mm","direction":"symmetric","convention":"total_distance","name":"Total","body_name":"Total"}),
);
assert_z(&s, "Total", -5.0, 5.0);
run(&mut s, "solid.extrude", json!({"sketch":"Square","distance":"10 mm","direction":"symmetric","operation":"new","body_name":"Solid"}));
assert_z(&s, "Solid", -5.0, 5.0);
// The saved marker, not the preference, decides on reopening.
s.surface_extrude_symmetric_per_side = false;
let reopened = Session::new(Document::from_json(&s.doc.to_json()).unwrap());
assert_z(&reopened, "Sheet", -10.0, 10.0);
// A design opened or started keeps the user's choice.
s.surface_extrude_symmetric_per_side = true;
s.replace_design(Session::default());
assert!(s.surface_extrude_symmetric_per_side);
}
/// A second distance goes the other way from the first side (#2700): along the normal when the
/// first side goes against it, whether by `direction: negative` or by a negative distance.
#[test]
+3 -13
View File
@@ -1120,18 +1120,6 @@ fn thin_regions(s: &Session, ss: &solidcraft_doc::SolvedSketch, thin: &ThinWall)
solidcraft_sketch::thin_wall_regions(&curves, inside, outside).map_err(|e| format!("thin wall: {e}"))
}
/// How a new extent of `cmd` (`solid.extrude` or `surface.extrude`) reads a symmetric distance
/// when the call doesn't say: the total, split evenly around the start plane, for both. A surface
/// extrude reads it on each side, as surface extrudes did before, while the user's preference
/// says so ([`Session::surface_extrude_symmetric_per_side`]).
pub(crate) fn default_extrude_convention(s: &Session, cmd: &str) -> solidcraft_doc::ExtrudeConvention {
if cmd == "surface.extrude" && s.surface_extrude_symmetric_per_side {
solidcraft_doc::ExtrudeConvention::LegacyPerSide
} else {
solidcraft_doc::ExtrudeConvention::TotalDistance
}
}
/// An extrude's extent from its parameters: distance (or through all, or to an object), the
/// side(s), a second distance, a start offset, a taper, and from an object.
pub(super) fn extent_param(s: &Session, p: &Value, cmd: &str) -> Result<Extent> {
@@ -1167,8 +1155,10 @@ pub(super) fn extent_param(s: &Session, p: &Value, cmd: &str) -> Result<Extent>
if let Some(d) = &start_offset {
check_expr(s, d, Kind::Length, cmd, "start_offset")?;
}
// A symmetric distance the call doesn't qualify is the total, split evenly around the start
// plane, for solid and surface extrudes alike.
let convention = match p.get("convention") {
None => default_extrude_convention(s, cmd),
None => solidcraft_doc::ExtrudeConvention::TotalDistance,
Some(value) => serde_json::from_value(value.clone()).map_err(|_| bad(cmd, "`convention` must be total_distance or legacy_per_side"))?,
};
Ok(Extent {
+1 -1
View File
@@ -39,7 +39,7 @@ pub static COMMANDS: &[CommandSpec] = &[
CommandSpec::new("surface.extrude", "Surface Extrude", surface_extrude)
.at("SURFACE", "CREATE")
.icon("surface_extrude")
.params("sketch?: id|name; curves?: [curve ids] (open or closed chains of curves meeting end to end) and/or profiles? (their boundaries; the default when no curves are given); distance: expr; direction?: positive|negative|symmetric (symmetric: distance is the total, split evenly around the start plane, as solid.extrude); convention?: total_distance|legacy_per_side (as solid.extrude; default total_distance, or legacy_per_side, the distance on each side, under the user's preference for the older reading); distance2?: expr; start_offset?: expr; name?, body_name? (a surface body: walls only, no caps)"),
.params("sketch?: id|name; curves?: [curve ids] (open or closed chains of curves meeting end to end) and/or profiles? (their boundaries; the default when no curves are given); distance: expr; direction?: positive|negative|symmetric (symmetric: distance is the total, split evenly around the start plane, as solid.extrude); convention?: total_distance|legacy_per_side (as solid.extrude; default total_distance; legacy_per_side: the distance on each side, as older symmetric surface extrudes keep); distance2?: expr; start_offset?: expr; name?, body_name? (a surface body: walls only, no caps)"),
CommandSpec::new("surface.revolve", "Surface Revolve", surface_revolve)
.at("SURFACE", "CREATE")
.icon("surface_revolve")
-8
View File
@@ -377,10 +377,6 @@ pub struct Session {
pub(crate) followed: Option<cmd::inspect::Followed>,
/// Project a face's edges into a sketch created on it (the default; `sketch.auto_project`).
pub auto_project: bool,
/// A new symmetric surface extrude given no `convention` reads its distance on each side, as
/// surface extrudes did before they matched solid ones (a user preference); false: the total,
/// split evenly around the start plane, as a solid extrude reads it.
pub surface_extrude_symmetric_per_side: bool,
/// How calls that break a command's parameter contract are treated (off by default). It
/// belongs to the session, not the design, so it survives opening and starting designs.
pub param_check: schema::ParamCheck,
@@ -648,7 +644,6 @@ impl Session {
document_id: cmd::inspect::new_document_id(),
followed: None,
auto_project: true,
surface_extrude_symmetric_per_side: false,
param_check: schema::ParamCheck::Off,
easyprint_upload: None,
live_links: None,
@@ -698,7 +693,6 @@ impl Session {
pub fn replace_design(&mut self, fresh: Session) {
let auto_project = self.auto_project;
let validation = self.validation();
let surface_extrude_symmetric_per_side = self.surface_extrude_symmetric_per_side;
let param_check = self.param_check;
let easyprint_upload = self.easyprint_upload.take();
let render_viewport = self.render_viewport.clone();
@@ -717,7 +711,6 @@ impl Session {
// The user's, like `auto_project`: the commands that open a design evaluate it under it
// already (`Session::opened_validating`).
self.set_validation(validation);
self.surface_extrude_symmetric_per_side = surface_extrude_symmetric_per_side;
self.param_check = param_check;
self.easyprint_upload = easyprint_upload;
self.render_viewport = render_viewport;
@@ -1474,7 +1467,6 @@ impl Session {
document_id: self.document_id.clone(),
followed: None,
auto_project: self.auto_project,
surface_extrude_symmetric_per_side: self.surface_extrude_symmetric_per_side,
param_check: self.param_check,
easyprint_upload: self.easyprint_upload.clone(),
live_links: None,
-1
View File
@@ -158,7 +158,6 @@ fn swap_in(app: &mut SolveApp, mut slot: DocSlot) -> DocSlot {
put_view(app, slot.view);
// Preferences the session holds go with the user, not the tab (#680).
app.session.auto_project = out.session.auto_project;
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());
-3
View File
@@ -978,9 +978,6 @@ 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 surface extrude command reads, current even before the next frame
// applies it.
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());
+7 -39
View File
@@ -633,10 +633,6 @@ preference_settings! {
/// Up/Down and the wheel over a focused value field step its plain number (`value_step`,
/// #2320). Off by default: the keys move the text cursor and the wheel scrolls, as before.
step_values: bool = false, PAGE_DESIGN => check_id("ui.step_values");
/// 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");
/// 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.
@@ -1126,8 +1122,6 @@ fn defaults(app: &mut SolveApp, page: usize) {
/// the interface scale.
pub fn apply(app: &mut SolveApp, ctx: &egui::Context) {
apply_network(&app.preferences);
// Surface Extrude reads its symmetric reading from the session.
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
@@ -2460,39 +2454,13 @@ mod tests {
#[test]
fn saved_preferences_with_a_removed_setting_still_load() {
use serde_json::json;
let saved = json!({"length_decimals": 4, "metric_tap_drill_major_less_pitch": true});
let p = serde_json::from_value::<super::Prefs>(saved.clone()).unwrap();
assert_eq!(p.length_decimals, 4);
assert_eq!(super::lenient(&saved).length_decimals, 4);
assert!(serde_json::to_value(&p).unwrap().get("metric_tap_drill_major_less_pitch").is_none(), "not written back");
}
/// 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);
for removed in ["metric_tap_drill_major_less_pitch", "surface_extrude_symmetric_per_side"] {
let saved = json!({"length_decimals": 4, removed: true});
let p = serde_json::from_value::<super::Prefs>(saved.clone()).unwrap();
assert_eq!(p.length_decimals, 4, "{removed}");
assert_eq!(super::lenient(&saved).length_decimals, 4, "{removed}");
assert!(serde_json::to_value(&p).unwrap().get(removed).is_none(), "{removed} is not written back");
}
}
#[test]
-4
View File
@@ -36,10 +36,6 @@ pub(super) fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
let step = setting::step_values(app, ui).on_hover_text(tip);
crate::scenario::publish_handle("prefs:step_values", step.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());
});
// 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;
@@ -2,7 +2,7 @@
"sources": [],
"what": "a line extruded symmetric 10 as a surface with no convention: the total, 5 each side of the sketch plane, as for a solid extrude (#9091)",
"source": "analytic",
"method": "surface.extrude without `convention` reads the distance as the total (default_extrude_convention): the plane y = 0, x 0..10, z -5..5, area 100, 4 free edges",
"method": "surface.extrude without `convention` reads the distance as the total: the plane y = 0, x 0..10, z -5..5, area 100, 4 free edges",
"valid": false,
"body_count": 1,
"faces": 1,