Compare commits

...

1 Commits

Author SHA1 Message Date
bherbruck cd45fc5fe6 Preferences: choose the Extrude dialog's starting distance (fixed or last used)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 11:43:48 +00:00
6 changed files with 125 additions and 2 deletions
+4
View File
@@ -1346,6 +1346,10 @@
"ui.newSketchGroup": "New Sketch Group",
"ui.new_body": "New Body",
"ui.new_design": "New Design",
"ui.new_extrude_distance": "New extrude distance",
"ui.new_extrude_distance_fixed": "Fixed (10 mm)",
"ui.new_extrude_distance_last_used": "Last used",
"ui.new_extrude_distance_tip": "The distance a new Extrude starts with. Editing an existing extrude keeps its own.",
"ui.no_slicer_found": "No slicer found",
"ui.normal": "Normal",
"ui.normalConstruction": "Normal / Construction",
+4
View File
@@ -1346,6 +1346,10 @@
"ui.newSketchGroup": "新しいスケッチグループ",
"ui.new_body": "新規ボディ",
"ui.new_design": "新規デザイン",
"ui.new_extrude_distance": "新規押し出しの距離",
"ui.new_extrude_distance_fixed": "固定 (10 mm)",
"ui.new_extrude_distance_last_used": "前回の値",
"ui.new_extrude_distance_tip": "新しい押し出しの開始距離です。既存の押し出しを編集するときは、その押し出しの値が使われます。",
"ui.no_slicer_found": "スライサーが見つかりません",
"ui.normal": "標準",
"ui.normalConstruction": "通常 / コンストラクション",
+4
View File
@@ -1346,6 +1346,10 @@
"ui.newSketchGroup": "新建草图组",
"ui.new_body": "新建实体",
"ui.new_design": "新建设计",
"ui.new_extrude_distance": "新建拉伸距离",
"ui.new_extrude_distance_fixed": "固定 (10 mm)",
"ui.new_extrude_distance_last_used": "上次使用",
"ui.new_extrude_distance_tip": "新建拉伸的起始距离。编辑已有拉伸时保留其自身的值。",
"ui.no_slicer_found": "未找到切片软件",
"ui.normal": "法向",
"ui.normalConstruction": "普通 / 构造",
+16 -1
View File
@@ -463,7 +463,7 @@ impl Dialog {
"sketch.create" => Dialog::new(Kind::Sketch, vec![SelInput::new("Plane", PLANES | PLANAR_FACES, false)]),
"solid.extrude" => Dialog::new(
Kind::Extrude {
distance: "10 mm".into(),
distance: crate::prefs::extrude_start_distance(app),
direction: 0,
operation: usize::from(has_bodies),
auto_op: true,
@@ -3616,6 +3616,7 @@ pub(crate) fn run_dialog(app: &mut SolveApp, d: &Dialog) -> Result<(), String> {
return crate::dialogs_save_mesh::run(app, k, &d.inputs);
}
let commands = apply_commands(app, d)?;
let new_extrude = remembered_extrude_distance(app, d);
if let Some((_, marker)) = d.editing {
// Record the actual pre-edit timeline state in the command's undo snapshot.
roll_marker(app, marker)?;
@@ -3639,9 +3640,23 @@ pub(crate) fn run_dialog(app: &mut SolveApp, d: &Dialog) -> Result<(), String> {
}
}
}
if let Some(distance) = new_extrude {
app.preferences.extrude_last_distance = distance;
}
Ok(())
}
/// The distance a new (not edited) extrude was committed with, in mm, for the "Last used"
/// starting distance; none when the extent isn't a plain distance.
pub(crate) fn remembered_extrude_distance(app: &SolveApp, d: &Dialog) -> Option<String> {
let Kind::Extrude { distance, all, .. } = &d.kind else { return None };
if d.editing.is_some() || *all || to_object_on(&d.inputs) {
return None;
}
let v = app.session.doc.eval(distance, ValueKind::Length).ok()?;
(v.is_finite() && v.abs() > 1e-9).then(|| format!("{v} mm"))
}
/// Placement used to edit a definition in the selected authoring occurrence.
pub(crate) fn feature_frame(s: &Session, component: u64) -> solidcraft_engine::doc::Mat {
if component == s.active_component { s.active_component_frame() } else { s.placed_document().component_transform(component) }
+96
View File
@@ -251,6 +251,22 @@ pub enum OrbitAround {
Cursor,
}
/// Where a new Extrude dialog starts its distance (Preferences: "New extrude distance").
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ExtrudeStart {
/// 10 mm, as it always was.
#[default]
Fixed,
/// The distance of the last extrude that was committed.
LastUsed,
/// The value typed in `extrude_custom_distance`.
Custom,
}
/// The distance the Extrude dialog opened at before this was a preference.
pub const DEFAULT_EXTRUDE_DISTANCE: &str = "10 mm";
/// The browser sidebar's first width, and the width [`Prefs::default`] restores; the panel
/// itself clamps what the user drags to its own range.
pub const DEFAULT_BROWSER_WIDTH: f32 = 250.0;
@@ -336,6 +352,12 @@ pub struct Prefs {
pub smooth_wheel_zoom: bool,
/// Two-finger trackpad scroll pans (off: it zooms like the wheel); pinch always zooms.
pub trackpad_pan: bool,
/// Where a new Extrude dialog starts its distance (editing an extrude keeps its own).
pub extrude_start: ExtrudeStart,
/// The distance a new Extrude starts at when `extrude_start` is `custom` (a length expression).
pub extrude_custom_distance: String,
/// The last distance a new Extrude was committed with, in mm (empty until one is).
pub extrude_last_distance: String,
/// Horizontal trackpad and thumb-wheel scrolling pans the view the other way (#2007).
pub pan_reverse_x: bool,
/// View changes (axis view, Look At, Home) animate; `look_at_speed` scales how fast.
@@ -459,6 +481,9 @@ impl Default for Prefs {
smooth_wheel_zoom: true,
trackpad_pan: true,
pan_reverse_x: false,
extrude_start: ExtrudeStart::Fixed,
extrude_custom_distance: DEFAULT_EXTRUDE_DISTANCE.into(),
extrude_last_distance: String::new(),
look_at_anim: true,
look_at_speed: 1.0,
up_axis: "z".into(),
@@ -757,6 +782,8 @@ fn defaults(app: &mut SolveApp, page: usize) {
PAGE_DESIGN => {
p.up_axis = d.up_axis;
p.offer_stand_up = d.offer_stand_up;
p.extrude_start = d.extrude_start;
p.extrude_custom_distance = d.extrude_custom_distance;
}
PAGE_GRAPHICS => {
let msaa = p.msaa != d.msaa;
@@ -856,6 +883,19 @@ pub fn nav_mode(app: &SolveApp, button: NavButton, mods: Modifiers) -> Option<Na
nav_preset(&app.preferences.nav).mode(button, mods)
}
/// The distance a new Extrude dialog opens with. Anything unusable (no extrude yet, a custom
/// value that isn't a length) falls back to the fixed 10 mm.
pub fn extrude_start_distance(app: &SolveApp) -> String {
let p = &app.preferences;
let wanted = match p.extrude_start {
ExtrudeStart::Fixed => return DEFAULT_EXTRUDE_DISTANCE.into(),
ExtrudeStart::LastUsed => p.extrude_last_distance.as_str(),
ExtrudeStart::Custom => p.extrude_custom_distance.as_str(),
};
let ok = app.session.doc.eval(wanted, solidcraft_engine::doc::expr::Kind::Length).is_ok_and(|v| v.is_finite() && v.abs() > 1e-9);
if ok { wanted.trim().to_string() } else { DEFAULT_EXTRUDE_DISTANCE.into() }
}
/// +1, or −1 when horizontal scrolling pans the other way (#2007).
pub fn pan_sign_x(app: &SolveApp) -> f32 {
if app.preferences.pan_reverse_x { -1.0 } else { 1.0 }
@@ -1365,6 +1405,18 @@ fn page(app: &mut SolveApp, ui: &mut egui::Ui, t: &Tokens) {
});
ui.end_row();
note(ui, crate::i18n::tr_label("Which axis points up in the views (axis view, Home, Look At). Designs are unchanged."));
ui.label(crate::i18n::tr_label("New extrude distance"));
ui.horizontal_wrapped(|ui| {
ui.radio_value(&mut app.preferences.extrude_start, ExtrudeStart::Fixed, crate::i18n::tr_label("Fixed (10 mm)"));
ui.radio_value(&mut app.preferences.extrude_start, ExtrudeStart::LastUsed, crate::i18n::tr_label("Last used"));
ui.radio_value(&mut app.preferences.extrude_start, ExtrudeStart::Custom, crate::i18n::tr_label("Custom"));
ui.add_enabled(
app.preferences.extrude_start == ExtrudeStart::Custom,
egui::TextEdit::singleline(&mut app.preferences.extrude_custom_distance).desired_width(80.0),
);
});
ui.end_row();
note(ui, crate::i18n::tr_label("The distance a new Extrude starts with. Editing an existing extrude keeps its own."));
ui.label(crate::i18n::tr_label("Offer to stand up Y-up files"));
ui.checkbox(&mut app.preferences.offer_stand_up, "")
.on_hover_text(crate::i18n::tr_label("After a STEP or IGES file opens with Z up, offer a quarter turn about X"));
@@ -2154,6 +2206,50 @@ mod tests {
assert_eq!(other.preferences.nav, OPENSCAD_NAV);
}
#[test]
fn extrude_start_distance_has_three_modes_and_defaults_to_ten_mm() {
let mut app = SolveApp::new(solidcraft_engine::Session::default(), Default::default());
assert_eq!(serde_json::from_value::<Prefs>(json!({})).unwrap().extrude_start, ExtrudeStart::Fixed);
let opened = |app: &SolveApp| match crate::dialogs::Dialog::for_command(app, "solid.extrude").unwrap().kind {
crate::dialogs::Kind::Extrude { distance, .. } => distance,
_ => unreachable!(),
};
assert_eq!(opened(&app), "10 mm", "the default is unchanged");
// Fixed ignores whatever was used or typed.
set(&mut app, &json!({"extrude_custom_distance": "25 mm", "extrude_last_distance": "7 mm"})).unwrap();
assert_eq!(opened(&app), "10 mm");
set(&mut app, &json!({"extrude_start": "custom"})).unwrap();
assert_eq!(opened(&app), "25 mm");
set(&mut app, &json!({"extrude_custom_distance": "nonsense"})).unwrap();
assert_eq!(opened(&app), "10 mm", "an unusable custom value falls back");
set(&mut app, &json!({"extrude_start": "last_used", "extrude_last_distance": ""})).unwrap();
assert_eq!(opened(&app), "10 mm", "nothing used yet");
set(&mut app, &json!({"extrude_last_distance": "7 mm"})).unwrap();
assert_eq!(opened(&app), "7 mm");
let mut other = SolveApp::new(solidcraft_engine::Session::default(), Default::default());
other.load_prefs(&app.prefs());
assert_eq!(opened(&other), "7 mm", "persists with the preferences");
}
#[test]
fn last_used_remembers_new_extrudes_only() {
let app = SolveApp::new(solidcraft_engine::Session::default(), Default::default());
let mut d = crate::dialogs::Dialog::for_command(&app, "solid.extrude").unwrap();
if let Some((_, _, text)) = d.primary() {
*text = "12.5 mm".into();
}
assert_eq!(crate::dialogs::remembered_extrude_distance(&app, &d).as_deref(), Some("12.5 mm"));
if let Some((_, _, text)) = d.primary() {
*text = "bogus".into();
}
assert_eq!(crate::dialogs::remembered_extrude_distance(&app, &d), None, "not a length");
if let Some((_, _, text)) = d.primary() {
*text = "3 mm".into();
}
d.editing = Some((1, None));
assert_eq!(crate::dialogs::remembered_extrude_distance(&app, &d), None, "editing an extrude doesn't count");
}
#[test]
fn pan_reverse_x_defaults_off_persists_and_flips_the_sign() {
assert!(!serde_json::from_value::<Prefs>(json!({})).unwrap().pan_reverse_x);
+1 -1
View File
@@ -152,7 +152,7 @@ labels; `prefs:measure_axis_distances` identifies the checkbox.
| `ui.home` | `open?`: bool; `sample?`: index | the start page: show or hide it, open a built-in sample (once built); returns `open` and the recent designs |
| `ui.shortcut` | `command`, `key?`, `replace?` | a command's key, or rebinds it (`""` clears; a key another command uses is refused unless `replace`) |
| `ui.help` | `item?`: `about`, `shortcuts` (read only), `report` (opens a report draft for review), `crashes` (the Crash Reports window) | the version, commit, build date and diagnostic text |
| `ui.prefs` | any of `default_units`, `nav` (`fusion`, `solidworks`, `inventor`, `alias`, `blender`, `tinkercad`, `openscad`, `touchpad`), `zoom_reverse`, `pan_reverse_x` (default false: horizontal scrolling pans the other way), `zoom_to_cursor`, `smooth_wheel_zoom` (default true), `trackpad_pan` (default true: two-finger scroll pans, except Touchpad always zooms; pinch zooms), `orbit_around` (`view_centre` (default: what Pan moves and Fit frames), `origin` (the world origin stays still on screen), `cursor` (the model point under the pointer when the drag began); the older `orbit_cursor` boolean still sets `cursor` when true), `look_at_anim`, `look_at_speed`, `middle_double_click` (`home` (default), `fit`, `off`), `msaa`, `show_frame_rate` (default false: frames per second in the view's bottom-right corner; the view redraws every frame while it is on), `length_decimals`, `angle_decimals`, `theme` (`dark`, `light`, `system` or a theme file's id: a built-in one such as `gruvbox-dark`, or an imported one; the reply's `theme_ids` lists them), `theme_import` (a `solidcraft-theme/1` file's JSON or text: adds the theme and turns it on; the reply's `theme_imported` is its id), `theme_remove` (an imported theme's id), `theme_export: true` (the reply's `theme_file` is the theme on now as a complete file; see [colour themes](book/src/guide/ui-tour.md#colour-themes)), `language` (`en`, `ja`, `de`, `fr`, `ko`, `ms`, `ru`, `uk`, `zh`; see [localization](localization.md)), `ui_scale`, `ui_font` (an installed font family or a font file for the interface; empty for the built-in font), `up_axis` (`z`, `y`), `offer_stand_up`, `color_vision` (`off` (default), `protan`, `deutan`, `tritan`: colour vision modes, #778), `check_updates` (default true; [updates](book/src/guide/updates.md)); `open` (a page: General, Navigation, Keyboard Shortcuts, Design, Graphics, Sketch, Selection, AI) opens the Preferences dialog there; `button`: `ok`, `cancel`, `apply`, `defaults`; `stand_up: true` stands up a just-opened Y-up file, `stand_up: false` leaves it as it is ("Leave it": the offer closes and `offer_stand_up` stays on); the offer's buttons are the handles `stand_up:stand`, `stand_up:leave` and `stand_up:never` | sets preferences (kept between runs), returns them all plus `dialog` (the open page or null), `dark` and `stand_up_offer` |
| `ui.prefs` | any of `default_units`, `nav` (`fusion`, `solidworks`, `inventor`, `alias`, `blender`, `tinkercad`, `openscad`, `touchpad`), `zoom_reverse`, `pan_reverse_x` (default false: horizontal scrolling pans the other way), `zoom_to_cursor`, `smooth_wheel_zoom` (default true), `trackpad_pan` (default true: two-finger scroll pans, except Touchpad always zooms; pinch zooms), `extrude_start` (`fixed` (default: a new Extrude opens at 10 mm), `last_used` (the last committed new extrude's distance), `custom` (`extrude_custom_distance`, default `10 mm`); `extrude_last_distance` holds the remembered one), `orbit_around` (`view_centre` (default: what Pan moves and Fit frames), `origin` (the world origin stays still on screen), `cursor` (the model point under the pointer when the drag began); the older `orbit_cursor` boolean still sets `cursor` when true), `look_at_anim`, `look_at_speed`, `middle_double_click` (`home` (default), `fit`, `off`), `msaa`, `show_frame_rate` (default false: frames per second in the view's bottom-right corner; the view redraws every frame while it is on), `length_decimals`, `angle_decimals`, `theme` (`dark`, `light`, `system` or a theme file's id: a built-in one such as `gruvbox-dark`, or an imported one; the reply's `theme_ids` lists them), `theme_import` (a `solidcraft-theme/1` file's JSON or text: adds the theme and turns it on; the reply's `theme_imported` is its id), `theme_remove` (an imported theme's id), `theme_export: true` (the reply's `theme_file` is the theme on now as a complete file; see [colour themes](book/src/guide/ui-tour.md#colour-themes)), `language` (`en`, `ja`, `de`, `fr`, `ko`, `ms`, `ru`, `uk`, `zh`; see [localization](localization.md)), `ui_scale`, `ui_font` (an installed font family or a font file for the interface; empty for the built-in font), `up_axis` (`z`, `y`), `offer_stand_up`, `color_vision` (`off` (default), `protan`, `deutan`, `tritan`: colour vision modes, #778), `check_updates` (default true; [updates](book/src/guide/updates.md)); `open` (a page: General, Navigation, Keyboard Shortcuts, Design, Graphics, Sketch, Selection, AI) opens the Preferences dialog there; `button`: `ok`, `cancel`, `apply`, `defaults`; `stand_up: true` stands up a just-opened Y-up file, `stand_up: false` leaves it as it is ("Leave it": the offer closes and `offer_stand_up` stays on); the offer's buttons are the handles `stand_up:stand`, `stand_up:leave` and `stand_up:never` | sets preferences (kept between runs), returns them all plus `dialog` (the open page or null), `dark` and `stand_up_offer` |
| `ui.capture` | `action?`: `capture`, `revert`, `cancel` | answers the Capture Position question (asked when a command starts or the design is saved while components were moved without capturing); without `action`, what waits |
| `ui.window` | `action`: minimize, maximize, restore, toggle, close | what the title bar's caption buttons do (on Windows and Linux the application bar is the title bar); close asks about unsaved designs first |
| `ui.agent_cursor` | `show` (bool), `speed`: a number from 1 to 5 (times the normal speed) or `"instant"` (no animation: the command runs at once and the pointer jumps to where it acted), `follow_camera` (bool); all optional | the settings. While the cursor is shown, `engine.execute` first glides to the command's toolbar button and to each pick, then runs and replies; later requests wait their turn. `"animate": false` on a call skips it |