Merge pull request #1314 from dotCooCoo/feat/grid-prefs-new-documents

Part of #394: new and imported documents use the Gridline every and Subdivisions preferences
This commit is contained in:
BFlat 2026-10-12 08:15:57 +07:00 committed by GitHub
commit f714a3f9ff
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 97 additions and 7 deletions

View File

@ -9,7 +9,7 @@ use vectorcraft_geom::Affine;
use super::super::*;
use super::pdfimport::LoadOptions;
use super::{Format, SAVE_FORMATS, absolute_path, file_stamp, format, format_for_name, psdread, read_file};
use crate::EngineError;
use crate::{EngineError, Prefs};
/// A file read into a document, with its format and non-fatal import notes.
pub struct Loaded {
@ -26,6 +26,18 @@ pub struct Loaded {
pub preview_only: bool,
}
impl Loaded {
/// The file with the grid of Preferences › Guides & Grid, unless it is a native document (also
/// one that an SVG, PDF, `.ai` or EPS file carries, `restored`), which keeps the grid it was
/// saved with.
pub(crate) fn with_grid_prefs(mut self, prefs: &Prefs) -> Self {
if !self.restored && !matches!(self.format.id, "vectorcraft" | "template") {
self.doc.grid = prefs.grid();
}
self
}
}
/// An image ready to embed: PNG/JPEG/GIF/WebP keep their bytes, other formats are stored as PNG
/// (what browsers, PDF and SVG viewers show).
pub struct RasterImage {
@ -289,7 +301,7 @@ pub fn open_template(s: &mut Session, name: &str, bytes: &[u8], path: Option<&st
/// Make a loaded file the new active document; `opts` are the options it was read with.
fn open_loaded(s: &mut Session, loaded: Loaded, path: Option<String>, opts: &LoadOptions, as_template: bool) -> Result<Value> {
let Loaded { mut doc, format, warnings, restored, converted, .. } = loaded;
let Loaded { mut doc, format, warnings, restored, converted, .. } = loaded.with_grid_prefs(&s.prefs);
// What reading the file left out of the document, if anything.
let losses: Vec<String> = warnings.iter().filter(|w| is_loss(w)).cloned().collect();
let source_path = path.clone().filter(|_| !losses.is_empty());

View File

@ -563,7 +563,7 @@ fn can_revert(s: &Session) -> std::result::Result<(), String> {
fn revert(s: &mut Session, _: &Value) -> Result<Value> {
let index = s.active_index().ok_or(crate::EngineError::NoDocument)?;
let path = s.doc()?.path.clone().ok_or_else(|| bad("file.revert", "the document has never been saved"))?;
let Loaded { mut doc, .. } = load(&path, &read_file(&path)?)?;
let Loaded { mut doc, .. } = load(&path, &read_file(&path)?)?.with_grid_prefs(&s.prefs);
doc.template = false;
// Back to the saved file: its recovery copy has nothing left to recover.
let uid = s.doc()?.uid;

View File

@ -565,6 +565,7 @@ pub(crate) fn file_new(s: &mut Session, p: &Value) -> Result<Value> {
settings.name = "Custom".into();
}
let mut d = settings.document();
d.grid = s.prefs.grid();
d.title = match str_param(p, "name").or(str_param(p, "title")).map(str::trim).filter(|t| !t.is_empty()) {
// New Document offers the next untitled name: taking it uses it up.
Some(t) if t == s.peek_untitled() => s.next_untitled(),

View File

@ -8,7 +8,7 @@ use serde_json::{Map, Value, json};
use super::*;
use crate::Prefs;
use crate::units::Measure;
use vectorcraft_doc::Unit;
use vectorcraft_doc::{GridPrefs, Unit};
/// What kind of value a preference holds.
#[derive(Clone, Copy, Debug)]
@ -503,6 +503,11 @@ impl Prefs {
*self = p;
}
}
/// The document grid Guides & Grid › Gridline every and Subdivisions set.
pub fn grid(&self) -> GridPrefs {
GridPrefs { spacing: self.gridline_every, subdivisions: self.grid_subdivisions }
}
}
impl Session {
@ -547,8 +552,7 @@ impl Session {
}
if grid_changed {
let d = std::sync::Arc::make_mut(&mut st.doc);
d.grid.spacing = self.prefs.gridline_every;
d.grid.subdivisions = self.prefs.grid_subdivisions;
d.grid = self.prefs.grid();
st.revision += 1;
}
// Hyphenation exceptions change every text object's line breaks.

View File

@ -81,6 +81,73 @@ fn grid_prefs_apply_to_open_documents() {
assert_eq!((g.spacing, g.subdivisions), (36.0, 4));
}
/// Guides & Grid › Gridline every and Subdivisions (#394): set with no document open, as the
/// desktop app restores them at launch, they give `file.new`'s document their grid.
#[test]
fn grid_prefs_apply_to_new_documents() {
let mut s = Session::new();
s.execute("prefs.set", &json!({"values": {"gridlineEvery": 36, "gridSubdivisions": 4}})).unwrap();
s.execute("file.new", &json!({"width": 400, "height": 400})).unwrap();
let g = &s.doc().unwrap().doc.grid;
assert_eq!((g.spacing, g.subdivisions), (36.0, 4));
}
/// An SVG file with no grid in it.
const GRIDLESS_SVG: &str = r#"<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"><rect width="10" height="10"/></svg>"#;
/// Gridline every and Subdivisions (#394): an SVG or a plain PDF opens with their grid, while a
/// native file, and the native document a PDF saved with Preserve Editing carries, keep the grid
/// they were saved with.
#[test]
fn grid_prefs_apply_to_opened_files_without_a_grid() {
let mut s = Session::new();
s.execute("file.new", &json!({"width": 400, "height": 400})).unwrap();
s.execute("prefs.set", &json!({"values": {"gridlineEvery": 18, "gridSubdivisions": 3}})).unwrap();
let native = vectorcraft_format::save(&s.doc().unwrap().doc, false);
let mut pdf = |editing: bool| {
let v = s.execute("document.exportPdf", &json!({"preserveEditing": editing})).unwrap();
vectorcraft_format::base64_decode(v["dataBase64"].as_str().unwrap()).unwrap()
};
let (editable, plain) = (pdf(true), pdf(false));
s.execute("prefs.set", &json!({"values": {"gridlineEvery": 36, "gridSubdivisions": 4}})).unwrap();
let mut open = |name: &str, bytes: &[u8]| {
let r = s.execute("document.open", &json!({"name": name, "dataBase64": vectorcraft_format::base64_encode(bytes)})).unwrap();
let g = &s.doc().unwrap().doc.grid;
(g.spacing, g.subdivisions, r["restored"].as_bool().unwrap())
};
assert_eq!(open("drawing.svg", GRIDLESS_SVG.as_bytes()), (36.0, 4, false));
assert_eq!(open("plain.pdf", &plain), (36.0, 4, false));
assert_eq!(open("saved.vectorcraft", &native), (18.0, 3, false));
assert_eq!(open("editable.pdf", &editable), (18.0, 3, true));
}
/// Gridline every and Subdivisions (#394): File › Revert reads the file again, and an SVG gets
/// their grid while a native file gets the grid it was saved with.
#[test]
fn grid_prefs_apply_to_reverted_files_without_a_grid() {
let dir = std::env::temp_dir().join(format!("vc-grid-revert-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = |name: &str| dir.join(name).to_string_lossy().to_string();
let (svg, native) = (path("drawing.svg"), path("saved.vectorcraft"));
std::fs::write(&svg, GRIDLESS_SVG).unwrap();
let mut s = Session::new();
s.execute("file.new", &json!({"width": 400, "height": 400})).unwrap();
s.execute("prefs.set", &json!({"values": {"gridlineEvery": 18, "gridSubdivisions": 3}})).unwrap();
s.execute("document.save", &json!({"path": native})).unwrap();
// The preference changes while the file is open, which sets the open documents' grid.
let mut reverted = |file: &str, every: f64| {
s.execute("document.open", &json!({"path": file})).unwrap();
s.execute("prefs.set", &json!({"values": {"gridlineEvery": every, "gridSubdivisions": 4}})).unwrap();
s.execute("shape.rectangle", &json!({"x": 20, "y": 20, "width": 10, "height": 10})).unwrap();
s.execute("file.revert", &json!({})).unwrap();
let g = &s.doc().unwrap().doc.grid;
(g.spacing, g.subdivisions)
};
assert_eq!(reverted(&svg, 36.0), (36.0, 4));
assert_eq!(reverted(&native, 24.0), (18.0, 3));
let _ = std::fs::remove_dir_all(dir);
}
#[test]
fn history_states_limit_undo_depth() {
let mut s = Session::new();

View File

@ -1593,6 +1593,12 @@ default: on, a click away from a panel popped out of the icon column puts it awa
default: View › Actual Size / `view.actualSize` is one document point per screen point; on, one document inch fills
96 screen points, an inch at the system's reference density whatever the display scaling).
`gridlineEvery` (72 pt by default) and `gridSubdivisions` (8 by default) set the document grid that View › Show Grid
draws, and View › Snap to Grid snaps to its subdivisions. Changing either sets the grid of every open document.
`file.new` gives a new document that grid, and `document.open`, `file.newFromTemplate` and `file.revert` give it to
every document they read except a native file and the native document that an SVG, PDF, `.ai` or EPS file carries
(`restored: true`), which keep the grid they were saved with.
The Smart Guides preferences (Preferences › Smart Guides) apply to `pointer_gesture` with Smart Guides on (the
default view) and to the mouse; they change what the tools show and how far a target pulls, and only Construction
Guides change where a snapped point lands:

View File

@ -405,7 +405,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
- Native macOS save sheets offer a localised Format menu (#866), built from the request's existing format filters. It selects the suggested filename's format and updates the suffix when changed, including legacy `.drawcraft` names. `apps/vectorcraft/src/mac_save_format.rs` has filename regressions; an actual native SVG save, selection of PDF and cancellation without changing the saved document were verified.
- Save / Don't Save / Cancel before closing or quitting with unsaved documents (tabs, Close, Close All, Quit, window close).
- Four brightness themes, persistent preferences.
- Preferences that act (#394, in part): Show Tool Tips, Double Click To Isolate, Zoom with Mouse Wheel (the wheel zooms about the pointer, Shift-wheel scrolls up and down, Cmd/Ctrl-wheel sideways; Alt/Option-wheel and a trackpad pinch zoom about the pointer either way), Use Precise Cursors; Selection & Anchor Display › Tolerance (objects, anchors and handles), Object Selection by Path Only, Command Click to Select Objects Behind (cycling down the stack), Zoom to Selection, anchor Size, Highlight anchors on mouse over; Type › Number of Recent Fonts (up to 15); Guides & Grid › guide and grid Color and Style (lines or dots) and Grids In Back. Then Snap to Point (its distance, for moves, drawn points and reference points), Move Locked and Hidden Artwork with Artboard, Enable Rubber Band for the Pen and Curvature tools, Handles style, Show handles when multiple anchors are selected, Hide Corner Widget for angles greater than; General › Transform Pattern Tiles (the transforms' and dialogs' Transform Patterns, Align and Distribute, and art moved or duplicated with an artboard) and Select Same Tint %; Type › Size/Leading, Tracking and Baseline Shift increments (`type.step`, Cmd+Shift+. and Cmd+Shift+, for the size, the Type tool's Alt+arrows), Enable Missing Glyph Protection and Fill New Type Objects With Placeholder Text. Then Smart Guides › Color, Alignment Guides, Anchor/Path Labels, Measurement Labels, Transform Tools, Object Highlighting (with Smart Guides on) and Snapping Tolerance (4 px by default, for every smart guide snap), then Construction Guides and their Angles (#506: a drawn point near a line at one of those angles through the point it leaves from lands on it). Then Type › Type Object Selection by Path Only (type is picked on the baseline of any of its lines, area type's frame or the path of type on a path, no longer anywhere in its bounds; the Type tools and the Eyedropper still take a click among the characters). Then General › Anti-aliased Artwork (off, the canvas draws the art with hard edges). Then Guides & Grid › Show Pixel Grid (Above 600% Zoom), a line at every document pixel over the art in Pixel Preview, turning with a rotated view as the document grid now does too. Then General › Show The Home Screen When No Documents Are Open (off, closing the last document leaves an empty window; the Home button still opens the screen). Then Units › Numbers Without Units Are Points (on by default: a number typed with no unit into a field in picas is read in points, a typed unit still wins; dimmed unless a unit is Picas). Then User Interface › Auto-Collapse Iconic Panels (off by default: on, a click away from a panel popped out of the icon column puts it away). Then Performance › Animated Zoom (on, with GPU Performance, which it needs and which dims it when off: the Zoom tool dragged sideways zooms about the press, right in and left out, and held still zooms on, Alt out; off, a drag zooms to the area dragged across, now drawn as a marquee), User Interface › Large Tabs (taller document tabs) and Scale Cursor Proportional to UI (off, the tool cursors keep their size whatever the UI Scaling), and File Handling › Display Bitmaps as Anti-aliased Images in Pixel Preview (off by default: Pixel Preview draws images with each pixel taking its nearest image pixel). Type › Auto Size New Area Type works too (new area type gets Auto Size). They reach agents too (`prefs.set`, then `pointer_gesture` or `press_key`; `ui.wheel` on the control channel). Then Smart Guides › Spacing Guides (moves space the selection evenly with its row or column, see Drawing aids). Then Hyphenation › Exceptions (comma-separated words that never break, or break only at their hyphens; `prefs.set` and restore push them into the hyphenator, #394). Then Type › Show Font Names in English (off, font menus label a family by its native-language name when the font has one; documents still use the English family, #394). Then General › Display Print Size at 100% Zoom (off by default: View › Actual Size is one document point per screen point; on, one document inch fills 96 screen points, an inch at the reference density display scaling is set against; `view.actualSize` / `prefs.set`, #394). About 17 stored options still do nothing yet (the rest of #394: Highlight Alternate Glyphs, Identify Objects By, East Asian Type units, Open Documents As Tabs, Real-time Drawing and Editing, GPU Performance beyond Animated Zoom, hyphenation language, scratch disks, the legacy clipboard, Appearance of Black, Touch Workspace).
- Preferences that act (#394, in part): Show Tool Tips, Double Click To Isolate, Zoom with Mouse Wheel (the wheel zooms about the pointer, Shift-wheel scrolls up and down, Cmd/Ctrl-wheel sideways; Alt/Option-wheel and a trackpad pinch zoom about the pointer either way), Use Precise Cursors; Selection & Anchor Display › Tolerance (objects, anchors and handles), Object Selection by Path Only, Command Click to Select Objects Behind (cycling down the stack), Zoom to Selection, anchor Size, Highlight anchors on mouse over; Type › Number of Recent Fonts (up to 15); Guides & Grid › guide and grid Color and Style (lines or dots) and Grids In Back. Then Snap to Point (its distance, for moves, drawn points and reference points), Move Locked and Hidden Artwork with Artboard, Enable Rubber Band for the Pen and Curvature tools, Handles style, Show handles when multiple anchors are selected, Hide Corner Widget for angles greater than; General › Transform Pattern Tiles (the transforms' and dialogs' Transform Patterns, Align and Distribute, and art moved or duplicated with an artboard) and Select Same Tint %; Type › Size/Leading, Tracking and Baseline Shift increments (`type.step`, Cmd+Shift+. and Cmd+Shift+, for the size, the Type tool's Alt+arrows), Enable Missing Glyph Protection and Fill New Type Objects With Placeholder Text. Then Smart Guides › Color, Alignment Guides, Anchor/Path Labels, Measurement Labels, Transform Tools, Object Highlighting (with Smart Guides on) and Snapping Tolerance (4 px by default, for every smart guide snap), then Construction Guides and their Angles (#506: a drawn point near a line at one of those angles through the point it leaves from lands on it). Then Type › Type Object Selection by Path Only (type is picked on the baseline of any of its lines, area type's frame or the path of type on a path, no longer anywhere in its bounds; the Type tools and the Eyedropper still take a click among the characters). Then General › Anti-aliased Artwork (off, the canvas draws the art with hard edges). Then Guides & Grid › Show Pixel Grid (Above 600% Zoom), a line at every document pixel over the art in Pixel Preview, turning with a rotated view as the document grid now does too. Then General › Show The Home Screen When No Documents Are Open (off, closing the last document leaves an empty window; the Home button still opens the screen). Then Units › Numbers Without Units Are Points (on by default: a number typed with no unit into a field in picas is read in points, a typed unit still wins; dimmed unless a unit is Picas). Then User Interface › Auto-Collapse Iconic Panels (off by default: on, a click away from a panel popped out of the icon column puts it away). Then Performance › Animated Zoom (on, with GPU Performance, which it needs and which dims it when off: the Zoom tool dragged sideways zooms about the press, right in and left out, and held still zooms on, Alt out; off, a drag zooms to the area dragged across, now drawn as a marquee), User Interface › Large Tabs (taller document tabs) and Scale Cursor Proportional to UI (off, the tool cursors keep their size whatever the UI Scaling), and File Handling › Display Bitmaps as Anti-aliased Images in Pixel Preview (off by default: Pixel Preview draws images with each pixel taking its nearest image pixel). Type › Auto Size New Area Type works too (new area type gets Auto Size). They reach agents too (`prefs.set`, then `pointer_gesture` or `press_key`; `ui.wheel` on the control channel). Then Smart Guides › Spacing Guides (moves space the selection evenly with its row or column, see Drawing aids). Then Hyphenation › Exceptions (comma-separated words that never break, or break only at their hyphens; `prefs.set` and restore push them into the hyphenator, #394). Then Type › Show Font Names in English (off, font menus label a family by its native-language name when the font has one; documents still use the English family, #394). Then General › Display Print Size at 100% Zoom (off by default: View › Actual Size is one document point per screen point; on, one document inch fills 96 screen points, an inch at the reference density display scaling is set against; `view.actualSize` / `prefs.set`, #394). Then Guides & Grid › Gridline every and Subdivisions (changing either sets the grid of every open document; new and opened documents take that grid too, except a native file and the native document an SVG, PDF, `.ai` or EPS file carries, which open with the grid they were saved with; `file.new`, `document.open`, `file.newFromTemplate`, `file.revert`, #394). About 17 stored options still do nothing yet (the rest of #394: Highlight Alternate Glyphs, Identify Objects By, East Asian Type units, Open Documents As Tabs, Real-time Drawing and Editing, GPU Performance beyond Animated Zoom, hyphenation language, scratch disks, the legacy clipboard, Appearance of Black, Touch Workspace).
- Dialogs move by dragging their heading (the band down from the top edge, as a title bar), stay on screen and keep where they were moved for the session; the form dialogs' choices are dropdowns (Offset Path's Joins, Average's and Shear's and Reflect's axis), and Offset Path lists Offset, Joins, Miter Limit and previews (#423).
- Dialogs open with their first field focused and its value selected: type a value and press Enter (Move, Rotate, Scale…). Number fields in panels, the Control bar and dialogs (preferences too) step with Up/Down, or with the mouse wheel over the focused field (#485: a notch a step; the panel under it then stays put, and an unfocused field leaves the wheel to scrolling) (Shift: ten, Ctrl/Cmd: a tenth of their unit), applied at once.
- Scrubby labels (#400, beyond Illustrator, as in Photoshop): dragging a numeric field's label left or right (the ↔ cursor shows over it) steps the value one unit every 2 points (Shift: ten, Ctrl/Cmd: a tenth; counts stay whole), in the field's unit and within its limits, applied live as the art follows; the whole drag is one undo step and Escape puts the value back. A click on the label focuses the field; the text box itself never scrubs, and a field with no label doesn't. Text labels before a field (Transform, Properties, Control bar, Stroke, Transparency, form rows in dialogs) and the Character and Paragraph panels' icons act as labels. Preferences › General › Scrub Numeric Fields by Dragging (on by default) turns it off.