diff --git a/crates/engine/src/cmd/prefscmds.rs b/crates/engine/src/cmd/prefscmds.rs index a73485f4fa..748e0a125a 100644 --- a/crates/engine/src/cmd/prefscmds.rs +++ b/crates/engine/src/cmd/prefscmds.rs @@ -454,10 +454,10 @@ pub fn spec(key: &str) -> Option<&'static PrefSpec> { /// Preferences kept as one object with a command of their own instead of [`PREF_SPECS`] rows (they /// aren't in the Preferences dialog): the Eyedropper Options (`eyedropper.setOptions`), the /// Perspective Grid Options (`perspective.widget.options`), the Pathfinder Options -/// (`object.pathfinder.options`) and the Snap to Glyph options (`view.snapToGlyph.options`). -/// `prefs.get` and `prefs.set` take them by key (a partial object updates what it names) and -/// `prefs.reset` without a category resets them. -pub const PREF_GROUPS: &[&str] = &["eyedropper", "perspectiveWidget", "pathfinder", "glyphSnap"]; +/// (`object.pathfinder.options`), the Snap to Glyph options (`view.snapToGlyph.options`) and the +/// Pixel Snapping Options (`view.snapToPixel.options`). `prefs.get` and `prefs.set` take them by key +/// (a partial object updates what it names) and `prefs.reset` without a category resets them. +pub const PREF_GROUPS: &[&str] = &["eyedropper", "perspectiveWidget", "pathfinder", "glyphSnap", "pixelSnap"]; /// Validate a value for preference group `key` against `current`. fn validate_group(key: &str, current: &Value, v: &Value) -> std::result::Result { @@ -477,6 +477,10 @@ fn validate_group(key: &str, current: &Value, v: &Value) -> std::result::Result< let cur: vectorcraft_tools::GlyphSnap = serde_json::from_value(current.clone()).unwrap_or_default(); cur.merged(v).map(|o| json!(o)) } + "pixelSnap" => { + let cur: vectorcraft_tools::PixelSnap = serde_json::from_value(current.clone()).unwrap_or_default(); + cur.merged(v).map(|o| json!(o)) + } "perspectiveWidget" => { let mut merged = current.clone(); if let (Some(o), Some(p)) = (merged.as_object_mut(), v.as_object()) { @@ -788,7 +792,7 @@ pub fn specs() -> Vec { "Export Preferences", [], None, - "{path?} write the preferences as a JSON file {format: \"vectorcraft-preferences\", version: 1, prefs: {key: value, …}}: every preference prefs.list lists and the eyedropper, perspectiveWidget, pathfinder and glyphSnap groups, without the folder preferences fontsFolder, pluginsFolder, recoveryFolder and templatesFolder and without saved presets; a file over 1 MB, which prefs.import would refuse, is an error and is not written → {path, count}; without path → {data: the file's text, count}", + "{path?} write the preferences as a JSON file {format: \"vectorcraft-preferences\", version: 1, prefs: {key: value, …}}: every preference prefs.list lists and the eyedropper, perspectiveWidget, pathfinder, glyphSnap and pixelSnap groups, without the folder preferences fontsFolder, pluginsFolder, recoveryFolder and templatesFolder and without saved presets; a file over 1 MB, which prefs.import would refuse, is an error and is not written → {path, count}; without path → {data: the file's text, count}", always, export ), diff --git a/crates/engine/src/cmd/views.rs b/crates/engine/src/cmd/views.rs index 560da3942c..6e26e9a9e6 100644 --- a/crates/engine/src/cmd/views.rs +++ b/crates/engine/src/cmd/views.rs @@ -1,5 +1,6 @@ //! View → New View… / Edit Views…: saved views stored in the document; per-document view -//! toggles (Show Transparency Grid); the Character panel's Snap to Glyph options. +//! toggles (Show Transparency Grid); the Character panel's Snap to Glyph options; the Pixel +//! Snapping Options. use serde_json::{Value, json}; use vectorcraft_apischema::{Bound, Param, ParamSchema, Type, Writes}; @@ -72,6 +73,18 @@ static GLYPH_SNAP: ParamSchema = ParamSchema::new( .returns("the options") .writes(Writes::Preferences); +static PIXEL_SNAP: ParamSchema = ParamSchema::new( + &[ + Param::new("drawing", Type::Boolean).optional(), + Param::new("moving", Type::Boolean).optional(), + Param::new("scaling", Type::Boolean).optional(), + ], + "the Pixel Snapping Options, kept in the preferences (the pixelSnap group, all on by default): which edits View › Snap to Pixel lands on whole pixels: drawing (the points the drawing tools place, the rulers' origin and measured points), moving (the selection, its anchors and segments, artboards and ruler guides moved, and the Rotate, Reflect and Shear tools' drags) and scaling (the edges a bounding-box, Free Transform, artboard or slice handle drags, and the Scale tool's drag); an edit whose option is off snaps as it does with Snap to Pixel off; {} reads them", +) +.returns("the options") +.writes(Writes::Preferences) +.since(4); + pub fn specs() -> Vec { vec![ cmd!( @@ -125,6 +138,16 @@ pub fn specs() -> Vec { glyph_snap_options, schema: &GLYPH_SNAP ), + cmd!( + query "view.snapToPixel.options", + "Pixel Snapping Options", + [], + None, + "{drawing?: bool, moving?: bool, scaling?: bool} the Pixel Snapping Options, kept in the preferences (the pixelSnap group, all on by default): which edits View › Snap to Pixel lands on whole pixels: drawing (the points the drawing tools place, the rulers' origin and measured points), moving (the selection, its anchors and segments, artboards and ruler guides moved, and the Rotate, Reflect and Shear tools' drags) and scaling (the edges a bounding-box, Free Transform, artboard or slice handle drags, and the Scale tool's drag); an edit whose option is off snaps as it does with Snap to Pixel off; {} reads them → the options", + always, + pixel_snap_options, + schema: &PIXEL_SNAP + ), ] } @@ -133,6 +156,11 @@ fn glyph_snap_options(s: &mut Session, p: &Value) -> Result { Ok(json!(s.prefs.glyph_snap)) } +fn pixel_snap_options(s: &mut Session, p: &Value) -> Result { + s.prefs.pixel_snap = s.prefs.pixel_snap.merged(p).map_err(|e| bad("view.snapToPixel.options", e))?; + Ok(json!(s.prefs.pixel_snap)) +} + fn caps_lock(s: &mut Session, p: &Value) -> Result { if let Some(on) = p.get("on") { s.caps_lock = on.as_bool().ok_or_else(|| bad("view.capsLock", "on must be true or false"))?; @@ -379,15 +407,17 @@ mod tests { ("view.saved.list", json!({"all": true}), "`all`"), ("view.transparencyGrid", json!({"on": 1}), "`on`"), ("view.snapToGlyph.options", json!({"bounds": "off"}), "`bounds`"), + ("view.snapToPixel.options", json!({"moving": "off"}), "`moving`"), + ("view.snapToPixel.options", json!({"drawing": false, "rotating": false}), "`rotating`"), ] { let st = s.doc().unwrap(); let doc = (st.doc.views.clone(), st.history.undo.len(), st.is_dirty()); - let (grid, caps, snap) = (st.transparency_grid, s.caps_lock, s.prefs.glyph_snap); + let (grid, caps, snap, pixel) = (st.transparency_grid, s.caps_lock, s.prefs.glyph_snap, s.prefs.pixel_snap); let e = s.execute(id, ¶ms).unwrap_err().to_string(); assert!(e.contains(id) && e.contains(named), "{id} {params}: {e}"); let st = s.doc().unwrap(); assert_eq!((&st.doc.views, st.history.undo.len(), st.is_dirty()), (&doc.0, doc.1, doc.2), "{id} {params}"); - assert_eq!((st.transparency_grid, s.caps_lock, s.prefs.glyph_snap), (grid, caps, snap), "{id} {params}"); + assert_eq!((st.transparency_grid, s.caps_lock, s.prefs.glyph_snap, s.prefs.pixel_snap), (grid, caps, snap, pixel), "{id} {params}"); } } diff --git a/crates/engine/src/prefs.rs b/crates/engine/src/prefs.rs index 05ef5e84f5..98813e999f 100644 --- a/crates/engine/src/prefs.rs +++ b/crates/engine/src/prefs.rs @@ -260,6 +260,9 @@ pub struct Prefs { /// Character panel › Show Snap to Glyph Options: which guides View › Snap to Glyph gives /// (`view.snapToGlyph.options`; a preference group, see [`cmd::prefscmds::PREF_GROUPS`]). pub glyph_snap: vectorcraft_tools::GlyphSnap, + /// Pixel Snapping Options: whether View › Snap to Pixel snaps drawing, moving and scaling + /// (`view.snapToPixel.options`; a preference group, see [`cmd::prefscmds::PREF_GROUPS`]). + pub pixel_snap: vectorcraft_tools::PixelSnap, /// Edit → Transparency Flattener Presets: the user's presets (the built-in ones aren't stored). /// Not a `prefs.set` key or group (resetting the preferences keeps them): `flattener.presets.*` /// edit it. @@ -523,6 +526,7 @@ impl Default for Prefs { eyedropper: Default::default(), pathfinder: Default::default(), glyph_snap: Default::default(), + pixel_snap: Default::default(), flattener_presets: vec![], trace_presets: vec![], gradient_presets: vec![], diff --git a/crates/engine/src/schemas.rs b/crates/engine/src/schemas.rs index 08bc0f719d..8be87b5ab8 100644 --- a/crates/engine/src/schemas.rs +++ b/crates/engine/src/schemas.rs @@ -26,10 +26,11 @@ use crate::cmd::{CommandSpec, bad, command_specs}; use crate::{EngineError, Result}; /// The API level of the commands' parameters. -pub const API_VERSION: u32 = 3; +pub const API_VERSION: u32 = 4; /// [`schema_hash`] at each API level, oldest first; the last row is [`API_VERSION`]'s. -pub const SCHEMA_HASHES: &[(u32, u64)] = &[(1, 0x9cdb_1087_7074_855c), (2, 0x0e31_5d96_7bc9_2653), (3, 0xffe0_58a3_2f0c_aa2a)]; +pub const SCHEMA_HASHES: &[(u32, u64)] = + &[(1, 0x9cdb_1087_7074_855c), (2, 0x0e31_5d96_7bc9_2653), (3, 0xffe0_58a3_2f0c_aa2a), (4, 0x1be4_be72_fe16_54c4)]; /// The hash of every built-in command's schema ([`vectorcraft_apischema::hash`]). pub fn schema_hash() -> u64 { @@ -311,12 +312,17 @@ mod tests { ("view.transparencyGrid", json!({"on": 1})), ("view.snapToGlyph.options", json!({"bounds": false, "angular": true})), ("view.snapToGlyph.options", json!({"bounds": null})), + ("view.snapToPixel.options", json!({"drawing": false, "scaling": true})), + ("view.snapToPixel.options", json!({"moving": null})), + ("view.snapToPixel.options", json!({"moving": "off"})), + ("view.snapToPixel.options", json!({"rotating": false})), ("view.capsLock", json!(null)), ("view.saved.new", json!(null)), ("view.saved.edit", json!(null)), ("view.saved.list", json!(null)), ("view.transparencyGrid", json!(null)), ("view.snapToGlyph.options", json!(null)), + ("view.snapToPixel.options", json!(null)), ("view.capsLock", json!([])), ("view.saved.new", json!(5)), ("view.transparencyGrid", json!("on")), @@ -325,7 +331,7 @@ mod tests { ("engine.apiReference", json!({"prefix": "view."})), ("engine.apiReference", json!([])), ]; - assert_eq!(schemas().len(), 7); + assert_eq!(schemas().len(), 8); for (id, schema) in schemas() { let written = schema.to_json(); check_schema(&written, id).unwrap_or_else(|e| panic!("{e}\n{written:#}")); @@ -423,6 +429,7 @@ mod tests { ("view.saved.list", "nothing"), ("view.transparencyGrid", "session"), ("view.snapToGlyph.options", "preferences"), + ("view.snapToPixel.options", "preferences"), ]; for (id, writes) in expected { let got = listed.iter().find(|(c, _)| *c == id).map(|(_, w)| w.clone()); diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 2352776651..6c231dc7e5 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -4033,6 +4033,38 @@ fn snap_to_pixel_rounds_drawing_and_moves() { assert_eq!((b.x0, b.y0), (17.0, 16.0)); } +/// Pixel Snapping Options (`view.snapToPixel.options`): with While Drawing off a rectangle drawn +/// with Snap to Pixel on keeps its fractional corners while a move still lands on whole pixels, and +/// with While Moving off a move keeps its fractional offset while drawing snaps again. +#[test] +fn pixel_snapping_options_choose_the_edits_snap_to_pixel_rounds() { + let mut s = session(); + let v = ViewInfo { snap_to_pixel: true, smart_guides: false, snap_to_point: false, ..Default::default() }; + let drag = |s: &mut Session, tool: &str, from: (f64, f64), to: (f64, f64)| { + s.select_tool(tool, v).unwrap(); + s.pointer(&PointerEvent::new(PointerKind::Down, from.0, from.1), v).unwrap(); + s.pointer(&PointerEvent::new(PointerKind::Drag, to.0, to.1), v).unwrap(); + s.pointer(&PointerEvent::new(PointerKind::Up, to.0, to.1), v).unwrap(); + }; + let bounds = |s: &Session, id| { + let b = s.doc().unwrap().doc.node(id).unwrap().geometric_bounds().unwrap(); + (b.x0, b.y0, b.x1, b.y1) + }; + s.execute("view.snapToPixel.options", &json!({"drawing": false})).unwrap(); + drag(&mut s, "rectangle", (10.25, 10.5), (60.5, 40.25)); + let id = s.doc().unwrap().selection.objects[0]; + assert_eq!(bounds(&s, id), (10.25, 10.5, 60.5, 40.25), "drawing does not snap"); + drag(&mut s, "selection", (30.0, 30.0), (37.5, 34.75)); + assert_eq!(bounds(&s, id).0, 18.0, "moving still snaps the left edge"); + assert_eq!(bounds(&s, id).1, 15.0, "moving still snaps the top edge"); + s.execute("view.snapToPixel.options", &json!({"drawing": true, "moving": false})).unwrap(); + drag(&mut s, "selection", (40.0, 30.0), (45.25, 33.5)); + assert_eq!((bounds(&s, id).0, bounds(&s, id).1), (23.25, 18.5), "moving does not snap"); + drag(&mut s, "rectangle", (100.25, 100.5), (140.5, 130.75)); + let drawn = s.doc().unwrap().selection.objects[0]; + assert_eq!(bounds(&s, drawn), (100.0, 101.0, 141.0, 131.0), "drawing snaps again"); +} + #[test] fn shaper_turns_rough_strokes_into_live_shapes_and_scribbles_punch_fills() { let mut s = session(); diff --git a/crates/engine/src/tests/prefs.rs b/crates/engine/src/tests/prefs.rs index 43c7b048c4..3808d06797 100644 --- a/crates/engine/src/tests/prefs.rs +++ b/crates/engine/src/tests/prefs.rs @@ -2798,3 +2798,33 @@ fn snap_to_glyph_options_read_set_and_reset() { s.execute("prefs.reset", &json!({})).unwrap(); assert_eq!(s.execute("view.snapToGlyph.options", &json!({})).unwrap(), all); } + +/// Pixel Snapping Options: `view.snapToPixel.options` reads and sets While Drawing, While Moving +/// and While Scaling, all on by default, kept as the `pixelSnap` preference group; bad keys and +/// values change nothing, preferences saved without the group read it as all on, and resetting the +/// preferences turns them all back on. +#[test] +fn pixel_snapping_options_read_set_and_reset() { + let mut s = Session::new(); + let all = json!({"drawing": true, "moving": true, "scaling": true}); + assert_eq!(s.execute("view.snapToPixel.options", &json!({})).unwrap(), all); + let set = json!({"drawing": true, "moving": false, "scaling": true}); + assert_eq!(s.execute("view.snapToPixel.options", &json!({"moving": false})).unwrap(), set); + assert_eq!(s.execute("prefs.get", &json!({"key": "pixelSnap"})).unwrap(), set); + assert!(!s.prefs.pixel_snap.moving && s.prefs.pixel_snap.drawing && s.prefs.pixel_snap.scaling); + for bad in [json!({"moving": 1}), json!({"rotating": false})] { + assert!(s.execute("view.snapToPixel.options", &bad).is_err(), "{bad}"); + assert!(s.execute("prefs.set", &json!({"key": "pixelSnap", "value": bad})).is_err(), "{bad}"); + assert_eq!(s.execute("view.snapToPixel.options", &json!({})).unwrap(), set, "{bad}"); + } + s.execute("prefs.set", &json!({"key": "pixelSnap", "value": {"scaling": false}})).unwrap(); + let both = json!({"drawing": true, "moving": false, "scaling": false}); + assert_eq!(s.execute("view.snapToPixel.options", &json!({})).unwrap(), both); + // Preferences written before the group existed, and a group that names only some options. + let older: Prefs = serde_json::from_value(json!({"keyboardIncrement": 4})).unwrap(); + assert_eq!(older.pixel_snap, vectorcraft_tools::PixelSnap::default()); + let partial: Prefs = serde_json::from_value(json!({"pixelSnap": {"drawing": false}})).unwrap(); + assert_eq!(partial.pixel_snap, vectorcraft_tools::PixelSnap { drawing: false, moving: true, scaling: true }); + s.execute("prefs.reset", &json!({})).unwrap(); + assert_eq!(s.execute("view.snapToPixel.options", &json!({})).unwrap(), all); +} diff --git a/crates/engine/src/tooling.rs b/crates/engine/src/tooling.rs index fa50c5ce6a..0452251a09 100644 --- a/crates/engine/src/tooling.rs +++ b/crates/engine/src/tooling.rs @@ -25,7 +25,8 @@ pub struct ViewInfo { /// View → Pixel Preview: while it is on, the tools use no Smart Guides. pub pixel_preview: bool, pub show_bbox: bool, - /// View → Snap to Pixel: drawing and moving land on whole pixels (points at 72 ppi). + /// View → Snap to Pixel: drawing, moving and scaling land on whole pixels (points at 72 ppi), + /// each while its Pixel Snapping option (`view.snapToPixel.options`) is on. pub snap_to_pixel: bool, /// View → Snap to Point: picked points (a transform's reference point) land on anchors. pub snap_to_point: bool, @@ -368,6 +369,7 @@ impl Session { // Snap to Grid and Snap to Pixel are exclusive, as in Illustrator; left both on, the grid // wins instead of pixels quietly rounding every drag to whole points (#1816). snap_to_pixel: view.snap_to_pixel && !view.snap_to_grid, + pixel_snap: self.prefs.pixel_snap, snap_to_point: view.snap_to_point, snap_tangent: self.prefs.snap_tangent, snap_perpendicular: self.prefs.snap_perpendicular, diff --git a/crates/mcp/src/ui_catalog.rs b/crates/mcp/src/ui_catalog.rs index e94164cc15..3e80e0a2dd 100644 --- a/crates/mcp/src/ui_catalog.rs +++ b/crates/mcp/src/ui_catalog.rs @@ -195,7 +195,12 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[ ("view.pixelPreview", "Pixel Preview", "Cmd+Alt+Y", "{}"), ("view.trimView", "Trim View", "", "{} toggle: hide everything outside the artboards"), ("view.cornerWidget", "Hide Corner Widget", "", "{} toggle the live corner widgets"), - ("view.snapToPixel", "Snap to Pixel", "", "{} toggle: drawing and moving land on whole pixels"), + ( + "view.snapToPixel", + "Snap to Pixel", + "", + "{} toggle: drawing, moving and scaling land on whole pixels, each while its option in view.snapToPixel.options is on", + ), ( "view.snapToGlyph", "Snap to Glyph", diff --git a/crates/tools/src/context.rs b/crates/tools/src/context.rs index 741e962290..e6db6069c7 100644 --- a/crates/tools/src/context.rs +++ b/crates/tools/src/context.rs @@ -117,6 +117,53 @@ impl GlyphSnap { } } +/// The Pixel Snapping Options (`view.snapToPixel.options`): which edits View › Snap to Pixel lands +/// on whole pixels. All on by default. +#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[serde(default, rename_all = "camelCase")] +pub struct PixelSnap { + /// While Drawing: points placed by the drawing tools, ruler origins and measurements. + pub drawing: bool, + /// While Moving: the selection, its anchors and segments, artboards and ruler guides moved. + pub moving: bool, + /// While Scaling: the edges a bounding-box, Free Transform, Scale tool, artboard or slice + /// handle drags. + pub scaling: bool, +} + +impl Default for PixelSnap { + fn default() -> Self { + Self { drawing: true, moving: true, scaling: true } + } +} + +impl PixelSnap { + /// These options with the ones `p` names (booleans), or why `p` can't set them. + pub fn merged(self, p: &serde_json::Value) -> Result { + let mut o = serde_json::to_value(self).map_err(|e| e.to_string())?; + match (p, o.as_object_mut()) { + (serde_json::Value::Null, _) => {} + (serde_json::Value::Object(given), Some(cur)) => { + for (k, v) in given { + let slot = cur.get_mut(k).ok_or_else(|| format!("unknown Pixel Snapping option `{k}`"))?; + *slot = serde_json::Value::Bool(v.as_bool().ok_or_else(|| format!("`{k}` must be true or false"))?); + } + } + _ => return Err("the Pixel Snapping options must be an object".into()), + } + serde_json::from_value(o).map_err(|e| e.to_string()) + } +} + +/// The kind of edit a snap serves, which picks the Pixel Snapping option that applies to it +/// ([`ToolContext::snaps_to_pixel`]). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum PixelOp { + Drawing, + Moving, + Scaling, +} + /// Read-only context a tool sees. pub struct ToolContext<'a> { pub doc: &'a Document, @@ -141,8 +188,11 @@ pub struct ToolContext<'a> { pub snap_to_grid: bool, /// Bounding box shown (View → Show/Hide Bounding Box). pub show_bbox: bool, - /// View → Snap to Pixel. + /// View → Snap to Pixel. Which edits it snaps is [`Self::pixel_snap`]'s choice + /// ([`Self::snaps_to_pixel`]). pub snap_to_pixel: bool, + /// The Pixel Snapping Options: whether Snap to Pixel snaps drawing, moving and scaling. + pub pixel_snap: PixelSnap, /// View → Snap to Point: with Smart Guides off, dragged selections, drawn points and picked /// points (a transform's reference point) land on anchors and ruler guides within /// [`Self::snap_tolerance`]. @@ -342,6 +392,16 @@ impl ToolContext<'_> { let (dist, ang, dx, dy) = xform::measure(a, b); format!("D: {}\nAngle: {ang:.1}°\n{}", self.len(dist), self.offset_label(dx, dy)) } + /// Does View › Snap to Pixel land an edit of kind `op` on whole pixels: is it on, with the + /// Pixel Snapping option for `op` on too? + pub fn snaps_to_pixel(&self, op: PixelOp) -> bool { + self.snap_to_pixel + && match op { + PixelOp::Drawing => self.pixel_snap.drawing, + PixelOp::Moving => self.pixel_snap.moving, + PixelOp::Scaling => self.pixel_snap.scaling, + } + } /// What Snap to Grid snaps to: the grid's subdivisions (or its lines without any). pub fn grid_step(&self) -> f64 { self.doc.grid.spacing / self.doc.grid.subdivisions.max(1) as f64 diff --git a/crates/tools/src/direct.rs b/crates/tools/src/direct.rs index 3addd48785..3030414b38 100644 --- a/crates/tools/src/direct.rs +++ b/crates/tools/src/direct.rs @@ -66,7 +66,7 @@ use crate::meshedit::{MeshEdit, grid_of}; use crate::pathtype::{self, BracketDrag, near_bracket}; use crate::rulerguide::GuideEdit; use crate::select::{MoveSnap, hit_center, hold_point, is_area_type, is_path_type, matrix_json, release_guide_at, selection_bounds}; -use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext, ToolKey}; +use crate::{Action, Cursor, Mods, Overlay, PixelOp, PointerEvent, PointerKind, Tool, ToolContext, ToolKey}; #[derive(Clone, Debug)] enum State { @@ -1465,11 +1465,11 @@ impl DirectSelectionTool { } out.push(Action::Begin("Move".into())); self.state = State::MoveAnchors { start, grab, began: true, segment }; - self.anchor_snap = Some(PointSnap::new(cx, || Targets::for_anchor_drag(cx.doc, cx.selection))); + self.anchor_snap = Some(PointSnap::moving(cx, || Targets::for_anchor_drag(cx.doc, cx.selection))); self.line_end = line_end_leave(cx, grab); // A segment held by a point along it lands its anchors on the grid (the box round // them, an edge on a gridline on each axis), not that point (#3835). - let grid = segment && (cx.snap_to_grid || cx.snap_to_pixel); + let grid = segment && (cx.snap_to_grid || cx.snaps_to_pixel(PixelOp::Moving)); self.partial_grid = grid.then(|| MoveSnap::grid_only(vectorcraft_effects::preview::anchor_bounds_with(cx.selection, cx.doc, cx.bounds_measure()))); } @@ -1505,8 +1505,8 @@ impl DirectSelectionTool { // them and the objects selected whole (`path.moveAnchors`), as an anchor drag // does, while Alt is up; the other anchors of a partly selected path stay in // place. The history names the move or the copy at the end. - self.partial_snap = (!cx.selection.anchors.is_empty()).then(|| PointSnap::new(cx, || Targets::for_anchor_drag(cx.doc, cx.selection))); - let grid = self.partial_snap.is_some() && grab.is_none() && (cx.snap_to_grid || cx.snap_to_pixel); + self.partial_snap = (!cx.selection.anchors.is_empty()).then(|| PointSnap::moving(cx, || Targets::for_anchor_drag(cx.doc, cx.selection))); + let grid = self.partial_snap.is_some() && grab.is_none() && (cx.snap_to_grid || cx.snaps_to_pixel(PixelOp::Moving)); self.partial_grid = grid.then(|| MoveSnap::grid_only(vectorcraft_effects::preview::anchor_bounds_with(cx.selection, cx.doc, cx.bounds_measure()))); let copy = ev.mods.alt && self.partial_snap.is_none(); @@ -1514,8 +1514,8 @@ impl DirectSelectionTool { self.state = State::MoveObject { start, grab, hold, began: true, deselect: None }; if grab.is_some() { // Alt can go down or up at any time: gather both while nothing has moved. - self.anchor_snap = Some(PointSnap::new(cx, || Targets::for_anchor_drag(cx.doc, cx.selection))); - self.copy_snap = Some(PointSnap::new(cx, || Targets::for_anchor_drag(cx.doc, &Selection::default()))); + self.anchor_snap = Some(PointSnap::moving(cx, || Targets::for_anchor_drag(cx.doc, cx.selection))); + self.copy_snap = Some(PointSnap::moving(cx, || Targets::for_anchor_drag(cx.doc, &Selection::default()))); // As a move with the Selection tool does ([`MoveSnap::new`]). let back = cx.smart_guides && !cx.snap_to_grid && cx.snap_to_last_location; self.last_location = back.then(|| (Targets::last_location(cx), selection_bounds(cx))); diff --git a/crates/tools/src/direct/tests.rs b/crates/tools/src/direct/tests.rs index 0ac443c731..7ce48bdc1e 100644 --- a/crates/tools/src/direct/tests.rs +++ b/crates/tools/src/direct/tests.rs @@ -634,6 +634,33 @@ fn snap_to_pixel_wins_over_snap_to_last_location_for_a_path_moved_by_its_anchor( assert!(labels.is_empty(), "{labels:?}"); } +/// Pixel Snapping Options › While Moving off: a direct-selected anchor dragged with Snap to Pixel +/// on keeps its fractional offset; with the option on it lands on whole pixels. +#[test] +fn an_anchor_drag_follows_the_while_moving_option() { + use crate::PixelSnap; + let mut d = vectorcraft_doc::Document::new(500.0, 500.0); + let l = d.layers[0].id; + let id = d.alloc_id(); + let square = vectorcraft_geom::shapes::rectangle(Rect::new(100.3, 100.4, 200.3, 200.4)); + d.insert(Some(l), 0, vectorcraft_doc::Node::path(id, square, vectorcraft_doc::Appearance::default_art())).unwrap(); + let s = anchors_of(id, &[(0, 0)]); + let p = paint(); + let moved = |pixel_snap: PixelSnap| { + let c = ToolContext { snap_to_pixel: true, pixel_snap, smart_guides: false, snap_to_point: false, ..cx(&d, &s, &p) }; + let mut t = DirectSelectionTool::new(false); + t.pointer(&c, &PointerEvent::new(PointerKind::Down, 100.3, 100.4)); + let a = t.pointer(&c, &PointerEvent::new(PointerKind::Drag, 106.3, 105.4)); + let Some(Action::Preview(command, v)) = a.last() else { panic!("{a:?}") }; + assert_eq!(command, "path.moveAnchors"); + Vec2::new(v["dx"].as_f64().unwrap_or(f64::NAN), v["dy"].as_f64().unwrap_or(f64::NAN)) + }; + let m = moved(PixelSnap { moving: false, ..PixelSnap::default() }); + assert!((m - Vec2::new(6.0, 5.0)).hypot() < 1e-9, "{m:?}"); + let m = moved(PixelSnap { drawing: false, scaling: false, moving: true }); + assert!((m - Vec2::new(5.7, 4.6)).hypot() < 1e-9, "{m:?}"); +} + /// A click (no drag) on an anchor that is selected with others, whether the whole path is or /// some of its anchors, selects that anchor alone, as in Illustrator: a press keeps the /// selection so a drag moves it all, the release without a drag narrows it. Shift still diff --git a/crates/tools/src/draw2/gesture.rs b/crates/tools/src/draw2/gesture.rs index e4811418c7..beb0346b90 100644 --- a/crates/tools/src/draw2/gesture.rs +++ b/crates/tools/src/draw2/gesture.rs @@ -216,10 +216,10 @@ impl GestureTool { } /// Does a straight Knife cut snap to anchors and paths? Only with Smart Guides on, which Snap to - /// Pixel and Snap to Grid turn off (`PointSnap::new`). Its ends snap as the Pencil's straight - /// line's do: the press, and the release unless Shift keeps it on a 45° step. + /// Pixel (for drawing) and Snap to Grid turn off (`PointSnap::new`). Its ends snap as the + /// Pencil's straight line's do: the press, and the release unless Shift keeps it on a 45° step. fn knife_snaps(&self, cx: &ToolContext) -> bool { - self.id == "knife" && self.straight && cx.smart_guides && !cx.snap_to_pixel && !cx.snap_to_grid + self.id == "knife" && self.straight && cx.smart_guides && !cx.snaps_to_pixel(crate::PixelOp::Drawing) && !cx.snap_to_grid } /// Does the end of a straight line snap: the Pencil's Alt line's, or a straight Knife cut's diff --git a/crates/tools/src/guides/snap.rs b/crates/tools/src/guides/snap.rs index 6f0b50d4b5..006f498b6e 100644 --- a/crates/tools/src/guides/snap.rs +++ b/crates/tools/src/guides/snap.rs @@ -3,6 +3,7 @@ use super::{ Affine, GlyphSnap, GuideStyle, Handle, HitOptions, Line, NodeId, OrientedBox, Overlay, ParamCurve, ParamCurveDeriv, ParamCurveNearest, PathSeg, Point, Rect, ScreenFrame, ToolContext, Vec2, constrain_angle_from, hit_test, }; +use crate::PixelOp; /// Where curves `a` and `b` cross near `p`: a point projected from one onto the other and back /// settles where they meet (none when they don't nearby). @@ -100,11 +101,29 @@ pub struct PointSnap { /// The smart guide targets, or with Smart Guides off those of Snap to Point, and how near (in /// screen pixels) they pull; none with pixels or the grid snapping instead, or nothing on. targets: Option<(Targets, f64)>, + /// The kind of edit the point serves: whether Snap to Pixel rounds it + /// ([`ToolContext::snaps_to_pixel`]). + op: PixelOp, } impl PointSnap { + /// Snapping for a drawn point. pub fn new(cx: &ToolContext, targets: impl FnOnce() -> Targets) -> Self { - let targets = if cx.snap_to_pixel || cx.snap_to_grid { + Self::for_op(cx, PixelOp::Drawing, targets) + } + + /// Snapping for a moved point (an anchor, or the selection held by one). + pub fn moving(cx: &ToolContext, targets: impl FnOnce() -> Targets) -> Self { + Self::for_op(cx, PixelOp::Moving, targets) + } + + /// Snapping for a dragged handle that scales ([`Self::snap_scale`]). + pub fn scaling(cx: &ToolContext, targets: impl FnOnce() -> Targets) -> Self { + Self::for_op(cx, PixelOp::Scaling, targets) + } + + fn for_op(cx: &ToolContext, op: PixelOp, targets: impl FnOnce() -> Targets) -> Self { + let targets = if cx.snaps_to_pixel(op) || cx.snap_to_grid { None } else if cx.smart_guides { // Snap to Point keeps pulling points onto the ruler guides with Smart Guides on (#812). @@ -116,7 +135,7 @@ impl PointSnap { } else { None }; - Self { targets } + Self { targets, op } } /// Snapping for a point placed anywhere, as a drawing tool's ([`DrawSnap`]) is: the art in the @@ -135,7 +154,7 @@ impl PointSnap { /// showing why. Pixels and the grid round it before Shift turns it onto its angle. pub fn snap_from(&self, cx: &ToolContext, p: Point, from: Option<&Leave>) -> (Point, Vec) { let keep = |q: Point| from.and_then(|f| f.constrained(q)).unwrap_or(q); - if cx.snap_to_pixel { + if cx.snaps_to_pixel(self.op) { return (keep(Point::new(p.x.round(), p.y.round())), vec![]); } if cx.snap_to_grid { @@ -148,9 +167,10 @@ impl PointSnap { } /// Snap a bounding-box resize ([`Targets::snap_scale`]) as [`Self::snap`] snaps a point. With - /// Snap to Pixel on, the corner or side the handle moves lines up with whole pixels, and with - /// Snap to Grid on it lines up with the nearest gridline, however far that is; neither shows - /// guides. Otherwise the handle snaps to the targets [`Self::new`] gathered. + /// Snap to Pixel on for scaling, the corner or side the handle moves lines up with whole + /// pixels, and with Snap to Grid on it lines up with the nearest gridline, however far that + /// is; neither shows guides. Otherwise the handle snaps to the targets [`Self::scaling`] + /// gathered. pub fn snap_scale( &self, cx: &ToolContext, @@ -160,7 +180,7 @@ impl PointSnap { proportional: bool, from_center: bool, ) -> (Affine, Vec) { - let step = if cx.snap_to_pixel { + let step = if cx.snaps_to_pixel(PixelOp::Scaling) { Some(1.0) } else if cx.snap_to_grid { Some(cx.grid_step()).filter(|s| s.is_finite() && *s > 0.0) @@ -327,7 +347,15 @@ impl SnapKey { revision: cx.revision, exclude: exclude.to_vec(), screen: cx.screen, - view: [cx.smart_guides, cx.snap_to_point, cx.snap_to_pixel, cx.snap_to_grid, cx.guides, cx.snap_tangent, cx.snap_perpendicular], + view: [ + cx.smart_guides, + cx.snap_to_point, + cx.snaps_to_pixel(PixelOp::Drawing), + cx.snap_to_grid, + cx.guides, + cx.snap_tangent, + cx.snap_perpendicular, + ], snap_to_glyph: (cx.snap_to_glyph && cx.smart_guides && cx.alignment_guides).then_some(cx.glyph_snap), style: GuideStyle::of(cx), tolerances: [cx.snapping_tolerance, cx.snap_tolerance], @@ -381,7 +409,7 @@ impl DrawSnap { /// Guides snapping tolerance (the Knife's straight cut). The targets serve this press only. pub fn press_on_paths(&mut self, cx: &ToolContext, p: Point) -> Point { let targets = Targets::for_drawing(cx.doc, &[], visible(cx), None).styled(cx).anchors_and_paths(); - self.targets = Some((None, PointSnap { targets: Some((targets, cx.snapping_tolerance)) })); + self.targets = Some((None, PointSnap { targets: Some((targets, cx.snapping_tolerance)), op: PixelOp::Drawing })); self.snap(cx, p, None) } diff --git a/crates/tools/src/guides/targets.rs b/crates/tools/src/guides/targets.rs index 41b9f517ee..cb0fdfc1dc 100644 --- a/crates/tools/src/guides/targets.rs +++ b/crates/tools/src/guides/targets.rs @@ -168,13 +168,13 @@ impl Targets { /// or nothing for a copy, which leaves it behind), styled as `cx` asks, and with Smart Guides › /// Spacing Guides the objects it spaces itself evenly with ([`Self::snap_rect`]). With Smart /// Guides › Snap to Last Location, a move (not a copy, which has nothing in `exclude`) goes back - /// to where it began when it comes near ([`Self::snap_back`]), unless Snap to Pixel is on. + /// to where it began when it comes near ([`Self::snap_back`]), unless Snap to Pixel snaps moves. pub fn for_move(cx: &ToolContext, exclude: &[NodeId]) -> Self { let mut t = Self::collect(cx.doc, exclude, None).styled(cx); if cx.spacing_guides { t.boxes = object_boxes(cx.doc, &cx.selection.objects); } - t.last_location = cx.snap_to_last_location && !cx.snap_to_pixel && !exclude.is_empty(); + t.last_location = cx.snap_to_last_location && !cx.snaps_to_pixel(crate::PixelOp::Moving) && !exclude.is_empty(); t } @@ -189,9 +189,9 @@ impl Targets { /// Targets that hold nothing but Smart Guides › Snap to Last Location ([`Self::snap_back`]), /// styled as `cx` asks: for a move of the selection whose other snaps come from other targets, /// such as paths selected whole that Direct Selection moves held by an anchor. Snap to Pixel - /// turns it off, as it does every Smart Guides snap. + /// snapping moves turns it off, as it does every Smart Guides snap. pub fn last_location(cx: &ToolContext) -> Self { - Self { last_location: cx.snap_to_last_location && !cx.snap_to_pixel, ..Self::default() }.styled(cx) + Self { last_location: cx.snap_to_last_location && !cx.snaps_to_pixel(crate::PixelOp::Moving), ..Self::default() }.styled(cx) } /// Targets for scaling the selection: the other art, styled as `cx` asks, and with Smart Guides › diff --git a/crates/tools/src/lib.rs b/crates/tools/src/lib.rs index baf461c6c7..ee752dd968 100644 --- a/crates/tools/src/lib.rs +++ b/crates/tools/src/lib.rs @@ -58,7 +58,7 @@ mod context; mod cursor; mod input; -pub use context::{GlyphSnap, MarqueeMode, PaintDefaults, ScreenFrame, ToolContext}; +pub use context::{GlyphSnap, MarqueeMode, PaintDefaults, PixelOp, PixelSnap, ScreenFrame, ToolContext}; pub use cursor::Cursor; pub use input::{Action, Mods, PenPose, PointerEvent, PointerKind, ToolKey}; @@ -542,6 +542,7 @@ pub(crate) mod testutil { snap_to_grid: false, show_bbox: true, snap_to_pixel: false, + pixel_snap: crate::PixelSnap::default(), snap_to_point: true, snap_tangent: true, snap_perpendicular: true, diff --git a/crates/tools/src/rulerguide.rs b/crates/tools/src/rulerguide.rs index a90770d3fb..8e037443cb 100644 --- a/crates/tools/src/rulerguide.rs +++ b/crates/tools/src/rulerguide.rs @@ -17,7 +17,7 @@ use vectorcraft_doc::{Guide, NodeId}; use vectorcraft_geom::{Point, Rect}; use crate::guides::{PointSnap, Targets}; -use crate::{Action, Cursor, Overlay, PointerEvent, PointerKind, ToolContext}; +use crate::{Action, Cursor, Overlay, PixelOp, PointerEvent, PointerKind, ToolContext}; /// The ruler guide under `p`, by its index in `guide.list`: the nearest within the selection /// tolerance (the newest of equals). @@ -123,7 +123,7 @@ struct GuideSnap { impl GuideSnap { fn new(cx: &ToolContext) -> Self { let targets = || Targets::collect(cx.doc, &[], None); - let targets = if cx.snap_to_pixel || cx.snap_to_grid { + let targets = if cx.snaps_to_pixel(PixelOp::Moving) || cx.snap_to_grid { None } else if cx.smart_guides { Some((targets().styled(cx), cx.snapping_tolerance)) @@ -136,7 +136,8 @@ impl GuideSnap { } /// Where a guide dragged to `v` (the x of a vertical one, the y of a horizontal one) goes: - /// with Shift on the nearest ruler tick, else on whole pixels, the grid or a target. + /// with Shift on the nearest ruler tick, else on whole pixels (Snap to Pixel's While Moving + /// option), the grid or a target. fn snap(&mut self, cx: &ToolContext, vertical: bool, v: f64, shift: bool) -> f64 { self.snapped.clear(); if shift { @@ -144,7 +145,7 @@ impl GuideSnap { let origin = if vertical { origin.x } else { origin.y }; return origin + cx.unit.snap_to_ruler_tick(v - origin, cx.zoom); } - if cx.snap_to_pixel { + if cx.snaps_to_pixel(PixelOp::Moving) { return v.round(); } if cx.snap_to_grid { diff --git a/crates/tools/src/rulerguide/tests.rs b/crates/tools/src/rulerguide/tests.rs index f80f31c776..b4b7c256d3 100644 --- a/crates/tools/src/rulerguide/tests.rs +++ b/crates/tools/src/rulerguide/tests.rs @@ -253,6 +253,24 @@ fn new_ruler_guide_alt_snaps_on_the_effective_axis() { } } +/// A guide dragged out of a ruler snaps to whole pixels with Snap to Pixel only while the Pixel +/// Snapping Options' While Moving is on; While Drawing and While Scaling do not change it. +#[test] +fn new_ruler_guide_follows_the_while_moving_option() { + use crate::PixelSnap; + let (d, _) = doc_with_rect(); + let (s, p) = (Selection::default(), paint()); + let off = ToolContext { smart_guides: false, snap_to_point: false, snap_to_pixel: true, ..cx(&d, &s, &p) }; + for (pixel_snap, expected) in + [(PixelSnap { moving: false, ..PixelSnap::default() }, 226.4), (PixelSnap { drawing: false, scaling: false, moving: true }, 226.0)] + { + let c = ToolContext { pixel_snap, ..off }; + let mut g = NewGuide::new(&c, true, None); + let actions = g.pointer(&c, &ev(PointerKind::Drag, 226.4, 237.6, Mods::default()), true); + assert_eq!(actions.last(), Some(&Action::Preview("guide.add".into(), json!({"vertical": true, "pos": expected}))), "{pixel_snap:?}"); + } +} + /// With Smart Guides off, Snap to Point pulls a dragged guide onto the anchors (within its /// distance), not the edges' midpoints. #[test] diff --git a/crates/tools/src/select.rs b/crates/tools/src/select.rs index 05b81ae049..70060ebc2b 100644 --- a/crates/tools/src/select.rs +++ b/crates/tools/src/select.rs @@ -56,7 +56,7 @@ use crate::pie::{self, PieDrag}; use crate::rulerguide::GuideEdit; use crate::shaperwidget; use crate::typewidget; -use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext, ToolKey, json_ids}; +use crate::{Action, Cursor, Mods, Overlay, PixelOp, PointerEvent, PointerKind, Tool, ToolContext, ToolKey, json_ids}; /// Direction is read on screen, even in a turned view. Within three pixels of the press's /// horizontal position, retain the previous rule so hand jitter does not flip it. @@ -228,8 +228,9 @@ fn distance_overlays(cx: &ToolContext, to: DistanceTo) -> Vec { /// Snap to Grid lands its edges on the grid (#740), taking over from the rest; otherwise Smart /// Guides line its bounds up with the other art, and with them off, View → Snap to Point lands the /// point it was grabbed by (an anchor pressed near, else the pressed point) on an anchor or a ruler -/// guide. Snap to Pixel puts its top-left on whole pixels. A copy (Alt, which can go down or up at -/// any time during the drag) leaves the original behind, so it snaps to the original too. +/// guide. Snap to Pixel puts its top-left on whole pixels while its While Moving option is on. A +/// copy (Alt, which can go down or up at any time during the drag) leaves the original behind, so +/// it snaps to the original too. pub(crate) struct MoveSnap { bounds: Option, targets: Option, @@ -314,7 +315,7 @@ impl MoveSnap { } } } - if cx.snap_to_pixel + if cx.snaps_to_pixel(PixelOp::Moving) && let Some(b) = self.bounds { d = Vec2::new((b.x0 + d.x).round() - b.x0, (b.y0 + d.y).round() - b.y0); @@ -388,7 +389,7 @@ impl MoveSnap { guides = ov; } } - if cx.snap_to_pixel + if cx.snaps_to_pixel(PixelOp::Moving) && let Some(b) = self.bounds { let at = Point::new(b.x0, b.y0) + d; @@ -1655,7 +1656,7 @@ impl SelectionTool { // What the handle moves snaps as a dragged point does: to pixels or the // grid, else with Smart Guides to the other objects, else with Snap to // Point to their anchors; the ruler guides with Snap to Point. - self.targets = Some(PointSnap::new(cx, || Targets::for_scale(cx))); + self.targets = Some(PointSnap::scaling(cx, || Targets::for_scale(cx))); self.last_backquote = false; self.last_box_pos = None; self.state = State::Scaling { handle, bx, areas }; diff --git a/crates/tools/src/select/tests.rs b/crates/tools/src/select/tests.rs index c41056b7aa..63ca697c7d 100644 --- a/crates/tools/src/select/tests.rs +++ b/crates/tools/src/select/tests.rs @@ -1794,6 +1794,51 @@ fn handle_drag_lands_what_it_moves_on_whole_pixels() { assert!(near(r, Rect::new(100.0, 100.0, 212.0, 231.0)), "{r:?}"); } +/// Pixel Snapping Options › While Scaling off: a handle drag keeps the fractional size it is +/// dragged to with Snap to Pixel on, and turned back on, the drag lands on whole pixels again; While +/// Drawing and While Moving do not change it. +#[test] +fn handle_drag_follows_the_while_scaling_option() { + use crate::PixelSnap; + let (d, id) = doc_with_rect(); + let mut s = Selection::default(); + s.add(id); + let p = paint(); + let drag = |pixel_snap: PixelSnap| { + let c = ToolContext { snap_to_pixel: true, pixel_snap, smart_guides: false, snap_to_point: false, ..cx(&d, &s, &p) }; + let mut t = SelectionTool::default(); + t.pointer(&c, &ev(PointerKind::Down, 200.0, 200.0)); + scaled(&t.pointer(&c, &ev(PointerKind::Drag, 212.4, 230.7))) + }; + let r = drag(PixelSnap { scaling: false, ..PixelSnap::default() }); + assert!(near(r, Rect::new(100.0, 100.0, 212.4, 230.7)), "{r:?}"); + let r = drag(PixelSnap { drawing: false, moving: false, scaling: true }); + assert!(near(r, Rect::new(100.0, 100.0, 212.0, 231.0)), "{r:?}"); +} + +/// Pixel Snapping Options › While Moving off: a move keeps its fractional offset with Snap to +/// Pixel on, and Smart Guides › Snap to Last Location works again for it. +#[test] +fn move_follows_the_while_moving_option() { + use crate::PixelSnap; + let (d, id) = doc_with_rect(); + let mut s = Selection::default(); + s.add(id); + let p = paint(); + let pixel_snap = PixelSnap { moving: false, ..PixelSnap::default() }; + let c = ToolContext { snap_to_pixel: true, pixel_snap, smart_guides: false, snap_to_point: false, ..cx(&d, &s, &p) }; + let snap = MoveSnap::new(&c); + let (dv, _) = snap.snap(&c, Point::new(150.0, 150.0), Vec2::new(3.3, 4.6), false); + assert!((dv - Vec2::new(3.3, 4.6)).hypot() < 1e-9, "{dv:?}"); + let on = ToolContext { pixel_snap: PixelSnap::default(), ..c }; + let (dv, _) = MoveSnap::new(&on).snap(&on, Point::new(150.0, 150.0), Vec2::new(3.3, 4.6), false); + assert!((dv - Vec2::new(3.0, 5.0)).hypot() < 1e-9, "{dv:?}"); + let back = ToolContext { smart_guides: true, snap_to_last_location: true, snapping_tolerance: 10.0, ..c }; + assert!(Targets::for_move(&back, &back.selection.objects).snap_back(None, Vec2::new(1.0, 1.0), back.snap_tol()).is_some()); + let pixels = ToolContext { pixel_snap: PixelSnap::default(), ..back }; + assert!(Targets::for_move(&pixels, &pixels.selection.objects).snap_back(None, Vec2::new(1.0, 1.0), pixels.snap_tol()).is_none()); +} + /// #2054: with Smart Guides off, View › Snap to Point lands a dragged handle on another /// object's anchor or a ruler guide within its distance (2 px); with Smart Guides on, the /// ruler guides still pull it within the Snapping Tolerance (4 px), as a move does (#1534). diff --git a/crates/tools/src/shape.rs b/crates/tools/src/shape.rs index fb291796f7..66728b4826 100644 --- a/crates/tools/src/shape.rs +++ b/crates/tools/src/shape.rs @@ -236,7 +236,7 @@ impl ShapeTool { self.selection = if let Some((handle, bx)) = self.over_handle(cx, ev.pos) { let is_area = |id: &vectorcraft_doc::NodeId| cx.doc.node(*id).is_some_and(is_area_type); let areas = cx.selection.objects.iter().any(is_area); - Some(SelectionDrag::Scale { handle, bx, areas, snap: Box::new(PointSnap::new(cx, || Targets::for_scale(cx))), last: None }) + Some(SelectionDrag::Scale { handle, bx, areas, snap: Box::new(PointSnap::scaling(cx, || Targets::for_scale(cx))), last: None }) } else { match hit_center(cx, ev.pos, cx.point_tol()) { Some((_, hold)) => Some(SelectionDrag::Move { start: ev.pos, hold, began: false, snap: Box::new(MoveSnap::new(cx)) }), diff --git a/crates/tools/src/slice.rs b/crates/tools/src/slice.rs index 69bbf9051f..0f804a8ef6 100644 --- a/crates/tools/src/slice.rs +++ b/crates/tools/src/slice.rs @@ -161,7 +161,7 @@ struct ResizeSnap { impl ResizeSnap { fn new(cx: &ToolContext) -> Self { - Self { art: PointSnap::new(cx, || Targets::collect(cx.doc, &[], None)), rulers: Targets::rulers(cx) } + Self { art: PointSnap::scaling(cx, || Targets::collect(cx.doc, &[], None)), rulers: Targets::rulers(cx) } } /// Snap the resize `a` ([`scale_for_drag`]) of the slice `rect` by `handle` as [`snap`] snaps diff --git a/crates/tools/src/xform/artboard.rs b/crates/tools/src/xform/artboard.rs index 4ffd435af6..5af642c44b 100644 --- a/crates/tools/src/xform/artboard.rs +++ b/crates/tools/src/xform/artboard.rs @@ -37,7 +37,7 @@ use vectorcraft_geom::{Affine, Point, Rect, Vec2}; use super::{BLUE, polygon, rect_corners}; use crate::bbox::{Handle, hit_handle, scale_for_drag}; use crate::guides::{DrawSnap, Leave, Targets}; -use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext, ToolKey}; +use crate::{Action, Cursor, Mods, Overlay, PixelOp, PointerEvent, PointerKind, Tool, ToolContext, ToolKey}; #[derive(Clone, Copy, Debug)] enum Drag { @@ -205,11 +205,12 @@ impl ArtboardTool { self.copy_targets = cx.smart_guides.then(|| Targets::for_artboard(cx.doc, usize::MAX, &[]).styled(cx)); } - /// Snap a dragged handle: to whole pixels, the grid, or Smart Guides (in that order, as when - /// drawing). With Smart Guides the bleed edge `bleed` beyond the handle may snap instead. + /// Snap a dragged handle: to whole pixels (Snap to Pixel's While Scaling option), the grid, or + /// Smart Guides (in that order, as when drawing). With Smart Guides the bleed edge `bleed` + /// beyond the handle may snap instead. fn snap_handle(&mut self, cx: &ToolContext, p: Point, bleed: Vec2) -> Point { self.guides.clear(); - if cx.snap_to_pixel { + if cx.snaps_to_pixel(PixelOp::Scaling) { return Point::new(p.x.round(), p.y.round()); } if cx.snap_to_grid { @@ -222,12 +223,13 @@ impl ArtboardTool { q } - /// Snap a move (or an Alt-drag `copy`) of `rect` by `d`: its top-left to whole pixels or the - /// grid, or any of its edges and centre to Smart Guides. Returns the snapped delta. + /// Snap a move (or an Alt-drag `copy`) of `rect` by `d`: its top-left to whole pixels (Snap to + /// Pixel's While Moving option) or the grid, or any of its edges and centre to Smart Guides. + /// Returns the snapped delta. fn snap_move(&mut self, cx: &ToolContext, rect: Rect, d: Vec2, copy: bool) -> Vec2 { self.guides.clear(); let tl = Point::new(rect.x0, rect.y0); - if cx.snap_to_pixel { + if cx.snaps_to_pixel(PixelOp::Moving) { return Vec2::new((tl.x + d.x).round() - tl.x, (tl.y + d.y).round() - tl.y); } if cx.snap_to_grid { diff --git a/crates/tools/src/xform/free.rs b/crates/tools/src/xform/free.rs index bd7c0c0ecd..2d137153fc 100644 --- a/crates/tools/src/xform/free.rs +++ b/crates/tools/src/xform/free.rs @@ -248,7 +248,7 @@ impl Tool for FreeTransformTool { self.drag = None; let Some(bx) = selection_box(cx) else { return vec![] }; if let Some(op) = self.classify(cx, &bx, p, ev.mods) { - self.targets = op.handle().is_some().then(|| PointSnap::new(cx, || Targets::for_scale(cx))); + self.targets = op.handle().is_some().then(|| PointSnap::scaling(cx, || Targets::for_scale(cx))); self.drag = Some(Drag { op, bx, start: p, began: false }); } vec![] diff --git a/crates/tools/src/xform/measure.rs b/crates/tools/src/xform/measure.rs index cccb1ebcae..0b575c503c 100644 --- a/crates/tools/src/xform/measure.rs +++ b/crates/tools/src/xform/measure.rs @@ -43,7 +43,7 @@ pub(crate) fn end_point(cx: &ToolContext, snap: &mut DrawSnap, a: Point, p: Poin return a; } let u = d / len; - let to_point = cx.snap_to_point && !(cx.smart_guides || cx.snap_to_grid || cx.snap_to_pixel); + let to_point = cx.snap_to_point && !(cx.smart_guides || cx.snap_to_grid || cx.snaps_to_pixel(crate::PixelOp::Drawing)); let s = if to_point { snap.measure(cx, p, None) } else { p }; let q = a + u * (s - a).dot(u); let leave = crate::guides::Leave::segment(cx, a, true); diff --git a/crates/tools/src/xform/transform.rs b/crates/tools/src/xform/transform.rs index 5cabbed6b8..263c0ea5e4 100644 --- a/crates/tools/src/xform/transform.rs +++ b/crates/tools/src/xform/transform.rs @@ -238,8 +238,11 @@ impl Tool for TransformTool { return out; } d.began = true; - // The selection moves, so only the other objects are targets. - self.snap = Some(PointSnap::new(cx, || Targets::collect(cx.doc, &cx.selection.objects, None))); + // The selection moves, so only the other objects are targets. The Scale tool's + // drag scales; the others move the art about the reference point. + let targets = || Targets::collect(cx.doc, &cx.selection.objects, None); + self.snap = + Some(if self.kind == TransformKind::Scale { PointSnap::scaling(cx, targets) } else { PointSnap::moving(cx, targets) }); out.push(Action::Begin(self.kind.label().into())); } let (p, guides) = self.snap.as_ref().map_or((p, vec![]), |s| s.snap(cx, p)); diff --git a/crates/ui-egui/src/chrome/control_bar.rs b/crates/ui-egui/src/chrome/control_bar.rs index 7cb577c812..5ac4025c24 100644 --- a/crates/ui-egui/src/chrome/control_bar.rs +++ b/crates/ui-egui/src/chrome/control_bar.rs @@ -78,7 +78,8 @@ pub(super) const ISOLATION_BUTTON: &str = "cb-isolation"; /// less, the menu is left out so those controls still fit the narrowest window. const ARTBOARD_SNAPPING_WIDTH: f32 = 900.0; -/// The Control bar's snapping quick access; every change uses its existing command. +/// The Control bar's snapping quick access; every change uses its existing command, and Pixel +/// Snapping Options… opens the dialog of `view.snapToPixel.options`. fn snapping_menu(app: &mut VectorcraftApp, ui: &mut Ui) { ui.menu_button(tl!("Snapping"), |ui| { for (label, cmd, on) in [ @@ -92,6 +93,10 @@ fn snapping_menu(app: &mut VectorcraftApp, ui: &mut Ui) { crate::menus::invoke(app, cmd, json!({})); } } + if ui.button(tl!("Pixel Snapping Options…")).clicked() { + ui.close(); + crate::menus::invoke(app, "view.snapToPixel.options", json!({})); + } ui.separator(); for (key, label, on) in [ ("alignmentGuides", tl!("Alignment Guides"), app.session.prefs.alignment_guides), diff --git a/crates/ui-egui/src/chrome/tests.rs b/crates/ui-egui/src/chrome/tests.rs index 346dcb9f05..376450937c 100644 --- a/crates/ui-egui/src/chrome/tests.rs +++ b/crates/ui-egui/src/chrome/tests.rs @@ -2247,6 +2247,82 @@ fn the_control_bar_snapping_menu_shares_glyph_and_alignment_settings() { check(&nodes, "Alignment Guides", true); } +/// The Control bar's Snapping menu opens Pixel Snapping Options with the current options; OK sets +/// the three choices the dialog shows, and a dialog closed without OK changes nothing. +#[test] +fn the_control_bar_snapping_menu_opens_pixel_snapping_options() { + struct RestoreLanguage(crate::i18n::Lang); + impl Drop for RestoreLanguage { + fn drop(&mut self) { + crate::i18n::set_current(self.0); + } + } + let _language = RestoreLanguage(crate::i18n::current()); + crate::i18n::set_current(crate::i18n::Lang::EN); + let mut app = crate::VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 400, "height": 300})).unwrap(); + app.run("view.snapToPixel.options", json!({"moving": false})).unwrap(); + let ctx = egui::Context::default(); + ctx.enable_accesskit(); + crate::theme::install_fonts(&ctx); + let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1200.0, 400.0)); + let frame = |app: &mut crate::VectorcraftApp, events: Vec| { + let mut out = ctx.run_ui(egui::RawInput { events, screen_rect: Some(screen), ..Default::default() }, |ui| super::control_bar(app, ui)); + out.textures_delta.clear(); + out.platform_output.accesskit_update.map(|u| u.nodes).unwrap_or_default() + }; + let center = |nodes: &[(egui::accesskit::NodeId, egui::accesskit::Node)], name: &str| { + let n = nodes.iter().find(|(_, n)| n.label() == Some(name)).map(|(_, n)| n.clone()).unwrap_or_else(|| panic!("missing {name}")); + let b = n.bounds().expect("a visible menu control"); + egui::pos2(((b.x0 + b.x1) / 2.0) as f32, ((b.y0 + b.y1) / 2.0) as f32) + }; + let click = |app: &mut crate::VectorcraftApp, at: egui::Pos2| { + let press = |pressed| egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() }; + frame(app, vec![egui::Event::PointerMoved(at), press(true)]); + frame(app, vec![press(false)]); + frame(app, vec![]); + frame(app, vec![]) + }; + frame(&mut app, vec![]); + let nodes = frame(&mut app, vec![]); + let menu = center(&nodes, "Snapping"); + let nodes = click(&mut app, menu); + let entry = center(&nodes, "Pixel Snapping Options…"); + click(&mut app, entry); + let d = app.ui.dialog.as_ref().expect("Pixel Snapping Options"); + assert_eq!(d.kind, crate::dialogs::pixel_snap_options::KIND); + assert_eq!(serde_json::Value::Object(d.fields.clone()), json!({"drawing": true, "moving": false, "scaling": true})); + // While View › Snap to Pixel is off, the dialog says the options wait for it. + // A new window is laid out unseen in its first frame, so the second frame is read. + let note = |app: &mut crate::VectorcraftApp| { + let mut nodes = vec![]; + for _ in 0..2 { + let mut out = ctx.run_ui(egui::RawInput { screen_rect: Some(screen), ..Default::default() }, |ui| crate::dialogs::show(app, ui.ctx())); + out.textures_delta.clear(); + nodes = out.platform_output.accesskit_update.map(|u| u.nodes).unwrap_or_default(); + } + let shown = |text: &str| nodes.iter().any(|(_, n)| n.label().or(n.value()).is_some_and(|l| l.contains(text))); + (shown("Snap to Pixel While Drawing"), shown("take effect when View › Snap to Pixel is on")) + }; + assert!(!app.ui.view.snap_to_pixel); + assert_eq!(note(&mut app), (true, true), "the note shows with Snap to Pixel off"); + app.ui.view.snap_to_pixel = true; + assert_eq!(note(&mut app), (true, false), "the note is hidden with Snap to Pixel on"); + let before = app.session.prefs.pixel_snap; + let d = app.ui.dialog.as_mut().unwrap(); + d.fields.insert("drawing".into(), json!(false)); + app.ui.dialog = None; + assert_eq!(app.session.prefs.pixel_snap, before, "a dialog closed without OK changes nothing"); + crate::menus::invoke(&mut app, "view.snapToPixel.options", json!({})); + let d = app.ui.dialog.as_mut().expect("Pixel Snapping Options"); + d.fields.insert("drawing".into(), json!(false)); + d.fields.insert("moving".into(), json!(true)); + d.fields.insert("scaling".into(), json!(false)); + crate::dialogs::confirm(&mut app).unwrap(); + assert!(app.ui.dialog.is_none()); + assert_eq!(app.session.prefs.pixel_snap, vectorcraft_tools::PixelSnap { drawing: false, moving: true, scaling: false }); +} + /// The window the Control bar docking tests draw the bar in. const DOCK_SCREEN: egui::Rect = egui::Rect { min: egui::Pos2::ZERO, max: egui::pos2(1200.0, 300.0) }; diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 197f8faaaa..6040ac8fb4 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -88,6 +88,7 @@ pub mod perspective_grid; pub mod perspective_options; pub mod perspective_plane; pub mod perspective_presets; +pub mod pixel_snap_options; pub mod place; pub mod placement_options; pub mod plugin; @@ -452,6 +453,7 @@ registry! { LayersPanelOptions: [layers_panel_options::KIND] => layers_panel_options::SPEC, NavigatorPanelOptions: [navigator_panel_options::KIND] => navigator_panel_options::SPEC, PathfinderOptions: [pathfinder_options::KIND] => pathfinder_options::SPEC, + PixelSnapOptions: [pixel_snap_options::KIND] => pixel_snap_options::SPEC, RearrangeArtboards: [rearrange_artboards::KIND] => rearrange_artboards::SPEC, RenameArtboards: [rename_artboards::KIND] => rename_artboards::SPEC, PathToolOptions: [crate::addons::pathtools::KIND] => crate::addons::pathtools::OPTIONS_SPEC, diff --git a/crates/ui-egui/src/dialogs/pixel_snap_options.rs b/crates/ui-egui/src/dialogs/pixel_snap_options.rs new file mode 100644 index 0000000000..43b369e105 --- /dev/null +++ b/crates/ui-egui/src/dialogs/pixel_snap_options.rs @@ -0,0 +1,46 @@ +//! Pixel Snapping Options (the Control bar's Snapping menu): whether View › Snap to Pixel lands +//! drawing, moving and scaling on whole pixels. OK runs `view.snapToPixel.options` (the options live +//! in the preferences); Cancel leaves them as they were. +//! +//! Fields: `drawing`, `moving` and `scaling` (bool). While Snap to Pixel is off, the options stay +//! editable and a note says they take effect once it is on. + +use serde_json::{Value, json}; + +use super::DialogSpec; +use crate::state::Dialog; +use crate::theme::Tokens; +use crate::{VectorcraftApp, widgets}; + +/// The dialog kind of Pixel Snapping Options. +pub const KIND: &str = "pixelSnapOptions"; + +pub(super) const SPEC: DialogSpec = + DialogSpec { heading: |_| tl!("Pixel Snapping Options").into(), body, confirm, min_width: 320.0, ..DialogSpec::FORM }; + +/// Open the dialog with the current options. +pub fn open(app: &mut VectorcraftApp) { + app.ui.dialog = Some(Dialog::new(KIND, json!(app.session.prefs.pixel_snap))); +} + +fn check(ui: &mut egui::Ui, d: &mut Dialog, key: &str, text: &str) { + let v = d.bool(key); + if widgets::check(ui, text, v, true) { + d.fields.insert(key.into(), json!(!v)); + } +} + +fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { + check(ui, d, "drawing", tl!("Snap to Pixel While Drawing")); + check(ui, d, "moving", tl!("Snap to Pixel While Moving")); + check(ui, d, "scaling", tl!("Snap to Pixel While Scaling")); + if !app.ui.view.snap_to_pixel { + let dim = Tokens::get(ui.ctx()).text_dim; + ui.label(egui::RichText::new(tl!("These options take effect when View › Snap to Pixel is on.")).color(dim)); + } + false +} + +fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result { + super::run_and_close(app, "view.snapToPixel.options", Value::Object(d.fields.clone())) +} diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index 06f2a59fc7..708ffe9db6 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -6184,6 +6184,13 @@ axis Y Axis Y ос Area Text Текст в зона # Transform panel menu › Measure Type by Glyph Bounds. Measure Type by Glyph Bounds Измерване на текста по границите на глифа +# Control bar › Snapping › Pixel Snapping Options… (view.snapToPixel.options). + Pixel Snapping Options Опции за прилепване към пиксел + Pixel Snapping Options… Опции за прилепване към пиксел… + Snap to Pixel While Drawing Прилепване към пиксел при рисуване + Snap to Pixel While Moving Прилепване към пиксел при преместване + Snap to Pixel While Scaling Прилепване към пиксел при мащабиране + These options take effect when View › Snap to Pixel is on. Тези опции действат, когато Изглед › Прилепване към пиксел е включено. # Character panel menu › Show Snap to Glyph Options (#4232). Show Snap to Glyph Options Показване на опциите за прилепване към глиф Hide Snap to Glyph Options Скриване на опциите за прилепване към глиф diff --git a/crates/ui-egui/src/i18n/bs.tsv b/crates/ui-egui/src/i18n/bs.tsv index b34575e8aa..8f5ea73487 100644 --- a/crates/ui-egui/src/i18n/bs.tsv +++ b/crates/ui-egui/src/i18n/bs.tsv @@ -6242,6 +6242,12 @@ axis Y Axis Y osa Area Text Tekst u oblasti # Transform panel menu › Measure Type by Glyph Bounds. Measure Type by Glyph Bounds Mjeri tekst po granicama glifa + Pixel Snapping Options Opcije prianjanja uz piksel + Pixel Snapping Options… Opcije prianjanja uz piksel… + Snap to Pixel While Drawing Prilijepi uz piksel pri crtanju + Snap to Pixel While Moving Prilijepi uz piksel pri pomjeranju + Snap to Pixel While Scaling Prilijepi uz piksel pri skaliranju + These options take effect when View › Snap to Pixel is on. Ove opcije djeluju kada je uključeno Prikaz › Prilijepi uz piksel. Show Snap to Glyph Options Prikaži opcije prianjanja uz glif Hide Snap to Glyph Options Sakrij opcije prianjanja uz glif Snap to Glyph Options Opcije prianjanja uz glif diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index b7ded40348..5516c9662f 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -6701,6 +6701,13 @@ theme Appearance Aparença Area Text Text d’àrea # Transform panel menu › Measure Type by Glyph Bounds. Measure Type by Glyph Bounds Mesura el text pels límits del glif +# Control bar › Snapping › Pixel Snapping Options… (view.snapToPixel.options). + Pixel Snapping Options Opcions d’ajust al píxel + Pixel Snapping Options… Opcions d’ajust al píxel… + Snap to Pixel While Drawing Ajusta al píxel en dibuixar + Snap to Pixel While Moving Ajusta al píxel en moure + Snap to Pixel While Scaling Ajusta al píxel en escalar + These options take effect when View › Snap to Pixel is on. Aquestes opcions tenen efecte quan Visualitza › Ajusta al píxel està activat. # Character panel menu › Show Snap to Glyph Options (#4232). Show Snap to Glyph Options Mostra les opcions d’ajust al glif Hide Snap to Glyph Options Amaga les opcions d’ajust al glif diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 76cf4b38bc..d03d192a3a 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -6738,6 +6738,13 @@ axis Y Axis osa Y Area Text Text v oblasti # Transform panel menu › Measure Type by Glyph Bounds. Measure Type by Glyph Bounds Měřit text podle hranic glyfů +# Control bar › Snapping › Pixel Snapping Options… (view.snapToPixel.options). + Pixel Snapping Options Možnosti přichytávání k obrazovým bodům + Pixel Snapping Options… Možnosti přichytávání k obrazovým bodům… + Snap to Pixel While Drawing Přichytávat k obrazovým bodům při kreslení + Snap to Pixel While Moving Přichytávat k obrazovým bodům při přesouvání + Snap to Pixel While Scaling Přichytávat k obrazovým bodům při změně velikosti + These options take effect when View › Snap to Pixel is on. Tyto volby se uplatní, když je zapnuto Zobrazení › Přichytávat k obrazovým bodům. # Character panel menu › Show Snap to Glyph Options (#4232). Show Snap to Glyph Options Zobrazit možnosti přichytávání ke glyfům Hide Snap to Glyph Options Skrýt možnosti přichytávání ke glyfům diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index 696a2cb193..00c85aaeb6 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -6200,6 +6200,13 @@ axis Y Axis Y-akse Area Text Områdetekst # Transform panel menu › Measure Type by Glyph Bounds. Measure Type by Glyph Bounds Mål tekst efter glyfafgrænsning +# Control bar › Snapping › Pixel Snapping Options… (view.snapToPixel.options). + Pixel Snapping Options Indstillinger for fastgørelse til pixel + Pixel Snapping Options… Indstillinger for fastgørelse til pixel… + Snap to Pixel While Drawing Fastgør til pixel ved tegning + Snap to Pixel While Moving Fastgør til pixel ved flytning + Snap to Pixel While Scaling Fastgør til pixel ved skalering + These options take effect when View › Snap to Pixel is on. Disse indstillinger virker, når Vis › Fastgør til pixel er slået til. # Character panel menu › Show Snap to Glyph Options (#4232). Show Snap to Glyph Options Vis indstillinger for fastgørelse til tegn Hide Snap to Glyph Options Skjul indstillinger for fastgørelse til tegn diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index d03f6e8da9..47337855aa 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -6330,6 +6330,13 @@ axis Y Axis Y-Achse Area Text Flächentext # Transform panel menu › Measure Type by Glyph Bounds. Measure Type by Glyph Bounds Text an Glyphenbegrenzung messen +# Control bar › Snapping › Pixel Snapping Options… (view.snapToPixel.options). + Pixel Snapping Options Optionen für „An Pixel ausrichten“ + Pixel Snapping Options… Optionen für „An Pixel ausrichten“… + Snap to Pixel While Drawing Beim Zeichnen an Pixel ausrichten + Snap to Pixel While Moving Beim Verschieben an Pixel ausrichten + Snap to Pixel While Scaling Beim Skalieren an Pixel ausrichten + These options take effect when View › Snap to Pixel is on. Diese Optionen wirken, wenn Ansicht › An Pixel ausrichten aktiviert ist. # Character panel menu › Show Snap to Glyph Options (#4232). Show Snap to Glyph Options Optionen für „An Glyphe ausrichten“ einblenden Hide Snap to Glyph Options Optionen für „An Glyphe ausrichten“ ausblenden diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 007dc5fe64..c319729a71 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -6690,6 +6690,13 @@ axis Y Axis Άξονας Y Area Text Κείμενο περιοχής # Transform panel menu › Measure Type by Glyph Bounds. Measure Type by Glyph Bounds Μέτρηση κειμένου με βάση τα όρια γλυφής +# Control bar › Snapping › Pixel Snapping Options… (view.snapToPixel.options). + Pixel Snapping Options Επιλογές προσκόλλησης σε εικονοστοιχείο + Pixel Snapping Options… Επιλογές προσκόλλησης σε εικονοστοιχείο… + Snap to Pixel While Drawing Προσκόλληση σε εικονοστοιχείο κατά τη σχεδίαση + Snap to Pixel While Moving Προσκόλληση σε εικονοστοιχείο κατά τη μετακίνηση + Snap to Pixel While Scaling Προσκόλληση σε εικονοστοιχείο κατά την αλλαγή κλίμακας + These options take effect when View › Snap to Pixel is on. Αυτές οι επιλογές ισχύουν όταν είναι ενεργή η επιλογή Προβολή › Προσκόλληση σε εικονοστοιχείο. # Character panel menu › Show Snap to Glyph Options (#4232). Show Snap to Glyph Options Εμφάνιση επιλογών προσκόλλησης σε γλυφή Hide Snap to Glyph Options Απόκρυψη επιλογών προσκόλλησης σε γλυφή diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 23dfc61bdb..5b203292a4 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -6680,6 +6680,13 @@ axis Y Axis Eje Y Area Text Texto de área # Transform panel menu › Measure Type by Glyph Bounds. Measure Type by Glyph Bounds Medir el texto por los límites del glifo +# Control bar › Snapping › Pixel Snapping Options… (view.snapToPixel.options). + Pixel Snapping Options Opciones de ajuste a píxel + Pixel Snapping Options… Opciones de ajuste a píxel… + Snap to Pixel While Drawing Ajustar a píxel al dibujar + Snap to Pixel While Moving Ajustar a píxel al mover + Snap to Pixel While Scaling Ajustar a píxel al escalar + These options take effect when View › Snap to Pixel is on. Estas opciones se aplican cuando Ver › Ajustar a píxel está activado. # Character panel menu › Show Snap to Glyph Options (#4232). Show Snap to Glyph Options Mostrar opciones de ajuste a glifo Hide Snap to Glyph Options Ocultar opciones de ajuste a glifo diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index c98960157d..b2c6f48d54 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -6768,6 +6768,13 @@ axis Y Axis Y-akseli Area Text Alueteksti # Transform panel menu › Measure Type by Glyph Bounds. Measure Type by Glyph Bounds Mittaa teksti merkin rajoista +# Control bar › Snapping › Pixel Snapping Options… (view.snapToPixel.options). + Pixel Snapping Options Pikseliin kohdistuksen asetukset + Pixel Snapping Options… Pikseliin kohdistuksen asetukset… + Snap to Pixel While Drawing Kohdista pikseliin piirrettäessä + Snap to Pixel While Moving Kohdista pikseliin siirrettäessä + Snap to Pixel While Scaling Kohdista pikseliin skaalattaessa + These options take effect when View › Snap to Pixel is on. Nämä asetukset ovat voimassa, kun Näytä › Kohdista pikseliin on käytössä. # Character panel menu › Show Snap to Glyph Options (#4232). Show Snap to Glyph Options Näytä merkkiin kohdistuksen asetukset Hide Snap to Glyph Options Piilota merkkiin kohdistuksen asetukset diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 54e06a7acd..e84f344060 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -6733,6 +6733,13 @@ axis Y Axis Axe Y Area Text Texte captif # Transform panel menu › Measure Type by Glyph Bounds. Measure Type by Glyph Bounds Mesurer le texte d’après les limites du glyphe +# Control bar › Snapping › Pixel Snapping Options… (view.snapToPixel.options). + Pixel Snapping Options Options de magnétisme du pixel + Pixel Snapping Options… Options de magnétisme du pixel… + Snap to Pixel While Drawing Magnétisme du pixel lors du dessin + Snap to Pixel While Moving Magnétisme du pixel lors du déplacement + Snap to Pixel While Scaling Magnétisme du pixel lors de la mise à l’échelle + These options take effect when View › Snap to Pixel is on. Ces options s’appliquent lorsque Affichage › Magnétisme du pixel est activé. # Character panel menu › Show Snap to Glyph Options (#4232). Show Snap to Glyph Options Afficher les options de magnétisme du glyphe Hide Snap to Glyph Options Masquer les options de magnétisme du glyphe diff --git a/crates/ui-egui/src/i18n/hi.tsv b/crates/ui-egui/src/i18n/hi.tsv index ea3d481710..017bc03750 100644 --- a/crates/ui-egui/src/i18n/hi.tsv +++ b/crates/ui-egui/src/i18n/hi.tsv @@ -4566,6 +4566,12 @@ antiAlias Sharp शार्प Show Slice Numbers स्लाइस नंबर दिखाएँ Show Slices स्लाइस दिखाएँ Show smooth color wheel चिकनी रंग पहिया दिखाएँ + Pixel Snapping Options पिक्सेल पर स्नैप ऑप्शन्स + Pixel Snapping Options… पिक्सेल पर स्नैप ऑप्शन्स… + Snap to Pixel While Drawing ड्रॉ करते समय पिक्सेल के लिए स्नैप + Snap to Pixel While Moving मूव करते समय पिक्सेल के लिए स्नैप + Snap to Pixel While Scaling स्केल करते समय पिक्सेल के लिए स्नैप + These options take effect when View › Snap to Pixel is on. ये ऑप्शन्स तब लागू होते हैं जब व्यू › पिक्सेल के लिए स्नैप चालू हो। Show Snap to Glyph Options ग्लिफ़ पर स्नैप ऑप्शन्स दिखाएँ Show Swatch Bounds स्वॉच की सीमाएँ दिखाएँ Show Swatch Kinds स्वॉच प्रकार दिखाएँ diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index aed2d4b3cb..effb31e5d8 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -6721,6 +6721,13 @@ axis Y Axis Y tengely Area Text Területszöveg # Transform panel menu › Measure Type by Glyph Bounds. Measure Type by Glyph Bounds Szöveg mérése a glifahatárok szerint +# Control bar › Snapping › Pixel Snapping Options… (view.snapToPixel.options). + Pixel Snapping Options Pixelhez illesztés beállításai + Pixel Snapping Options… Pixelhez illesztés beállításai… + Snap to Pixel While Drawing Illesztés pixelhez rajzoláskor + Snap to Pixel While Moving Illesztés pixelhez mozgatáskor + Snap to Pixel While Scaling Illesztés pixelhez méretezéskor + These options take effect when View › Snap to Pixel is on. Ezek a beállítások akkor érvényesek, ha a Nézet › Illesztés pixelhez be van kapcsolva. # Character panel menu › Show Snap to Glyph Options (#4232). Show Snap to Glyph Options Glifához illesztés beállításainak megjelenítése Hide Snap to Glyph Options Glifához illesztés beállításainak elrejtése diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index 329ab96cf2..912c6954eb 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -6200,6 +6200,13 @@ axis Y Axis Sumbu Y Area Text Teks area # Transform panel menu › Measure Type by Glyph Bounds. Measure Type by Glyph Bounds Ukur teks menurut batas glif +# Control bar › Snapping › Pixel Snapping Options… (view.snapToPixel.options). + Pixel Snapping Options Opsi snap ke piksel + Pixel Snapping Options… Opsi snap ke piksel… + Snap to Pixel While Drawing Snap ke piksel saat menggambar + Snap to Pixel While Moving Snap ke piksel saat memindahkan + Snap to Pixel While Scaling Snap ke piksel saat menskalakan + These options take effect when View › Snap to Pixel is on. Opsi ini berlaku saat Tampilan › Snap ke piksel aktif. # Character panel menu › Show Snap to Glyph Options (#4232). Show Snap to Glyph Options Tampilkan opsi snap ke glif Hide Snap to Glyph Options Sembunyikan opsi snap ke glif diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index bfeb24721f..2d6c7e2186 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -6364,6 +6364,13 @@ axis Y Axis Asse Y Area Text Testo in area # Transform panel menu › Measure Type by Glyph Bounds. Measure Type by Glyph Bounds Misura il testo in base ai limiti del glifo +# Control bar › Snapping › Pixel Snapping Options… (view.snapToPixel.options). + Pixel Snapping Options Opzioni di aggancio ai pixel + Pixel Snapping Options… Opzioni di aggancio ai pixel… + Snap to Pixel While Drawing Aggancia ai pixel durante il disegno + Snap to Pixel While Moving Aggancia ai pixel durante lo spostamento + Snap to Pixel While Scaling Aggancia ai pixel durante il ridimensionamento + These options take effect when View › Snap to Pixel is on. Queste opzioni hanno effetto quando Visualizza › Aggancia ai pixel è attivo. # Character panel menu › Show Snap to Glyph Options (#4232). Show Snap to Glyph Options Mostra opzioni di aggancio a glifo Hide Snap to Glyph Options Nascondi opzioni di aggancio a glifo diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 8b7803f24f..17444d894e 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -6429,6 +6429,13 @@ axis Y Axis Y 軸 Area Text エリア内文字 # Transform panel menu › Measure Type by Glyph Bounds. Measure Type by Glyph Bounds 文字を字形の境界で測定 +# Control bar › Snapping › Pixel Snapping Options… (view.snapToPixel.options). + Pixel Snapping Options ピクセルにスナップのオプション + Pixel Snapping Options… ピクセルにスナップのオプション… + Snap to Pixel While Drawing 描画時にピクセルにスナップ + Snap to Pixel While Moving 移動時にピクセルにスナップ + Snap to Pixel While Scaling 拡大・縮小時にピクセルにスナップ + These options take effect when View › Snap to Pixel is on. これらのオプションは、表示 › ピクセルにスナップがオンのときに有効になります。 # Character panel menu › Show Snap to Glyph Options (#4232). Show Snap to Glyph Options グリフにスナップのオプションを表示 Hide Snap to Glyph Options グリフにスナップのオプションを隠す diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index dea09ab43c..be8a847cb2 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -6762,6 +6762,13 @@ swatch Bright Violet კაშკაშა იისფერი Area Text არის ტექსტი # Transform panel menu › Measure Type by Glyph Bounds. Measure Type by Glyph Bounds ტექსტის გაზომვა გლიფის საზღვრებით +# Control bar › Snapping › Pixel Snapping Options… (view.snapToPixel.options). + Pixel Snapping Options პიქსელზე მიბმის პარამეტრები + Pixel Snapping Options… პიქსელზე მიბმის პარამეტრები… + Snap to Pixel While Drawing პიქსელზე მიბმა ხატვისას + Snap to Pixel While Moving პიქსელზე მიბმა გადაადგილებისას + Snap to Pixel While Scaling პიქსელზე მიბმა მასშტაბირებისას + These options take effect when View › Snap to Pixel is on. ეს პარამეტრები მოქმედებს, როცა ხედი › პიქსელზე მიბმა ჩართულია. # Character panel menu › Show Snap to Glyph Options (#4232). Show Snap to Glyph Options გლიფზე მიბმის პარამეტრების ჩვენება Hide Snap to Glyph Options გლიფზე მიბმის პარამეტრების დამალვა diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index 7825a79e06..991daba6d0 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -6325,6 +6325,13 @@ axis Y Axis Y축 Area Text 영역 문자 # Transform panel menu › Measure Type by Glyph Bounds. Measure Type by Glyph Bounds 글리프 경계로 문자 측정 +# Control bar › Snapping › Pixel Snapping Options… (view.snapToPixel.options). + Pixel Snapping Options 픽셀에 스냅 옵션 + Pixel Snapping Options… 픽셀에 스냅 옵션… + Snap to Pixel While Drawing 그리는 동안 픽셀에 스냅 + Snap to Pixel While Moving 이동하는 동안 픽셀에 스냅 + Snap to Pixel While Scaling 크기를 조절하는 동안 픽셀에 스냅 + These options take effect when View › Snap to Pixel is on. 이 옵션은 보기 › 픽셀에 스냅이 켜져 있을 때 적용됩니다. # Character panel menu › Show Snap to Glyph Options (#4232). Show Snap to Glyph Options 글리프에 스냅 옵션 표시 Hide Snap to Glyph Options 글리프에 스냅 옵션 숨기기 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index ac0caaf045..b8ca33858f 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -6813,6 +6813,13 @@ axis Y Axis Y-as Area Text Vlaktekst # Transform panel menu › Measure Type by Glyph Bounds. Measure Type by Glyph Bounds Tekst meten op glyphbegrenzing +# Control bar › Snapping › Pixel Snapping Options… (view.snapToPixel.options). + Pixel Snapping Options Opties voor uitlijnen op pixel + Pixel Snapping Options… Opties voor uitlijnen op pixel… + Snap to Pixel While Drawing Uitlijnen op pixel bij tekenen + Snap to Pixel While Moving Uitlijnen op pixel bij verplaatsen + Snap to Pixel While Scaling Uitlijnen op pixel bij schalen + These options take effect when View › Snap to Pixel is on. Deze opties werken wanneer Beeld › Uitlijnen op pixel is ingeschakeld. # Character panel menu › Show Snap to Glyph Options (#4232). Show Snap to Glyph Options Opties voor uitlijnen op glyph tonen Hide Snap to Glyph Options Opties voor uitlijnen op glyph verbergen diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 950326a839..eb5afe87cc 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -6593,6 +6593,13 @@ axis Y Axis Oś Y Area Text Tekst akapitowy # Transform panel menu › Measure Type by Glyph Bounds. Measure Type by Glyph Bounds Mierz tekst według granic glifów +# Control bar › Snapping › Pixel Snapping Options… (view.snapToPixel.options). + Pixel Snapping Options Opcje przyciągania do pikseli + Pixel Snapping Options… Opcje przyciągania do pikseli… + Snap to Pixel While Drawing Przyciągaj do pikseli podczas rysowania + Snap to Pixel While Moving Przyciągaj do pikseli podczas przesuwania + Snap to Pixel While Scaling Przyciągaj do pikseli podczas skalowania + These options take effect when View › Snap to Pixel is on. Te opcje działają, gdy włączona jest opcja Widok › Przyciągaj do pikseli. # Character panel menu › Show Snap to Glyph Options (#4232). Show Snap to Glyph Options Pokaż opcje przyciągania do glifów Hide Snap to Glyph Options Ukryj opcje przyciągania do glifów diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index be1dad14e9..80615d6370 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -6653,6 +6653,13 @@ axis Y Axis Eixo Y Area Text Texto de Área # Transform panel menu › Measure Type by Glyph Bounds. Measure Type by Glyph Bounds Medir Texto pelos Limites do Glifo +# Control bar › Snapping › Pixel Snapping Options… (view.snapToPixel.options). + Pixel Snapping Options Opções de Ajuste ao Pixel + Pixel Snapping Options… Opções de Ajuste ao Pixel… + Snap to Pixel While Drawing Ajustar ao Pixel ao Desenhar + Snap to Pixel While Moving Ajustar ao Pixel ao Mover + Snap to Pixel While Scaling Ajustar ao Pixel ao Dimensionar + These options take effect when View › Snap to Pixel is on. Essas opções têm efeito quando Exibir › Ajustar ao Pixel está ativado. # Character panel menu › Show Snap to Glyph Options (#4232). Show Snap to Glyph Options Mostrar Opções de Ajuste ao Glifo Hide Snap to Glyph Options Ocultar Opções de Ajuste ao Glifo diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 7f31596134..c830e69b37 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -6715,6 +6715,13 @@ theme Appearance Оформление Area Text Текст в области # Transform panel menu › Measure Type by Glyph Bounds. Measure Type by Glyph Bounds Измерять текст по границам глифа +# Control bar › Snapping › Pixel Snapping Options… (view.snapToPixel.options). + Pixel Snapping Options Параметры привязки к пикселю + Pixel Snapping Options… Параметры привязки к пикселю… + Snap to Pixel While Drawing Привязать к пикселю при рисовании + Snap to Pixel While Moving Привязать к пикселю при перемещении + Snap to Pixel While Scaling Привязать к пикселю при масштабировании + These options take effect when View › Snap to Pixel is on. Эти параметры действуют, когда включено Просмотр › Привязать к пикселю. # Character panel menu › Show Snap to Glyph Options (#4232). Show Snap to Glyph Options Показать параметры привязки к глифу Hide Snap to Glyph Options Скрыть параметры привязки к глифу diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index 505afd1540..d4304b97bd 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -6719,6 +6719,13 @@ axis Y Axis Y-axel Area Text Områdestext # Transform panel menu › Measure Type by Glyph Bounds. Measure Type by Glyph Bounds Mät text efter glyfgränser +# Control bar › Snapping › Pixel Snapping Options… (view.snapToPixel.options). + Pixel Snapping Options Alternativ för fäst mot pixel + Pixel Snapping Options… Alternativ för fäst mot pixel… + Snap to Pixel While Drawing Fäst mot pixel vid ritning + Snap to Pixel While Moving Fäst mot pixel vid flyttning + Snap to Pixel While Scaling Fäst mot pixel vid skalning + These options take effect when View › Snap to Pixel is on. De här alternativen gäller när Visa › Fäst mot pixel är på. # Character panel menu › Show Snap to Glyph Options (#4232). Show Snap to Glyph Options Visa alternativ för fäst mot glyf Hide Snap to Glyph Options Dölj alternativ för fäst mot glyf diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index e1b5541f27..f5fc694ec2 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -6694,6 +6694,13 @@ axis Y Axis Y ekseni Area Text Alan Metni # Transform panel menu › Measure Type by Glyph Bounds. Measure Type by Glyph Bounds Yazıyı Glif Sınırlarına Göre Ölç +# Control bar › Snapping › Pixel Snapping Options… (view.snapToPixel.options). + Pixel Snapping Options Piksel'e Yapışma Seçenekleri + Pixel Snapping Options… Piksel'e Yapışma Seçenekleri… + Snap to Pixel While Drawing Çizerken Piksel'e Yapış + Snap to Pixel While Moving Taşırken Piksel'e Yapış + Snap to Pixel While Scaling Ölçeklerken Piksel'e Yapış + These options take effect when View › Snap to Pixel is on. Bu seçenekler Görünüm › Piksel'e Yapış açıkken etkili olur. # Character panel menu › Show Snap to Glyph Options (#4232). Show Snap to Glyph Options Glif'e Yapışma Seçeneklerini Göster Hide Snap to Glyph Options Glif'e Yapışma Seçeneklerini Gizle diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index b5c4c1d0b7..5a36512db0 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -6344,6 +6344,13 @@ axis Y Axis Вісь Y Area Text Текст в області # Transform panel menu › Measure Type by Glyph Bounds. Measure Type by Glyph Bounds Вимірювати текст за межами гліфа +# Control bar › Snapping › Pixel Snapping Options… (view.snapToPixel.options). + Pixel Snapping Options Параметри прив’язки до пікселя + Pixel Snapping Options… Параметри прив’язки до пікселя… + Snap to Pixel While Drawing Прив’язати до пікселя під час малювання + Snap to Pixel While Moving Прив’язати до пікселя під час переміщення + Snap to Pixel While Scaling Прив’язати до пікселя під час масштабування + These options take effect when View › Snap to Pixel is on. Ці параметри діють, коли ввімкнено Перегляд › Прив’язати до пікселя. # Character panel menu › Show Snap to Glyph Options (#4232). Show Snap to Glyph Options Показати параметри прив’язки до гліфа Hide Snap to Glyph Options Приховати параметри прив’язки до гліфа diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index 93bdca786d..839f5c7405 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -6345,6 +6345,13 @@ axis Y Axis Trục Y Area Text Chữ vùng # Transform panel menu › Measure Type by Glyph Bounds. Measure Type by Glyph Bounds Đo chữ theo giới hạn glyph +# Control bar › Snapping › Pixel Snapping Options… (view.snapToPixel.options). + Pixel Snapping Options Tùy chọn hút vào pixel + Pixel Snapping Options… Tùy chọn hút vào pixel… + Snap to Pixel While Drawing Hút vào pixel khi vẽ + Snap to Pixel While Moving Hút vào pixel khi di chuyển + Snap to Pixel While Scaling Hút vào pixel khi thay đổi tỉ lệ + These options take effect when View › Snap to Pixel is on. Các tùy chọn này có hiệu lực khi Hiển thị › Hút vào pixel đang bật. # Character panel menu › Show Snap to Glyph Options (#4232). Show Snap to Glyph Options Hiện tùy chọn hút vào glyph Hide Snap to Glyph Options Ẩn tùy chọn hút vào glyph diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index eaca26b55d..b0d6001e32 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -6576,6 +6576,13 @@ axis Y Axis Y 轴 Area Text 区域文字 # Transform panel menu › Measure Type by Glyph Bounds. Measure Type by Glyph Bounds 按字形边界度量文字 +# Control bar › Snapping › Pixel Snapping Options… (view.snapToPixel.options). + Pixel Snapping Options 对齐像素选项 + Pixel Snapping Options… 对齐像素选项… + Snap to Pixel While Drawing 绘制时对齐像素 + Snap to Pixel While Moving 移动时对齐像素 + Snap to Pixel While Scaling 缩放时对齐像素 + These options take effect when View › Snap to Pixel is on. 这些选项在“视图 › 对齐像素”打开时生效。 # Character panel menu › Show Snap to Glyph Options (#4232). Show Snap to Glyph Options 显示靠齐字符选项 Hide Snap to Glyph Options 隐藏靠齐字符选项 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index cb71376f1a..c24ae37609 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -6382,6 +6382,13 @@ axis Y Axis Y 軸 Area Text 區域文字 # Transform panel menu › Measure Type by Glyph Bounds. Measure Type by Glyph Bounds 依字符邊界測量文字 +# Control bar › Snapping › Pixel Snapping Options… (view.snapToPixel.options). + Pixel Snapping Options 靠齊像素選項 + Pixel Snapping Options… 靠齊像素選項… + Snap to Pixel While Drawing 繪製時靠齊像素 + Snap to Pixel While Moving 移動時靠齊像素 + Snap to Pixel While Scaling 縮放時靠齊像素 + These options take effect when View › Snap to Pixel is on. 這些選項會在「檢視 › 靠齊像素」開啟時生效。 # Character panel menu › Show Snap to Glyph Options (#4232). Show Snap to Glyph Options 顯示靠齊字符選項 Hide Snap to Glyph Options 隱藏靠齊字符選項 diff --git a/crates/ui-egui/src/menus/invoke.rs b/crates/ui-egui/src/menus/invoke.rs index 262e346c2c..3aab5e1e44 100644 --- a/crates/ui-egui/src/menus/invoke.rs +++ b/crates/ui-egui/src/menus/invoke.rs @@ -203,7 +203,7 @@ pub fn invoke(app: &mut VectorcraftApp, id: &str, p: Value) { } } -/// The dialogs the Envelope, Variables, Pathfinder, Repeat, type, graph and View commands open when called without +/// The dialogs the Envelope, Variables, Pathfinder, Pixel Snapping, Repeat, type, graph and View commands open when called without /// params; whether `id` was one of them. fn open_view_dialog(app: &mut VectorcraftApp, id: &str, p: &Value) -> bool { // Envelope Distort: Warp Options, Envelope Mesh and Envelope Options, from the selection. @@ -223,6 +223,11 @@ fn open_view_dialog(app: &mut VectorcraftApp, id: &str, p: &Value) -> bool { crate::dialogs::pathfinder_options::open(app); return true; } + // Pixel Snapping Options: a dialog with the current options. + if id == "view.snapToPixel.options" && p.as_object().is_none_or(|o| o.is_empty()) { + crate::dialogs::pixel_snap_options::open(app); + return true; + } // Repeat Options: a dialog with the selected repeat's current values. if id == "object.repeat.options" && p.as_object().is_none_or(|o| o.is_empty()) diff --git a/crates/ui-egui/src/menus/ui_commands.rs b/crates/ui-egui/src/menus/ui_commands.rs index 6defd9c020..31340a3a48 100644 --- a/crates/ui-egui/src/menus/ui_commands.rs +++ b/crates/ui-egui/src/menus/ui_commands.rs @@ -196,7 +196,12 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[ ("view.pixelPreview", "Pixel Preview", "Cmd+Alt+Y", "{}"), ("view.trimView", "Trim View", "", "{} toggle: hide everything outside the artboards"), ("view.cornerWidget", "Hide Corner Widget", "", "{} toggle the live corner widgets"), - ("view.snapToPixel", "Snap to Pixel", "", "{} toggle: drawing and moving land on whole pixels"), + ( + "view.snapToPixel", + "Snap to Pixel", + "", + "{} toggle: drawing, moving and scaling land on whole pixels, each while its option in view.snapToPixel.options is on", + ), ( "view.snapToGlyph", "Snap to Glyph", diff --git a/docs/mcp.md b/docs/mcp.md index dc368d6da2..59bb3c78ad 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -1783,7 +1783,7 @@ vectorcraft-cli run --cmd file.new --params '{"width":600,"height":200}' --cmd ### Parameter schemas Some commands have a machine-readable parameter schema: at present a small set including `view.capsLock`, `view.saved.new`, -`view.saved.edit`, `view.saved.list`, `view.transparencyGrid`, `view.snapToGlyph.options` and `engine.apiReference` +`view.saved.edit`, `view.saved.list`, `view.transparencyGrid`, `view.snapToGlyph.options`, `view.snapToPixel.options` and `engine.apiReference` (most of the catalogue still documents params in prose only; schemas grow command by command). The engine's `engine.commands` list gives such a command a `schema` field: an object schema in the shape of JSON Schema 2020-12 (https://json-schema.org/draft/2020-12). Its root has `$schema` (the 2020-12 dialect), `description` when the @@ -1836,9 +1836,10 @@ as the English gives them. An omitted value with a `default` takes it (`rotation` 0 in `view.saved.new`); the journal keeps the params as they were given. Saved steps played from a journal or an action keep a key the schema does not name, unless its value nests -more than 16 levels deep, and the values of the parameters it names are checked as in a new call. The schemas of built-in commands belong to API level 3 +more than 16 levels deep, and the values of the parameters it names are checked as in a new call. The schemas of built-in commands belong to API level 4 (`vectorcraft_engine::schemas::API_VERSION`); a change to any of them raises the level. Level 2 added -`engine.apiReference`; level 3 says that `rotation` in `view.saved.new` and `view.saved.list` turns clockwise. Commands that plug-ins add have no schema here. +`engine.apiReference`; level 3 says that `rotation` in `view.saved.new` and `view.saved.list` turns clockwise; level 4 +adds `view.snapToPixel.options`. Commands that plug-ins add have no schema here. `engine.apiReference {}` → `{markdown}` returns the command API reference, written from these schemas each time it runs; no file of it is kept. It needs no open document and changes nothing. The reference has a `##` section for each @@ -5757,6 +5758,23 @@ know or a value that isn't a boolean is refused and changes nothing. {"name":"run_command","arguments":{"command":"view.snapToGlyph.options","params":{"angular":false,"anchors":false}}} ``` +The Pixel Snapping Options (the desktop app's Control bar › Snapping menu › Pixel Snapping Options…, described on the +help page, +): +`view.snapToPixel.options {drawing?, moving?, +scaling?}` (or `prefs.get`/`prefs.set` with key `pixelSnap`, a partial object) reads and sets which edits View › Snap to +Pixel lands on whole pixels. All three are on by default, which is VectorCraft's own default. `drawing` covers the +points the drawing tools place, the rulers' origin and measured points; `moving` covers the selection, its anchors and +segments, artboards and ruler guides moved by dragging, and the Rotate, Reflect and Shear tools' drags; `scaling` +covers the edges a bounding-box, Free Transform, artboard or slice handle drags, and the Scale tool's drag. An edit +whose option is off snaps as it does with Snap to Pixel off. With Snap to Pixel off, the options do nothing. `{}` reads +them; a key it doesn't know or a value that isn't a boolean is refused and changes nothing. Align New Objects to Pixel +Grid and the objects' pixel-aligned property (see Align to Pixel Grid) do not depend on these options. + +```json +{"name":"run_command","arguments":{"command":"view.snapToPixel.options","params":{"scaling":false}}} +``` + With Smart Guides on, a bounding-box handle dragged with the Selection or Free Transform tool (`mods.shift` proportional, `mods.alt` from the centre) lands on another object's anchor or centre (labelled "anchor" or "center"), else its corners and side line up with the edges and centres of the other objects and the artboards (a @@ -5984,8 +6002,8 @@ Preferences. `prefs.reset` resets the preferences at once, without the question. `prefs.export {path?}` writes the preferences as a JSON file, `{"format": "vectorcraft-preferences", "version": 1, "prefs": {…}}`, and replies `{path, count}`; without `path` it replies `{data, count}` with the file's text. The file -holds every preference `prefs.list` lists and the `eyedropper`, `perspectiveWidget`, `pathfinder` and `glyphSnap` -groups. It leaves out the folder preferences (`fontsFolder`, `pluginsFolder`, `recoveryFolder`, `templatesFolder`) and +holds every preference `prefs.list` lists and the `eyedropper`, `perspectiveWidget`, `pathfinder`, `glyphSnap` and +`pixelSnap` groups. It leaves out the folder preferences (`fontsFolder`, `pluginsFolder`, `recoveryFolder`, `templatesFolder`) and saved presets. An export over 1 MB, the most `prefs.import` reads, is an error and writes nothing. `prefs.import {path | data}` applies such a file and replies `{imported}`, the number of preferences it set. It checks every value first, as `prefs.set` does: a file over 1 MB, malformed JSON, another `format`, a newer diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 5897b33dff..7044797d5e 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -916,6 +916,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - Save / Don't Save / Cancel before closing or quitting with unsaved documents (tabs, Close, Close All, Quit, window close). - Four brightness themes, persistent preferences. - **Snapping quick access (#6164):** Control bar › Snapping includes Snap to Glyph (`view.snapToGlyph`) and Alignment Guides (`prefs.set`, `alignmentGuides`). The headless pointer/accessibility test `the_control_bar_snapping_menu_shares_glyph_and_alignment_settings` covers both commands, independent state, checked states after reopening, and all eight menu rows within a 1200 × 400-point window. Floating-panel parity remains unverified; scores and estimates unchanged. + - **Pixel Snapping Options:** Control bar › Snapping › Pixel Snapping Options… sets whether View › Snap to Pixel snaps drawing, moving and scaling, each on by default (`view.snapToPixel.options`, the `pixelSnap` preference group; [the help page](https://helpx.adobe.com/illustrator/desktop/measure-and-align/grids-and-guides/draw-pixel-perfect-art.html)). Tests: `pixel_snapping_options_read_set_and_reset`, `pixel_snapping_options_choose_the_edits_snap_to_pixel_rounds`, `handle_drag_follows_the_while_scaling_option`, `move_follows_the_while_moving_option`, `an_anchor_drag_follows_the_while_moving_option`, `new_ruler_guide_follows_the_while_moving_option` and `the_control_bar_snapping_menu_opens_pixel_snapping_options`. Scores and estimates unchanged. - **Measure tools and Snap to Glyph:** with Snap to Glyph, Smart Guides and Alignment Guides on, and Snap to Grid and Snap to Pixel off, both ends of a Measure or Measure & Inspect drag land within the snapping tolerance on the guides of live type that the Character panel's options allow, except Anchor Point, and a Shift-drag's end slides along its 45° step to them. The guide shows during the drag. This is VectorCraft's own rule (`Targets::for_measure`, `xform::measure::tests::its_ends_land_on_glyph_guides_with_snap_to_glyph`); scores and estimates unchanged. - **Drawing readout without the rubber band:** with Enable Rubber Band for the Pen or Curvature tool off and Measurement Labels on, the Pen, the Smart Pen and the Curvature tool hide the preview segment but still show the D / Angle / dX / dY readout at the snapped pointer before the next click, and the Curvature tool also shows it when going on from the current end of a selected path. Measurement Labels show distance and placement while drawing ([the help page](https://helpx.adobe.com/illustrator/desktop/measure-and-align/grids-and-guides/smart-guides-options.html)), and the rubber band preference turns off the path preview ([the help page](https://helpx.adobe.com/illustrator/desktop/draw-shapes-and-paths/draw-shapes/preview-paths-drawn.html)). Showing the readout with the rubber band off is VectorCraft's own rule (`pen::tests::measurement_label_shows_with_the_rubber_band_off`, and `the_measurement_label_shows_with_the_rubber_band_off` in `draw2::curvature::tests` and `addons::smart_pen::tests`); scores and estimates unchanged. - 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, art moved or duplicated with an artboard, and objects moved whole with direct-selected anchors) 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). Then Appearance of Black › On Screen (Display All Blacks Accurately by default: pure CMYK black shows through the CMYK profile, a dark grey; Display All Blacks as Rich Black shows it on the canvas as RGB black, also in CMYK documents drawn by ink planes, while rich blacks, black tints and images show as before; `RenderOptions::rich_black`, #394). Then Appearance of Black › Printing / Exporting (Output All Blacks Accurately by default; Output All Blacks as Rich Black draws pure CMYK black as RGB black in RGB raster exports and Print as Bitmap; CMYK ink exports and the document stay unchanged; `prefs.blackOutput` / `richBlack`, #394). Then Units › Identify Objects By (Object Name by default: Layers, Variables and Layer Options show and edit free names; XML ID shows and edits AI-style XML ids, `_` for a space and `_xHH_` escapes, and `layer.setProps` / rename refuse invalid ids; the document still stores the object name, #394). Then Type › Highlight Alternate Glyphs (off by default: on, pink behind OpenType substitutions on screen — ligatures, contextual forms; Document Setup's Highlight Substituted Glyphs stays separate; `prefs.set`, #394). About 14 stored options still do nothing yet (the rest of #394: Open Documents As Tabs, Real-time Drawing and Editing, GPU Performance beyond Animated Zoom, hyphenation language, scratch disks, the legacy clipboard, Touch Workspace).