Merge main into feat/script-run

This commit is contained in:
dotCooCoo
2026-11-09 20:30:53 -08:00
64 changed files with 1678 additions and 440 deletions
+23 -16
View File
@@ -183,7 +183,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Lock Guides",
["View", "Guides"],
Some("Cmd+Alt+;"),
"{locked?: bool} toggle (or set) the session's guide lock; locking deselects the ruler guides and guide paths in every open document → {locked}",
"{locked?: bool} toggle (or set) the session's guide lock, leaving the lockGuidesByDefault preference as it is; locking deselects the ruler guides and guide paths in every open document; guide.add and a guide dragged out of a ruler still make new guides while locked → {locked}",
has_doc,
lock_guides
),
@@ -277,6 +277,27 @@ impl Session {
pub fn guides_locked(&self) -> bool {
self.menu.guides_locked
}
/// Set View → Guides → Lock Guides. Locked guides can't stay selected, ruler guides or guide
/// paths, in any open document ([`Session::select`] lets those go in the active one).
pub(crate) fn set_guides_locked(&mut self, locked: bool) {
self.menu.guides_locked = locked;
if !locked {
return;
}
let guides_selected =
self.doc().is_ok_and(|st| !st.selection.guides.is_empty() || st.selection.objects.iter().any(|id| crate::is_guide_path(&st.doc, *id)));
// It fails only without a document, and then no guide is selected.
if guides_selected {
let _ = self.select(|_, sel| sel.guides.clear());
}
let active = self.active;
for (i, st) in self.docs.iter_mut().enumerate() {
if Some(i) != active && crate::deselect_hidden(&self.menu, &st.doc, &mut st.selection) {
st.revision += 1;
}
}
}
}
// ---------- Select ----------
@@ -717,21 +738,7 @@ fn release_guides(s: &mut Session, p: &Value) -> Result<Value> {
fn lock_guides(s: &mut Session, p: &Value) -> Result<Value> {
let v = switch_or(p, "locked", !s.menu.guides_locked, "view.guides.lock")?;
s.menu.guides_locked = v;
// Locked guides can't stay selected, ruler guides or guide paths, in any open document
// ([`Session::select`] lets those go in the active one).
let st = s.doc()?;
if v && (!st.selection.guides.is_empty() || st.selection.objects.iter().any(|id| crate::is_guide_path(&st.doc, *id))) {
s.select(|_, sel| sel.guides.clear())?;
}
if v {
let active = s.active;
for (i, st) in s.docs.iter_mut().enumerate() {
if Some(i) != active && crate::deselect_hidden(&s.menu, &st.doc, &mut st.selection) {
st.revision += 1;
}
}
}
s.set_guides_locked(v);
Ok(json!({ "locked": v }))
}
+16
View File
@@ -285,6 +285,22 @@ fn revolve_on_a_fill_is_checked_in_that_fills_paint() {
assert!(err.contains("simplify the gradient"), "{err}");
}
/// A hidden Revolve still counts as the object's one Revolve: a second one is refused, and the
/// hidden one can be shown again over the geometry effects after it.
#[test]
fn a_second_revolve_is_refused_over_a_hidden_one() {
let (mut s, id) = session_with_rect();
s.execute("effect.apply", &json!({"effect": "threeD.revolve", "ids": [id.0], "item": null})).unwrap();
s.execute("effect.setParams", &json!({"index": 0, "ids": [id.0], "item": null, "visible": false})).unwrap();
s.execute("effect.apply", &json!({"effect": "distort.roughen", "ids": [id.0], "item": null})).unwrap();
let err = s.execute("effect.apply", &json!({"effect": "threeD.revolve", "ids": [id.0], "item": null})).unwrap_err().to_string();
assert!(err.contains("one Revolve effect"), "{err}");
let ids: Vec<_> = node(&s, id).appearance.effects.iter().map(|e| e.id.clone()).collect();
assert_eq!(ids, ["threeD.revolve", "distort.roughen"]);
s.execute("effect.setParams", &json!({"index": 0, "ids": [id.0], "item": null, "visible": true})).unwrap();
assert!(node(&s, id).appearance.effects[0].visible);
}
/// Revolve on a group sweeps every member about one axis on the left edge of their bounds, keeps
/// each member's paint, follows an edited member, expands and undoes as one step.
#[test]
+7
View File
@@ -228,6 +228,7 @@ pub const PREF_SPECS: &[PrefSpec] = &[
// Guides & Grid
p!("guideColor", "Guides & Grid", "Guides", "Color", color),
p!("guideStyle", "Guides & Grid", "Guides", "Style", choice(LINE_STYLE)),
p!("lockGuidesByDefault", "Guides & Grid", "Guides", "Lock Guides by Default", bool),
p!("gridColor", "Guides & Grid", "Grid", "Color", color),
p!("gridStyle", "Guides & Grid", "Grid", "Style", choice(LINE_STYLE)),
p!("gridlineEvery", "Guides & Grid", "Grid", "Gridline every", len(1.0, 16383.0, General)),
@@ -691,10 +692,16 @@ impl Session {
let fonts_folder_changed = p.fonts_folder != self.prefs.fonts_folder;
let hyphen_exceptions_changed = p.hyphenation_exceptions != self.prefs.hyphenation_exceptions;
let corner_radius_changed = p.corner_radius != self.prefs.corner_radius;
let lock_guides_changed = p.lock_guides_by_default != self.prefs.lock_guides_by_default;
if p.trace_presets != self.prefs.trace_presets {
self.trace_preset_changes.replaced();
}
self.prefs = p;
// Guides & Grid › Lock Guides by Default sets the lock when it changes, the saved value at
// start included. Otherwise the lock stays as View › Guides › Lock Guides left it.
if lock_guides_changed {
self.set_guides_locked(self.prefs.lock_guides_by_default);
}
if corner_radius_changed {
// General › Corner Radius is the Rounded Rectangle tool's radius (#394): a tool already
// made (active, or lent a selection tool for a drag) takes the new value.