diff --git a/ROADMAP.md b/ROADMAP.md index c74969d3..e4e8320e 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-09 (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), the toolbar's double arrow and Window › Toolbars › Double Column switching between one and two columns in any window height (#455), the Smart Guides display options and Snapping Tolerance (#394), Font Size and Leading combo boxes with preset lists in the Character and Properties panels and the Control bar (#458), screenshots pasted from the clipboard on macOS, Windows and the web (#457), the Properties panel's Fill and Stroke matching the Control bar's, Illustrator's Window menu panel keys (Shift+F7 for Align…) and Alt-wheel zoom whatever the wheel preference (Discord feedback), the point/area type widget (Discord feedback), groups in legacy Illustrator .ai files (Discord feedback), a rotating desktop log file (#465), image controls in the Control bar and Properties panel: Image Trace with its presets, Mask, Crop Image, Embed/Unembed, and an Image Trace object's preset, Expand and Release (Discord feedback), the Pen working as the Anchor Point tool with Alt held over a selected path's handles and anchors, and anchor buttons that convert, connect and cut paths at the selected anchors (Discord feedback), Japanese hanging punctuation (burasagari, #463), Escape leaving Presentation Mode with no screen mode saved across restarts, documents dropped on the desktop app opening as tabs of their own and a note that an `.ai` file's art off its artboards isn't in the part read (#472), Add/Delete Anchor Point tool icons drawn from the Pen tool's (#479), EPS import running more of PostScript Level 3 (patterns, Type 3 fonts, mesh shadings, masked images, VM and resource probes), and a preview that stands in saying why (Discord feedback, #466, #469, #474), chords with modifiers recorded in Edit › Keyboard Shortcuts (#487), Type Object Selection by Path Only (#394), Anti-aliased Artwork (#394), Show Pixel Grid (#394), pen and touch taps pressed and lifted within one frame clicking where they tapped and the Linux/Wayland pen-tablet workaround (#491), bounding-box resizes landing on the other objects' anchors and centres with labels, and the Rotate, Scale, Shear and Reflect tools snapping their drags (#482), Shift-drag marquees that toggle the objects or anchors they reach and a Lasso whose Alt subtraction keeps the rest of the selection (#483), the mouse wheel stepping the focused numeric field, the dialogs' and preferences' drag values made such fields (#485), Show The Home Screen When No Documents Are Open (#394), Numbers Without Units Are Points (#394), Auto-Collapse Iconic Panels (#394), the Contextual Task Bar moved by its handle and its More Options menu pinning and resetting its position, a designed macOS DMG window (#493), panels, panel groups and the Tools panel undocking into floating panels that move, stack and dock again (#495), the 0.5.0 startup crash on dual-GPU Linux desktops under Wayland fixed: the desktop's own GPU by default and a restart on the next GPU when one can't show the window (#502), path editing while drawing: Cmd/Ctrl with the Pen borrowing Direct Selection and drawing on afterwards, Pen+Alt reshaping segments, handles of a whole selected path dragging, and handle drags no longer re-gathering snap targets every move (#494), the Control bar's and Properties panel's Convert buttons for a path selected as a whole with Direct Selection, the Pen and the anchor tools (#504), the Pen closing a path dragged out of its first anchor every time, without flicker (#501), the Paste menu checking the system clipboard on a background thread on Linux so an X11 clipboard owner that never answers no longer freezes the window (#510), PDF/`.ai` import keeping a type object's outline stroke on the type, wrapped lines as area type, barcode digits in place and layers in the file's stacking order (#508), the Italian interface (#514), EPS images with several data sources read in turn (#498), `restore` undoing dictionary changes in EPS import (part of #505), the macOS menu bar laid out as a Mac app's (the VectorCraft menu with Settings, Language and Appearance, the system's Hide, Services and Window items; owner's request), Object › Image Trace › Make tracing with the Default preset instead of asking for a preset's name (#543), the rulers' unit menu (#528), Live Corners on stars, polygons and pen paths (#511), the drawing tools snapping to Smart Guides as the Selection tool does, with Construction Guides (#506), the in-window menus' Select All, Cut, Copy and Paste acting on the text field they were opened from (#516), Windows rendering through DX12 before Vulkan (#545), Image Trace tracing colour layers in parallel and refusing traces too large for memory, with the canvas outlining huge selections by their bounds (#525); see [Honest assessment](#honest-assessment-2026-10-05))._ +_Last updated: 2026-10-09 (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), the toolbar's double arrow and Window › Toolbars › Double Column switching between one and two columns in any window height (#455), the Smart Guides display options and Snapping Tolerance (#394), Font Size and Leading combo boxes with preset lists in the Character and Properties panels and the Control bar (#458), screenshots pasted from the clipboard on macOS, Windows and the web (#457), the Properties panel's Fill and Stroke matching the Control bar's, Illustrator's Window menu panel keys (Shift+F7 for Align…) and Alt-wheel zoom whatever the wheel preference (Discord feedback), the point/area type widget (Discord feedback), groups in legacy Illustrator .ai files (Discord feedback), a rotating desktop log file (#465), image controls in the Control bar and Properties panel: Image Trace with its presets, Mask, Crop Image, Embed/Unembed, and an Image Trace object's preset, Expand and Release (Discord feedback), the Pen working as the Anchor Point tool with Alt held over a selected path's handles and anchors, and anchor buttons that convert, connect and cut paths at the selected anchors (Discord feedback), Japanese hanging punctuation (burasagari, #463), Escape leaving Presentation Mode with no screen mode saved across restarts, documents dropped on the desktop app opening as tabs of their own and a note that an `.ai` file's art off its artboards isn't in the part read (#472), Add/Delete Anchor Point tool icons drawn from the Pen tool's (#479), EPS import running more of PostScript Level 3 (patterns, Type 3 fonts, mesh shadings, masked images, VM and resource probes), and a preview that stands in saying why (Discord feedback, #466, #469, #474), chords with modifiers recorded in Edit › Keyboard Shortcuts (#487), Type Object Selection by Path Only (#394), Anti-aliased Artwork (#394), Show Pixel Grid (#394), pen and touch taps pressed and lifted within one frame clicking where they tapped and the Linux/Wayland pen-tablet workaround (#491), bounding-box resizes landing on the other objects' anchors and centres with labels, and the Rotate, Scale, Shear and Reflect tools snapping their drags (#482), Shift-drag marquees that toggle the objects or anchors they reach and a Lasso whose Alt subtraction keeps the rest of the selection (#483), the mouse wheel stepping the focused numeric field, the dialogs' and preferences' drag values made such fields (#485), Show The Home Screen When No Documents Are Open (#394), Numbers Without Units Are Points (#394), Auto-Collapse Iconic Panels (#394), the Contextual Task Bar moved by its handle and its More Options menu pinning and resetting its position, a designed macOS DMG window (#493), panels, panel groups and the Tools panel undocking into floating panels that move, stack and dock again (#495), the 0.5.0 startup crash on dual-GPU Linux desktops under Wayland fixed: the desktop's own GPU by default and a restart on the next GPU when one can't show the window (#502), path editing while drawing: Cmd/Ctrl with the Pen borrowing Direct Selection and drawing on afterwards, Pen+Alt reshaping segments, handles of a whole selected path dragging, and handle drags no longer re-gathering snap targets every move (#494), the Control bar's and Properties panel's Convert buttons for a path selected as a whole with Direct Selection, the Pen and the anchor tools (#504), the Pen closing a path dragged out of its first anchor every time, without flicker (#501), the Paste menu checking the system clipboard on a background thread on Linux so an X11 clipboard owner that never answers no longer freezes the window (#510), PDF/`.ai` import keeping a type object's outline stroke on the type, wrapped lines as area type, barcode digits in place and layers in the file's stacking order (#508), the Italian interface (#514), EPS images with several data sources read in turn (#498), `restore` undoing dictionary changes in EPS import (part of #505), the macOS menu bar laid out as a Mac app's (the VectorCraft menu with Settings, Language and Appearance, the system's Hide, Services and Window items; owner's request), Object › Image Trace › Make tracing with the Default preset instead of asking for a preset's name (#543), the rulers' unit menu (#528), Live Corners on stars, polygons and pen paths (#511), the drawing tools snapping to Smart Guides as the Selection tool does, with Construction Guides (#506), the in-window menus' Select All, Cut, Copy and Paste acting on the text field they were opened from (#516), Windows rendering through DX12 before Vulkan (#545), Image Trace tracing colour layers in parallel and refusing traces too large for memory, with the canvas outlining huge selections by their bounds (#525), SVGs of chosen artboards holding only the art over each artboard (#550); see [Honest assessment](#honest-assessment-2026-10-05))._ ## Where we are diff --git a/crates/engine/src/cmd/fileio/export.rs b/crates/engine/src/cmd/fileio/export.rs index 543eda48..af2d441e 100644 --- a/crates/engine/src/cmd/fileio/export.rs +++ b/crates/engine/src/cmd/fileio/export.rs @@ -87,6 +87,26 @@ fn selected_part(n: &Node, keep: &HashSet) -> Option { Some(out) } +/// `doc` with only the art over `rect` (an artboard): objects whose visual bounds miss it are left +/// out, at any depth of layers and sublayers, and objects that reach it stay whole (the file's +/// bounds clip them), as DXF's crop leaves them. A clipping layer keeps every object, its clipping +/// path among them. An artboard's file then holds its own art, not a whole document of artboards. +pub(super) fn art_over(doc: &Document, rect: vectorcraft_geom::Rect) -> Document { + fn kept(n: &Arc, rect: vectorcraft_geom::Rect) -> Option> { + if matches!(n.kind, NodeKind::Layer { clip: false, .. }) { + let mut layer = Node::clone(n); + let children = layer.children_mut()?; + *children = children.iter().filter_map(|c| kept(c, rect)).collect(); + return Some(Arc::new(layer)); + } + let over = |b: vectorcraft_geom::Rect| b.x0 <= rect.x1 && rect.x0 <= b.x1 && b.y0 <= rect.y1 && rect.y0 <= b.y1; + n.visual_bounds().is_none_or(over).then(|| n.clone()) + } + let mut d = doc.clone(); + d.layers = doc.layers.iter().filter_map(|l| kept(l, rect)).collect(); + d +} + /// `p` without its artboard choice, also inside its SVG options (for documents made of one /// synthetic artboard, and for callers that pick the artboard themselves). pub(super) fn without_artboards(p: &Value) -> Value { diff --git a/crates/engine/src/cmd/fileio/svg.rs b/crates/engine/src/cmd/fileio/svg.rs index 49a05878..263d5fd9 100644 --- a/crates/engine/src/cmd/fileio/svg.rs +++ b/crates/engine/src/cmd/fileio/svg.rs @@ -19,7 +19,7 @@ pub const OPTIONS: &[FormatOption] = &[ name: "useArtboards", ty: "boolean", default: "true", - description: "false: one SVG of the bounds of all art instead of artboards (several artboards write one file each)", + description: "false: one SVG of the bounds of all art instead of artboards (several artboards write one file each; a named artboard's file holds only the art over it)", }, FormatOption { name: "styling", @@ -125,8 +125,10 @@ pub fn options_map(p: &Value) -> Result, String> { Ok(m) } -/// The writer options and the artboards (`None`: the art bounds) an SVG export of `doc` covers. -fn plan(doc: &Document, p: &Value) -> Result<(ExportOptions, Vec>), String> { +/// The writer options and the artboards (`None`: the art bounds) an SVG export of `doc` covers, +/// and whether `p` named those artboards (else the first stands for the whole document, as Save +/// writes it). +fn plan(doc: &Document, p: &Value) -> Result<(ExportOptions, Vec>, bool), String> { let mut m = options_map(p)?; // Fonts: unless chosen, Document Setup → Type → Export decides (appearance = outlines). if !m.contains_key("outlineText") && doc.setup.export_text == vectorcraft_doc::ExportText::Appearance { @@ -138,21 +140,24 @@ fn plan(doc: &Document, p: &Value) -> Result<(ExportOptions, Vec>) opts.check().map_err(|e| format!("SVG options: {e}"))?; let n = doc.artboards.len(); if n == 0 || boards.use_artboards == Some(false) { - return Ok((opts, vec![None])); + return Ok((opts, vec![None], false)); } - let picked = boards.pick.resolve(n)?.unwrap_or_else(|| vec![0]); - Ok((opts, picked.into_iter().map(Some).collect())) + let named = boards.pick.resolve(n)?; + let chosen = named.is_some(); + Ok((opts, named.unwrap_or_else(|| vec![0]).into_iter().map(Some).collect(), chosen)) } /// Encode `doc` as SVG (or gzipped, SVGZ): one file per chosen artboard, with the images they /// link to and the writer's warnings. pub(super) fn encode(doc: &Document, p: &Value, compressed: bool) -> Result { - let (opts, boards) = plan(doc, p)?; + let (opts, boards, chosen) = plan(doc, p)?; // Preserve editing embeds the native document (once, shared by every file). let native = opts.preserve_editing.then(|| vectorcraft_format::save(doc, false)); let mut enc = Encoded::default(); for artboard in boards { - let mut out = vectorcraft_svg::export_full(doc, &ExportOptions { artboard, ..opts.clone() }, native.as_deref()); + // A chosen artboard's file holds the art over it (#550). + let over = artboard.filter(|_| chosen).and_then(|b| doc.artboards.get(b)).map(|a| super::export::art_over(doc, a.rect)); + let mut out = vectorcraft_svg::export_full(over.as_ref().unwrap_or(doc), &ExportOptions { artboard, ..opts.clone() }, native.as_deref()); let bytes = out.take_bytes(); for l in out.linked { if !enc.linked.iter().any(|e| e.name == l.name) { diff --git a/crates/engine/src/cmd/fileio/tests_svgenc.rs b/crates/engine/src/cmd/fileio/tests_svgenc.rs index 5daf5479..e51f2b26 100644 --- a/crates/engine/src/cmd/fileio/tests_svgenc.rs +++ b/crates/engine/src/cmd/fileio/tests_svgenc.rs @@ -49,3 +49,28 @@ fn profile_and_embedded_fonts_reach_the_writer() { assert!(s.execute("document.serialize", &json!({"format": "svg", "svg": {"profile": "svg2"}})).is_err(), "unknown profiles are refused"); assert!(s.execute("document.serialize", &json!({"format": "svg", "encoding": "ebcdic"})).is_err(), "unknown encodings are refused"); } + +/// #550: an SVG of chosen artboards holds the art over each, not every artboard's art; with no +/// artboard named (as Save writes it) the whole document's art is there. +#[test] +fn a_chosen_artboard_s_svg_holds_only_its_own_art() { + let mut s = Session::new(); + s.execute("file.new", &json!({"width": 100, "height": 100, "artboards": 3})).unwrap(); + let x = |s: &Session, b: usize| s.doc().unwrap().doc.artboards[b].rect.x0; + let (first, third) = (x(&s, 0), x(&s, 2)); + s.execute("shape.rectangle", &json!({"x": first + 10.0, "y": 10, "width": 30, "height": 30})).unwrap(); + s.execute("shape.ellipse", &json!({"x": third + 10.0, "y": 10, "width": 30, "height": 30})).unwrap(); + // A rectangle has straight sides only; the ellipse is curves. + let shapes = |svg: &str| (svg.matches(" = all["files"].as_array().unwrap().iter().map(text).collect(); + assert_eq!(files.iter().map(|f| shapes(f)).collect::>(), [(1, false), (0, false), (1, true)]); + // Export for Screens names one artboard at a time the same way. + let screen = text(&s.execute("document.serialize", &json!({"format": "svg", "artboard": 0})).unwrap()); + assert_eq!(shapes(&screen), (1, false)); + let whole = text(&s.execute("document.serialize", &json!({"format": "svg"})).unwrap()); + assert_eq!(shapes(&whole).0, 2, "nothing named: the whole document's art"); +} diff --git a/crates/ui-egui/src/dialogs/svg_options.rs b/crates/ui-egui/src/dialogs/svg_options.rs index daa9a9e0..fe9c9345 100644 --- a/crates/ui-egui/src/dialogs/svg_options.rs +++ b/crates/ui-egui/src/dialogs/svg_options.rs @@ -360,7 +360,7 @@ mod tests { let f = &mut app.ui.dialog.as_mut().unwrap().fields; f.insert("decimals".into(), json!(1)); f.insert("allArtboards".into(), json!(false)); - f.insert("range".into(), json!("2")); + f.insert("range".into(), json!("1")); super::super::confirm(&mut app).unwrap(); assert!(app.ui.dialog.is_none()); let svg = text(&written, 0); diff --git a/crates/ui-egui/src/dialogs/tests_export.rs b/crates/ui-egui/src/dialogs/tests_export.rs index 7e67a439..90780ad3 100644 --- a/crates/ui-egui/src/dialogs/tests_export.rs +++ b/crates/ui-egui/src/dialogs/tests_export.rs @@ -240,3 +240,29 @@ fn pdf_artboards_continue_in_the_save_pdf_dialog() { let pdf = vectorcraft_pdf::import(&written.borrow()[0].1).unwrap(); assert_eq!(pdf.artboards.len(), 2, "one page per artboard in the range"); } + +/// #546, #550: Export As with Use Artboards and a range writes the chosen artboard alone in every +/// format, each file holding that artboard's art (the ellipse is on artboard 1). +#[test] +fn export_as_writes_only_the_chosen_artboard_in_every_format() { + for (format, ellipse) in [("svg", "` per line of type). Unknown keys inside `svg` are rejected; `run_command document.formats` lists every option with its default. `encoding` is `utf8`, `utf16` (big-endian after a byte order mark) or `latin1` (ISO 8859-1, other characters as `&#x…;` references); `document.serialize` still answers `text`, plus `dataBase64`