diff --git a/ROADMAP.md b/ROADMAP.md index 04eab3e4..7e3f4710 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -123,7 +123,7 @@ Ranked; detail in [docs/roadmap.md](docs/roadmap.md) and [docs/gaps.md](docs/gap | Date | What landed | |---|---| -| 2026-10-11 | Effect › Brush Strokes: Accented Edges, Angled Strokes, Crosshatch, Dark Strokes, Ink Outlines, Spatter, Sprayed Strokes, Sumi-e (G2). Command parameters from agents and files checked before anything changes (paint, place, cuts, dashes, crop, asset ids, anchors, paths, distort; #984–#999); command dialogs keep whole numbers whole, so Create Object Mosaic takes the tiles typed (#1000); stepper arrows take Shift (×10) and Ctrl/Cmd (÷10) like the arrow keys (#991). Exports and Rasterize supersample Art Optimized edges, so shapes meeting edge to edge leave no seam (#983). Character and paragraph styles take leading, kerning, mojikumi and the other attributes left out at their defaults (#1001). Drawing, typing and pasting with no layer shown and unlocked is refused instead of adding art to a hidden or locked layer (#1002). MCP `add_text` gives type on or in a path the colour asked for (#1003). MCP `screenshot` of a connected app renders the artboard asked for (#1004). `vectorcraft-cli run` ends quietly when its reader closes early (#1005). Select › All on Active Artboard selects on the artboard made active, not always the first (#1006). Reverse Gradient keeps off-centre midpoints, mirrored (#1007). A polar grid with no radial dividers draws none (#1008). Use Precise Cursors also covers the Pen's join pointer (#1009). | +| 2026-10-11 | Effect › Brush Strokes: Accented Edges, Angled Strokes, Crosshatch, Dark Strokes, Ink Outlines, Spatter, Sprayed Strokes, Sumi-e (G2). Command parameters from agents and files checked before anything changes (paint, place, cuts, dashes, crop, asset ids, anchors, paths, distort; #984–#999); command dialogs keep whole numbers whole, so Create Object Mosaic takes the tiles typed (#1000); stepper arrows take Shift (×10) and Ctrl/Cmd (÷10) like the arrow keys (#991). Exports and Rasterize supersample Art Optimized edges, so shapes meeting edge to edge leave no seam (#983). Character and paragraph styles take leading, kerning, mojikumi and the other attributes left out at their defaults (#1001). Drawing, typing and pasting with no layer shown and unlocked is refused instead of adding art to a hidden or locked layer (#1002). MCP `add_text` gives type on or in a path the colour asked for (#1003). MCP `screenshot` of a connected app renders the artboard asked for (#1004). `vectorcraft-cli run` ends quietly when its reader closes early (#1005). Select › All on Active Artboard selects on the artboard made active, not always the first (#1006). Reverse Gradient keeps off-centre midpoints, mirrored (#1007). A polar grid with no radial dividers draws none (#1008). Use Precise Cursors also covers the Pen's join pointer (#1009). SVGs with one absolute and one unitless root side import at their size, unstretched (#1010). | | 2026-10-10 | Initial live Revolve (Effect › 3D and Materials, #846, #605), in its own `three-d` crate. Progress docs re-measured and restructured to the craftrules standard (this page, `docs/target-app-parity.md`, `gaps.md`, `ui-parity.md`, the format, hardware, localization, effects and type checklists). Same day: v0.8.0; Effect › Distort, Pixelate and Texture filters (13); German interface; Variables (data merge); Graph Type value axes and tick marks; restart when the first frame never reaches the screen (#964) | | 2026-10-09 | Radial Blur, Smart Blur, Unsharp Mask (gap 2 begins); 340 commits, the busiest day | | 2026-10-08 | v0.5.0–v0.7.0; community contributions: Pen and shape-tool modifiers, Layers, Artboards, Japanese composition, Hebrew/Arabic type, interface languages, saved selections | diff --git a/crates/svg/src/import.rs b/crates/svg/src/import.rs index d03e29aa..a3529278 100644 --- a/crates/svg/src/import.rs +++ b/crates/svg/src/import.rs @@ -47,8 +47,8 @@ pub(crate) fn import(svg: &str, opts: &ImportOptions) -> Result<(Document, Vec Result<(Document, Vec Result<(Document, Vec| matches!(u, Some(U::In | U::Cm | U::Mm | U::Pt | U::Pc)); let (w, h) = (unit("width"), unit("height")); - let k = |u| if physical(u) { PT_PER_IN / CSS_PX_PER_IN } else { 1.0 }; - let dpi = if physical(w) || physical(h) { CSS_PX_PER_IN } else { PT_PER_IN }; + let (dpi, k) = if physical(w) || physical(h) { (CSS_PX_PER_IN, PT_PER_IN / CSS_PX_PER_IN) } else { (PT_PER_IN, 1.0) }; let unit = match w { Some(U::Mm) => Unit::Millimeters, Some(U::Cm) => Unit::Centimeters, @@ -238,7 +239,7 @@ impl RootUnits { Some(U::Pc) => Unit::Picas, _ => Unit::Pixels, }; - Self { dpi, k: (k(w), k(h)), unit } + Self { dpi, k, unit } } } diff --git a/crates/svg/src/lib.rs b/crates/svg/src/lib.rs index aae41bda..3789d8f4 100644 --- a/crates/svg/src/lib.rs +++ b/crates/svg/src/lib.rs @@ -41,8 +41,9 @@ //! * One CSS pixel (user unit) is one point, as on export, and absolute lengths (`in`, `cm`, `mm`, //! `pt`, `pc`) keep their physical size (72 pt per inch): `font-size="12pt"` is 12 pt. A root //! `width`/`height` in absolute units keeps its physical size, so a 210 mm SVG opens on a 210 mm -//! artboard, its user units being CSS pixels of it (96 per inch). The document's units follow the -//! unit of the root `width` (pixels when it has none). +//! artboard, its user units being CSS pixels of it (96 per inch), the other side's too when only +//! one is absolute. The document's units follow the unit of the root `width` (pixels when it has +//! none). //! * `` imports as a luminance opacity mask; a mask we exported keeps its options and art //! (its `data-vectorcraft-mask="noclip invert"` lists the options that differ from clipping and //! not inverted). Nested clip paths (``) are clip groups in clip groups. diff --git a/crates/svg/tests/roundtrip.rs b/crates/svg/tests/roundtrip.rs index 3ae90e78..c7433a7a 100644 --- a/crates/svg/tests/roundtrip.rs +++ b/crates/svg/tests/roundtrip.rs @@ -811,6 +811,31 @@ fn import_absolute_units_keep_their_physical_size() { } } +/// #1010: a root with one absolute side and one unitless or px side opens as if both were +/// absolute (CSS pixels at 96 per inch), so nothing is stretched and the viewport keeps its size. +#[test] +fn import_mixed_root_units_scale_both_sides_alike() { + let rect = |d: &Document| art(d)[0].geometric_bounds().unwrap(); + for (size, board) in [ + (r#"width="96pt" height="96""#, Rect::new(0.0, 0.0, 96.0, 72.0)), + (r#"width="96pt" height="96px""#, Rect::new(0.0, 0.0, 96.0, 72.0)), + (r#"width="96pt" height="72pt""#, Rect::new(0.0, 0.0, 96.0, 72.0)), + (r#"width="128" height="72pt""#, Rect::new(0.0, 0.0, 96.0, 72.0)), + ] { + let svg = + |extra: &str| format!(r##""##); + let d = import(&svg("")).unwrap(); + assert!(close_rect(d.artboards[0].rect, board, 1e-6), "{size}: {:?}", d.artboards[0].rect); + assert!(close_rect(rect(&d), Rect::new(0.0, 0.0, 24.0, 24.0), 1e-6), "{size}: {:?}", rect(&d)); + // With a viewBox: fitted uniformly (xMidYMid meet), unless preserveAspectRatio is none. + let d = import(&svg(r#"viewBox="0 0 32 32""#)).unwrap(); + assert!(close_rect(d.artboards[0].rect, board, 1e-6), "{size}: {:?}", d.artboards[0].rect); + assert!(close_rect(rect(&d), Rect::new(12.0, 0.0, 84.0, 72.0), 1e-6), "{size} viewBox: {:?}", rect(&d)); + let d = import(&svg(r#"viewBox="0 0 32 32" preserveAspectRatio="none""#)).unwrap(); + assert!(close_rect(rect(&d), board, 1e-6), "{size} none: {:?}", rect(&d)); + } +} + #[test] fn full_document_roundtrip_is_stable() { // export → import → export produces identical SVG.