View: Pixel Snapping Options choose whether Snap to Pixel snaps drawing, moving and scaling

Control bar > Snapping > Pixel Snapping Options... opens a dialog with
three checkboxes: Snap to Pixel While Drawing, While Moving and While
Scaling. OK sets them; Cancel leaves them as they were. While View >
Snap to Pixel is off, the dialog notes that the options take effect once
it is on.

- view.snapToPixel.options {drawing?, moving?, scaling?} reads and sets
  them. They are kept in the preferences as the pixelSnap group, all on
  by default, so turning Snap to Pixel on behaves as before. An unknown
  key or a value that is not a boolean is refused and changes nothing.
  The command has a parameter schema, which raises the API level to 4.
- Drawing covers points the drawing tools place, the Knife's straight
  cut, the Measure tool and the rulers' origin. Moving covers Selection
  and Direct Selection moves, ruler guides, artboard moves and the
  Rotate, Reflect and Shear tools' drags. Scaling covers bounding-box,
  Free Transform, artboard and slice handles and the Scale tool's drag.
  An edit whose option is off snaps as it does with Snap to Pixel off.
- Align New Objects to Pixel Grid and the objects' pixel-aligned
  property do not depend on these options.

Docs: docs/mcp.md, docs/target-app-parity.md.
This commit is contained in:
dotCooCoo
2026-11-13 01:38:58 -08:00
parent 3b48070751
commit f81793d671
60 changed files with 714 additions and 69 deletions
+9 -5
View File
@@ -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<Value, String> {
@@ -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<CommandSpec> {
"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
),
+33 -3
View File
@@ -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<CommandSpec> {
vec![
cmd!(
@@ -125,6 +138,16 @@ pub fn specs() -> Vec<CommandSpec> {
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<Value> {
Ok(json!(s.prefs.glyph_snap))
}
fn pixel_snap_options(s: &mut Session, p: &Value) -> Result<Value> {
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<Value> {
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, &params).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}");
}
}
+4
View File
@@ -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![],
+10 -3
View File
@@ -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());
+32
View File
@@ -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();
+30
View File
@@ -2736,3 +2736,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);
}
+3 -1
View File
@@ -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,
@@ -355,6 +356,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,
+6 -1
View File
@@ -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",
+61 -1
View File
@@ -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<Self, String> {
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
+7 -7
View File
@@ -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)));
+27
View File
@@ -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
+3 -3
View File
@@ -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
+37 -9
View File
@@ -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<Overlay>) {
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<Overlay>) {
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)
}
+4 -4
View File
@@ -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 ›
+2 -1
View File
@@ -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,
+5 -4
View File
@@ -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 {
+18
View File
@@ -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]
+7 -6
View File
@@ -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<Overlay> {
/// 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<Rect>,
targets: Option<Targets>,
@@ -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 };
+45
View File
@@ -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).
+1 -1
View File
@@ -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)) }),
+1 -1
View File
@@ -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
+9 -7
View File
@@ -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 {
+1 -1
View File
@@ -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![]
+1 -1
View File
@@ -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);
+5 -2
View File
@@ -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));
+6 -1
View File
@@ -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),
+76
View File
@@ -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<egui::Event>| {
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) };
+2
View File
@@ -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,
@@ -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<Value, String> {
super::run_and_close(app, "view.snapToPixel.options", Value::Object(d.fields.clone()))
}
+7
View File
@@ -6159,6 +6159,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 Скриване на опциите за прилепване към глиф
Can't render this file because it contains an unexpected character in line 2 and column 73.
+6
View File
@@ -6218,6 +6218,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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+7
View File
@@ -6676,6 +6676,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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+7
View File
@@ -6713,6 +6713,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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+7
View File
@@ -6176,6 +6176,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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+7
View File
@@ -6304,6 +6304,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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+7
View File
@@ -6664,6 +6664,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 Απόκρυψη επιλογών προσκόλλησης σε γλυφή
Can't render this file because it is too large.
+7
View File
@@ -6655,6 +6655,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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+7
View File
@@ -6743,6 +6743,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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+7
View File
@@ -6708,6 +6708,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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+6
View File
@@ -4556,6 +4556,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 स्वॉच प्रकार दिखाएँ
Can't render this file because it is too large.
+7
View File
@@ -6696,6 +6696,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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+7
View File
@@ -6176,6 +6176,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
Can't render this file because it contains an unexpected character in line 2418 and column 5.
+7
View File
@@ -6338,6 +6338,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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+7
View File
@@ -6403,6 +6403,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 グリフにスナップのオプションを隠す
Can't render this file because it contains an unexpected character in line 2 and column 73.
+7
View File
@@ -6737,6 +6737,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 გლიფზე მიბმის პარამეტრების დამალვა
Can't render this file because it is too large.
+7
View File
@@ -6299,6 +6299,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 글리프에 스냅 옵션 숨기기
Can't render this file because it contains an unexpected character in line 2 and column 73.
+7
View File
@@ -6227,6 +6227,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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+7
View File
@@ -6567,6 +6567,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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+7
View File
@@ -6628,6 +6628,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
Can't render this file because it contains an unexpected character in line 6 and column 47.
+7
View File
@@ -6690,6 +6690,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 Скрыть параметры привязки к глифу
Can't render this file because it is too large.
+7
View File
@@ -6694,6 +6694,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
Can't render this file because it is too large.
+7
View File
@@ -6669,6 +6669,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
Can't render this file because it contains an unexpected character in line 5 and column 25.
+7
View File
@@ -6318,6 +6318,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 Приховати параметри прив’язки до гліфа
Can't render this file because it is too large.
+7
View File
@@ -6319,6 +6319,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
Can't render this file because it contains an unexpected character in line 204 and column 5.
+7
View File
@@ -6551,6 +6551,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 隐藏靠齐字符选项
Can't render this file because it contains an unexpected character in line 2 and column 73.
+7
View File
@@ -6356,6 +6356,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 隱藏靠齊字符選項
Can't render this file because it contains an unexpected character in line 6 and column 47.
+6 -1
View File
@@ -206,7 +206,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.
@@ -226,6 +226,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())
+6 -1
View File
@@ -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",
+23 -5
View File
@@ -1777,7 +1777,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
@@ -1830,9 +1830,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
@@ -5636,6 +5637,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,
<https://helpx.adobe.com/illustrator/desktop/measure-and-align/grids-and-guides/draw-pixel-perfect-art.html>):
`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
@@ -5863,8 +5881,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
File diff suppressed because one or more lines are too long