diff --git a/crates/engine/src/cmd/views.rs b/crates/engine/src/cmd/views.rs index 6a678bb2..65c55f93 100644 --- a/crates/engine/src/cmd/views.rs +++ b/crates/engine/src/cmd/views.rs @@ -1,4 +1,5 @@ -//! View → New View… / Edit Views…: saved views stored in the document. +//! View → New View… / Edit Views…: saved views stored in the document; per-document view +//! toggles (Show Transparency Grid). use serde_json::{Value, json}; use vectorcraft_doc::SavedView; @@ -30,9 +31,24 @@ pub fn specs() -> Vec { edit_views ), cmd!(query "view.saved.list", "Saved Views", [], None, "{} → {views: [{name, center, zoom, rotation}]}", has_doc, list_views), + cmd!( + query "view.transparencyGrid", + "Show Transparency Grid", + ["View"], + Some("Cmd+Shift+D"), + "{on?: bool (default: toggle)} show the transparency grid behind the active document's artboards (each document has its own setting) → {on}", + has_doc, + transparency_grid + ), ] } +fn transparency_grid(s: &mut Session, p: &Value) -> Result { + let st = s.doc_mut()?; + st.transparency_grid = p.get("on").and_then(Value::as_bool).unwrap_or(!st.transparency_grid); + Ok(json!({ "on": st.transparency_grid })) +} + fn list_views(s: &mut Session, _: &Value) -> Result { Ok(json!({ "views": s.doc()?.doc.views })) } diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index 0b04c305..8637b9dc 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -104,6 +104,8 @@ pub struct DocState { /// View Opacity Mask (Alt-click the mask thumbnail): the masked object whose mask the canvas /// shows alone, in greyscale, while that mask is edited (see [`DocState::shown_mask`]). pub mask_view: Option, + /// View → Show Transparency Grid, per document (view state: not saved, not undoable). + pub transparency_grid: bool, } static NEXT_DOC_UID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1); @@ -126,6 +128,7 @@ impl DocState { last_selection_cmd: None, uid: NEXT_DOC_UID.fetch_add(1, std::sync::atomic::Ordering::Relaxed), mask_view: None, + transparency_grid: false, } } /// Unsaved changes: the document differs from the saved one (selection changes don't count). @@ -884,6 +887,8 @@ mod tests_tileedge; #[cfg(test)] mod tests_tints; #[cfg(test)] +mod tests_transparencygrid; +#[cfg(test)] mod tests_widthprofiles; #[cfg(test)] mod tests_xform; diff --git a/crates/engine/src/tests_transparencygrid.rs b/crates/engine/src/tests_transparencygrid.rs new file mode 100644 index 00000000..80e10491 --- /dev/null +++ b/crates/engine/src/tests_transparencygrid.rs @@ -0,0 +1,28 @@ +//! View → Show Transparency Grid is engine view state of each open document (M3.88). + +use serde_json::json; + +use super::*; + +#[test] +fn transparency_grid_is_per_document_view_state() { + let mut s = Session::new(); + s.execute("file.new", &json!({})).unwrap(); + let grid = |s: &Session| s.doc().unwrap().transparency_grid; + assert!(!grid(&s)); + assert_eq!(s.execute("view.transparencyGrid", &json!({})).unwrap(), json!({"on": true})); + assert!(grid(&s)); + // Not an edit: nothing to undo, the document stays clean, the journal skips it. + let st = s.doc().unwrap(); + assert!(st.history.undo.is_empty() && !st.is_dirty()); + // Another document has its own setting. + s.execute("file.new", &json!({})).unwrap(); + assert!(!grid(&s)); + assert_eq!(s.execute("view.transparencyGrid", &json!({"on": false})).unwrap(), json!({"on": false})); + s.execute("document.activate", &json!({"index": 0})).unwrap(); + assert!(grid(&s)); + assert_eq!(s.execute("view.transparencyGrid", &json!({})).unwrap(), json!({"on": false})); + assert_eq!(s.execute("view.transparencyGrid", &json!({"on": true})).unwrap(), json!({"on": true})); + s.execute("document.activate", &json!({"index": 1})).unwrap(); + assert!(!grid(&s)); +} diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 72f39d96..4c257f80 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -157,7 +157,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let shift = |d: f32| q.iter().map(|p| *p + vec2(d, d)).collect::>(); painter.add(Shape::convex_polygon(shift(2.0), Color32::from_black_alpha(26), Stroke::NONE)); painter.add(Shape::convex_polygon(shift(1.0), Color32::from_black_alpha(52), Stroke::NONE)); - if app.ui.view.transparency_grid && xf.rot == 0.0 { + if st.transparency_grid && xf.rot == 0.0 { checker(&painter, r); } else { painter.add(Shape::convex_polygon(q, Color32::WHITE, Stroke::NONE)); diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index d0d46a11..631b43ae 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -38,6 +38,8 @@ mod tests_labels; mod tests_overprint; #[cfg(test)] mod tests_paintchips; +#[cfg(test)] +mod tests_transparencygrid; use std::sync::mpsc::{Receiver, Sender}; use std::sync::{Arc, Mutex}; diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 3cea890b..12967fdb 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -125,7 +125,6 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[ ("view.artboards", "Hide Artboards", "Cmd+Shift+H", "{}"), ("view.rulers", "Show Rulers", "Cmd+R", "{}"), ("view.boundingBox", "Hide Bounding Box", "Cmd+Shift+B", "{}"), - ("view.transparencyGrid", "Show Transparency Grid", "Cmd+Shift+D", "{}"), ("view.guides", "Hide Guides", "Cmd+;", "{}"), ("view.smartGuides", "Smart Guides", "Cmd+U", "{}"), ("view.grid", "Show Grid", "Cmd+'", "{}"), @@ -470,7 +469,6 @@ pub fn run_ui_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option flag(&mut app.ui.view.artboards), "view.rulers" => flag(&mut app.ui.view.rulers), "view.boundingBox" => flag(&mut app.ui.view.bounding_box), - "view.transparencyGrid" => flag(&mut app.ui.view.transparency_grid), "view.guides" => flag(&mut app.ui.view.guides), "view.smartGuides" => flag(&mut app.ui.view.smart_guides), "view.grid" => flag(&mut app.ui.view.grid), @@ -756,7 +754,9 @@ pub fn dynamic_label(app: &VectorcraftApp, id: &str, label: &str) -> String { "view.artboards" => if v.artboards { "Hide Artboards" } else { "Show Artboards" }.into(), "view.rulers" => if v.rulers { "Hide Rulers" } else { "Show Rulers" }.into(), "view.boundingBox" => if v.bounding_box { "Hide Bounding Box" } else { "Show Bounding Box" }.into(), - "view.transparencyGrid" => if v.transparency_grid { "Hide Transparency Grid" } else { "Show Transparency Grid" }.into(), + "view.transparencyGrid" => { + if app.session.active().is_some_and(|d| d.transparency_grid) { "Hide Transparency Grid" } else { "Show Transparency Grid" }.into() + } "view.guides" => if v.guides { "Hide Guides" } else { "Show Guides" }.into(), "view.grid" => if v.grid { "Hide Grid" } else { "Show Grid" }.into(), "view.guides.lock" => if app.session.guides_locked() { "Unlock Guides" } else { "Lock Guides" }.into(), diff --git a/crates/ui-egui/src/panels/properties.rs b/crates/ui-egui/src/panels/properties.rs index 5d9b9959..f881815f 100644 --- a/crates/ui-egui/src/panels/properties.rs +++ b/crates/ui-egui/src/panels/properties.rs @@ -154,7 +154,8 @@ fn document_sections(app: &mut VectorcraftApp, ui: &mut Ui) { if widgets::icon_button(ui, "grid-3x3", "Show Grid (⌘')", v.grid, 26.0).clicked() { app.run("view.grid", json!({})).ok(); } - if widgets::icon_button(ui, "square-dashed", "Show Transparency Grid", v.transparency_grid, 26.0).clicked() { + let transparency_grid = app.session.active().is_some_and(|d| d.transparency_grid); + if widgets::icon_button(ui, "square-dashed", "Show Transparency Grid", transparency_grid, 26.0).clicked() { app.run("view.transparencyGrid", json!({})).ok(); } }); diff --git a/crates/ui-egui/src/state.rs b/crates/ui-egui/src/state.rs index a16b4111..b76b9d52 100644 --- a/crates/ui-egui/src/state.rs +++ b/crates/ui-egui/src/state.rs @@ -115,7 +115,6 @@ pub struct ViewFlags { pub snap_to_grid: bool, pub snap_to_point: bool, pub artboards: bool, - pub transparency_grid: bool, pub edges: bool, /// View → Trim View: hide everything outside the artboards. pub trim_view: bool, @@ -144,7 +143,6 @@ impl Default for ViewFlags { snap_to_grid: false, snap_to_point: true, artboards: true, - transparency_grid: false, edges: true, trim_view: false, corner_widgets: true, diff --git a/crates/ui-egui/src/tests_transparencygrid.rs b/crates/ui-egui/src/tests_transparencygrid.rs new file mode 100644 index 00000000..2307429e --- /dev/null +++ b/crates/ui-egui/src/tests_transparencygrid.rs @@ -0,0 +1,41 @@ +//! The canvas draws the transparency grid behind the artboards of the documents that show it +//! (M3.88), and the View menu item follows the active document. + +use serde_json::json; +use vectorcraft_engine::Session; + +use crate::{VectorcraftApp, canvas, menus}; + +/// Whether one headless canvas frame paints the grid's grey squares. +fn draws_grid(app: &mut VectorcraftApp) -> bool { + fn grey(s: &egui::Shape) -> bool { + match s { + egui::Shape::Rect(r) => r.fill == egui::Color32::from_gray(204), + egui::Shape::Vec(v) => v.iter().any(grey), + _ => false, + } + } + let ctx = egui::Context::default(); + let raw = egui::RawInput { screen_rect: Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(800.0, 600.0))), ..Default::default() }; + let mut out = ctx.run_ui(raw, |ui| canvas::show(app, ui)); + out.textures_delta.clear(); + out.shapes.iter().any(|c| grey(&c.shape)) +} + +#[test] +fn the_canvas_shows_the_grid_only_for_documents_that_turn_it_on() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 200, "height": 200})).unwrap(); + app.run("file.new", json!({"width": 200, "height": 200})).unwrap(); + assert!(!draws_grid(&mut app)); + assert_eq!(menus::dynamic_label(&app, "view.transparencyGrid", ""), "Show Transparency Grid"); + // The menu's id reaches the engine command. + assert_eq!(app.run("view.transparencyGrid", json!({})).unwrap(), json!({"on": true})); + assert!(draws_grid(&mut app)); + assert_eq!(menus::dynamic_label(&app, "view.transparencyGrid", ""), "Hide Transparency Grid"); + app.run("document.activate", json!({"index": 0})).unwrap(); + assert!(!draws_grid(&mut app), "the other document keeps its own setting"); + assert_eq!(menus::dynamic_label(&app, "view.transparencyGrid", ""), "Show Transparency Grid"); + app.run("document.activate", json!({"index": 1})).unwrap(); + assert!(draws_grid(&mut app)); +} diff --git a/docs/mcp.md b/docs/mcp.md index 7247d212..dc9b9830 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -230,6 +230,10 @@ then composite against the art below it). SVG has no non-isolated groups, so the mask is edited; called again it shows the artwork, still editing. It is view state of the open document: the canvas (and `ui.screenshot`) shows it, exports never do, and it ends when mask editing ends. +`view.transparencyGrid {on?}` (View → Show Transparency Grid; default: toggle) is view state of each open document, +like the mask view: the grid shows behind the artboards of the documents that turn it on, on the canvas and in +`ui.screenshot`, never in exports. It returns `{on}`, isn't undoable and doesn't mark the document changed. + ## Clipping masks `object.clippingMask.make` clips the selected objects by the topmost one, which may be a path, a compound path or a