From 93ccc6fbfa498a6e9af34fa444ef30fe9dbb181e Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Wed, 11 Nov 2026 17:41:21 -0800 Subject: [PATCH] Transform: Align New Objects to Pixel Grid makes new paths pixel-aligned Document Setup gains alignNewObjectsToPixelGrid. When it is on, every path or compound path drawn in the document (shape tools, Pen, Curvature, Pencil, Paintbrush, Blob Brush, grids, flares and add-on shapes) gets the pixel-aligned property and snaps in the same undo step. Text, paste, duplicate, Place and import are not affected. - The Transform panel menu has an Align New Objects to Pixel Grid item for the active document. - New Document's Advanced Options has the checkbox; the Mobile and Web presets turn it on, other presets and older saved presets leave it off, and file.new, Save Preset and Recent keep it. - The setting is saved with the document only when it is on. https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/pixel-preview-mode-for-web-graphics.html Docs: docs/mcp.md, docs/target-app-parity.md, docs/ui-parity.md. --- crates/doc/src/setup.rs | 8 ++ crates/engine/src/cmd/addons/shapes.rs | 3 + crates/engine/src/cmd/create.rs | 6 +- crates/engine/src/cmd/docsetup.rs | 4 +- crates/engine/src/cmd/edit.rs | 2 +- crates/engine/src/cmd/newdoc.rs | 20 ++- crates/engine/src/cmd/object.rs | 4 +- crates/engine/src/cmd/object/pixelgrid.rs | 10 ++ crates/engine/src/tests/newdoc.rs | 51 ++++++++ crates/engine/src/tests/pixelalign.rs | 125 +++++++++++++++++++ crates/ui-egui/src/dialogs/new_document.rs | 81 +++++++++++- crates/ui-egui/src/i18n/bg.tsv | 1 + crates/ui-egui/src/i18n/bs.tsv | 1 + crates/ui-egui/src/i18n/ca.tsv | 1 + crates/ui-egui/src/i18n/cs.tsv | 1 + crates/ui-egui/src/i18n/da.tsv | 1 + crates/ui-egui/src/i18n/de.tsv | 1 + crates/ui-egui/src/i18n/el.tsv | 1 + crates/ui-egui/src/i18n/es.tsv | 1 + crates/ui-egui/src/i18n/fi.tsv | 1 + crates/ui-egui/src/i18n/fr.tsv | 1 + crates/ui-egui/src/i18n/hi.tsv | 1 + crates/ui-egui/src/i18n/hu.tsv | 1 + crates/ui-egui/src/i18n/id.tsv | 1 + crates/ui-egui/src/i18n/it.tsv | 1 + crates/ui-egui/src/i18n/ja.tsv | 1 + crates/ui-egui/src/i18n/ka.tsv | 1 + crates/ui-egui/src/i18n/ko.tsv | 1 + crates/ui-egui/src/i18n/nl.tsv | 1 + crates/ui-egui/src/i18n/pl.tsv | 1 + crates/ui-egui/src/i18n/pt-br.tsv | 1 + crates/ui-egui/src/i18n/ru.tsv | 1 + crates/ui-egui/src/i18n/sv.tsv | 1 + crates/ui-egui/src/i18n/tr.tsv | 1 + crates/ui-egui/src/i18n/uk.tsv | 1 + crates/ui-egui/src/i18n/vi.tsv | 1 + crates/ui-egui/src/i18n/zh-hans.tsv | 1 + crates/ui-egui/src/i18n/zh-hant.tsv | 1 + crates/ui-egui/src/panels/transform.rs | 7 ++ crates/ui-egui/src/panels/transform/tests.rs | 31 +++++ docs/mcp.md | 29 ++++- docs/target-app-parity.md | 1 + docs/ui-parity.md | 1 + 43 files changed, 397 insertions(+), 13 deletions(-) diff --git a/crates/doc/src/setup.rs b/crates/doc/src/setup.rs index 781bec84e..bc558d270 100644 --- a/crates/doc/src/setup.rs +++ b/crates/doc/src/setup.rs @@ -314,6 +314,13 @@ pub struct DocSetup { /// #[serde(default, rename = "systemLayout", skip_serializing_if = "crate::skip::is_default")] pub system_layout: bool, + /// Align New Objects to Pixel Grid (New Document › Advanced Options, Transform panel menu): + /// paths and compound paths drawn from then on are pixel-aligned. Objects already in the + /// document, and objects pasted, duplicated or placed, keep their own setting. Off by default + /// and for documents from before it, and saved only when on. + /// + #[serde(default, skip_serializing_if = "crate::skip::is_default")] + pub align_new_objects_to_pixel_grid: bool, } impl Default for DocSetup { @@ -337,6 +344,7 @@ impl Default for DocSetup { export_text: ExportText::Editable, background: Background::Transparent, system_layout: false, + align_new_objects_to_pixel_grid: false, } } } diff --git a/crates/engine/src/cmd/addons/shapes.rs b/crates/engine/src/cmd/addons/shapes.rs index 24d0e4c3a..87549e369 100644 --- a/crates/engine/src/cmd/addons/shapes.rs +++ b/crates/engine/src/cmd/addons/shapes.rs @@ -131,6 +131,8 @@ fn insert_custom(s: &mut Session, label: &str, live: Option, cs: Cust && let Some(n) = std::sync::Arc::make_mut(&mut st.doc).node_mut(NodeId(raw_id)) { cs.write_to_node(n); + // A shape drawn pixel-aligned (Align New Objects to Pixel Grid) snaps its stored outline. + super::super::object::snap_if_aligned(n); } Ok(val) } @@ -270,6 +272,7 @@ fn box3d(s: &mut Session, p: &Value) -> Result { && let Some(n) = std::sync::Arc::make_mut(&mut st.doc).node_mut(NodeId(raw_id)) { cs.write_to_node(n); + super::super::object::snap_if_aligned(n); } Ok(val) } diff --git a/crates/engine/src/cmd/create.rs b/crates/engine/src/cmd/create.rs index 973c82af5..2e9893b03 100644 --- a/crates/engine/src/cmd/create.rs +++ b/crates/engine/src/cmd/create.rs @@ -162,7 +162,8 @@ fn constrained_turn(s: &Session, rotation: f64) -> f64 { } } -/// [`insert_look`] with the new object's bounding box turned to `bbox_angle`. +/// [`insert_look`] with the new object's bounding box turned to `bbox_angle`. A new path or +/// compound path is pixel-aligned while the document's Align New Objects to Pixel Grid is on. fn insert_look_turned( s: &mut Session, label: &str, @@ -180,6 +181,7 @@ fn insert_look_turned( let id = d.alloc_id(); let mut n = Node::new(id, kind); look.apply(d, &mut n); + super::object::align_new_object(d, &mut n); n.name = name; n.bbox_angle = bbox_angle; let mut into = None; @@ -512,6 +514,7 @@ fn flare(s: &mut Session, p: &Value) -> Result { n.name = Some(name.into()); n.blend = BlendMode::Screen; n.opacity = op; + super::object::align_new_object(d, &mut n); std::sync::Arc::new(n) }) .collect(); @@ -603,6 +606,7 @@ fn grid_group(s: &mut Session, label: &str, (paths, bbox_angle): (Vec, Node::path(d.alloc_id(), pd, Appearance::default()) }; look.apply(d, &mut n); + super::object::align_new_object(d, &mut n); n.bbox_angle = bbox_angle; std::sync::Arc::new(n) }) diff --git a/crates/engine/src/cmd/docsetup.rs b/crates/engine/src/cmd/docsetup.rs index 894d9b90a..dadafaa1b 100644 --- a/crates/engine/src/cmd/docsetup.rs +++ b/crates/engine/src/cmd/docsetup.rs @@ -17,7 +17,7 @@ pub fn specs() -> Vec { "Document Setup", [], None, - "{units?: \"Points\"|\"Inches\"|\"Millimeters\"|…, bleed?: pt (all sides)|[top, bottom, left, right]|{top?, bottom?, left?, right?} (0–72 pt), outlineImages?: bool (show images in Outline mode), highlightSubstitutedGlyphs?: bool (Highlight Substituted Fonts is a preference: prefs.set highlightSubstitutedFonts; a highlightSubstitutedFonts key is ignored and the reply carries a warning),gridSize?: \"small\"|\"medium\"|\"large\", gridColors?: \"Light\"|\"Medium\"|\"Dark\"|\"Red\"|\"Orange\"|\"Green\"|\"Blue\"|\"Purple\"|[colour, colour] (transparency grid; the first is also the paper colour; #rrggbbaa and #rgba are refused), simulatePaper?: bool, flattenerPreset?: \"High Resolution\"|\"Medium Resolution\"|\"Low Resolution\"|a saved preset (flattener.presets.list), discardWhiteOverprint?: bool, language?: \"English: USA\"|\"German\"|\"French\"|… (also picks its quotes), quotes?: {double?: \"“”\", single?: \"‘’\"} (beside a language: only if they differ from the current ones), typographersQuotes?: bool (typed straight quotes become the document's quotes), superscript?: {size?: %, position?: %}, subscript?: {size?: %, position?: %}, smallCapsSize?: %, exportText?: \"editable\"|\"appearance\" (SVG text as text or as outlines), backgroundContents?: \"transparent\"|\"white\" (white: raster exports are white behind the art), …the keys of its own reply (it can be passed back; the values it only reports are ignored)} change the document setup in one undo step; no params → the current setup", + "{units?: \"Points\"|\"Inches\"|\"Millimeters\"|…, bleed?: pt (all sides)|[top, bottom, left, right]|{top?, bottom?, left?, right?} (0–72 pt), outlineImages?: bool (show images in Outline mode), highlightSubstitutedGlyphs?: bool (Highlight Substituted Fonts is a preference: prefs.set highlightSubstitutedFonts; a highlightSubstitutedFonts key is ignored and the reply carries a warning),gridSize?: \"small\"|\"medium\"|\"large\", gridColors?: \"Light\"|\"Medium\"|\"Dark\"|\"Red\"|\"Orange\"|\"Green\"|\"Blue\"|\"Purple\"|[colour, colour] (transparency grid; the first is also the paper colour; #rrggbbaa and #rgba are refused), simulatePaper?: bool, flattenerPreset?: \"High Resolution\"|\"Medium Resolution\"|\"Low Resolution\"|a saved preset (flattener.presets.list), discardWhiteOverprint?: bool, language?: \"English: USA\"|\"German\"|\"French\"|… (also picks its quotes), quotes?: {double?: \"“”\", single?: \"‘’\"} (beside a language: only if they differ from the current ones), typographersQuotes?: bool (typed straight quotes become the document's quotes), superscript?: {size?: %, position?: %}, subscript?: {size?: %, position?: %}, smallCapsSize?: %, exportText?: \"editable\"|\"appearance\" (SVG text as text or as outlines), backgroundContents?: \"transparent\"|\"white\" (white: raster exports are white behind the art), alignNewObjectsToPixelGrid?: bool (Align New Objects to Pixel Grid: paths and compound paths drawn from then on are pixel-aligned, as object.setPixelAligned; art already in the document, pasted, duplicated or placed keeps its own setting), …the keys of its own reply (it can be passed back; the values it only reports are ignored)} change the document setup in one undo step; no params → the current setup", has_doc, setup )] @@ -46,6 +46,7 @@ pub(crate) fn setup_json(d: &Document) -> Value { "smallCapsSize": s.small_caps_size, "exportText": s.export_text.id(), "backgroundContents": s.background.id(), + "alignNewObjectsToPixelGrid": s.align_new_objects_to_pixel_grid, }) } @@ -140,6 +141,7 @@ fn apply(s: &mut DocSetup, units: &mut vectorcraft_doc::Unit, k: &str, v: &Value "smallCapsSize" => s.small_caps_size = percent(k, v, 1.0, vectorcraft_doc::setup::MAX_SMALL_CAPS)?, "exportText" => s.export_text = v.as_str().and_then(ExportText::parse).ok_or_else(|| bad(C, "exportText must be editable or appearance"))?, "backgroundContents" => s.background = background_param(v, C)?, + "alignNewObjectsToPixelGrid" => s.align_new_objects_to_pixel_grid = flag(k, v)?, // Read-only values `document.setup` reports. "gridColorsName" | "warnings" => {} _ => return Err(bad(C, format!("unknown setting `{k}`"))), diff --git a/crates/engine/src/cmd/edit.rs b/crates/engine/src/cmd/edit.rs index 7a8a91f0a..4295205ab 100644 --- a/crates/engine/src/cmd/edit.rs +++ b/crates/engine/src/cmd/edit.rs @@ -23,7 +23,7 @@ pub fn specs() -> Vec { "New…", ["File"], None, - "{preset?: a name from file.newPresets (start from it; the other params change it), name?|title?: document name (default Untitled-N), width?: pt=612|\"210 mm\", height?: pt=792, units?: \"Pixels\"|\"Points\"|\"Picas\"|\"Inches\"|\"Millimeters\"|\"Centimeters\"|\"Feet\"|\"Yards\"|\"Meters\"|\"Feet & Inches\" (default: prefs unitsGeneral, as print presets; screen presets: Pixels), orientation?: \"portrait\"|\"landscape\" (swaps width and height to match), artboards?: n (1–1000), artboardLayout?: {layout?: \"gridByRow\"|\"gridByColumn\"|\"row\"|\"column\", columns?: n (default: all in one row), spacing?: pt=20, rightToLeft?: bool}, bleed?: pt|[top, bottom, left, right]|{top?, …} (0–72 pt), backgroundContents?: \"transparent\"|\"white\" (a white artboard background, not an object), colorMode?: \"rgb\"|\"cmyk\" (a CMYK document starts with CMYK swatches and stores the colours applied to it as CMYK), rasterEffectsPpi?: 72|150|300 (1–2400), previewMode?: \"default\"|\"pixel\"|\"overprint\" (overprint turns Overprint Preview on, pixel Pixel Preview in the app; default leaves both), created?: Unix seconds|null (File Info's created date; default now, recorded in the journal so a replay matches), size?: a preset's size label (a preset file.newPresets lists can be passed back; ignored)} → {index, previewMode}; the size is remembered in file.newPresets' Recent", + "{preset?: a name from file.newPresets (start from it; the other params change it), name?|title?: document name (default Untitled-N), width?: pt=612|\"210 mm\", height?: pt=792, units?: \"Pixels\"|\"Points\"|\"Picas\"|\"Inches\"|\"Millimeters\"|\"Centimeters\"|\"Feet\"|\"Yards\"|\"Meters\"|\"Feet & Inches\" (default: prefs unitsGeneral, as print presets; screen presets: Pixels), orientation?: \"portrait\"|\"landscape\" (swaps width and height to match), artboards?: n (1–1000), artboardLayout?: {layout?: \"gridByRow\"|\"gridByColumn\"|\"row\"|\"column\", columns?: n (default: all in one row), spacing?: pt=20, rightToLeft?: bool}, bleed?: pt|[top, bottom, left, right]|{top?, …} (0–72 pt), backgroundContents?: \"transparent\"|\"white\" (a white artboard background, not an object), colorMode?: \"rgb\"|\"cmyk\" (a CMYK document starts with CMYK swatches and stores the colours applied to it as CMYK), rasterEffectsPpi?: 72|150|300 (1–2400), previewMode?: \"default\"|\"pixel\"|\"overprint\" (overprint turns Overprint Preview on, pixel Pixel Preview in the app; default leaves both), alignNewObjectsToPixelGrid?: bool (Advanced Options › Align New Objects to Pixel Grid: paths drawn in the document are pixel-aligned; default on for the Mobile and Web presets, else off; document.setup changes it later), created?: Unix seconds|null (File Info's created date; default now, recorded in the journal so a replay matches), size?: a preset's size label (a preset file.newPresets lists can be passed back; ignored)} → {index, previewMode}; the size is remembered in file.newPresets' Recent", always, super::newdoc::file_new ), diff --git a/crates/engine/src/cmd/newdoc.rs b/crates/engine/src/cmd/newdoc.rs index 20dc187ab..bd15d748c 100644 --- a/crates/engine/src/cmd/newdoc.rs +++ b/crates/engine/src/cmd/newdoc.rs @@ -7,7 +7,7 @@ use serde_json::{Value, json}; use vectorcraft_doc::{Artboard, Background, ColorMode, Document, Unit}; use vectorcraft_geom::{Point, Rect}; -use super::{CommandSpec, Result, Session, always, bad, clock_date, cmd, date_param, str_param}; +use super::{CommandSpec, Result, Session, always, bad, clock_date, cmd, date_param, str_param, switch_or}; pub fn specs() -> Vec { vec![ @@ -16,7 +16,7 @@ pub fn specs() -> Vec { "New Document Presets", [], None, - "{category?: \"Recent\"|\"Saved\"|\"Mobile\"|\"Web\"|\"Print\"|\"Film & Video\"|\"Art & Illustration\"|\"Branding\"|\"Social\"} the New Document presets by category (Recent: the last sizes used; Saved: the user's presets) → {categories: [{name, presets: [{name, size: \"1920 × 1080 px\", width: pt, height: pt, units, orientation, artboards, artboardLayout: {layout, columns, spacing, rightToLeft}, bleed, backgroundContents, colorMode, rasterEffectsPpi, previewMode}]}]}; file.new {preset: name} starts from one", + "{category?: \"Recent\"|\"Saved\"|\"Mobile\"|\"Web\"|\"Print\"|\"Film & Video\"|\"Art & Illustration\"|\"Branding\"|\"Social\"} the New Document presets by category (Recent: the last sizes used; Saved: the user's presets) → {categories: [{name, presets: [{name, size: \"1920 × 1080 px\", width: pt, height: pt, units, orientation, artboards, artboardLayout: {layout, columns, spacing, rightToLeft}, bleed, backgroundContents, colorMode, rasterEffectsPpi, previewMode, alignNewObjectsToPixelGrid (on in the Mobile and Web presets)}]}]}; file.new {preset: name} starts from one", always, list ), @@ -144,6 +144,10 @@ pub struct DocSettings { #[serde(deserialize_with = "de_ppi")] pub raster_effects_ppi: f64, pub preview_mode: PreviewMode, + /// Align New Objects to Pixel Grid ([`vectorcraft_doc::DocSetup::align_new_objects_to_pixel_grid`]): + /// on in the Mobile and Web presets, off in the others and in a saved or Recent preset from + /// before it. + pub align_new_objects_to_pixel_grid: bool, } impl Default for DocSettings { @@ -163,6 +167,7 @@ impl Default for DocSettings { color_mode: ColorMode::Rgb, raster_effects_ppi: 72.0, preview_mode: PreviewMode::Default, + align_new_objects_to_pixel_grid: false, } } } @@ -211,6 +216,7 @@ impl DocSettings { "colorMode": color_mode_id(self.color_mode), "rasterEffectsPpi": self.raster_effects_ppi, "previewMode": self.preview_mode.id(), + "alignNewObjectsToPixelGrid": self.align_new_objects_to_pixel_grid, }) } @@ -273,6 +279,7 @@ impl DocSettings { if let Some(m) = p.get("previewMode").filter(|v| !v.is_null()) { self.preview_mode = m.as_str().and_then(PreviewMode::parse).ok_or_else(|| bad(cmd, "previewMode must be default, pixel or overprint"))?; } + self.align_new_objects_to_pixel_grid = switch_or(p, "alignNewObjectsToPixelGrid", self.align_new_objects_to_pixel_grid, cmd)?; let extent = self.boards().iter().fold(Rect::ZERO, |a, r| a.union(*r)); if extent.width().max(extent.height()) + self.bleed.iter().fold(0.0f64, |a, b| a.max(*b)) * 2.0 > crate::MAX_COORD { return Err(bad(cmd, "the artboards don't fit on the canvas: fewer artboards, less spacing or a smaller size")); @@ -330,6 +337,7 @@ impl DocSettings { d.raster_effects_ppi = self.raster_effects_ppi; d.setup.bleed = self.bleed; d.setup.background = self.background; + d.setup.align_new_objects_to_pixel_grid = self.align_new_objects_to_pixel_grid; d.artboards = self .boards() .into_iter() @@ -543,6 +551,10 @@ pub const CATEGORIES: &[Category] = &[ ), ]; +/// The built-in categories whose presets start with Align New Objects to Pixel Grid on +/// (). +const PIXEL_ALIGNED_CATEGORIES: [&str; 2] = ["Mobile", "Web"]; + /// The category tabs in order: Recent, Saved, then the built-in categories. pub fn category_names() -> impl Iterator { ["Recent", "Saved"].into_iter().chain(CATEGORIES.iter().map(|c| c.0)) @@ -551,7 +563,8 @@ pub fn category_names() -> impl Iterator { /// The built-in presets of one category: screen sizes in pixels, the others in `general` /// (Preferences ▸ Units ▸ General). fn builtin(c: &Category, general: Unit) -> impl Iterator + '_ { - let (_, color_mode, ppi, presets) = *c; + let (category, color_mode, ppi, presets) = *c; + let align = PIXEL_ALIGNED_CATEGORIES.contains(&category); presets.iter().map(move |&(name, w, h, units)| DocSettings { name: name.into(), width: units.to_pt(w), @@ -559,6 +572,7 @@ fn builtin(c: &Category, general: Unit) -> impl Iterator + ' units: if units == Unit::Pixels { units } else { general }, color_mode, raster_effects_ppi: ppi, + align_new_objects_to_pixel_grid: align, ..DocSettings::default() }) } diff --git a/crates/engine/src/cmd/object.rs b/crates/engine/src/cmd/object.rs index c467f06e5..8e15e1dd4 100644 --- a/crates/engine/src/cmd/object.rs +++ b/crates/engine/src/cmd/object.rs @@ -36,6 +36,8 @@ pub(crate) use arrange::group_nodes; pub(crate) use bounds::{bounds_rewrite, object_bounds_transform}; pub use commands::specs; pub(super) use compound::{as_clipping_path, make_clipping_path, release_clip}; -pub(crate) use pixelgrid::{keep_aligned_in, keep_compound_aligned, keep_pixel_aligned, refresh as refresh_pixel_aligned, resnap, snap_if_aligned}; +pub(crate) use pixelgrid::{ + align_new_object, keep_aligned_in, keep_compound_aligned, keep_pixel_aligned, refresh as refresh_pixel_aligned, resnap, snap_if_aligned, +}; pub(super) use shape::recut; pub(crate) use transform::{apply_transform, preview_copies, scaling, transform_node, transform_patterns, transforms_objects}; diff --git a/crates/engine/src/cmd/object/pixelgrid.rs b/crates/engine/src/cmd/object/pixelgrid.rs index 6441fda0c..64d47ca0a 100644 --- a/crates/engine/src/cmd/object/pixelgrid.rs +++ b/crates/engine/src/cmd/object/pixelgrid.rs @@ -22,6 +22,16 @@ pub(crate) fn can_align(n: &Node) -> bool { matches!(n.kind, NodeKind::Path { guide: false, .. } | NodeKind::Compound { .. }) } +/// For an object being drawn into `d`: when the document's Align New Objects to Pixel Grid is on +/// and [`can_align`] accepts `n`, turn `n`'s pixel-aligned property on. The edit that adds it snaps +/// it to the pixel grid ([`refresh`]). Pasted, duplicated, placed and imported objects do not come +/// through here. +pub(crate) fn align_new_object(d: &Document, n: &mut Node) { + if d.setup.align_new_objects_to_pixel_grid && can_align(n) { + n.extras_mut().pixel_aligned = true; + } +} + /// `object.setPixelAligned`: turn the pixel-aligned property of the objects given (or the selected /// objects) on or off. Turning it on snaps them to the pixel grid at once. Every object must be /// one [`can_align`] accepts, else nothing changes. When every object already has the value, the diff --git a/crates/engine/src/tests/newdoc.rs b/crates/engine/src/tests/newdoc.rs index 067631eec..4bc58e5f6 100644 --- a/crates/engine/src/tests/newdoc.rs +++ b/crates/engine/src/tests/newdoc.rs @@ -292,6 +292,57 @@ fn a_saved_preset_with_the_resolution_out_of_range_reads_in_range() { assert_eq!(s.doc().unwrap().doc.raster_effects_ppi, 1.0); } +/// Advanced Options › Align New Objects to Pixel Grid starts on in the Mobile and Web presets and +/// off in the others and without a preset; a `file.new` param overrides the preset; the document +/// takes the value; saved presets and Recent keep it, and a saved preset from before it reads off. +#[test] +fn align_new_objects_to_pixel_grid_follows_the_preset() { + let align = |s: &Session| s.doc().unwrap().doc.setup.align_new_objects_to_pixel_grid; + let mut s = Session::new(); + for category in ["Mobile", "Web"] { + assert!(presets(&mut s, category).iter().all(|p| p["alignNewObjectsToPixelGrid"] == true), "{category}"); + } + for category in ["Print", "Film & Video", "Art & Illustration", "Branding", "Social"] { + assert!(presets(&mut s, category).iter().all(|p| p["alignNewObjectsToPixelGrid"] == false), "{category}"); + } + s.execute("file.new", &json!({"preset": "Web 1920×1080"})).unwrap(); + assert!(align(&s)); + assert_eq!(presets(&mut s, "Recent")[0]["alignNewObjectsToPixelGrid"], true); + s.execute("file.new", &json!({"preset": "Phone 390×844"})).unwrap(); + assert!(align(&s)); + s.execute("file.new", &json!({"preset": "Letter"})).unwrap(); + assert!(!align(&s)); + s.execute("file.new", &json!({"width": 300, "height": 200})).unwrap(); + assert!(!align(&s)); + s.execute("file.new", &json!({"preset": "Web 1920×1080", "alignNewObjectsToPixelGrid": false})).unwrap(); + assert!(!align(&s)); + s.execute("file.new", &json!({"preset": "A4", "alignNewObjectsToPixelGrid": true})).unwrap(); + assert!(align(&s)); + assert!(Session::new().execute("file.new", &json!({"alignNewObjectsToPixelGrid": "on"})).is_err()); + // A Recent entry passed back makes the document it was made as. + let recent = presets(&mut s, "Recent")[0].clone(); + assert_eq!((recent["name"].as_str(), recent["alignNewObjectsToPixelGrid"].as_bool()), (Some("A4"), Some(true))); + s.execute("file.new", &recent).unwrap(); + assert!(align(&s)); + // Saved presets keep it. + s.execute("file.newPresets.save", &json!({"name": "Icons", "width": 64, "height": 64, "alignNewObjectsToPixelGrid": true})).unwrap(); + s.execute("file.newPresets.save", &json!({"name": "Banner", "preset": "Web 1920×1080", "alignNewObjectsToPixelGrid": false})).unwrap(); + let saved: Vec = presets(&mut s, "Saved").iter().map(|p| p["alignNewObjectsToPixelGrid"].clone()).collect(); + assert_eq!(saved, [json!(true), json!(false)]); + s.execute("file.new", &json!({"preset": "Icons"})).unwrap(); + assert!(align(&s)); + s.execute("file.new", &json!({"preset": "Banner"})).unwrap(); + assert!(!align(&s)); + let prefs: crate::Prefs = serde_json::from_value(s.prefs.to_json()).unwrap(); + assert_eq!(prefs.new_doc_presets, s.prefs.new_doc_presets); + // A saved preset from before the setting reads off, whatever its name. + let mut old = Session::new(); + old.apply_prefs(serde_json::from_value(json!({"newDocPresets": [{"name": "My Web", "width": 100, "height": 100}]})).unwrap()); + assert_eq!(presets(&mut old, "Saved")[0]["alignNewObjectsToPixelGrid"], false); + old.execute("file.new", &json!({"preset": "My Web"})).unwrap(); + assert!(!align(&old)); +} + #[test] fn rearrange_keeps_its_grid() { let mut s = new_doc(json!({"width": 100, "height": 50, "artboards": 3})); diff --git a/crates/engine/src/tests/pixelalign.rs b/crates/engine/src/tests/pixelalign.rs index d348b13ae..52bb9ba78 100644 --- a/crates/engine/src/tests/pixelalign.rs +++ b/crates/engine/src/tests/pixelalign.rs @@ -766,3 +766,128 @@ fn objects_on_path_members_stay_on_the_grid() { assert!(whole(&points(&s, a)), "path edited: {:?}", points(&s, a)); assert!(points(&s, a).iter().all(|(_, y)| *y > 105.0), "followed the path: {:?}", points(&s, a)); } + +fn align_new(s: &Session) -> bool { + s.doc().unwrap().doc.setup.align_new_objects_to_pixel_grid +} + +/// Align New Objects to Pixel Grid (`document.setup`): while it is on, a rectangle and an open path +/// drawn in the document are pixel-aligned and snapped in the step that makes them, and type is +/// not; art already in the document, duplicates and pastes keep their own setting; turning it off +/// leaves the next drawing as it is drawn, and the object's own setting can be turned off while +/// the document's stays on. +#[test] +fn align_new_objects_makes_new_paths_pixel_aligned() { + let mut s = new_doc(); + assert!(!align_new(&s)); + assert_eq!(s.execute("document.setup", &json!({})).unwrap()["alignNewObjectsToPixelGrid"], false); + let old = path(&mut s, "M10.3 120.6 L40.7 120.6 L40.7 130.3 L10.3 130.3 Z", 2.0); + let old_points = points(&s, old); + let steps = undo_len(&s); + let r = s.execute("document.setup", &json!({"alignNewObjectsToPixelGrid": true})).unwrap(); + assert_eq!(r["alignNewObjectsToPixelGrid"], true); + assert!(align_new(&s)); + assert_eq!(undo_len(&s), steps + 1, "one undo step"); + // Art already in the document stays as it was. + assert!(!aligned(&s, old)); + assert_eq!(points(&s, old), old_points); + // A rectangle is drawn pixel-aligned: with the 2 pt stroke `path` left as the current one, its + // edges go to whole points. + let steps = undo_len(&s); + let rect = s.execute("shape.rectangle", &json!({"x": 10.3, "y": 20.6, "width": 30.4, "height": 9.7})).unwrap()["id"].as_u64().unwrap(); + assert!(aligned(&s, rect)); + assert_eq!(points(&s, rect), [(10.0, 21.0), (41.0, 21.0), (41.0, 30.0), (10.0, 30.0)]); + assert_eq!(undo_len(&s), steps + 1, "made and snapped in one undo step"); + s.execute("edit.undo", &json!({})).unwrap(); + assert!(s.doc().unwrap().doc.node(NodeId(rect)).is_none()); + s.execute("edit.redo", &json!({})).unwrap(); + assert!(aligned(&s, rect)); + // An open path from the Pen. + let line = s.execute("path.create", &json!({"anchors": [{"x": 50.3, "y": 60.2}, {"x": 90.6, "y": 60.2}]})).unwrap()["id"].as_u64().unwrap(); + assert!(aligned(&s, line)); + // Its horizontal segment goes to a whole point. + assert!(points(&s, line).iter().all(|(_, y)| *y == 60.0), "{:?}", points(&s, line)); + // Type cannot be pixel-aligned. + let text = s.execute("text.create", &json!({"x": 10.3, "y": 200.6, "text": "Hi"})).unwrap()["id"].as_u64().unwrap(); + assert!(!aligned(&s, text)); + // A duplicate and a paste of art that is not pixel-aligned keep it so. + s.execute("select.set", &json!({"ids": [old]})).unwrap(); + s.execute("edit.duplicate", &json!({"dx": 100.3, "dy": 0.4})).unwrap(); + let copy = s.doc().unwrap().selection.objects[0].0; + assert!(copy != old && !aligned(&s, copy)); + s.execute("select.set", &json!({"ids": [old]})).unwrap(); + s.execute("edit.copy", &json!({})).unwrap(); + s.execute("edit.paste", &json!({})).unwrap(); + let pasted = s.doc().unwrap().selection.objects[0].0; + assert!(pasted != old && pasted != copy && !aligned(&s, pasted)); + // The object's own setting turns off while the document's stays on. + s.execute("object.setPixelAligned", &json!({"ids": [rect], "on": false})).unwrap(); + assert!(!aligned(&s, rect) && align_new(&s)); + // Off: the next rectangle is drawn as it is given, and the first one stays pixel-aligned. + s.execute("document.setup", &json!({"alignNewObjectsToPixelGrid": false})).unwrap(); + let plain = s.execute("shape.rectangle", &json!({"x": 10.3, "y": 20.6, "width": 30.4, "height": 9.7})).unwrap()["id"].as_u64().unwrap(); + assert!(!aligned(&s, plain)); + let drawn = [(10.3, 20.6), (40.7, 20.6), (40.7, 30.3), (10.3, 30.3)]; + assert!(points(&s, plain).iter().zip(drawn).all(|(a, b)| close(*a, b)), "{:?}", points(&s, plain)); + assert!(aligned(&s, line)); + // A value that is not true or false changes nothing. + let steps = undo_len(&s); + assert!(s.execute("document.setup", &json!({"alignNewObjectsToPixelGrid": "yes"})).is_err()); + assert!(!align_new(&s)); + assert_eq!(undo_len(&s), steps); +} + +/// While Align New Objects to Pixel Grid is on, the paths of a new rectangular grid, polar grid +/// and flare are pixel-aligned (the group is not), and a new 3D Box add-on shape is snapped as +/// Align to Pixel Grid snaps the same box drawn without it. +#[test] +fn align_new_objects_reaches_grids_flares_and_add_on_shapes() { + let mut s = new_doc(); + s.execute("document.setup", &json!({"alignNewObjectsToPixelGrid": true})).unwrap(); + let doc = |s: &Session| s.doc().unwrap().doc.clone(); + let children = |s: &Session, id: u64| -> Vec { doc(s).node(NodeId(id)).unwrap().children().unwrap().iter().map(|c| c.id.0).collect() }; + let grid = s.execute("shape.rectangularGrid", &json!({"x": 10.3, "y": 20.6, "width": 100.4, "height": 50.7})).unwrap()["id"].as_u64().unwrap(); + assert!(!aligned(&s, grid)); + let lines = children(&s, grid); + assert!(!lines.is_empty() && lines.iter().all(|id| aligned(&s, *id))); + // Under the 1 pt stroke, a divider sits on a half point across its length, and the frame + // has every corner on half points. + let half = |v: f64| ((v - 0.5) - (v - 0.5).round()).abs() < 1e-9; + let snapped = + |p: &[(f64, f64)]| halves(p) || (p.iter().all(|q| q.1 == p[0].1) && half(p[0].1)) || (p.iter().all(|q| q.0 == p[0].0) && half(p[0].0)); + assert!(lines.iter().all(|id| snapped(&points(&s, *id))), "{:?}", lines.iter().map(|id| points(&s, *id)).collect::>()); + let polar = s.execute("shape.polarGrid", &json!({"x": 200.3, "y": 20.6, "width": 80.4, "height": 80.7})).unwrap()["id"].as_u64().unwrap(); + assert!(!aligned(&s, polar) && children(&s, polar).iter().all(|id| aligned(&s, *id))); + let flare = s.execute("shape.flare", &json!({"cx": 100.3, "cy": 200.6})).unwrap()["id"].as_u64().unwrap(); + assert!(!aligned(&s, flare) && children(&s, flare).iter().all(|id| aligned(&s, *id))); + // Seen straight on, the box's front face has horizontal and vertical edges to snap. + let params = json!({"x": 20.3, "y": 300.6, "width": 60.4, "height": 40.7, "angleX": 0, "angleY": 0, "angleZ": 0}); + let drawn = s.execute("addon.shapes.box3d", ¶ms).unwrap()["id"].as_u64().unwrap(); + assert!(aligned(&s, drawn)); + s.execute("document.setup", &json!({"alignNewObjectsToPixelGrid": false})).unwrap(); + let later = s.execute("addon.shapes.box3d", ¶ms).unwrap()["id"].as_u64().unwrap(); + assert!(!aligned(&s, later)); + let unaligned = points(&s, later); + s.execute("object.setPixelAligned", &json!({"ids": [later]})).unwrap(); + assert_eq!(points(&s, drawn), points(&s, later)); + assert_ne!(points(&s, drawn), unaligned, "the box has segments to snap"); +} + +/// The document's Align New Objects to Pixel Grid is saved in the native file only when on and +/// read back; a file from before it opens with it off. +#[test] +fn align_new_objects_is_saved_and_read_back() { + let mut s = new_doc(); + let off = serde_json::to_value(&s.doc().unwrap().doc.setup).unwrap(); + assert!(off.get("alignNewObjectsToPixelGrid").is_none(), "{off}"); + let back = vectorcraft_format::load(&vectorcraft_format::save(&s.doc().unwrap().doc, false)).unwrap(); + assert!(!back.setup.align_new_objects_to_pixel_grid); + s.execute("document.setup", &json!({"alignNewObjectsToPixelGrid": true})).unwrap(); + let on = serde_json::to_value(&s.doc().unwrap().doc.setup).unwrap(); + assert_eq!(on["alignNewObjectsToPixelGrid"], true); + let back = vectorcraft_format::load(&vectorcraft_format::save(&s.doc().unwrap().doc, false)).unwrap(); + assert!(back.setup.align_new_objects_to_pixel_grid); + let mut older = on; + older.as_object_mut().unwrap().remove("alignNewObjectsToPixelGrid"); + assert!(!serde_json::from_value::(older).unwrap().align_new_objects_to_pixel_grid); +} diff --git a/crates/ui-egui/src/dialogs/new_document.rs b/crates/ui-egui/src/dialogs/new_document.rs index 8e0200933..76403ff45 100644 --- a/crates/ui-egui/src/dialogs/new_document.rs +++ b/crates/ui-egui/src/dialogs/new_document.rs @@ -8,7 +8,7 @@ //! //! Fields: `file.new`'s params (`preset`, `name`, `width`, `height` (pt), `units`, `artboards`, //! `artboardLayout`, `bleed`, `backgroundContents`, `colorMode`, `rasterEffectsPpi`, -//! `previewMode`), plus `category` (the tab), `advanced` (Advanced Options open), `bleedLinked` and +//! `previewMode`, `alignNewObjectsToPixelGrid`), plus `category` (the tab), `advanced` (Advanced Options open), `bleedLinked` and //! `presetName` (Save Preset's name). Setting `preset` fills the fields from that preset; changing //! any of them afterwards makes the settings custom. @@ -29,7 +29,7 @@ pub(super) const SPEC: DialogSpec = DialogSpec { inputs: |_| &["presetName"], .. pub(super) const MORE_SPEC: DialogSpec = SPEC; /// The fields `file.new` takes. -const PARAMS: [&str; 12] = [ +const PARAMS: [&str; 13] = [ "preset", "name", "width", @@ -42,6 +42,7 @@ const PARAMS: [&str; 12] = [ "colorMode", "rasterEffectsPpi", "previewMode", + "alignNewObjectsToPixelGrid", ]; /// The preset the fields were last filled from, as `apply` set them; a Recent or Saved card keeps /// its own units here, which the units chosen in Preset Details don't change. @@ -694,7 +695,8 @@ fn choice(ui: &mut egui::Ui, d: &mut Dialog, key: &str, options: &[(Value, Strin } } -/// The collapsible Advanced Options: Color Mode, Raster Effects and Preview Mode. +/// The collapsible Advanced Options: Color Mode, Raster Effects, Preview Mode and Align New Objects +/// to Pixel Grid. fn advanced(ui: &mut egui::Ui, d: &mut Dialog, label: f32, field: f32) { let open = d.bool("advanced"); if widgets::section_toggle(ui, tl!("Advanced Options"), open) { @@ -714,6 +716,25 @@ fn advanced(ui: &mut egui::Ui, d: &mut Dialog, label: f32, field: f32) { ] { widgets::label_row(ui, name, label, |ui| choice(ui, d, key, options, field)); } + wrapping_check(ui, d, "alignNewObjectsToPixelGrid", tl!("Align New Objects to Pixel Grid")); +} + +/// A checkbox for the boolean field `key` whose `label` (already translated) wraps within the +/// column; a click on the box or the label toggles the field. +fn wrapping_check(ui: &mut egui::Ui, d: &mut Dialog, key: &str, label: &str) { + let t = Tokens::get(ui.ctx()); + let on = d.bool(key); + let toggled = ui + .horizontal_top(|ui| { + let boxed = widgets::check_box(ui, label, on, true); + let text = egui::RichText::new(label).font(egui::FontId::proportional(12.5)).color(t.text); + let clicked = ui.add(egui::Label::new(text).wrap().sense(egui::Sense::click())).clicked(); + boxed || clicked + }) + .inner; + if toggled { + d.fields.insert(key.into(), json!(!on)); + } } #[cfg(test)] @@ -1433,6 +1454,60 @@ mod tests { assert!(h.state().session.documents().is_empty()); } + /// Advanced Options › Align New Objects to Pixel Grid is checked for a Web preset and unchecked + /// for Letter; the checkbox changes it and makes the settings custom, Save Preset keeps it, and + /// Create makes the document with it. + #[test] + fn align_new_objects_checkbox_follows_the_preset_and_reaches_the_document() { + const KEY: &str = "alignNewObjectsToPixelGrid"; + const LABEL: &str = "Align New Objects to Pixel Grid"; + let mut h = big(); + assert_eq!((field(h.state(), "preset"), field(h.state(), KEY)), (json!("Letter"), json!(false))); + set(h.state_mut(), "category", json!("Web")); + set(h.state_mut(), "preset", json!("Web 1920×1080")); + set(h.state_mut(), "advanced", json!(true)); + h.run_steps(3); + assert_eq!(field(h.state(), KEY), json!(true)); + h.get_by_role_and_label(egui::accesskit::Role::CheckBox, LABEL).click(); + h.run_steps(3); + assert_eq!((field(h.state(), "preset"), field(h.state(), KEY)), (json!(""), json!(false)), "custom settings"); + set(h.state_mut(), "category", json!("Print")); + set(h.state_mut(), "preset", json!("Letter")); + h.run_steps(3); + assert_eq!(field(h.state(), KEY), json!(false)); + h.get_by_role_and_label(egui::accesskit::Role::Label, LABEL).click(); + h.run_steps(3); + assert_eq!(field(h.state(), KEY), json!(true), "a click on the label toggles it too"); + h.get_by_label("Save Preset").click(); + h.run_steps(4); + set(h.state_mut(), "presetName", json!("Pixel Letter")); + h.run_steps(2); + h.get_by_label("Save").click(); + h.run_steps(3); + let saved = h.state().session.prefs.new_doc_presets.iter().find(|p| p.name == "Pixel Letter").expect("saved"); + assert!(saved.align_new_objects_to_pixel_grid); + assert_eq!((field(h.state(), "preset"), field(h.state(), KEY)), (json!("Pixel Letter"), json!(true))); + h.get_by_label("Create").click(); + h.run_steps(3); + assert!(h.state().ui.dialog.is_none()); + assert!(h.state().session.active().unwrap().doc.setup.align_new_objects_to_pixel_grid); + } + + /// A long translation of Align New Objects to Pixel Grid wraps inside the Preset Details column. + #[test] + fn align_new_objects_label_wraps_inside_the_details_column() { + const LABEL: &str = "Στοίχιση νέων αντικειμένων στο πλέγμα εικονοστοιχείων"; + crate::i18n::set_current(crate::i18n::Lang::from_code("el").unwrap()); + let mut h = big(); + set(h.state_mut(), "advanced", json!(true)); + h.run_steps(3); + let boxed = h.get_by_role_and_label(egui::accesskit::Role::CheckBox, LABEL).rect(); + let text = h.get_by_role_and_label(egui::accesskit::Role::Label, LABEL).rect(); + crate::i18n::set_current(crate::i18n::Lang::EN); + assert!(text.right() <= boxed.left() + super::DETAILS + 0.5, "{boxed:?} {text:?}"); + assert!(text.height() > boxed.height(), "the label takes two lines: {text:?}"); + } + /// In a small window the pop-up stays on screen, and its button stays in view under rows that scroll. #[test] fn the_popup_stays_inside_a_small_window() { diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index d83b5ac3c..9ad5e279d 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -6520,6 +6520,7 @@ breakText Glyphs Глифи # Align to Pixel Grid Align to Pixel Grid Подравняване към пикселната мрежа + Align New Objects to Pixel Grid Подравняване на новите обекти към пикселната мрежа @msg side is left or right, not `{s}` side е left или right, а не `{s}` Edit Custom Dictionary Редактиране на потребителски речник Entry: Запис: diff --git a/crates/ui-egui/src/i18n/bs.tsv b/crates/ui-egui/src/i18n/bs.tsv index 663e95cea..7bf1d0335 100644 --- a/crates/ui-egui/src/i18n/bs.tsv +++ b/crates/ui-egui/src/i18n/bs.tsv @@ -6571,6 +6571,7 @@ shape Open Otvoren # Align to Pixel Grid Align to Pixel Grid Poravnaj s mrežom piksela + Align New Objects to Pixel Grid Poravnaj nove objekte s mrežom piksela @msg side is left or right, not `{s}` side je left ili right, a ne `{s}` Edit Custom Dictionary Uredi prilagođeni rječnik Entry: Unos: diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index 9c60e87e0..ca7579e18 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -6772,6 +6772,7 @@ breakText Glyphs Glifs # Align to Pixel Grid Align to Pixel Grid Alinea a la quadrícula de píxels + Align New Objects to Pixel Grid Alinea els objectes nous a la quadrícula de píxels @msg only paths and compound paths can be aligned to the pixel grid només els traçats i els traçats compostos es poden alinear a la quadrícula de píxels @msg side is left or right, not `{s}` side ha de ser left o right, no `{s}` Ignore All Ignora-ho tot diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index e8f9666fa..0d05b184c 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -6791,6 +6791,7 @@ breakText Glyphs Glyfy # Align to Pixel Grid Align to Pixel Grid Zarovnat na mřížku obrazových bodů + Align New Objects to Pixel Grid Zarovnat nové objekty na mřížku obrazových bodů @msg only paths and compound paths can be aligned to the pixel grid na mřížku obrazových bodů lze zarovnat jen cesty a složené cesty @msg side is left or right, not `{s}` side je left nebo right, ne `{s}` Ignore All Ignorovat vše diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index 31915ed6b..58445ab89 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -6534,6 +6534,7 @@ breakText Glyphs Glyffer # Align to Pixel Grid Align to Pixel Grid Juster efter pixelgitter + Align New Objects to Pixel Grid Juster nye objekter efter pixelgitter @msg side is left or right, not `{s}` side er left eller right, ikke `{s}` Edit Custom Dictionary Redigér egen ordliste Entry: Opslag: diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 2c5424d81..047f7ca12 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -6378,6 +6378,7 @@ breakText Glyphs Glyphen # Align to Pixel Grid Align to Pixel Grid Am Pixelraster ausrichten + Align New Objects to Pixel Grid Neue Objekte am Pixelraster ausrichten @msg only paths and compound paths can be aligned to the pixel grid nur Pfade und zusammengesetzte Pfade können am Pixelraster ausgerichtet werden @msg side is left or right, not `{s}` side ist left oder right, nicht `{s}` Ignore All Alle ignorieren diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 6071ed5be..59f16c337 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -6743,6 +6743,7 @@ breakText Glyphs Γλύφοι # Align to Pixel Grid Align to Pixel Grid Στοίχιση στο πλέγμα εικονοστοιχείων + Align New Objects to Pixel Grid Στοίχιση νέων αντικειμένων στο πλέγμα εικονοστοιχείων @msg only paths and compound paths can be aligned to the pixel grid μόνο διαδρομές και σύνθετες διαδρομές μπορούν να στοιχιστούν στο πλέγμα εικονοστοιχείων @msg side is left or right, not `{s}` το side είναι left ή right, όχι `{s}` Ignore All Παράβλεψη όλων diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 4a74e3ae3..21b707032 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -6734,6 +6734,7 @@ breakText Glyphs Glifos # Align to Pixel Grid Align to Pixel Grid Alinear con cuadrícula de píxeles + Align New Objects to Pixel Grid Alinear objetos nuevos con cuadrícula de píxeles @msg only paths and compound paths can be aligned to the pixel grid solo los trazados y los trazados compuestos se pueden alinear con la cuadrícula de píxeles @msg side is left or right, not `{s}` side es left o right, no `{s}` Ignore All Omitir todo diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index f2ec64f60..f50a46219 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -6826,6 +6826,7 @@ breakText Glyphs Merkit # Align to Pixel Grid Align to Pixel Grid Tasaa pikseliruudukkoon + Align New Objects to Pixel Grid Tasaa uudet objektit pikseliruudukkoon @msg only paths and compound paths can be aligned to the pixel grid vain polut ja yhdistelmäpolut voi tasata pikseliruudukkoon @msg side is left or right, not `{s}` side on left tai right, ei `{s}` Ignore All Ohita kaikki diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 807b3363b..3603c8d8d 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -6786,6 +6786,7 @@ breakText Glyphs Glyphes # Align to Pixel Grid Align to Pixel Grid Aligner sur la grille de pixels + Align New Objects to Pixel Grid Aligner les nouveaux objets sur la grille de pixels @msg only paths and compound paths can be aligned to the pixel grid seuls les tracés et les tracés transparents peuvent être alignés sur la grille de pixels @msg side is left or right, not `{s}` side vaut left ou right, pas `{s}` Ignore All Tout ignorer diff --git a/crates/ui-egui/src/i18n/hi.tsv b/crates/ui-egui/src/i18n/hi.tsv index 572a19779..06b987865 100644 --- a/crates/ui-egui/src/i18n/hi.tsv +++ b/crates/ui-egui/src/i18n/hi.tsv @@ -8208,6 +8208,7 @@ theme Appearance अपीयरेंस # Align to Pixel Grid Align to Pixel Grid पिक्सेल ग्रिड से संरेखित करें + Align New Objects to Pixel Grid नए ऑब्जेक्ट्स को पिक्सेल ग्रिड से संरेखित करें @msg only paths and compound paths can be aligned to the pixel grid केवल पाथ और कंपाउंड पाथ को पिक्सेल ग्रिड से संरेखित किया जा सकता है @msg the name `{name}` isn't UTF-8 and was read as {label}: if it looks wrong, open the file with another legacyNameEncoding नाम `{name}` UTF-8 नहीं है और इसे {label} के रूप में पढ़ा गया: यदि यह गलत दिखे, तो फ़ाइल को किसी अन्य legacyNameEncoding से खोलें @msg the name `{name}` isn't UTF-8, and parts of it aren't valid {label} either: open the file with another legacyNameEncoding नाम `{name}` UTF-8 नहीं है, और इसके कुछ हिस्से {label} में भी मान्य नहीं हैं: फ़ाइल को किसी अन्य legacyNameEncoding से खोलें diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 8c2d2339b..8c13bfb26 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -6775,6 +6775,7 @@ breakText Glyphs Glifák # Align to Pixel Grid Align to Pixel Grid Igazítás a pixelrácshoz + Align New Objects to Pixel Grid Új objektumok igazítása a pixelrácshoz @msg only paths and compound paths can be aligned to the pixel grid csak útvonalak és összetett útvonalak igazíthatók a pixelrácshoz @msg side is left or right, not `{s}` a side értéke left vagy right lehet, nem `{s}` Ignore All Az összes kihagyása diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index 606ffae97..7c552f5ae 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -6534,6 +6534,7 @@ breakText Glyphs Glif # Align to Pixel Grid Align to Pixel Grid Sejajarkan ke grid piksel + Align New Objects to Pixel Grid Sejajarkan objek baru ke grid piksel @msg side is left or right, not `{s}` side adalah left atau right, bukan `{s}` Edit Custom Dictionary Edit kamus kustom Entry: Entri: diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index bf4c9e7b5..c3720f2bc 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -6412,6 +6412,7 @@ breakText Glyphs Glifi # Align to Pixel Grid Align to Pixel Grid Allinea alla griglia pixel + Align New Objects to Pixel Grid Allinea nuovi oggetti alla griglia pixel @msg only paths and compound paths can be aligned to the pixel grid solo i tracciati e i tracciati composti possono essere allineati alla griglia pixel @msg side is left or right, not `{s}` side è left o right, non `{s}` Ignore All Ignora tutto diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 7a12afcad..26868f5ed 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -6477,6 +6477,7 @@ breakText Glyphs 字形 # Align to Pixel Grid Align to Pixel Grid ピクセルグリッドに整合 + Align New Objects to Pixel Grid 新規オブジェクトをピクセルグリッドに整合 @msg only paths and compound paths can be aligned to the pixel grid ピクセルグリッドに整合できるのはパスと複合パスのみです @msg side is left or right, not `{s}` side は left または right です(`{s}` ではありません) Ignore All すべて無視 diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index c58480c42..82a07e5c6 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -6900,6 +6900,7 @@ font-script Japanese იაპონური # Align to Pixel Grid Align to Pixel Grid პიქსელურ ბადეზე გასწორება + Align New Objects to Pixel Grid ახალი ობიექტების პიქსელურ ბადეზე გასწორება @msg only paths and compound paths can be aligned to the pixel grid პიქსელურ ბადეზე მხოლოდ ტრაექტორიების და შედგენილი ტრაექტორიების გასწორებაა შესაძლებელი @msg side is left or right, not `{s}` side არის left ან right და არა `{s}` Ignore All ყველას იგნორირება diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index 39efdecf2..5e3e026d7 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -6373,6 +6373,7 @@ breakText Glyphs 글리프 # Align to Pixel Grid Align to Pixel Grid 픽셀 격자에 정렬 + Align New Objects to Pixel Grid 새 개체를 픽셀 격자에 정렬 @msg only paths and compound paths can be aligned to the pixel grid 패스와 컴파운드 패스만 픽셀 격자에 정렬할 수 있습니다 @msg side is left or right, not `{s}` side는 left 또는 right입니다(`{s}` 아님) Ignore All 모두 무시 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index dd1d22edc..421fd6e37 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -6585,6 +6585,7 @@ breakText Glyphs Glyphs # Align to Pixel Grid Align to Pixel Grid Uitlijnen op pixelraster + Align New Objects to Pixel Grid Nieuwe objecten uitlijnen op pixelraster @msg side is left or right, not `{s}` side is left of right, niet `{s}` Edit Custom Dictionary Aangepast woordenboek bewerken Entry: Item: diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index dd850a3a4..47d2e382f 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -6647,6 +6647,7 @@ breakText Glyphs Glify # Align to Pixel Grid Align to Pixel Grid Wyrównaj do siatki pikseli + Align New Objects to Pixel Grid Wyrównaj nowe obiekty do siatki pikseli @msg only paths and compound paths can be aligned to the pixel grid do siatki pikseli można wyrównać tylko ścieżki i ścieżki złożone @msg side is left or right, not `{s}` side to left lub right, a nie `{s}` Ignore All Ignoruj wszystko diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index f67ae6cda..6900a36ac 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -6864,6 +6864,7 @@ swatch Yellow Green Verde-amarelado # Align to Pixel Grid Align to Pixel Grid Alinhar à Grade de Pixels + Align New Objects to Pixel Grid Alinhar Novos Objetos à Grade de Pixels @msg only paths and compound paths can be aligned to the pixel grid somente caminhos e caminhos compostos podem ser alinhados à grade de pixels @msg side is left or right, not `{s}` side é left ou right, não `{s}` Ignore All Ignorar Tudo diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 8eb542582..238c3f650 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -6880,6 +6880,7 @@ shape Open Открытый # Align to Pixel Grid Align to Pixel Grid Выровнять по пиксельной сетке + Align New Objects to Pixel Grid Выравнивать новые объекты по пиксельной сетке @msg only paths and compound paths can be aligned to the pixel grid по пиксельной сетке можно выровнять только контуры и составные контуры @msg side is left or right, not `{s}` side — это left или right, а не `{s}` Ignore All Пропустить все diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index fb2c463de..543914cb1 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -8276,6 +8276,7 @@ digits Hindi Hindi # Align to Pixel Grid Align to Pixel Grid Justera mot pixelrutnät + Align New Objects to Pixel Grid Justera nya objekt mot pixelrutnät @msg only paths and compound paths can be aligned to the pixel grid bara banor och sammansatta banor kan justeras mot pixelrutnätet @msg side is left or right, not `{s}` side är left eller right, inte `{s}` Ignore All Ignorera alla diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index d45a171cc..e320f6607 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -6747,6 +6747,7 @@ breakText Glyphs Glifler # Align to Pixel Grid Align to Pixel Grid Piksel Izgarasına Hizala + Align New Objects to Pixel Grid Yeni Nesneleri Piksel Izgarasına Hizala @msg only paths and compound paths can be aligned to the pixel grid piksel ızgarasına yalnızca yollar ve bileşik yollar hizalanabilir @msg side is left or right, not `{s}` side left veya right olmalıdır, `{s}` değil Ignore All Tümünü Yoksay diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index acaf20b6b..627dfddf6 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -6392,6 +6392,7 @@ breakText Glyphs Гліфи # Align to Pixel Grid Align to Pixel Grid Вирівняти за піксельною сіткою + Align New Objects to Pixel Grid Вирівнювати нові об’єкти за піксельною сіткою @msg only paths and compound paths can be aligned to the pixel grid за піксельною сіткою можна вирівняти лише контури і складені контури @msg side is left or right, not `{s}` side — це left або right, а не `{s}` Ignore All Пропустити все diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index 4a2bbd5a6..7233395d7 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -6393,6 +6393,7 @@ breakText Glyphs Glyph # Align to Pixel Grid Align to Pixel Grid Căn theo lưới pixel + Align New Objects to Pixel Grid Căn đối tượng mới theo lưới pixel @msg only paths and compound paths can be aligned to the pixel grid chỉ đường dẫn và đường dẫn phức mới căn theo lưới pixel được @msg side is left or right, not `{s}` side là left hoặc right, không phải `{s}` Ignore All Bỏ qua tất cả diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 0eb7c7ebe..fd555b20c 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -6632,6 +6632,7 @@ breakText Glyphs 字形 # Align to Pixel Grid Align to Pixel Grid 对齐像素网格 + Align New Objects to Pixel Grid 将新对象对齐像素网格 @msg only paths and compound paths can be aligned to the pixel grid 只有路径和复合路径可以对齐像素网格 @msg side is left or right, not `{s}` side 应为 left 或 right,而不是 `{s}` Edit Custom Dictionary 编辑自定义词典 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 084a7e770..cca290a62 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -6430,6 +6430,7 @@ breakText Glyphs 字符 # Align to Pixel Grid Align to Pixel Grid 對齊像素格點 + Align New Objects to Pixel Grid 將新物件對齊像素格點 @msg only paths and compound paths can be aligned to the pixel grid 只有路徑和複合路徑可以對齊像素格點 @msg side is left or right, not `{s}` side 應為 left 或 right,而不是 `{s}` Edit Custom Dictionary 編輯自訂字典 diff --git a/crates/ui-egui/src/panels/transform.rs b/crates/ui-egui/src/panels/transform.rs index 25ddd4076..dc1e69548 100644 --- a/crates/ui-egui/src/panels/transform.rs +++ b/crates/ui-egui/src/panels/transform.rs @@ -581,6 +581,13 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { if menu_item(ui, tl!("Measure Type by Glyph Bounds"), true, glyphs) { set_pref(app, "typeGlyphBounds", !glyphs); } + // The active document's Align New Objects to Pixel Grid (`document.setup`). + let align_new = app.session.active().map(|st| st.doc.setup.align_new_objects_to_pixel_grid); + if menu_item(ui, tl!("Align New Objects to Pixel Grid"), align_new.is_some(), align_new == Some(true)) + && let Err(e) = app.run("document.setup", json!({"alignNewObjectsToPixelGrid": align_new != Some(true)})) + { + app.status(e); + } ui.separator(); // What the panel transforms (Transform Object Only / Pattern Only / Both), one checked. let current = pattern_mode(app); diff --git a/crates/ui-egui/src/panels/transform/tests.rs b/crates/ui-egui/src/panels/transform/tests.rs index 67cebc7cd..dd2752cb2 100644 --- a/crates/ui-egui/src/panels/transform/tests.rs +++ b/crates/ui-egui/src/panels/transform/tests.rs @@ -567,3 +567,34 @@ fn align_to_pixel_grid_shows_and_sets_the_property() { app.run("select.set", json!({"ids": [member]})).unwrap(); assert_eq!(pixel_aligned_state(&app).map(|s| s.1), Some(false)); } + +/// The panel menu's Align New Objects to Pixel Grid shows and toggles the active document's +/// setting, follows a switch to another document, and is unavailable with no document open. +#[test] +fn align_new_objects_menu_item_toggles_the_active_document() { + let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default()); + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + let item = "Align New Objects to Pixel Grid"; + let shown = + |ctx: &egui::Context, app: &mut VectorcraftApp| menu_texts(ctx, app).into_iter().find(|t| t.trim_start_matches('✓').trim_start() == item); + assert!(shown(&ctx, &mut app).is_some_and(|t| !t.starts_with('✓')), "no document"); + menu_click(&ctx, &mut app, item); + assert!(app.session.active().is_none()); + app.run("file.new", json!({"width": 300, "height": 300})).unwrap(); + app.run("file.new", json!({"preset": "Web 1920×1080"})).unwrap(); + let on = |app: &VectorcraftApp, i: usize| app.session.documents()[i].doc.setup.align_new_objects_to_pixel_grid; + assert_eq!((on(&app, 0), on(&app, 1)), (false, true)); + assert!(shown(&ctx, &mut app).is_some_and(|t| t.starts_with('✓')), "the Web document"); + app.session.set_active(0); + assert!(shown(&ctx, &mut app).is_some_and(|t| !t.starts_with('✓')), "the Letter-sized document"); + menu_click(&ctx, &mut app, item); + assert_eq!((on(&app, 0), on(&app, 1)), (true, true)); + assert!(shown(&ctx, &mut app).is_some_and(|t| t.starts_with('✓'))); + let r = app.run("shape.rectangle", json!({"x": 10.3, "y": 20.6, "width": 30.4, "height": 9.7})).unwrap(); + let id = vectorcraft_doc::NodeId(r["id"].as_u64().unwrap()); + assert!(app.session.active().unwrap().doc.node(id).unwrap().extras().pixel_aligned); + app.session.set_active(1); + menu_click(&ctx, &mut app, item); + assert_eq!((on(&app, 0), on(&app, 1)), (true, false)); +} diff --git a/docs/mcp.md b/docs/mcp.md index 5826c5c8c..3f65ac06e 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -4282,6 +4282,27 @@ files saved without it read back with it off. {"name":"run_command","arguments":{"command":"object.setPixelAligned","params":{"ids":[12],"on":false}}} ``` +### Align New Objects to Pixel Grid + +`document.setup {alignNewObjectsToPixelGrid: true|false}` turns the document's Align New Objects to Pixel Grid on or +off in one undo step, as the New Document dialog's Advanced Options checkbox and the Transform panel menu's checked item +do (described on the help page, +); +`document.setup` with no params reports it. While it is on, every path and compound path drawn in the document (the +shape, line, arc, spiral, Pen, Curvature, Pencil, Paintbrush, Blob Brush and `addon.shapes.*` commands, and the paths +in a new rectangular grid, polar grid or flare) takes the pixel-aligned property and is snapped in the undo step that +makes it. These rules are VectorCraft's own: type, images, groups and guides do not take it; art already in the document stays as it is when the setting changes; objects that Paste, Duplicate, Place and +import bring in keep their own property; and `object.setPixelAligned` turns one object's property off without changing +the document's setting. `file.new {alignNewObjectsToPixelGrid}` sets it for the new document. The Mobile and Web presets +start with it on and the other built-in presets with it off; saved presets and Recent entries keep the value they were +made with, and a saved preset from before the setting reads off. The setting is saved in the native file only when on, +and files saved without it open with it off. + +```json +{"name":"run_command","arguments":{"command":"document.setup","params":{"alignNewObjectsToPixelGrid":true}}} +{"name":"run_command","arguments":{"command":"file.new","params":{"preset":"Letter","alignNewObjectsToPixelGrid":true}}} +``` + ## Live Corners `object.setLiveShape {id?, ids?, radius?, kind?, corners?, items?}` sets the corners of any path (one undo step): a live @@ -4966,8 +4987,9 @@ Exporting and importing preferences is an optional VectorCraft feature. as a red outline around each artboard), the transparency grid (size and two colours; the first is also the simulated paper colour), the flattener preset and Discard White Overprint (Overprint Preview keeps white overprints visible while it is on), the type options (language and its quotes, Use Typographer's Quotes for typed straight quotes and a typed `”` where a quote opens, superscript, subscript -and small caps proportions, SVG text export) and the background contents (white: raster exports are white behind the -art). Pass any of these keys to change them in one undo step; `document.setUnits` stays as an alias for `units`. +and small caps proportions, SVG text export), the background contents (white: raster exports are white behind the +art) and Align New Objects to Pixel Grid (see [Align New Objects to Pixel Grid](#align-new-objects-to-pixel-grid)). +Pass any of these keys to change them in one undo step; `document.setUnits` stays as an alias for `units`. A file whose bleed (0 to 72 pt a side), superscript and subscript sizes (1 to 500%) and positions (-500 to 500%) or small caps size (1 to 200%) lies outside those ranges opens with the nearer end of each range. @@ -4980,7 +5002,8 @@ small caps size (1 to 200%) lies outside those ranges opens with the nearer end `file.new` takes everything New Document sets: `preset` (a name from `file.newPresets`), `name`, `width`/`height` (points, or lengths such as `"210 mm"`), `units`, `orientation`, `artboards` with `artboardLayout {layout, columns, spacing, rightToLeft}`, `bleed`, `backgroundContents`, `colorMode`, `rasterEffectsPpi` (1 to 2,400; a saved preset's -value outside that range is read as the nearer end) and `previewMode`. +value outside that range is read as the nearer end), `previewMode` and `alignNewObjectsToPixelGrid` (see +[Align New Objects to Pixel Grid](#align-new-objects-to-pixel-grid)). `file.newPresets {category?}` lists the categories and presets (Recent: the last sizes used; Saved: the user's presets, kept in the preferences by `file.newPresets.save` and removed by `file.newPresets.delete`). A listed preset can be passed straight back to `file.new`. Print presets and sizes without `units` start in `unitsGeneral`; screen presets diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index b4c949f74..2e5982da8 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -799,6 +799,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - The New Document from State button at the bottom of the History panel (`edit.newDocumentFromState`) makes a new document, which becomes active, from the state the panel has selected ([public source](https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/undo-edits-and-manage-design-history.html)). VectorCraft's own rules: the new document takes the next Untitled-N name, is marked unsaved, has nothing selected and has no steps to undo or redo, and the source document and its steps stay as they are. - The Transform panel menu's Transform Object Only, Transform Pattern Only and Transform Both (`transformPanelPatterns`) choose whether the fields, flips and menu flips of the Transform panel and the Properties panel's Transform section move the object and leave its pattern tiles, move the tiles alone, or both (until a choice they follow General › Transform Pattern Tiles, as before); Use Registration Point for Symbol (`useRegistrationPointForSymbol`) measures and transforms one selected symbol instance from its symbol's registration point, where the instance's flips, rotations and shears put it ([Transform panel overview](https://helpx.adobe.com/illustrator/desktop/manage-objects/reshape-transform-objects/transform-panel-overview.html); `panels::transform::tests::the_menu_chooses_whether_the_object_its_pattern_or_both_transform`, `use_registration_point_for_symbol_measures_an_instance_from_its_registration_point`, `use_registration_point_for_symbol_follows_a_transformed_instance`, `tests_prefs::transform_panel_patterns_defaults_to_following_the_general_preference`). - The Transform panel's Align to Pixel Grid checkbox (`object.setPixelAligned`) gives paths and compound paths a pixel-aligned property, saved in the native file: their horizontal and vertical straight segments sit on whole points (half points under an odd centered stroke) with whole stroke weights, and every move, scale, rotation, reflection, Transform Each, Align move, Transform panel change, move of direct-selected anchors or of one anchor, move of a compound path's path on its own, artboard move, resize or copy that carries the art, Duplicate or paste that moves a copy, stroke edit and appearance edit that adds, removes, hides, repaints or reweighs a stroke snaps them again, while diagonal segments and curves keep their shape; a compound path made from an aligned path or compound path is aligned and snaps its paths with its own stroke; the box shows a dash for a mixed selection and is unavailable for type, images and groups ([Pixel-aligned property](https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/pixel-preview-mode-for-web-graphics.html); `tests::pixelalign::turning_it_on_snaps_the_path_in_one_undo_step`, `it_stays_aligned_through_transforms`, `it_stays_aligned_when_its_artboard_moves`, `diagonal_segments_and_curves_keep_their_shape`, `type_and_images_cannot_be_aligned`, `the_property_is_saved_and_read_back`, `stroke_edits_keep_it_aligned`, `a_compound_path_of_aligned_paths_is_aligned`, `a_compound_path_of_an_aligned_compound_path_is_aligned`, `a_stroke_without_paint_snaps_to_whole_points`, `moving_direct_selected_anchors_keeps_it_aligned`, `moving_one_anchor_keeps_it_aligned`, `moving_a_path_of_an_aligned_compound_path_keeps_it_aligned`, `artboard_options_and_artboard_copies_keep_it_aligned`, `appearance_edits_keep_it_aligned`, `placed_gradients_follow_the_snapped_path`, `duplicates_and_pastes_keep_it_aligned`, `paths_added_to_or_moved_in_an_aligned_compound_path_keep_it_aligned`, `the_art_of_a_linked_opacity_mask_keeps_it_aligned`, `a_linked_opacity_mask_follows_a_snap`, `aligning_direct_selected_anchors_keeps_it_aligned`, `global_edit_keeps_its_members_aligned`, `a_path_with_live_corners_snaps_its_inner_edges`, `geometry_edits_keep_it_aligned`, `other_path_edits_keep_it_aligned`, `panels::transform::tests::align_to_pixel_grid_shows_and_sets_the_property`). + - Align New Objects to Pixel Grid (`document.setup {alignNewObjectsToPixelGrid}`, a checkbox in New Document's Advanced Options and a checked item in the Transform panel menu) gives every path and compound path drawn in the document, the paths of a new grid or flare included, the pixel-aligned property, snapped in the step that draws it; it is on in the Mobile and Web presets and off in the others, saved presets and Recent keep it, and the document saves it. Type, images and groups do not take it, and art already in the document, pasted, duplicated or placed keeps its own property (VectorCraft's own rules) ([Pixel-aligned property](https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/pixel-preview-mode-for-web-graphics.html); `tests::pixelalign::align_new_objects_makes_new_paths_pixel_aligned`, `align_new_objects_reaches_grids_flares_and_add_on_shapes`, `align_new_objects_is_saved_and_read_back`, `tests::newdoc::align_new_objects_to_pixel_grid_follows_the_preset`, `panels::transform::tests::align_new_objects_menu_item_toggles_the_active_document`, `dialogs::new_document::tests::align_new_objects_checkbox_follows_the_preset_and_reaches_the_document`). - Object › Transform › Reset Bounding Box squares the box of every object inside the selected ones as well: the children of groups, compound paths, clip groups, blends, envelopes, repeats and intertwines, and the art of linked opacity masks. Ungroup and the release commands then bring back boxes square to the page (VectorCraft's own behavior; `bboxrotate::reset_bounding_box_squares_the_objects_inside_a_group`, `reset_bounding_box_squares_the_turned_members_of_a_square_group`, `reset_bounding_box_then_release_clipping_mask` and the other `reset_bounding_box_then_*` tests, `reset_bounding_box_leaves_the_art_of_an_unlinked_opacity_mask`). - Native macOS menu bar, vector tool cursors, a ⌘K command palette, middle-button panning with any tool. Native menus explicitly end tracking on command selection, outside window presses and Escape; menu updates wait until tracking ends, and a dismissing press never starts a canvas gesture (M14.8). - The errors of the blend spine commands (Move Spine Point, Reshape Spine Segment, Add Spine Point and Delete Spine Point) show in the status bar in the interface language: each has a row in every catalog that translates all status and error messages and in Bulgarian (`blend_spine_param_errors_are_translated`). diff --git a/docs/ui-parity.md b/docs/ui-parity.md index 296826671..a1d72ed8c 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -271,6 +271,7 @@ tools, never checked side by side) isn't captured row by row. | Split Into Grid dialog: number, height or width, gutter and total for rows and columns, Add Guides, Preview | done | #1462; opens at the selection's size; several paths become one grid painted as the topmost one; a typed height or width keeps the gutter and changes the total (our choice) | | Reference point locator (nine squares) in Control bar and Transform panel | done | | | Transform panel menu: Transform Object Only, Transform Pattern Only, Transform Both (one checked), Use Registration Point for Symbol | done | `transformPanelPatterns` (`object`, `pattern`, `both`) sends `objects` and `patterns` with every field, flip and menu flip of the Transform panel and the Properties panel's Transform section; `useRegistrationPointForSymbol` makes them measure and transform one selected symbol instance from its symbol's registration point (Symbol Options › Registration), where the instance's flips, rotations and shears put it, instead of the reference point; the Properties panel's rotation and flips turn and mirror about it too ([Transform panel overview](https://helpx.adobe.com/illustrator/desktop/manage-objects/reshape-transform-objects/transform-panel-overview.html)); until a choice is made (`default`) the panels follow General › Transform Pattern Tiles as before, the menu checking Transform Both while it is on and Transform Object Only while it is off | +| Transform panel menu and New Document › Advanced Options: Align New Objects to Pixel Grid | done | `document.setup {alignNewObjectsToPixelGrid}` and `file.new {alignNewObjectsToPixelGrid}`: paths and compound paths drawn while it is on are pixel-aligned; on in the Mobile and Web presets; art already in the document, pasted, duplicated or placed keeps its own property (our choice) | | Transform panel, Properties and Control bar X/Y/W/H of the anchor points selected with Direct Selection | done | #1839 (fixes #1824): they show and edit the selected points' bounds; a whole-object selection is unchanged | ## Nudging, numeric entry and precision