Merge pull request #267 from theironchef/fix/artboard-navigator

Status bar: the artboard navigator works, and fitting shows its artboard
This commit is contained in:
BFlat 2026-10-07 12:31:37 +07:00 committed by GitHub
commit b8f77aefb6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 117 additions and 8 deletions

View File

@ -112,6 +112,7 @@ grade by behaviour against `plan/illustrator/`, not by whether a menu item exist
- **Other:** Eyedropper, Gradient annotator, Artboard, Measure, Type, Hand, Zoom, Rotate View. - **Other:** Eyedropper, Gradient annotator, Artboard, Measure, Type, Hand, Zoom, Rotate View.
- **Tool double-clicks:** double-clicking a tool's button opens its options: Hand fits the artboard in the window, Zoom shows 100 %, Rotate, Scale, Reflect and Shear open their Object › Transform dialogs, Gradient, Eyedropper, Blend and the Liquify tools their panels or options. - **Tool double-clicks:** double-clicking a tool's button opens its options: Hand fits the artboard in the window, Zoom shows 100 %, Rotate, Scale, Reflect and Shear open their Object › Transform dialogs, Gradient, Eyedropper, Blend and the Liquify tools their panels or options.
- **Drawing aids:** Smart Guides and snapping, guides dragged out of the rulers, and Draw Normal / Behind / Inside modes. - **Drawing aids:** Smart Guides and snapping, guides dragged out of the rulers, and Draw Normal / Behind / Inside modes.
- **Views:** the status bar's artboard navigator (first, previous, next, last; `view.goToArtboard`) goes to an artboard and fits it, and Fit Artboard in Window and Actual Size show the navigator's artboard.
- **Geometry and effects:** - **Geometry and effects:**
- Pathfinder (10 exact curve booleans), Offset, Outline Stroke, Simplify, Clean Up, Split Into Grid, Divide Objects Below. - 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). - 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).
@ -154,7 +155,7 @@ grade by behaviour against `plan/illustrator/`, not by whether a menu item exist
| M8 | Transform & distort (Puppet Warp, Liquify tools, Envelopes, Blends, Perspective Grid) | ✅ done (M8.1–M8.20 on 2026-10-06, fidelity pass): Liquify Tool Options with one shared brush, pen pressure, hold-to-apply, a safe scope and incremental strokes; tool options that last; Puppet Warp rotation, multi-select, Control bar and a rest-shape session that follows Undo; Width tool on compound paths; Envelope dialogs with preview, Reset with Warp/Mesh, full Envelope Options, type/images/symbols/appearance/gradients/patterns distorted on canvas and in every export, warps that keep their frame, Edit Contents, mesh handles and Control bar; Blend Options, anchor-targeted Blend tool, editable spine, keys picked on the canvas, faithful interpolation and knockout; Perspective Grid with Define Grid, presets, lock/snap/rulers, widgets, the 1–4 plane switch, movable planes, Perspective Selection scale/copy/perpendicular/Transform Again, and type and symbols truly projected with Edit Text. Left: Anti-Alias and Preserve Shape output, warp-envelope point editing, brush interpolation in blends, Free Transform/Scale/Reflect along a rotated box | 2–4 | | M8 | Transform & distort (Puppet Warp, Liquify tools, Envelopes, Blends, Perspective Grid) | ✅ done (M8.1–M8.20 on 2026-10-06, fidelity pass): Liquify Tool Options with one shared brush, pen pressure, hold-to-apply, a safe scope and incremental strokes; tool options that last; Puppet Warp rotation, multi-select, Control bar and a rest-shape session that follows Undo; Width tool on compound paths; Envelope dialogs with preview, Reset with Warp/Mesh, full Envelope Options, type/images/symbols/appearance/gradients/patterns distorted on canvas and in every export, warps that keep their frame, Edit Contents, mesh handles and Control bar; Blend Options, anchor-targeted Blend tool, editable spine, keys picked on the canvas, faithful interpolation and knockout; Perspective Grid with Define Grid, presets, lock/snap/rulers, widgets, the 1–4 plane switch, movable planes, Perspective Selection scale/copy/perpendicular/Transform Again, and type and symbols truly projected with Edit Text. Left: Anti-Alias and Preserve Shape output, warp-envelope point editing, brush interpolation in blends, Free Transform/Scale/Reflect along a rotated box | 2–4 |
| M9 | Live effects (+ 3D & Materials) | 🟡 2D effects done incl. Effect → Pathfinder and Color Adjustments; SVG Filters, Document Raster Effects Settings, 3D pending | 86–135 | | M9 | Live effects (+ 3D & Materials) | 🟡 2D effects done incl. Effect → Pathfinder and Color Adjustments; SVG Filters, Document Raster Effects Settings, 3D pending | 86–135 |
| M10 | Brushes, symbols, patterns, Repeat | 🟡 pattern swatches (5 tile types, Pattern Options, editing mode, SVG `<pattern>`/PDF export) and live Repeat (radial/grid/mirror) done; brushes/symbols in progress | 23–37 | | M10 | Brushes, symbols, patterns, Repeat | 🟡 pattern swatches (5 tile types, Pattern Options, editing mode, SVG `<pattern>`/PDF export) and live Repeat (radial/grid/mirror) done; brushes/symbols in progress | 23–37 |
| M11 | Artboards & views (artboard panel/tool, Trim View, middle-button pan and print tiling done; multiple windows, presentation polish) | 🟡 | 15–25 | | M11 | Artboards & views (artboard panel/tool, Trim View, middle-button pan, print tiling and the status bar's artboard navigator done; multiple windows, presentation polish) | 🟡 | 15–25 |
| M12 | Advanced color & art (CMYK/ICC, separations, Gradient Mesh, Live Paint, Image Trace, Graphs) | 🟡 Gradient Mesh, Live Paint, Image Trace (12 presets, 18 ms/1k² image), Recolor Artwork, colour management (ICC, soft proofing, separations preview), Graphs (all 9 tools, Graph Data/Type, regenerate in place) done; graph Design/Column/Marker designs pending | 6–10 | | M12 | Advanced color & art (CMYK/ICC, separations, Gradient Mesh, Live Paint, Image Trace, Graphs) | 🟡 Gradient Mesh, Live Paint, Image Trace (12 presets, 18 ms/1k² image), Recolor Artwork, colour management (ICC, soft proofing, separations preview), Graphs (all 9 tools, Graph Data/Type, regenerate in place) done; graph Design/Column/Marker designs pending | 6–10 |
| M13 | Automation (Actions ✅ record/playback, persisted; variables, scripting, batch) | 🟡 | 18–27 | | M13 | Automation (Actions ✅ record/playback, persisted; variables, scripting, batch) | 🟡 | 18–27 |
| M14 | 1.0 polish (preferences, shortcut editor, workspaces, accessibility, packaging for all OSes) | 🟡 Preferences, shortcut editor, workspaces, a custom title bar on Windows/Linux with a Home button, content-sized dialogs and menus that scroll when longer than the window done; accessibility, Windows/Linux packaging pending | 18–28 | | M14 | 1.0 polish (preferences, shortcut editor, workspaces, accessibility, packaging for all OSes) | 🟡 Preferences, shortcut editor, workspaces, a custom title bar on Windows/Linux with a Home button, content-sized dialogs and menus that scroll when longer than the window done; accessibility, Windows/Linux packaging pending | 18–28 |

View File

@ -120,12 +120,14 @@ pub fn fit(app: &mut VectorcraftApp, how: &str) {
} }
return; return;
}; };
let current = app.view().map_or(0, |v| v.artboard);
let Some(st) = app.session.active() else { return }; let Some(st) = app.session.active() else { return };
let target = match how { let target = match how {
"view.fitAll" => { "view.fitAll" => {
st.doc.art_bounds().map(|a| st.doc.artboards.iter().fold(a, |r, ab| r.union(ab.rect))).or(st.doc.artboards.first().map(|a| a.rect)) st.doc.art_bounds().map(|a| st.doc.artboards.iter().fold(a, |r, ab| r.union(ab.rect))).or(st.doc.artboards.first().map(|a| a.rect))
} }
_ => st.doc.artboards.first().map(|a| a.rect), // The navigator's artboard (the first if it has gone).
_ => st.doc.artboards.get(current).or(st.doc.artboards.first()).map(|a| a.rect),
}; };
let Some(target) = target else { return }; let Some(target) = target else { return };
let Some(v) = app.view_mut() else { return }; let Some(v) = app.view_mut() else { return };

