M3.88: Show Transparency Grid is per-document engine view state

view.transparencyGrid {on?} -> {on} is now an engine command (DocState.
transparency_grid; not undoable, doesn't dirty the document), so agents can
turn the grid on headless and each open document keeps its own setting. The
menu item, its Show/Hide label, the shortcut and the Properties panel button
use the same id; the canvas draws the grid for the active document only.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
bflatasticandClaude Opus 5.5 committed 2026-10-05 03:11:04 +07:00
1 parent 1603729ec3
commit 799b507564
10 files changed
+103 -8

No files matched your search

+17 -1
View File
@@ -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<CommandSpec> {
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<Value> {
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<Value> {
Ok(json!({ "views": s.doc()?.doc.views }))
}
+5
View File
@@ -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<NodeId>,
/// 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;
@@ -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));
}
+1 -1
View File
@@ -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::<Vec<_>>();
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));
+2
View File
@@ -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};
+3 -3
View File
@@ -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<R
"view.artboards" => 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(),
+2 -1
View File
@@ -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();
}
});
-2
View File
@@ -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,
@@ -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));
}
+4
View File
@@ -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