mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Merge pull request #2677 from dotCooCoo/feat/rotate-view-dial
Rotate View: a dial shows the angle while a drag rotates the view
This commit is contained in:
@@ -519,6 +519,11 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
}
|
||||
ime_output(app, ui.ctx(), &xf);
|
||||
crate::place::paint_drop_highlight(app, ui.ctx(), &painter, rect);
|
||||
// A Rotate View drag (the tool, or Space+Shift with any tool) shows the view's angle on a dial
|
||||
// at the canvas center, the point the view turns about.
|
||||
if app.ui.screen_mode < 3 && matches!(ui.data(|d| d.get_temp::<Drag>(drag_id())), Some(Drag::RotateView { .. })) {
|
||||
rotate_view_dial(&painter, rect.center(), v.rotation, &t);
|
||||
}
|
||||
|
||||
if app.ui.view.rulers && app.ui.screen_mode < 3 {
|
||||
if let Some(origin) = origin_preview {
|
||||
@@ -2299,6 +2304,26 @@ fn mirror_axis_overlay(app: &VectorcraftApp, p: &egui::Painter, xf: &Xf) {
|
||||
p.rect_stroke(handle, 2.0, Stroke::new(2.0, t.measure_bg), StrokeKind::Outside);
|
||||
}
|
||||
|
||||
/// The radius of the Rotate View dial, in screen points.
|
||||
const DIAL_RADIUS: f32 = 40.0;
|
||||
|
||||
/// The dial a Rotate View drag shows, centered on `c`: a tick every 15° (longer every 90°), a
|
||||
/// needle pointing where the document's top points in a view turned by `rotation` degrees, and the
|
||||
/// angle under the center, rounded to whole degrees as in the status bar.
|
||||
fn rotate_view_dial(p: &egui::Painter, c: Pos2, rotation: f64, t: &Tokens) {
|
||||
// `dir(deg)` is the screen direction `deg` degrees clockwise from straight up.
|
||||
let dir = |deg: f32| vec2(deg.to_radians().sin(), -deg.to_radians().cos());
|
||||
p.circle(c, DIAL_RADIUS, t.panel.gamma_multiply(0.85), Stroke::new(1.0, t.border));
|
||||
for i in 0..24u8 {
|
||||
let len = if i % 6 == 0 { 8.0 } else { 4.0 };
|
||||
let d = dir(f32::from(i) * 15.0);
|
||||
p.line_segment([c + d * (DIAL_RADIUS - len), c + d * DIAL_RADIUS], Stroke::new(1.0, t.text_dim));
|
||||
}
|
||||
p.line_segment([c, c + dir(rotation as f32) * DIAL_RADIUS], Stroke::new(2.0, t.accent));
|
||||
p.circle_filled(c, 2.5, t.accent);
|
||||
p.text(c + vec2(0.0, 8.0), egui::Align2::CENTER_TOP, format!("{rotation:.0}°"), egui::FontId::proportional(11.0), t.text);
|
||||
}
|
||||
|
||||
fn selection_overlay(app: &mut VectorcraftApp, p: &egui::Painter, xf: &Xf, curve_points_from_tool: bool) {
|
||||
// The Selection tool's bounding box (rotated with the objects after a rotation). It hides while
|
||||
// a drag moves or resizes the selection, so only the art is seen going with the pointer (#712).
|
||||
@@ -6126,6 +6151,59 @@ mod tests {
|
||||
assert_eq!(shown(&mut app, vec![Event::PointerMoved(c)]), CursorIcon::Alias, "the Rotate View tool");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_rotate_view_drag_shows_a_dial_with_the_angle() {
|
||||
use egui::{Event, Modifiers};
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.session.execute("file.new", &json!({"width": 400, "height": 300})).unwrap();
|
||||
app.select_tool("rotateView");
|
||||
let ctx = egui::Context::default();
|
||||
frame(&mut app, &ctx, vec![]);
|
||||
let c = app.canvas_rect.unwrap().center();
|
||||
let space = |pressed, modifiers| Event::Key { key: egui::Key::Space, physical_key: None, pressed, repeat: false, modifiers };
|
||||
let button = |pos, pressed, modifiers| Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers };
|
||||
// `dial` returns, from the shapes of one frame, the center of the dial's circle, the needle's
|
||||
// direction in whole degrees clockwise from straight up, and the angle text.
|
||||
let dial = |app: &mut VectorcraftApp| {
|
||||
let s = shapes(app, &ctx);
|
||||
let circle = s.iter().find_map(|s| if let Shape::Circle(k) = s { (k.radius == DIAL_RADIUS).then_some(k.center) } else { None });
|
||||
let needle = s.iter().find_map(|s| match s {
|
||||
Shape::LineSegment { points: [a, b], stroke } if stroke.width == 2.0 && *a == c => {
|
||||
Some((b.x - a.x).atan2(a.y - b.y).to_degrees().round() as i32)
|
||||
}
|
||||
_ => None,
|
||||
});
|
||||
let text =
|
||||
s.iter().find_map(|s| if let Shape::Text(t) = s { Some(t.galley.text().to_string()).filter(|t| t.ends_with('°')) } else { None });
|
||||
(circle, needle, text)
|
||||
};
|
||||
assert_eq!(dial(&mut app), (None, None, None), "no dial before a drag");
|
||||
// The Rotate View tool: a drag from 0° to 30° about the canvas center.
|
||||
let (right, at_30, below) = (c + vec2(100.0, 0.0), c + vec2(86.6, 50.0), c + vec2(0.0, 100.0));
|
||||
frame(&mut app, &ctx, vec![Event::PointerMoved(right)]);
|
||||
frame(&mut app, &ctx, vec![button(right, true, Modifiers::NONE)]);
|
||||
frame(&mut app, &ctx, vec![Event::PointerMoved(at_30)]);
|
||||
assert_eq!(dial(&mut app), (Some(c), Some(30), Some("30°".to_string())));
|
||||
frame(&mut app, &ctx, vec![button(at_30, false, Modifiers::NONE)]);
|
||||
assert_eq!(dial(&mut app), (None, None, None), "the dial goes with the release");
|
||||
assert!((app.view().unwrap().rotation - 30.0).abs() < 1e-2, "{:?}", app.view());
|
||||
// A Space+Shift drag with another tool shows it too: 30° and 90° more make 120°.
|
||||
app.select_tool("rectangle");
|
||||
frame(&mut app, &ctx, vec![Event::ModifiersChanged(Modifiers::SHIFT), space(true, Modifiers::SHIFT), Event::PointerMoved(right)]);
|
||||
frame(&mut app, &ctx, vec![button(right, true, Modifiers::SHIFT)]);
|
||||
frame(&mut app, &ctx, vec![Event::PointerMoved(below)]);
|
||||
assert_eq!(dial(&mut app), (Some(c), Some(120), Some("120°".to_string())));
|
||||
assert!((app.view().unwrap().rotation - 120.0).abs() < 1e-2, "{:?}", app.view());
|
||||
// Presentation Mode turned on during the drag hides the dial.
|
||||
let mode = app.ui.screen_mode;
|
||||
app.ui.screen_mode = 3;
|
||||
assert_eq!(dial(&mut app), (None, None, None), "no dial in Presentation Mode");
|
||||
app.ui.screen_mode = mode;
|
||||
frame(&mut app, &ctx, vec![button(below, false, Modifiers::SHIFT)]);
|
||||
frame(&mut app, &ctx, vec![space(false, Modifiers::NONE), Event::ModifiersChanged(Modifiers::NONE)]);
|
||||
assert_eq!(dial(&mut app), (None, None, None), "the dial goes with the release");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn space_pressed_mid_drag_moves_what_the_tool_draws() {
|
||||
use egui::{Event, Modifiers};
|
||||
|
||||
@@ -551,6 +551,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- **Graphs:** Column, Stacked Column, Bar, Stacked Bar, Line, Area, Scatter, Pie and Radar graph tools with Graph Data and Graph Type. A series starts in greyscale; Group Selection's fill and stroke are kept separately on the graph and reapplied when its data, type or size changes, and through save, open and undo. Graph Type's Override Calculated Values (both `axisMin` and `axisMax`) uses exactly that value-axis range, split into the tick count (5 when automatic). Line graphs take Edge-to-Edge Lines (off by default: the points sit at the centres of their categories; graphs saved before the option keep their edge-to-edge lines), and radar graphs follow Mark Data Points and Connect Data Points. Blank cells (an empty CSV cell or `null`) are values left out, not zeros: columns, bars and stacked segments skip them, lines and scatter lines break around them and the value axis ignores them, while area, pie and radar draw them as 0; a number in straight quotes is a label, and the data window writes it back quoted. Graph types combine: Graph Type on series picked with Group Selection (or `series` indexes) draws them as their own type, with column, stacked column, line and area mixing on the vertical value axis and bar and stacked bar on the horizontal one; stacked columns take one slot of the cluster, lines draw in front of columns and areas behind. The value axis goes on the left, the right or both sides (column, stacked column, line, area and scatter graphs; bar graphs keep theirs along the bottom), and Separate Scales gives the series assigned to the right axis a scale of their own with its own tick values. Each value axis takes tick marks of no, short or full (across the plot) length and a count per division, and a prefix and suffix for its numbers; the category axis takes tick marks too (none by default), at or between its labels. Graph designs: Object › Graph › Design lists the designs with a preview of the selected one, and New Design (the selected art), Rename, Delete Design, Paste Design (a copy back to edit) and Select Unused, and Marker draws the data points and legend swatches of line, scatter and radar series with a design, scaled so its backmost object fills the default marker's square. Graph Data transposes rows and columns and switches a scatter graph's x and y, and a `|` in a category or series label starts a new line. Pie graphs take Legends in Wedges, Position (Even, Ratio sized by area, Stacked on one centre) and Sort (None, All, First). Graph Type takes column and cluster widths up to 1000% (overlapping past 100%) with First Row and First Column in Front (First Row orders each series' categories; across series the series order decides, so series stay one group each for Group Selection), draws filled lines, adds drop shadows behind columns, bars, lines and wedges, and puts the legend in rows across the top. Text formatted with the Character or Appearance controls, on the whole graph or on labels picked with Group Selection, is kept per kind of label (value axis numbers, category labels, legend labels) through the graph being drawn again. Object › Graph › Data opens a worksheet: cells picked with a click and edited above the sheet (Tab moves right, Enter down, the arrows between cells), Import Data from a tab-separated text file, Transpose, Switch x/y, Cell Style (decimals shown, column width), Revert and Apply. `graph.setData` returns and takes the data as CSV with each value written exactly, not rounded to two decimals, so applying the returned CSV changes nothing (#1485).
|
||||
- **Graph column designs:** Object › Graph › Column (`graph.column`) draws the columns, bars or stacked segments and the legend swatch of the series picked with Group Selection with a graph design: Vertically Scaled stretches it to each column's length, Uniformly Scaled scales it both ways and centers it, Repeating stacks a copy for each Each Design Represents value and chops or scales the last one (For Fractions), and Sliding keeps the part above the design's guide line in proportion and stretches the part below. The dialog lists the designs with a preview; the choice is saved with the graph and follows the design's rename and delete.
|
||||
- **Other:** Eyedropper, Gradient annotator, Artboard (Alt-drag duplicates an artboard, and its art with Move/Copy Artwork with Artboard on; Copy, Cut and Paste take the artboard with its art, Paste right of the last artboard, Paste in Place where it was, into this or another document, #446; Object › Artboards › Rearrange All Artboards lays them out in a grid by row or by column, in one row or one column, left to right or right to left, from the menu, the Artboards panel or Properties' Rearrange All, #681; Scale Artwork with Artboard, in the Control bar and Properties beside Move Artwork with Artboard, resizes the artboard proportionally with the art fully inside it and its guides, strokes and corners per the Scale preferences, #602), Measure, Type, Hand, Zoom, Rotate View.
|
||||
While a drag rotates the view, a dial at the center of the canvas shows the angle.
|
||||
- **Editing type:** double-clicking type with a selection tool (Selection or Direct Selection) switches to the Type tool with the caret where it was clicked; pressing Escape finishes editing and switches back to the selection tool used before double-clicking it (when in isolation mode, the first Escape exits text editing while preserving isolation mode, and a second Escape exits isolation).
|
||||
While a Type tool edits type, a click that lands on no type ends the editing without making new type, and that tool stays active; the next click makes new type (#2871, #2854).
|
||||
Duplicate Artboards, the Artboard tool's Alt-drag and Paste give the new artboard an id that no other artboard has, also when an artboard's id is the largest possible value (#1718). Deleting the copy or the original then leaves the other's guides.
|
||||
|
||||
Reference in New Issue
Block a user