diff --git a/ROADMAP.md b/ROADMAP.md index d8244ea4..e97edecb 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -110,7 +110,7 @@ grade by behaviour against `plan/illustrator/`, not by whether a menu item exist - **Live Corners:** drag a live rectangle's corner widgets (Selection or Direct Selection) to round all corners, with a radius readout. - **Graphs:** Column, Stacked Column, Bar, Stacked Bar, Line, Area, Scatter, Pie and Radar graph tools with Graph Data and Graph Type. - **Other:** Eyedropper, Gradient annotator, Artboard, Measure, Type, Hand, Zoom, Rotate View. -- **Drawing aids:** Smart Guides and snapping, and Draw Normal / Behind / Inside modes. +- **Drawing aids:** Smart Guides and snapping, guides dragged out of the rulers, and Draw Normal / Behind / Inside modes. - **Geometry and effects:** - Pathfinder (10 exact curve booleans), Offset, Outline Stroke, Simplify, Clean Up, Split Into Grid, Divide Objects Below. - Live effects with previewing dialogs: Distort & Transform, Path, Convert to Shape, 15 Warp styles, Round Corners, Scribble, Effect → Pathfinder (all 10 operations, live on groups; several loose objects are grouped first, in one undo step), Color Adjustments (Brightness/Contrast, Curves, Levels, Hue/Saturation, Shift to Color, Temperature/Tint, on vectors, live type and embedded images), and raster drop shadow, glows and feather. SVG and PDF export keep live effects (geometry baked; SVG raster effects as filters). @@ -190,7 +190,7 @@ about 1–1.5 agent-hours each, so the other rows (estimated on the older scale) | Image Trace, graphs, image tools | 3 | 70% | graph Design/Column/Marker, Create Object Mosaic, Crop Image polish (Vector Halftone is done) | 6–10 | | Layers, artboards, document setup | 5 | 80% | Layers panel options depth, artboard presets/rearrange polish (Document Setup and New Document are done) | 5–8 | | View & navigation | 3 | 70% | New View/Edit Views, multiple windows/arrange, Snap to Pixel/Glyph (print tiling is done) | 9–14 | -| Guides, grids, smart guides, snapping, rulers | 3 | 75% | global/video rulers, smart-guide preference depth | 4–8 | +| Guides, grids, smart guides, snapping, rulers | 3 | 75% | global/video rulers, smart-guide preference depth, ruler-drag modifiers (Shift snaps to ticks, Alt swaps orientation) and dragging existing guides | 4–8 | | File formats | 6 | 85% | DWG (no open spec: DXF instead), PSD placement as layers, Illustrator EPS checked against real files (only reduced repros so far), the remaining fidelity polish; EPS, DXF, EMF/WMF, TIFF, BMP, Targa, PSD export, SVGZ, PDF security, PDF/X, PDF layers and presets are done | 6–10 | | Export for Screens, Asset Export, slices, Save for Web | 3 | 85% | polish only (all four are done) | 2–4 | | Print, colour management, separations, flattener | 3 | 80% | print preview fidelity (flattener presets in PDF and print, overprint in PDF and composite print, Print dialog, presets, PostScript, marks, separations, Print Tiling, print overprint/flattener/bitmap options and printer profile are done) | 1–2 | diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 12fa00f4..78f5d06c 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -342,6 +342,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { if app.ui.view.rulers && app.ui.screen_mode < 3 { rulers(ui, full, &xf, app.hover_doc, app.session.general_unit(), &t); + ruler_guides(app, ui, full, rect, &xf, &t); } if app.ui.task_bar && !app.session.tool_busy() && app.ui.screen_mode < 3 { task_bar(app, ui, &xf); @@ -680,11 +681,39 @@ pub(crate) fn ruler_label(v: f64, step: f64) -> String { } /// The rulers, numbered in `unit` (the General unit). -fn rulers(ui: &Ui, full: egui::Rect, xf: &Xf, hover: Option, unit: Unit, t: &Tokens) { - let p = ui.painter(); +/// The top ruler, the left ruler and the box where they meet. +fn ruler_rects(full: egui::Rect) -> [egui::Rect; 3] { let top = egui::Rect::from_min_max(pos2(full.left() + RULER, full.top()), pos2(full.right(), full.top() + RULER)); let left = egui::Rect::from_min_max(pos2(full.left(), full.top() + RULER), pos2(full.left() + RULER, full.bottom())); - let corner = egui::Rect::from_min_size(full.min, vec2(RULER, RULER)); + [top, left, egui::Rect::from_min_size(full.min, vec2(RULER, RULER))] +} + +/// A drag from a ruler onto the canvas makes a guide where the button is released: a horizontal +/// one from the top ruler, a vertical one from the left ruler. Released anywhere else, it makes +/// none. +fn ruler_guides(app: &mut VectorcraftApp, ui: &Ui, full: egui::Rect, canvas: egui::Rect, xf: &Xf, t: &Tokens) { + let [top, left, _] = ruler_rects(full); + for (r, vertical, id) in [(top, false, "ruler-top"), (left, true, "ruler-left")] { + let resp = ui.interact(r, egui::Id::new(id), Sense::drag()); + let Some(p) = resp.interact_pointer_pos().filter(|p| canvas.contains(*p)) else { continue }; + if resp.drag_stopped() { + let d = xf.to_doc(p); + // The new guide shows even if guides were hidden. + app.ui.view.guides = true; + if let Err(e) = app.run("guide.add", json!({ "vertical": vertical, "pos": if vertical { d.x } else { d.y } })) { + app.status(e); + } + } else if resp.dragged() { + let line = + if vertical { [pos2(p.x, canvas.top()), pos2(p.x, canvas.bottom())] } else { [pos2(canvas.left(), p.y), pos2(canvas.right(), p.y)] }; + ui.painter_at(canvas).line_segment(line, Stroke::new(1.0, t.guide)); + } + } +} + +fn rulers(ui: &Ui, full: egui::Rect, xf: &Xf, hover: Option, unit: Unit, t: &Tokens) { + let p = ui.painter(); + let [top, left, corner] = ruler_rects(full); for r in [top, left, corner] { p.rect_filled(r, 0.0, t.ruler); } @@ -1475,6 +1504,41 @@ mod tests { assert_eq!(selected(&app), vec![vectorcraft_doc::NodeId(id)]); } + #[test] + fn a_drag_from_a_ruler_onto_the_canvas_makes_a_guide() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.session.execute("file.new", &json!({"width": 400, "height": 300})).unwrap(); + app.ui.view.rulers = true; + app.ui.view.guides = false; + // A drawing tool: the drag must not draw. + app.select_tool("rectangle"); + let ctx = egui::Context::default(); + frame(&mut app, &ctx, vec![]); + let rect = app.canvas_rect.unwrap(); + let xf = Xf::new(rect, app.view().unwrap()); + let button = |pos, pressed| egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() }; + let drag = |app: &mut VectorcraftApp, from: Pos2, to: Pos2| { + frame(app, &ctx, vec![egui::Event::PointerMoved(from)]); + frame(app, &ctx, vec![button(from, true)]); + frame(app, &ctx, vec![egui::Event::PointerMoved(to)]); + frame(app, &ctx, vec![button(to, false)]); + }; + let guides = |app: &VectorcraftApp| app.session.active().unwrap().doc.guides.iter().map(|g| (g.vertical, g.pos)).collect::>(); + // From the top ruler: a horizontal guide where the button was released. + let to = pos2(rect.center().x, rect.top() + 120.0); + drag(&mut app, pos2(rect.center().x, rect.top() - RULER / 2.0), to); + assert_eq!(guides(&app), [(false, xf.to_doc(to).y)]); + assert!(app.ui.view.guides, "the new guide shows"); + // From the left ruler: a vertical one. + let to2 = pos2(rect.left() + 200.0, rect.center().y); + drag(&mut app, pos2(rect.left() - RULER / 2.0, rect.center().y), to2); + assert_eq!(guides(&app), [(false, xf.to_doc(to).y), (true, xf.to_doc(to2).x)]); + // Released back on the ruler: no guide. + drag(&mut app, pos2(rect.center().x, rect.top() - RULER / 2.0), pos2(rect.center().x + 40.0, rect.top() - 4.0)); + assert_eq!(guides(&app).len(), 2); + assert_eq!(app.session.active().unwrap().doc.art_bounds(), None, "the tool drew nothing"); + } + #[test] fn middle_drag_pans_the_view_with_any_tool() { let mut app = VectorcraftApp::new(Session::new(), Default::default());