Merge pull request #1918 from sanyamsingh001/codex/r65-guide-origin

G1: align guide snapping and readouts with ruler origin
This commit is contained in:
BFlat
2026-10-16 06:37:36 +07:00
committed by GitHub
5 changed files with 412 additions and 11 deletions
+10
View File
@@ -123,6 +123,9 @@ use serde::{Deserialize, Serialize};
use vectorcraft_color::{Swatch, SwatchGroup};
use vectorcraft_geom::{Affine, Point, Rect, Vec2};
/// Coordinates beyond this many points exceed the supported document range.
pub const MAX_COORD: f64 = 4.0e6;
#[derive(Debug, thiserror::Error, PartialEq)]
pub enum DocError {
#[error("no such object {0}")]
@@ -749,6 +752,13 @@ type Places = BTreeMap<Vec<usize>, Vec<(usize, usize)>>;
pub const FORMAT_VERSION: u32 = 1;
impl Document {
/// The ruler zero point, or zero on both axes when either stored coordinate is invalid.
/// Rulers, coordinate fields and guide snapping use the same supported range.
pub fn ruler_origin_or_zero(&self) -> Point {
let origin = self.ruler_origin;
if origin.is_finite() && origin.x.abs() <= MAX_COORD && origin.y.abs() <= MAX_COORD { origin } else { Point::ZERO }
}
/// A new RGB document with one artboard of `size` and one layer ("Layer 1").
pub fn new(width: f64, height: f64) -> Self {
Self::new_with_mode(width, height, ColorMode::Rgb)
+1 -1
View File
@@ -546,7 +546,7 @@ impl DocState {
}
/// Coordinates beyond this (points) are rejected: ~1,400 m, far past Illustrator's large canvas.
pub const MAX_COORD: f64 = 4.0e6;
pub use vectorcraft_doc::MAX_COORD;
/// The commands that act on the active artboard (#693), which the app passes as their `artboard`:
/// Paste in Place, in Front and in Back paste onto it, Align to Artboard aligns to it, Distribute
+207 -3
View File
@@ -112,7 +112,9 @@ impl GuideSnap {
fn snap(&mut self, cx: &ToolContext, vertical: bool, v: f64, shift: bool) -> f64 {
self.snapped.clear();
if shift {
return cx.unit.snap_to_ruler_tick(v, cx.zoom);
let origin = cx.doc.ruler_origin_or_zero();
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 {
return v.round();
@@ -130,8 +132,9 @@ impl GuideSnap {
fn overlays(&self, cx: &ToolContext, vertical: bool, at: f64, pointer: Point) -> Vec<Overlay> {
let mut o = self.snapped.clone();
if cx.measurement_labels {
let axis = if vertical { "X" } else { "Y" };
o.push(Overlay::Measure { p: pointer, text: format!("{axis}: {}", cx.len(at)) });
let origin = cx.doc.ruler_origin_or_zero();
let (axis, zero) = if vertical { ("X", origin.x) } else { ("Y", origin.y) };
o.push(Overlay::Measure { p: pointer, text: format!("{axis}: {}", cx.len(at - zero)) });
}
o
}
@@ -631,4 +634,205 @@ mod tests {
g.pointer(&c, &ev(PointerKind::Drag, 50.0, -6.0, m));
assert_eq!(g.pointer(&c, &ev(PointerKind::Up, 50.0, -6.0, m)), Some(vec![Action::Commit]));
}
// Tick positions are explicit fixtures in each unit, independent of the snap implementation.
fn origin_tick_cases() -> Vec<(vectorcraft_doc::Unit, f64, Point, Point, Point, f64)> {
use vectorcraft_doc::Unit;
let mm = |v| Unit::Millimeters.to_pt(v);
let inch = |v| Unit::Inches.to_pt(v);
vec![
(Unit::Points, 1.0, Point::new(17.0, 23.0), Point::new(223.0, 237.0), Point::new(222.0, 238.0), 5.0),
(Unit::Points, 1.0, Point::new(-17.0, -23.0), Point::new(223.0, 237.0), Point::new(223.0, 237.0), 5.0),
(Unit::Points, 5.0, Point::new(17.25, 23.75), Point::new(223.4, 237.2), Point::new(223.25, 236.75), 1.0),
(Unit::Millimeters, 1.0, Point::new(mm(1.0), mm(-1.0)), Point::new(mm(78.7), mm(83.6)), Point::new(mm(78.5), mm(84.0)), mm(2.5)),
(
Unit::Inches,
2.0,
Point::new(inch(0.01), inch(0.03)),
Point::new(inch(3.07), inch(3.27)),
Point::new(inch(3.06), inch(3.28)),
inch(0.05),
),
]
}
fn guide_preview_position(actions: &[Action], vertical: bool, from: Option<f64>) -> f64 {
let Some(Action::Preview(command, p)) = actions.iter().find(|a| matches!(a, Action::Preview(_, _))) else {
panic!("missing guide preview: {actions:?}");
};
if let Some(from) = from {
assert_eq!(command, "guide.move");
from + p[if vertical { "dx" } else { "dy" }].as_f64().unwrap()
} else {
assert_eq!(command, "guide.add");
assert_eq!(p["vertical"], json!(vertical));
p["pos"].as_f64().unwrap()
}
}
#[test]
fn ruler_guide_shift_new_uses_document_origin() {
for (unit, zoom, origin, pointer, expected, tick) in origin_tick_cases() {
let (mut doc, _) = doc_with_rect();
doc.ruler_origin = origin;
let (selection, paint) = (Selection::default(), paint());
let c = ToolContext { unit, zoom, smart_guides: false, snap_to_point: false, ..cx(&doc, &selection, &paint) };
for vertical in [false, true] {
let mut guide = NewGuide::new(&c, vertical, None);
let mods = Mods { shift: true, ..Default::default() };
let actions = guide.pointer(&c, &ev(PointerKind::Drag, pointer.x, pointer.y, mods), true);
assert_eq!(actions.first(), Some(&Action::Begin("New Guide".into())));
let want = if vertical { expected.x } else { expected.y };
let actual = guide_preview_position(&actions, vertical, None);
assert!((actual - want).abs() < 1e-8, "{unit:?} {origin:?} vertical={vertical} actual={actual} want={want}");
assert!(guide.pointer(&c, &ev(PointerKind::Drag, pointer.x, pointer.y, mods), true).is_empty());
// Release updates to its own tick rather than committing a stale preview.
let actions = guide.pointer(&c, &ev(PointerKind::Up, pointer.x + tick * 2.0, pointer.y + tick * 2.0, mods), true);
assert!((guide_preview_position(&actions, vertical, None) - (want + tick * 2.0)).abs() < 1e-8);
assert_eq!(actions.last(), Some(&Action::Commit));
assert!(guide.overlays(&c).is_empty());
}
}
}
#[test]
fn ruler_guide_shift_move_uses_document_origin() {
for (unit, zoom, origin, pointer, expected, _) in origin_tick_cases() {
let mut doc = guides_doc();
doc.ruler_origin = origin;
let paint = paint();
for (index, vertical, from) in [(0, true, 300.0), (1, false, 400.0)] {
let mut selection = Selection::default();
selection.set_guides([index]);
let c = ToolContext { unit, zoom, smart_guides: false, snap_to_point: false, ..cx(&doc, &selection, &paint) };
for copy in [false, true] {
let mut guide = GuideEdit::default();
let (start, to, want) = if vertical {
(Point::new(from, 50.0), Point::new(pointer.x, 50.0), expected.x)
} else {
(Point::new(50.0, from), Point::new(50.0, pointer.y), expected.y)
};
assert!(guide.press(&c, &ev(PointerKind::Down, start.x, start.y, Mods::default())).unwrap().is_empty());
let mods = Mods { shift: true, alt: copy, ..Default::default() };
let actions = guide.pointer(&c, &ev(PointerKind::Drag, to.x, to.y, mods)).unwrap();
assert_eq!(actions.first(), Some(&Action::Begin(if copy { "Copy Guide" } else { "Move Guide" }.into())));
let actual = guide_preview_position(&actions, vertical, Some(from));
assert!((actual - want).abs() < 1e-8, "{unit:?} {origin:?} vertical={vertical} copy={copy} actual={actual} want={want}");
assert!(matches!(actions.last(), Some(Action::Preview(_, p)) if p["copy"] == json!(copy)));
assert_eq!(guide.pointer(&c, &ev(PointerKind::Up, to.x, to.y, mods)), Some(vec![Action::Commit]));
assert!(!guide.busy());
}
}
}
}
#[test]
fn ruler_guide_shift_zero_and_invalid_origins_use_zero() {
// One bad axis invalidates the entire origin, matching the app's ruler display.
for origin in [
Point::ZERO,
Point::new(f64::NAN, 23.0),
Point::new(17.0, f64::NAN),
Point::new(f64::INFINITY, 23.0),
Point::new(17.0, f64::NEG_INFINITY),
Point::new(4_000_001.0, 23.0),
Point::new(17.0, -4_000_001.0),
] {
let mut doc = guides_doc();
doc.ruler_origin = origin;
let paint = paint();
for (index, vertical, from, want) in [(0, true, 300.0, 225.0), (1, false, 400.0, 235.0)] {
let mut selection = Selection::default();
selection.set_guides([index]);
let c = ToolContext { smart_guides: false, snap_to_point: false, ..cx(&doc, &selection, &paint) };
let mods = Mods { shift: true, ..Default::default() };
let mut new = NewGuide::new(&c, vertical, None);
let actions = new.pointer(&c, &ev(PointerKind::Drag, 223.0, 237.0, mods), true);
assert_eq!(guide_preview_position(&actions, vertical, None), want);
let mut moved = GuideEdit::default();
let (start, to) = if vertical { ((from, 50.0), (223.0, 50.0)) } else { ((50.0, from), (50.0, 237.0)) };
moved.press(&c, &ev(PointerKind::Down, start.0, start.1, Mods::default())).unwrap();
let actions = moved.pointer(&c, &ev(PointerKind::Drag, to.0, to.1, mods)).unwrap();
assert_eq!(guide_preview_position(&actions, vertical, Some(from)), want);
}
}
}
#[test]
fn ruler_guide_origin_leaves_other_snap_modes_unchanged() {
for origin in [Point::ZERO, Point::new(17.0, 23.0), Point::new(-17.0, -23.0)] {
let mut doc = guides_doc();
doc.ruler_origin = origin;
let paint = paint();
for (index, vertical, from) in [(0, true, 300.0), (1, false, 400.0)] {
let mut selection = Selection::default();
selection.set_guides([index]);
let off = ToolContext { smart_guides: false, snap_to_point: false, ..cx(&doc, &selection, &paint) };
for (c, pointer, expected) in [
(ToolContext { ..off }, Point::new(226.4, 237.6), Point::new(226.4, 237.6)),
(ToolContext { snap_to_pixel: true, ..off }, Point::new(226.4, 237.6), Point::new(226.0, 238.0)),
(ToolContext { snap_to_grid: true, ..off }, Point::new(226.4, 237.6), Point::new(225.0, 234.0)),
(ToolContext { smart_guides: true, ..off }, Point::new(203.0, 102.0), Point::new(200.0, 100.0)),
(ToolContext { snap_to_point: true, ..off }, Point::new(201.0, 102.0), Point::new(200.0, 100.0)),
] {
let want = if vertical { expected.x } else { expected.y };
let mut new = NewGuide::new(&c, vertical, None);
let actions = new.pointer(&c, &ev(PointerKind::Drag, pointer.x, pointer.y, Mods::default()), true);
assert!((guide_preview_position(&actions, vertical, None) - want).abs() < 1e-8);
let mut moved = GuideEdit::default();
let (start, to) =
if vertical { ((from, pointer.y), (pointer.x, pointer.y)) } else { ((pointer.x, from), (pointer.x, pointer.y)) };
moved.press(&c, &ev(PointerKind::Down, start.0, start.1, Mods::default())).unwrap();
let actions = moved.pointer(&c, &ev(PointerKind::Drag, to.0, to.1, Mods::default())).unwrap();
assert!((guide_preview_position(&actions, vertical, Some(from)) - want).abs() < 1e-8);
}
}
}
}
#[test]
fn ruler_guide_origin_readouts_are_relative_for_new_and_moved_guides() {
for (origin, expected) in [
(Point::new(17.0, 23.0), Point::new(205.0, 215.0)),
(Point::new(-17.0, -23.0), Point::new(239.0, 261.0)),
(Point::ZERO, Point::new(222.0, 238.0)),
(Point::new(f64::NAN, 23.0), Point::new(222.0, 238.0)),
(Point::new(17.0, 4_000_001.0), Point::new(222.0, 238.0)),
] {
let mut doc = guides_doc();
doc.ruler_origin = origin;
let paint = paint();
for unit in [vectorcraft_doc::Unit::Points, vectorcraft_doc::Unit::Millimeters] {
for (index, vertical, from) in [(0, true, 300.0), (1, false, 400.0)] {
let mut selection = Selection::default();
selection.set_guides([index]);
let c = ToolContext { unit, smart_guides: false, snap_to_point: false, ..cx(&doc, &selection, &paint) };
let want = format!("{}: {}", if vertical { "X" } else { "Y" }, c.len(if vertical { expected.x } else { expected.y }));
let check = |overlays: Vec<Overlay>| {
assert!(
matches!(overlays.last(), Some(Overlay::Measure { text, .. }) if text == &want),
"{origin:?} {unit:?}: {overlays:?}, want {want}"
);
};
let mut new = NewGuide::new(&c, vertical, None);
let actions = new.pointer(&c, &ev(PointerKind::Drag, 222.0, 238.0, Mods::default()), true);
assert_eq!(
guide_preview_position(&actions, vertical, None),
if vertical { 222.0 } else { 238.0 },
"commands keep absolute coordinates"
);
check(new.overlays(&c));
for copy in [false, true] {
let mut moved = GuideEdit::default();
let (start, to) = if vertical { ((from, 50.0), (222.0, 50.0)) } else { ((50.0, from), (50.0, 238.0)) };
moved.press(&c, &ev(PointerKind::Down, start.0, start.1, Mods::default())).unwrap();
moved.pointer(&c, &ev(PointerKind::Drag, to.0, to.1, Mods { alt: copy, ..Default::default() })).unwrap();
check(moved.overlays(&c));
}
let quiet = ToolContext { measurement_labels: false, ..c };
assert!(new.overlays(&quiet).is_empty(), "measurement-label preference is preserved");
}
}
}
}
}
+193
View File
@@ -5018,4 +5018,197 @@ mod tests {
near(select(&mut app, &a), under_a);
near(select(&mut app, &b), under_b);
}
fn ruler_guide_origin_ui(alt: bool, cancel: bool) {
use egui::{Event, Modifiers};
for (origin, preview, release) in [
(Point::new(17.0, 23.0), Point::new(222.0, 238.0), Point::new(232.0, 243.0)),
(Point::new(-17.0, -23.0), Point::new(223.0, 237.0), Point::new(233.0, 242.0)),
(Point::ZERO, Point::new(225.0, 235.0), Point::new(230.0, 245.0)),
] {
for source_vertical in [false, true] {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 400, "height": 300})).unwrap();
app.run("document.setRulerOrigin", json!({"x": origin.x, "y": origin.y})).unwrap();
app.run("shape.rectangle", json!({"x": 40, "y": 50, "width": 20, "height": 30})).unwrap();
let redo = app.session.active().unwrap().doc.clone();
app.run("edit.undo", json!({})).unwrap();
app.session.active_mut().unwrap().mark_saved();
app.ui.view.rulers = true;
app.ui.view.smart_guides = false;
app.ui.view.snap_to_point = false;
app.ui.view.snap_to_grid = false;
app.ui.view.snap_to_pixel = false;
app.select_tool("rectangle");
let ctx = egui::Context::default();
frame(&mut app, &ctx, vec![]);
let view = app.view_mut().unwrap();
view.zoom = 1.0;
view.rotation = 0.0;
view.center = Point::new(200.0, 150.0);
frame(&mut app, &ctx, vec![]);
let rect = app.canvas_rect.unwrap();
let xf = Xf::new(rect, app.view().unwrap());
let from =
if source_vertical { pos2(rect.left() - RULER / 2.0, rect.center().y) } else { pos2(rect.center().x, rect.top() - RULER / 2.0) };
let to = xf.to_screen(Point::new(223.0, 237.0));
let end = xf.to_screen(Point::new(231.0, 244.0));
assert!(rect.contains(to) && rect.contains(end));
let mods = if alt { Modifiers::SHIFT | Modifiers::ALT } else { Modifiers::SHIFT };
let button = |pos, pressed| Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers: mods };
let before = app.session.active().unwrap().doc.clone();
let selection = app.session.active().unwrap().selection.clone();
let history = |app: &VectorcraftApp| {
let st = app.session.active().unwrap();
(st.history.undo.len(), st.history.redo.len(), st.is_dirty())
};
let original_history = history(&app);
assert_eq!(original_history.1, 1);
frame(&mut app, &ctx, vec![Event::ModifiersChanged(mods), Event::PointerMoved(from)]);
frame(&mut app, &ctx, vec![button(from, true)]);
frame(&mut app, &ctx, vec![Event::PointerMoved(to)]);
let check = |app: &VectorcraftApp, switched: bool, expected: Point| {
let guides = &app.session.active().unwrap().doc.guides;
assert_eq!(guides.len(), 1);
let vertical = source_vertical != switched;
assert_eq!(guides[0].vertical, vertical);
let want = if vertical { expected.x } else { expected.y };
assert!((guides[0].pos - want).abs() < 1e-4, "origin={origin:?} vertical={vertical}: {:?}, want {want}", guides[0]);
};
assert_eq!(history(&app).0, original_history.0, "preview has no undo entry");
if cancel {
// Cancellation is a baseline positive control, independent of snap correctness.
assert_eq!(app.session.active().unwrap().doc.guides.len(), 1);
frame(&mut app, &ctx, vec![Event::PointerMoved(from), button(from, false)]);
assert_eq!(app.session.active().unwrap().doc, before);
assert_eq!(app.session.active().unwrap().selection, selection);
assert_eq!(history(&app), original_history);
app.run("edit.redo", json!({})).unwrap();
assert_eq!(app.session.active().unwrap().doc, redo);
continue;
}
check(&app, alt, preview);
if alt {
// Used only by the private #1782 compatibility regression, not standalone RED.
frame(&mut app, &ctx, vec![Event::ModifiersChanged(Modifiers::SHIFT)]);
check(&app, false, preview);
frame(&mut app, &ctx, vec![Event::ModifiersChanged(mods)]);
check(&app, true, preview);
}
frame(&mut app, &ctx, vec![Event::PointerMoved(end), button(end, false)]);
check(&app, alt, release);
let st = app.session.active().unwrap();
assert_eq!(st.doc.ruler_origin, origin);
assert_eq!(st.doc.layers, before.layers, "the rectangle tool draws no artwork");
assert_eq!(st.selection, selection);
assert_eq!(history(&app), (original_history.0 + 1, 0, true));
assert_eq!(st.history.undo.last().unwrap().label, "New Guide");
let after = st.doc.clone();
app.run("edit.undo", json!({})).unwrap();
assert_eq!(app.session.active().unwrap().doc, before);
assert_eq!(history(&app), (original_history.0, 1, false));
app.run("edit.redo", json!({})).unwrap();
assert_eq!(app.session.active().unwrap().doc, after);
}
}
}
#[test]
fn ruler_guide_shift_origin_preview_release_and_history() {
ruler_guide_origin_ui(false, false);
}
#[test]
fn ruler_guide_shift_origin_cancel_preserves_redo() {
ruler_guide_origin_ui(false, true);
}
#[test]
fn ruler_guide_origin_display_fallback_accepts_only_bounded_finite_pairs() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({})).unwrap();
let max = vectorcraft_engine::MAX_COORD;
for (origin, expected) in [
(Point::ZERO, Point::ZERO),
(Point::new(17.0, -23.0), Point::new(17.0, -23.0)),
(Point::new(max, -max), Point::new(max, -max)),
(Point::new(f64::NAN, 23.0), Point::ZERO),
(Point::new(17.0, f64::INFINITY), Point::ZERO),
(Point::new(max + 1.0, 23.0), Point::ZERO),
(Point::new(17.0, -max - 1.0), Point::ZERO),
] {
std::sync::Arc::make_mut(&mut app.session.active_mut().unwrap().doc).ruler_origin = origin;
assert_eq!(app.ruler_origin(), expected);
}
}
#[test]
fn ruler_guide_origin_tooltip_matches_selected_guide_fields() {
use egui::Event;
for (vertical, target, shown) in [(true, 222.0, 205.0), (false, 238.0, 215.0)] {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 400, "height": 300})).unwrap();
app.run("document.setRulerOrigin", json!({"x": 17, "y": 23})).unwrap();
app.run("guide.add", json!({"vertical": vertical, "pos": 100})).unwrap();
app.ui.view.guides = true;
app.ui.view.smart_guides = false;
app.ui.view.snap_to_point = false;
app.ui.view.snap_to_grid = false;
app.ui.view.snap_to_pixel = false;
app.select_tool("selection");
let ctx = egui::Context::default();
frame(&mut app, &ctx, vec![]);
let view = app.view_mut().unwrap();
view.zoom = 1.0;
view.rotation = 0.0;
view.center = Point::new(200.0, 150.0);
frame(&mut app, &ctx, vec![]);
app.session.active_mut().unwrap().mark_saved();
let before = app.session.active().unwrap().doc.clone();
let undo = app.session.active().unwrap().history.undo.len();
let rect = app.canvas_rect.unwrap();
let xf = Xf::new(rect, app.view().unwrap());
let from = xf.to_screen(if vertical { Point::new(100.0, 50.0) } else { Point::new(50.0, 100.0) });
let to = xf.to_screen(if vertical { Point::new(target, 50.0) } else { Point::new(50.0, target) });
assert!(rect.contains(from) && rect.contains(to));
let button = |pos, pressed| Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() };
frame(&mut app, &ctx, vec![Event::PointerMoved(from)]);
frame(&mut app, &ctx, vec![button(from, true)]);
frame(&mut app, &ctx, vec![Event::PointerMoved(to)]);
assert!((app.session.active().unwrap().doc.guides[0].pos - target).abs() < 1e-4);
assert_eq!(app.session.active().unwrap().selection.guides, vec![0]);
// Observe the real selected-guide field; no expected strings are inserted into UI state.
let fields = egui::Context::default();
crate::theme::install_fonts(&fields);
let mut texts = vec![];
for _ in 0..3 {
let input = egui::RawInput { screen_rect: Some(egui::Rect::from_min_size(Pos2::ZERO, vec2(400.0, 300.0))), ..Default::default() };
let mut out = fields.run_ui(input, |ui| crate::panels::transform::show(&mut app, ui));
out.textures_delta.clear();
fn collect(shape: &egui::Shape, texts: &mut Vec<String>) {
match shape {
egui::Shape::Text(t) => texts.push(t.galley.text().to_string()),
egui::Shape::Vec(shapes) => shapes.iter().for_each(|s| collect(s, texts)),
_ => {}
}
}
texts.clear();
out.shapes.iter().for_each(|s| collect(&s.shape, &mut texts));
}
assert!(texts.iter().any(|t| t == &Unit::Points.format(shown)), "{texts:?}");
let view = app.view_info();
let overlays = app.session.overlays(view);
let want = format!("{}: {shown:.2} pt", if vertical { "X" } else { "Y" });
assert!(
overlays.iter().any(|o| matches!(o, vectorcraft_tools::Overlay::Measure { text, .. } if text == &want)),
"{overlays:?}, field={shown}"
);
assert_eq!(app.session.active().unwrap().history.undo.len(), undo);
frame(&mut app, &ctx, vec![button(to, false)]);
assert_eq!(app.session.active().unwrap().history.undo.len(), undo + 1);
assert!((app.session.active().unwrap().doc.guides[0].pos - target).abs() < 1e-4, "stored coordinate remains absolute");
app.run("edit.undo", json!({})).unwrap();
assert_eq!(app.session.active().unwrap().doc, before);
}
}
}
+1 -7
View File
@@ -618,13 +618,7 @@ impl VectorcraftApp {
/// The zero point shown by rulers and position fields; command coordinates stay absolute.
pub(crate) fn ruler_origin(&self) -> vectorcraft_geom::Point {
let origin = self.session.active().map_or(vectorcraft_geom::Point::ZERO, |d| d.doc.ruler_origin);
// An untrusted file cannot make the ruler's tick loop or position fields overflow.
if origin.is_finite() && origin.x.abs() <= vectorcraft_engine::MAX_COORD && origin.y.abs() <= vectorcraft_engine::MAX_COORD {
origin
} else {
vectorcraft_geom::Point::ZERO
}
self.session.active().map_or(vectorcraft_geom::Point::ZERO, |d| d.doc.ruler_origin_or_zero())
}
pub fn view_mut(&mut self) -> Option<&mut View> {
self.sync_views();