diff --git a/ROADMAP.md b/ROADMAP.md index 04e5438a..0e9e625f 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -4,7 +4,7 @@ VectorCraft is a clean-room, open-source, pure-Rust reimplementation of the Adob This file tracks **how far we are and what's left**. Time estimates are wall-clock hours of continuous Claude Opus 5.5 agent work (including builds and the CI gate), given both for **one agent** and for **4–6 parallel agents** on disjoint crates. They are counted from the remaining work (see [Parity estimate](#parity-estimate)), calibrated against measured throughput, and updated as work lands. -_Last updated: 2026-10-08 (after M4.14–M4.98, M8.1–M8.20, M14.4–M14.7, the MCP prompts, resource templates, completions and logging, the 2026-10-07 issue fixes, the Layers panel rework and the 2026-10-08 community contributions: Pen and shape-tool modifiers, path editing, Layers, Artboards, Swatches/Brushes and dialogs, Japanese composition and PDF text, Hebrew/Arabic type, interface languages (Spanish among them), the font menu, saved selections, MCP and EPS, the missing-font export warning (#319), open paths in the Shape Builder (#374), PDF/`.ai` import keeping strokes live and hidden and nested layers (#372), per-corner Live Corners (#381), SVG type opening at the size it draws at (#396), the Wayland clipboard with copied-file paste (#398), ruler guides that can be selected, moved and deleted (#414), two batches of Preferences options that were stored but unused (#394), skeleton reads with `document.find` for large documents over MCP (#416) and anchors dragged with Direct Selection snapping to points, segments and smart guides, clipped SVG layers opening as clipping layers (#420), PDF/`.ai` layers read on pages of tens of millions of operators (#419), Document Color Mode putting RGB greys on the black plate on request and converting pattern tiles (#421) and movable dialogs with Offset Path's Joins dropdown (#423) and numeric fields scrubbed by dragging their label (#400), stroke weight presets in the stroke unit (#426), the Appearance panel's Opacity popup (#430), type on a path brackets, flip and Type on a Path Options that keep the type in place (#429), Layers rows dragged with a ghost and dropped on the trash or New Layer (#435), tool flyouts opening on a right or corner press (#436), tool flyouts torn off into floating toolbars (#437), the Shape Builder's erase mode deleting edges too (#439), installed fonts registered outside the font folders and fonts installed while the app runs listed in the font menus (#443), alike circular corners on rectangles that aren't square (#442), tool cursors as OS cursors so the pointer no longer trails over the canvas (#444), artboards copied and pasted with their art (#446), guides dragged out of the rulers snapping to the art's side midpoints and the artboards, and artboard guides (#451); see [Honest assessment](#honest-assessment-2026-10-05))._ +_Last updated: 2026-10-08 (after M4.14–M4.98, M8.1–M8.20, M14.4–M14.7, the MCP prompts, resource templates, completions and logging, the 2026-10-07 issue fixes, the Layers panel rework and the 2026-10-08 community contributions: Pen and shape-tool modifiers, path editing, Layers, Artboards, Swatches/Brushes and dialogs, Japanese composition and PDF text, Hebrew/Arabic type, interface languages (Spanish among them), the font menu, saved selections, MCP and EPS, the missing-font export warning (#319), open paths in the Shape Builder (#374), PDF/`.ai` import keeping strokes live and hidden and nested layers (#372), per-corner Live Corners (#381), SVG type opening at the size it draws at (#396), the Wayland clipboard with copied-file paste (#398), ruler guides that can be selected, moved and deleted (#414), two batches of Preferences options that were stored but unused (#394), skeleton reads with `document.find` for large documents over MCP (#416) and anchors dragged with Direct Selection snapping to points, segments and smart guides, clipped SVG layers opening as clipping layers (#420), PDF/`.ai` layers read on pages of tens of millions of operators (#419), Document Color Mode putting RGB greys on the black plate on request and converting pattern tiles (#421) and movable dialogs with Offset Path's Joins dropdown (#423) and numeric fields scrubbed by dragging their label (#400), stroke weight presets in the stroke unit (#426), the Appearance panel's Opacity popup (#430), type on a path brackets, flip and Type on a Path Options that keep the type in place (#429), Layers rows dragged with a ghost and dropped on the trash or New Layer (#435), tool flyouts opening on a right or corner press (#436), tool flyouts torn off into floating toolbars (#437), the Shape Builder's erase mode deleting edges too (#439), installed fonts registered outside the font folders and fonts installed while the app runs listed in the font menus (#443), alike circular corners on rectangles that aren't square (#442), tool cursors as OS cursors so the pointer no longer trails over the canvas (#444), artboards copied and pasted with their art (#446), guides dragged out of the rulers snapping to the art's side midpoints and the artboards, artboard guides (#451), and the toolbar's double arrow and Window › Toolbars › Double Column switching between one and two columns in any window height (#455); see [Honest assessment](#honest-assessment-2026-10-05))._ ## Where we are diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 0494f993..4fb7612a 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -491,7 +491,7 @@ Tools Nástroje Toolbars Panely nástrojů Advanced Rozšířený - Single / Double Column Jeden / dva sloupce + Double Column Dva sloupce Actions Akce Asset Export Export prvků Attributes Atributy diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 3e28f05b..fed45ff6 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -2430,7 +2430,7 @@ Simplify… Simplificar… Simulate Colored Paper Simular papel de color Simulate Paper Simular papel - Single / Double Column Columna simple / doble + Double Column Columna doble Single Left Quotation Mark Comilla simple de apertura Single Quotes: Comillas simples: Single Right Quotation Mark Comilla simple de cierre @@ -3539,3 +3539,4 @@ axis Both Ambos @msg floating must be true or false floating debe ser true o false @msg tool (a tool id) is required se requiere tool (un id de herramienta) @msg {tool} isn't in a toolbar group of several tools {tool} no está en un grupo de la barra de herramientas con varias herramientas +@msg double must be true or false double debe ser true o false diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index d65c9e04..4805643f 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -503,7 +503,7 @@ Tools ツール Toolbars ツールバー Advanced 詳細 - Single / Double Column 1 列/2 列 + Double Column 2 列 Actions アクション Asset Export アセットの書き出し Attributes 属性 diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index b8db64ba..a89432e1 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -2153,7 +2153,7 @@ Simplify… Simplificar… Simulate Colored Paper Simular Papel Colorido Simulate Paper Simular Papel - Single / Double Column Coluna Única / Dupla + Double Column Coluna Dupla Single Left Quotation Mark Aspa Esquerda Única Single Quotes: Aspas Simples: Single Right Quotation Mark Aspa Direita Única diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 3a66c620..79fff53e 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -2366,7 +2366,7 @@ Simplify… 简化… Simulate Colored Paper 模拟有色纸张 Simulate Paper 模拟纸张 - Single / Double Column 单列/双列 + Double Column 双列 Single Left Quotation Mark 左单引号 Single Quotes: 单引号: Single Right Quotation Mark 右单引号 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 84abaaa3..e4a21a7e 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -2448,7 +2448,7 @@ Simplify… 簡化… Simulate Colored Paper 模擬有色紙張 Simulate Paper 模擬紙張 - Single / Double Column 單欄 / 雙欄 + Double Column 雙欄 Single Left Quotation Mark 左單引號 Single Quotes: 單引號: Single Right Quotation Mark 右單引號 diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index c4322a90..bb40d6c9 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -197,7 +197,12 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[ ("view.rotateReset", "Reset Rotate View", "Cmd+Shift+1", "{}"), ("window.control", "Control", "", "{}"), ("window.toolbar", "Tools", "", "{}"), - ("window.toolbarColumns", "Toolbar: Single/Double Column", "", "{}"), + ( + "window.toolbarColumns", + "Toolbar: Single/Double Column", + "", + "{double?: bool} show the toolbar's tools in two columns (true), one (false) or toggle (omitted), as the double arrow at the top of the toolbar does; returns the new state", + ), ("window.toolbarAdvanced", "Toolbar: Advanced / Basic", "", "{}"), ( "window.floatTools", @@ -984,7 +989,14 @@ pub fn run_ui_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option flag(&mut app.ui.control_bar), "window.toolbar" => flag(&mut app.ui.toolbar), - "window.toolbarColumns" => flag(&mut app.ui.toolbar_double), + "window.toolbarColumns" => { + app.ui.toolbar_double = match p.get("double") { + None | Some(Value::Null) => !app.ui.toolbar_double, + Some(Value::Bool(b)) => *b, + Some(_) => return Some(Err("double must be true or false".into())), + }; + Ok(json!(app.ui.toolbar_double)) + } "window.toolbarAdvanced" => flag(&mut app.ui.toolbar_advanced), "window.floatTools" => { let floating = match p.get("floating") { @@ -1317,6 +1329,7 @@ pub fn checked(app: &VectorcraftApp, id: &str, p: &Value) -> Option { "window.control" => app.ui.control_bar, "window.toolbar" => app.ui.toolbar, "window.toolbarAdvanced" => app.ui.toolbar_advanced, + "window.toolbarColumns" => app.ui.toolbar_double, "window.floatTools" => { let tool = p.get("tool").and_then(Value::as_str).unwrap_or(""); app.ui.floating_flyouts.iter().any(|f| f.tools.iter().any(|id| id == tool)) @@ -2305,7 +2318,7 @@ pub fn menu_tree() -> Vec<(&'static str, Vec)> { c("Control", "window.control"), c("Contextual Task Bar", "window.taskBar"), c("Tools", "window.toolbar"), - sub("Toolbars", vec![c("Advanced", "window.toolbarAdvanced"), c("Single / Double Column", "window.toolbarColumns")]), + sub("Toolbars", vec![c("Advanced", "window.toolbarAdvanced"), c("Double Column", "window.toolbarColumns")]), Sep, panel("Actions", "actions"), panel("Align", "align"), diff --git a/crates/ui-egui/src/toolbar.rs b/crates/ui-egui/src/toolbar.rs index 3eec7513..de28b9b6 100644 --- a/crates/ui-egui/src/toolbar.rs +++ b/crates/ui-egui/src/toolbar.rs @@ -1,5 +1,7 @@ //! The Tools panel. Default: Illustrator 2026's categorized single-column toolbar (Select, Shapes, -//! Draw, Modify, Type, Navigate, Color). Window → Toolbars → Advanced shows every tool group. +//! Draw, Modify, Type, Navigate, Color). Window → Toolbars → Advanced shows every tool group; the +//! double arrow at the top (or Window → Toolbars → Double Column, `window.toolbarColumns`) lays the +//! tools out in one or two columns. //! Bottom: fill/stroke proxy, colour/gradient/none, drawing modes, screen mode, Edit Toolbar. use egui::{Color32, CornerRadius, Sense, Stroke, Ui, pos2, vec2}; @@ -88,10 +90,8 @@ pub fn remember(app: &mut VectorcraftApp, id: &str) { pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); let all = slots(app); - let avail = ui.available_height(); - let labels = all.iter().filter(|s| s.0.is_some()).count() as f32 * 20.0; - let need = all.len() as f32 * PITCH + labels + 190.0; - let cols = if app.ui.toolbar_double || avail < need { 2 } else { 1 }; + // The user's choice alone: a window too short for the tools scrolls them. + let cols = if app.ui.toolbar_double { 2 } else { 1 }; let w = if cols == 2 { 76.0 } else { WIDTH }; egui::Panel::left("toolbar") .resizable(false) @@ -108,8 +108,11 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { egui::Rect::from_min_size(hdr.left_top() + vec2(3.0, 2.0), vec2(10.0, 10.0)), if hresp.hovered() { t.text_strong } else { t.text }, ); - if hresp.on_hover_text(tl!("Toggle single/double column")).clicked() { - app.ui.toolbar_double = !app.ui.toolbar_double; + if hresp.on_hover_text(tl!("Toggle single/double column")).clicked() + // It only fails on bad params, which this never sends; show it all the same. + && let Err(e) = app.run("window.toolbarColumns", json!({ "double": cols == 1 })) + { + app.ui.status = e; } let (grip, _) = ui.allocate_exact_size(vec2(ui.available_width(), 6.0), Sense::hover()); for k in 0..6 { @@ -982,10 +985,52 @@ pub(crate) mod tests { /// A click at `at`. fn click(app: &mut VectorcraftApp, ctx: &egui::Context, time: f64, at: Pos2, button: PointerButton) { + click_in(app, ctx, time, at, button, 1200.0); + } + + /// [`click`] in a window `height` points tall; returns the tool buttons' rects after it. + fn click_in(app: &mut VectorcraftApp, ctx: &egui::Context, time: f64, at: Pos2, button: PointerButton, height: f32) -> Vec { let b = |pressed| Event::PointerButton { pos: at, button, pressed, modifiers: Default::default() }; - frame(app, ctx, time, vec![Event::PointerMoved(at), b(true)]); - frame(app, ctx, time + 0.05, vec![b(false)]); - frame(app, ctx, time + 0.1, vec![]); + frame_in(app, ctx, time, vec![Event::PointerMoved(at), b(true)], height); + frame_in(app, ctx, time + 0.05, vec![b(false)], height); + frame_in(app, ctx, time + 0.1, vec![], height) + } + + /// How many columns of tool buttons `rects` make. + fn columns(rects: &[egui::Rect]) -> usize { + let mut lefts: Vec = rects.iter().map(|r| r.left().round() as i32).collect(); + lefts.sort_unstable(); + lefts.dedup(); + lefts.len() + } + + #[test] + fn the_double_arrow_toggles_one_and_two_columns_in_a_short_window() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + let ctx = egui::Context::default(); + // 700 pt: too short for the single column, which scrolls instead of turning into two. + assert_eq!(columns(&frame_in(&mut app, &ctx, 0.0, vec![], 700.0)), 1); + let arrow = pos2(8.0, 7.0); + assert_eq!(columns(&click_in(&mut app, &ctx, 1.0, arrow, PointerButton::Primary, 700.0)), 2); + assert!(app.ui.toolbar_double && crate::menus::checked(&app, "window.toolbarColumns", &json!({})) == Some(true)); + assert_eq!(columns(&click_in(&mut app, &ctx, 2.0, arrow, PointerButton::Primary, 700.0)), 1); + assert!(!app.ui.toolbar_double); + } + + #[test] + fn the_columns_command_toggles_or_sets_the_toolbar_columns() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + let ctx = egui::Context::default(); + assert_eq!(app.run("window.toolbarColumns", json!({})).unwrap(), json!(true)); + assert_eq!(columns(&frame_in(&mut app, &ctx, 0.0, vec![], 700.0)), 2); + assert_eq!(app.run("window.toolbarColumns", json!({"double": true})).unwrap(), json!(true)); + assert_eq!(app.run("window.toolbarColumns", json!({})).unwrap(), json!(false)); + assert_eq!(crate::menus::checked(&app, "window.toolbarColumns", &json!({})), Some(false)); + assert_eq!(columns(&frame_in(&mut app, &ctx, 1.0, vec![], 700.0)), 1); + assert!(app.run("window.toolbarColumns", json!({"double": "yes"})).is_err()); + // Advanced (every tool group) is taller still and keeps the column the user chose too. + app.run("window.toolbarAdvanced", json!({})).unwrap(); + assert_eq!(columns(&frame_in(&mut app, &ctx, 2.0, vec![], 700.0)), 1); } #[test]