View File

@ -372,13 +372,25 @@ pub fn status_bar(app: &mut VectorcraftApp, ui: &mut Ui) {
}, },
); );
ui.separator(); ui.separator();
// The artboard navigator: first, previous, the current artboard's number, next, last.
let nab = app.session.active().map(|d| d.doc.artboards.len()).unwrap_or(0); let nab = app.session.active().map(|d| d.doc.artboards.len()).unwrap_or(0);
for icon in ["chevrons-left", "chevron-left"] { let cur = app.view().map_or(0, |v| v.artboard).min(nab.saturating_sub(1));
widgets::icon_button(ui, icon, "", false, 18.0); let mut go = None;
for (icon, to) in [("chevrons-left", "first"), ("chevron-left", "previous")] {
if widgets::icon_button_enabled(ui, icon, "", false, cur > 0, 18.0).clicked() {
go = Some(to);
}
} }
ui.label(egui::RichText::new(if nab > 0 { "1" } else { "–" }).size(11.5)); ui.label(egui::RichText::new(if nab > 0 { (cur + 1).to_string() } else { "–".into() }).size(11.5));
for icon in ["chevron-right", "chevrons-right"] { for (icon, to) in [("chevron-right", "next"), ("chevrons-right", "last")] {
widgets::icon_button(ui, icon, "", false, 18.0); if widgets::icon_button_enabled(ui, icon, "", false, cur + 1 < nab, 18.0).clicked() {
go = Some(to);
}
}
if let Some(to) = go
&& let Err(e) = app.run("view.goToArtboard", json!({ "index": to }))
{
app.status(e);
} }
ui.separator(); ui.separator();
let tool = vectorcraft_tools::tool_info(app.session.tool_id()) let tool = vectorcraft_tools::tool_info(app.session.tool_id())
@ -667,6 +679,62 @@ mod tests {
} }
} }
/// One headless frame of the status bar; returns the artboard navigator's buttons, left to
/// right: first, previous, next, last.
fn navigator(app: &mut crate::VectorcraftApp, ctx: &egui::Context, events: Vec<egui::Event>) -> Vec<egui::Rect> {
let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1200.0, 300.0));
let mut out = ctx.run_ui(egui::RawInput { screen_rect: Some(screen), events, ..Default::default() }, |ui| super::status_bar(app, ui));
out.textures_delta.clear();
let size = egui::vec2(18.0, 18.0);
let mut r: Vec<egui::Rect> =
ctx.viewport(|vp| vp.prev_pass.widgets.layers().flat_map(|(_, w)| w.iter()).filter(|w| w.rect.size() == size).map(|w| w.rect).collect());
r.sort_by(|a, b| a.left().total_cmp(&b.left()));
r
}
#[test]
fn the_artboard_navigator_goes_to_artboards_and_fitting_shows_the_current_one() {
use vectorcraft_geom::Point;
let mut app = crate::VectorcraftApp::new(Session::new(), Default::default());
// Three 200 × 100 artboards in a row, centred at x = 100, 320 and 540.
app.run("file.new", json!({"width": 200, "height": 100})).unwrap();
app.run("artboard.new", json!({})).unwrap();
app.run("artboard.new", json!({})).unwrap();
app.canvas_rect = Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(460.0, 260.0)));
let ctx = egui::Context::default();
let buttons = navigator(&mut app, &ctx, vec![]);
assert_eq!(buttons.len(), 4);
let click = |app: &mut crate::VectorcraftApp, r: egui::Rect| {
let b = |pressed| egui::Event::PointerButton {
pos: r.center(),
button: egui::PointerButton::Primary,
pressed,
modifiers: Default::default(),
};
navigator(app, &ctx, vec![egui::Event::PointerMoved(r.center())]);
navigator(app, &ctx, vec![b(true)]);
navigator(app, &ctx, vec![b(false)]);
};
let at = |app: &crate::VectorcraftApp| (app.view().unwrap().artboard, app.view().unwrap().center);
click(&mut app, buttons[2]);
assert_eq!(at(&app), (1, Point::new(320.0, 50.0)), "next");
click(&mut app, buttons[3]);
assert_eq!(at(&app), (2, Point::new(540.0, 50.0)), "last");
click(&mut app, buttons[1]);
assert_eq!(at(&app), (1, Point::new(320.0, 50.0)), "previous");
// Fit Artboard in Window and Actual Size show the navigator's artboard.
app.run("view.setZoom", json!({"zoom": 300, "center": [0, 0]})).unwrap();
app.run("view.fitArtboard", json!({})).unwrap();
assert_eq!(at(&app), (1, Point::new(320.0, 50.0)));
app.run("view.actualSize", json!({})).unwrap();
assert_eq!((at(&app).1, app.view().unwrap().zoom), (Point::new(320.0, 50.0), 1.0));
click(&mut app, buttons[0]);
assert_eq!(at(&app), (0, Point::new(100.0, 50.0)), "first");
// The command also takes an index, clamped to the last artboard.
assert_eq!(app.run("view.goToArtboard", json!({"index": 9})), Ok(json!({"index": 2})));
assert!(app.run("view.goToArtboard", json!({"index": "sideways"})).is_err());
}
#[test] #[test]
fn the_tab_title_names_the_opacity_mask_while_it_is_edited() { fn the_tab_title_names_the_opacity_mask_while_it_is_edited() {
let mut s = Session::new(); let mut s = Session::new();

View File

@ -988,6 +988,7 @@
Global Process Color, CMYK 整體印刷色,CMYK Global Process Color, CMYK 整體印刷色,CMYK
glyph {id} 字符 {id} glyph {id} 字符 {id}
Glyphs 字符 Glyphs 字符
Go to Artboard 前往工作區域
Go To Link 前往連結 Go To Link 前往連結
GPL palettes keep solid colours only, as RGB. GPL 調色盤僅保留純色,並以 RGB 儲存。 GPL palettes keep solid colours only, as RGB. GPL 調色盤僅保留純色,並以 RGB 儲存。
GPU Performance GPU 效能 GPU Performance GPU 效能

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -147,6 +147,12 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
("view.fitAll", "Fit All in Window", "Cmd+Alt+0", "{}"), ("view.fitAll", "Fit All in Window", "Cmd+Alt+0", "{}"),
("view.actualSize", "Actual Size", "Cmd+1", "{}"), ("view.actualSize", "Actual Size", "Cmd+1", "{}"),
("view.setZoom", "Set Zoom", "", "{zoom: percent, center?: [x,y]}"), ("view.setZoom", "Set Zoom", "", "{zoom: percent, center?: [x,y]}"),
(
"view.goToArtboard",
"Go to Artboard",
"",
"{index: 0-based number | \"first\" | \"previous\" | \"next\" | \"last\"} make that artboard the status bar navigator's (the one Fit Artboard in Window and Actual Size show) and fit it in the window → {index}",
),
("view.edges", "Hide Edges", "Cmd+H", "{}"), ("view.edges", "Hide Edges", "Cmd+H", "{}"),
("view.artboards", "Hide Artboards", "Cmd+Shift+H", "{}"), ("view.artboards", "Hide Artboards", "Cmd+Shift+H", "{}"),
("view.rulers", "Show Rulers", "Cmd+R", "{}"), ("view.rulers", "Show Rulers", "Cmd+R", "{}"),
@ -859,6 +865,7 @@ pub fn run_ui_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<R
crate::canvas::fit(app, id); crate::canvas::fit(app, id);
Ok(Value::Null) Ok(Value::Null)
} }
"view.goToArtboard" => go_to_artboard(app, p),
"view.presentation" => { "view.presentation" => {
app.ui.screen_mode = if app.ui.screen_mode == 3 { 0 } else { 3 }; app.ui.screen_mode = if app.ui.screen_mode == 3 { 0 } else { 3 };
Ok(json!(app.ui.screen_mode)) Ok(json!(app.ui.screen_mode))
@ -1118,6 +1125,31 @@ pub fn run_ui_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<R
Some(r) Some(r)
} }
/// `view.goToArtboard`: make artboard `index` (a number, or first / previous / next / last from
/// the current one) the navigator's, and fit it in the window.
fn go_to_artboard(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String> {
let n = app.session.active().map(|d| d.doc.artboards.len()).ok_or("no document open")?;
let last = n.checked_sub(1).ok_or("the document has no artboards")?;
let current = app.view().map_or(0, |v| v.artboard).min(last);
let to = match p.get("index") {
Some(Value::String(s)) => match s.as_str() {
"first" => 0,
"previous" => current.saturating_sub(1),
"next" => current + 1,
"last" => last,
other => return Err(format!("index must be a number or first, previous, next or last, not `{other}`")),
},
Some(v) => v.as_u64().and_then(|i| usize::try_from(i).ok()).ok_or("index must be a number or first, previous, next or last")?,
None => return Err("missing index".into()),
}
.min(last);
if let Some(v) = app.view_mut() {
v.artboard = to;
}
crate::canvas::fit(app, "view.fitArtboard");
Ok(json!({ "index": to }))
}
/// Checked state for toggle items. /// Checked state for toggle items.
pub fn checked(app: &VectorcraftApp, id: &str, p: &Value) -> Option<bool> { pub fn checked(app: &VectorcraftApp, id: &str, p: &Value) -> Option<bool> {
if id == "app.language" { if id == "app.language" {

View File

@ -17,11 +17,15 @@ pub struct View {
pub fitted: bool, pub fitted: bool,
/// View rotation in degrees (Rotate View tool). /// View rotation in degrees (Rotate View tool).
pub rotation: f64, pub rotation: f64,
/// The artboard the status bar's navigator is on (an index): Fit Artboard in Window and Actual
/// Size show it.
#[serde(default)]
pub artboard: usize,
} }
impl Default for View { impl Default for View {
fn default() -> Self { fn default() -> Self {
Self { zoom: 1.0, center: Point::new(306.0, 396.0), fitted: false, rotation: 0.0 } Self { zoom: 1.0, center: Point::new(306.0, 396.0), fitted: false, rotation: 0.0, artboard: 0 }
} }
} }
@ -34,6 +38,7 @@ impl View {
center: v.center, center: v.center,
fitted: true, fitted: true,
rotation: if v.rotation.is_finite() { v.rotation } else { 0.0 }, rotation: if v.rotation.is_finite() { v.rotation } else { 0.0 },
artboard: 0,
}, },
_ => Self::default(), _ => Self::default(),
} }