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:
commit
b8f77aefb6
|
|
@ -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 |
|
||||||
|
|
|
||||||
|
|
@ -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 };
|
||||||
|
|
|
||||||
|
|
@ -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));
|
}
|
||||||
for icon in ["chevron-right", "chevrons-right"] {
|
ui.label(egui::RichText::new(if nab > 0 { (cur + 1).to_string() } else { "–".into() }).size(11.5));
|
||||||
widgets::icon_button(ui, icon, "", false, 18.0);
|
for (icon, to) in [("chevron-right", "next"), ("chevrons-right", "last")] {
|
||||||
|
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();
|
||||||
|
|
|
||||||
|
|
@ -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.
|
|
|
@ -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" {
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue