Preferences › Sketch: auto-projection holds across designs (#680) (#959)

Session::replace_design keeps auto_project when file.new, doc.open and doc.recover swap in another design, and the UI keeps it across tabs, adopt and the last-tab close. Regression tests in engine (sketch_project) and ui-egui (documents).
This commit is contained in:
HEOJUNFO authored and GitHub committed 2026-10-13 23:49:51 -04:00
1 parent 4bea4c7785
commit f204042bf2
4 files changed
+68 -6

No files matched your search

+4 -4
View File
@@ -51,7 +51,7 @@ pub static COMMANDS: &[CommandSpec] = &[
fn new_doc(s: &mut Session, p: &Value) -> Result<Value> {
let name = str_(p, "name").unwrap_or("Untitled");
*s = Session::new(Document::new(name));
s.replace_design(Session::new(Document::new(name)));
Ok(json!({"name": name}))
}
@@ -130,7 +130,7 @@ fn open(s: &mut Session, p: &Value) -> Result<Value> {
let mut fresh = Session::new(Document::new(&stem));
let r = if solidcraft_io::is_step_path(path) { add_step(&mut fresh, path, None)? } else { add_mesh(&mut fresh, path, None)? };
fresh.undo.clear();
*s = fresh;
s.replace_design(fresh);
let errors = s.model.results.iter().filter(|r| r.error.is_some()).count();
return Ok(json!({"path": path, "features": s.doc.features.len(), "errors": errors, "bodies": r["bodies"], "warnings": r["warnings"]}));
}
@@ -138,7 +138,7 @@ fn open(s: &mut Session, p: &Value) -> Result<Value> {
if let Some(stem) = default_name_for(&doc.name, path) {
doc.name = stem;
}
*s = Session::new(doc);
s.replace_design(Session::new(doc));
s.path = Some(path.to_string());
let errors = s.model.results.iter().filter(|r| r.error.is_some()).count();
Ok(json!({"path": path, "features": s.doc.features.len(), "errors": errors, "name": s.doc.name}))
@@ -421,7 +421,7 @@ fn recover(s: &mut Session, p: &Value) -> Result<Value> {
if let Some(stem) = entry.path.as_deref().and_then(|path| default_name_for(&doc.name, path)) {
doc.name = stem;
}
*s = Session::new(doc);
s.replace_design(Session::new(doc));
s.path = entry.path.clone();
s.mark_unsaved();
if bool_(p, "discard").unwrap_or(false) {
@@ -148,6 +148,30 @@ fn auto_project_can_be_turned_off() {
assert!(r["projected"].is_null());
}
/// #680: auto-projection is a preference, not part of a design: turning it off holds for the
/// next design started or opened in the session (`doc.recover` replaces the session the same way).
#[test]
fn auto_project_off_holds_for_designs_started_and_opened() {
let dir = std::env::temp_dir().join(format!("solidcraft-auto-project-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let file = dir.join("box.solidcraft");
let mut s = Session::default();
box_part(&mut s);
run(&mut s, "file.save_as", json!({"path": file.to_string_lossy()}));
run(&mut s, "sketch.auto_project", json!({"value": false}));
run(&mut s, "file.new", json!({}));
assert!(!s.auto_project, "file.new");
run(&mut s, "doc.open", json!({"path": file.to_string_lossy()}));
assert!(!s.auto_project, "doc.open");
let r = run(&mut s, "sketch.create", json!({"plane": {"face": [20, 15, 20]}}));
assert!(r["projected"].is_null(), "{r}");
run(&mut s, "sketch.auto_project", json!({"value": true}));
run(&mut s, "file.new", json!({}));
assert!(s.auto_project, "turned back on, it stays on");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn project_edges_bodies_sketches_and_origin() {
let mut s = Session::default();
+8
View File
@@ -328,6 +328,14 @@ impl Session {
}
}
/// Become `fresh` (another design opened, recovered or started), keeping the settings that
/// are the user's rather than the design's: `auto_project` is a preference (#680).
pub fn replace_design(&mut self, fresh: Session) {
let auto_project = self.auto_project;
*self = fresh;
self.auto_project = auto_project;
}
/// The design has changes not in its file: in the document, or in what is hidden.
pub fn is_dirty(&self) -> bool {
(!Arc::ptr_eq(&self.doc, &self.saved) && *self.doc != *self.saved) || self.visibility != self.saved_visibility
+32 -2
View File
@@ -101,6 +101,8 @@ fn swap_in(app: &mut SolveApp, slot: DocSlot) -> DocSlot {
view: take_view(app),
};
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;
// Views cache by revision: the incoming design must not look unchanged.
app.session.invalidate_view(out_rev);
app.ui.tab = if app.session.active_sketch.is_some() { "SKETCH".into() } else { "SOLID".into() };
@@ -153,7 +155,7 @@ pub fn adopt(app: &mut SolveApp, session: Session) {
app.tool = None;
app.menu.props = None;
crate::sketch_attachment_ui::clear(app);
app.session = session;
app.session.replace_design(session);
app.sketch_choices = crate::sketch_families::Choices::default();
app.session.invalidate_view(rev);
app.cam = Camera::default();
@@ -201,7 +203,7 @@ pub fn close(app: &mut SolveApp, i: usize, force: bool) {
let rev = app.session.revision;
crate::dialogs::cancel(app);
app.tool = None;
app.session = Session::default();
app.session.replace_design(Session::default());
app.session.revision = rev + 1;
app.cam = Camera::default();
app.fit_view();
@@ -683,6 +685,34 @@ mod tests {
let _ = std::fs::remove_file(&file);
}
/// #680: "Project face edges into new sketches" is a preference: it holds in new tabs, in
/// the tab switched to, in a design adopted over a blank one and after the last tab closes,
/// and it is what the preferences file keeps.
#[test]
fn auto_project_follows_the_user_across_tabs() {
let mut a = app();
a.run("solid.box", json!({"length": 10, "width": 10, "height": 10})).unwrap();
a.run("sketch.auto_project", json!({"value": false})).unwrap();
new_design(&mut a);
assert_eq!(a.docs.count(), 2);
assert!(!a.session.auto_project, "a new tab");
a.run("solid.box", json!({"length": 5, "width": 5, "height": 5})).unwrap();
a.run("sketch.auto_project", json!({"value": true})).unwrap();
switch(&mut a, 0);
assert!(a.session.auto_project, "the tab switched to");
a.run("sketch.auto_project", json!({"value": false})).unwrap();
switch(&mut a, 1);
assert!(!a.session.auto_project);
let prefs: Value = serde_json::from_str(&a.prefs()).unwrap();
assert_eq!(prefs["auto_project"], false);
close(&mut a, 1, true);
close(&mut a, 0, true);
assert!(active_is_blank(&a) && !a.session.auto_project, "after the last tab closes");
adopt(&mut a, Session::default());
assert_eq!(a.docs.count(), 1);
assert!(!a.session.auto_project, "a design adopted over a blank one");
}
/// A design built elsewhere replaces a blank one and gets a tab otherwise.
#[test]
fn adopt_reuses_a_blank_design() {