M11.3: View → Snap to Pixel (drawing points and moved selections land on whole pixels)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
16a73dc02a
commit
8e735f1de9
|
|
@ -14,11 +14,13 @@ pub struct ViewInfo {
|
|||
pub smart_guides: bool,
|
||||
pub snap_to_grid: bool,
|
||||
pub show_bbox: bool,
|
||||
/// View → Snap to Pixel: drawing and moving land on whole pixels (points at 72 ppi).
|
||||
pub snap_to_pixel: bool,
|
||||
}
|
||||
|
||||
impl Default for ViewInfo {
|
||||
fn default() -> Self {
|
||||
Self { zoom: 1.0, outline: false, smart_guides: true, snap_to_grid: false, show_bbox: true }
|
||||
Self { zoom: 1.0, outline: false, smart_guides: true, snap_to_grid: false, show_bbox: true, snap_to_pixel: false }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -60,6 +62,7 @@ impl Session {
|
|||
smart_guides: view.smart_guides,
|
||||
snap_to_grid: view.snap_to_grid,
|
||||
show_bbox: view.show_bbox,
|
||||
snap_to_pixel: view.snap_to_pixel,
|
||||
};
|
||||
f(self.tool.as_mut(), &cx)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -135,6 +135,9 @@ impl Targets {
|
|||
|
||||
/// Snap `p` for a drawing tool when smart guides (or grid snapping) are on.
|
||||
pub fn snap_draw(cx: &ToolContext, p: Point, exclude: &[NodeId]) -> (Point, Vec<Overlay>) {
|
||||
if cx.snap_to_pixel {
|
||||
return (Point::new(p.x.round(), p.y.round()), vec![]);
|
||||
}
|
||||
if cx.snap_to_grid {
|
||||
let s = cx.doc.grid.spacing / cx.doc.grid.subdivisions.max(1) as f64;
|
||||
return (vectorcraft_geom::snap::snap_point_to_grid(p, s), vec![]);
|
||||
|
|
|
|||
|
|
@ -148,6 +148,8 @@ 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.
|
||||
pub snap_to_pixel: bool,
|
||||
}
|
||||
|
||||
impl ToolContext<'_> {
|
||||
|
|
@ -312,6 +314,7 @@ pub(crate) mod testutil {
|
|||
smart_guides: true,
|
||||
snap_to_grid: false,
|
||||
show_bbox: true,
|
||||
snap_to_pixel: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -146,6 +146,13 @@ impl Tool for SelectionTool {
|
|||
d += adj;
|
||||
self.guides = ov;
|
||||
}
|
||||
// Snap to Pixel: the moved selection's top-left lands on whole pixels.
|
||||
if cx.snap_to_pixel
|
||||
&& let Some(b) = self.start_bounds
|
||||
{
|
||||
d = vectorcraft_geom::Vec2::new((b.x0 + d.x).round() - b.x0, (b.y0 + d.y).round() - b.y0);
|
||||
self.guides.clear();
|
||||
}
|
||||
self.state = State::Moving { start, began: true };
|
||||
self.measure = Some((p, format!("dX: {:.2} pt\ndY: {:.2} pt", d.x, d.y)));
|
||||
out.push(Action::Preview("object.transform".into(), json!({ "matrix": matrix_json(Affine::translate(d)), "copy": m.alt })));
|
||||
|
|
|
|||
|
|
@ -210,6 +210,7 @@ impl VectorcraftApp {
|
|||
outline: self.ui.view.outline,
|
||||
smart_guides: self.ui.view.smart_guides,
|
||||
snap_to_grid: self.ui.view.snap_to_grid,
|
||||
snap_to_pixel: self.ui.view.snap_to_pixel,
|
||||
show_bbox: self.ui.view.bounding_box,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -91,6 +91,7 @@ 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.zoomIn", "Zoom In", "Cmd+=", "{}"),
|
||||
("view.zoomOut", "Zoom Out", "Cmd+-", "{}"),
|
||||
("view.fitArtboard", "Fit Artboard in Window", "Cmd+0", "{}"),
|
||||
|
|
@ -248,6 +249,29 @@ pub fn run_ui_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<R
|
|||
"view.pixelPreview" => flag(&mut app.ui.view.pixel_preview),
|
||||
"view.trimView" => flag(&mut app.ui.view.trim_view),
|
||||
"view.cornerWidget" => flag(&mut app.ui.view.corner_widgets),
|
||||
// New View: the engine stores the current zoom, centre and rotation under the given name.
|
||||
"view.saved.new" if p.get("zoom").is_none() => {
|
||||
let Some(v) = app.view().copied() else { return Some(Err("no document".into())) };
|
||||
let mut params = if p.is_object() { p.clone() } else { json!({}) };
|
||||
params["center"] = json!([v.center.x, v.center.y]);
|
||||
params["zoom"] = json!(v.zoom);
|
||||
params["rotation"] = json!(v.rotation);
|
||||
app.session.execute(id, ¶ms).map_err(|e| e.to_string())
|
||||
}
|
||||
id if id.starts_with("view.goto") => {
|
||||
let n: usize = id["view.goto".len()..].parse().unwrap_or(0);
|
||||
let saved = app.session.active().and_then(|d| n.checked_sub(1).and_then(|i| d.doc.views.get(i)).cloned());
|
||||
match (saved, app.view_mut()) {
|
||||
(Some(sv), Some(v)) => {
|
||||
v.center = sv.center;
|
||||
v.zoom = sv.zoom;
|
||||
v.rotation = sv.rotation;
|
||||
Ok(json!({ "name": sv.name }))
|
||||
}
|
||||
_ => Err("no such view".into()),
|
||||
}
|
||||
}
|
||||
"view.snapToPixel" => flag(&mut app.ui.view.snap_to_pixel),
|
||||
id if id.starts_with("type.recentFont") => {
|
||||
let n: usize = id["type.recentFont".len()..].parse().unwrap_or(0);
|
||||
match n.checked_sub(1).and_then(|i| app.ui.recent_fonts.get(i)).cloned() {
|
||||
|
|
@ -430,6 +454,7 @@ pub fn checked(app: &VectorcraftApp, id: &str, p: &Value) -> Option<bool> {
|
|||
"view.outline" => v.outline,
|
||||
"view.pixelPreview" => v.pixel_preview,
|
||||
"view.trimView" => v.trim_view,
|
||||
"view.snapToPixel" => v.snap_to_pixel,
|
||||
// Type on a Path effect of the selected path type.
|
||||
"type.pathOptions" if p.get("effect").is_some() => {
|
||||
let st = app.session.active()?;
|
||||
|
|
@ -1072,12 +1097,23 @@ pub fn menu_tree() -> Vec<(&'static str, Vec<Item>)> {
|
|||
),
|
||||
c("Show Grid", "view.grid"),
|
||||
c("Snap to Grid", "view.snapToGrid"),
|
||||
todo("Snap to Pixel"),
|
||||
c("Snap to Pixel", "view.snapToPixel"),
|
||||
c("Snap to Point", "view.snapToPoint"),
|
||||
todo("Snap to Glyph"),
|
||||
Sep,
|
||||
todo("New View…"),
|
||||
todo("Edit Views…"),
|
||||
c("New View…", "view.saved.new"),
|
||||
c("Edit Views…", "view.saved.edit"),
|
||||
Sep,
|
||||
c("Saved View 1", "view.goto1"),
|
||||
c("Saved View 2", "view.goto2"),
|
||||
c("Saved View 3", "view.goto3"),
|
||||
c("Saved View 4", "view.goto4"),
|
||||
c("Saved View 5", "view.goto5"),
|
||||
c("Saved View 6", "view.goto6"),
|
||||
c("Saved View 7", "view.goto7"),
|
||||
c("Saved View 8", "view.goto8"),
|
||||
c("Saved View 9", "view.goto9"),
|
||||
c("Saved View 10", "view.goto10"),
|
||||
],
|
||||
),
|
||||
(
|
||||
|
|
|
|||
|
|
@ -119,6 +119,8 @@ pub struct ViewFlags {
|
|||
pub trim_view: bool,
|
||||
/// View → Hide/Show Corner Widget (live corner widgets on rectangles).
|
||||
pub corner_widgets: bool,
|
||||
/// View → Snap to Pixel.
|
||||
pub snap_to_pixel: bool,
|
||||
}
|
||||
|
||||
impl Default for ViewFlags {
|
||||
|
|
@ -138,6 +140,7 @@ impl Default for ViewFlags {
|
|||
edges: true,
|
||||
trim_view: false,
|
||||
corner_widgets: true,
|
||||
snap_to_pixel: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue