From e71b4783f26c817235cf4e7f9eb31ac1be2c2089 Mon Sep 17 00:00:00 2001 From: z3r0pix3l Date: Sun, 8 Nov 2026 21:13:54 +0300 Subject: [PATCH 01/38] PDF import: Arabic ligatures keep their letters in order (#6072) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A PDF names a ligature's characters in logical order (lam then alef for the lam-alef glyph), while a horizontal line of type is gathered glyph by glyph in visual order and then put back in logical order character by character. The ligature's characters were turned round with the line: لا came in as ال, بلال as بالل and الله as هللا. Each glyph's characters now go into the line in visual order, so putting the line back restores them. --- crates/pdf/src/import/builder.rs | 16 +++++++++++++++ crates/pdf/src/tests/live_text.rs | 34 +++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/crates/pdf/src/import/builder.rs b/crates/pdf/src/import/builder.rs index 86a4a89a3..b74498926 100644 --- a/crates/pdf/src/import/builder.rs +++ b/crates/pdf/src/import/builder.rs @@ -842,6 +842,9 @@ impl<'p> Builder<'p> { let vx = self.fonts.get(&key).and_then(|f| f.vertical.as_ref()).map_or(500.0, |v| v.centre(glyph)); m * kurbo::Point::new(vx, 880.0) }); + // A horizontal line is gathered in visual order and put back in logical order as a whole: + // a ligature's characters (lam-alef, lam-lam-heh) go in visual order too (#6072). + let text = if top.is_none() { visual_glyph_text(text) } else { text }; let place = |line: &mut TextLine| match top { Some(top) => line.push_upright(&look, at, opacity, Upright { top }, &text), None => line.push(&look, at, opacity, advance, &text), @@ -871,3 +874,16 @@ impl<'p> Builder<'p> { true } } + +/// The characters of one glyph in the order they are drawn: a PDF names a ligature's characters +/// in logical order (lam then alef for لا), while a horizontal line of type is gathered glyph by +/// glyph in visual order and only then put back in logical order. Right-to-left characters are +/// turned round here so that putting the line back restores them. +fn visual_glyph_text(text: String) -> String { + if text.chars().nth(1).is_none() { + return text; + } + let Some((order, _)) = vectorcraft_text::logical_order(&text) else { return text }; + let chars: Vec = text.chars().collect(); + order.iter().filter_map(|&i| chars.get(i)).collect() +} diff --git a/crates/pdf/src/tests/live_text.rs b/crates/pdf/src/tests/live_text.rs index 4cb4417bc..2cc9f938c 100644 --- a/crates/pdf/src/tests/live_text.rs +++ b/crates/pdf/src/tests/live_text.rs @@ -477,3 +477,37 @@ fn hebrew_comes_back_in_logical_order() { assert_eq!(shown(&t[0]), shown(&original), "{text}"); } } + +/// The text objects a PDF of `text` set in `face` imports as. +fn imported_texts(face: &vectorcraft_text::FontFace, text: &str) -> Vec { + let mut pdf = krilla::Document::new(); + let mut page = pdf.start_page_with(PageSettings::from_wh(300.0, 200.0).unwrap()); + let mut s = page.surface(); + let font = Font::new(face.file_data().to_vec().into(), face.face_index()).unwrap(); + s.draw_text(KPoint::from_xy(20.0, 50.0), font, 24.0, text, false, TextDirection::Auto); + s.finish(); + page.finish(); + let r = import_with_report(&pdf.finish().unwrap(), &ImportOptions::default()).unwrap(); + texts(&r.document).iter().map(|t| t.plain_text()).collect() +} + +#[test] +fn arabic_ligatures_keep_their_letters_in_order() { + // A lam-alef ligature is one glyph whose text is lam then alef: it must not be turned round + // with the line it sits in (#6072). + let db = vectorcraft_text::FontDb::global(); + let preferred = ["DejaVu Sans", "Arial", "Times New Roman", "Noto Sans Arabic", "Noto Naskh Arabic"].map(String::from); + // An installed Arabic font whose glyphs come back as live type. + let Some(face) = preferred + .iter() + .chain(&db.families()) + .filter_map(|f| db.face(f, "Regular")) + .filter(|f| f.covers('\u{0644}') && f.covers('\u{0627}')) + .find(|f| !imported_texts(f, "بب").is_empty()) + else { + return; // No such font on this host. + }; + for text in ["لا", "بلال", "السلام عليكم", "لا إله إلا الله"] { + assert_eq!(imported_texts(&face, text), [text], "in {}", face.family); + } +} From e0336d861a63a2059c5d23bc11d4b4ae605090ef Mon Sep 17 00:00:00 2001 From: z3r0pix3l Date: Sun, 8 Nov 2026 22:52:47 +0300 Subject: [PATCH 02/38] Test: use a font that draws lam-alef as one glyph (#6072) --- crates/pdf/src/tests/live_text.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/crates/pdf/src/tests/live_text.rs b/crates/pdf/src/tests/live_text.rs index 2cc9f938c..bddeb758d 100644 --- a/crates/pdf/src/tests/live_text.rs +++ b/crates/pdf/src/tests/live_text.rs @@ -497,12 +497,18 @@ fn arabic_ligatures_keep_their_letters_in_order() { // with the line it sits in (#6072). let db = vectorcraft_text::FontDb::global(); let preferred = ["DejaVu Sans", "Arial", "Times New Roman", "Noto Sans Arabic", "Noto Naskh Arabic"].map(String::from); - // An installed Arabic font whose glyphs come back as live type. + // Whether `face` draws lam-alef as one glyph, the ligature the fix is about. + let ligates = |face: &vectorcraft_text::FontFace| { + let style = vectorcraft_doc::CharStyle { font_family: face.family.clone(), font_style: face.style.clone(), ..Default::default() }; + let t = TextObject::point(kurbo::Point::ORIGIN, "لا", style); + vectorcraft_text::layout(db, &t).glyphs.len() == 1 + }; + // An installed Arabic font with a lam-alef ligature whose glyphs come back as live type. let Some(face) = preferred .iter() .chain(&db.families()) .filter_map(|f| db.face(f, "Regular")) - .filter(|f| f.covers('\u{0644}') && f.covers('\u{0627}')) + .filter(|f| f.covers('\u{0644}') && f.covers('\u{0627}') && ligates(f)) .find(|f| !imported_texts(f, "بب").is_empty()) else { return; // No such font on this host. From ac674accfd8b4500ab73d06ab8af7f15f975666d Mon Sep 17 00:00:00 2001 From: Patrick Nadeau Date: Sat, 10 Oct 2026 18:19:12 -0400 Subject: [PATCH 03/38] M4: fix PDF gradient placement in transparency groups Move form transforms into page space so Quartz keeps gradients aligned with their artwork. Add macOS rendering regressions and round-trip coverage for transparency, masks, layers and overprint. Update the fix for the shared craft-pdf writer and the split exporter modules. Preserve upstream mask, coordinate-limit and layer-box fixes, moving the layer's paper bounds into the form's new space too. Verified all three Quartz regressions fail on upstream 65adb7b21 and all five gradient-form tests pass with the fix. The new translated-layer regression also fails without moving the layer's paper bounds. Before the final rebase onto 782f213e4: 463 PDF tests passed (one existing ignored test), workspace fmt, PDF Clippy, wasm, pdf-deps, layers, assets and brands passed. The final rebase reconciles documentation only; the PDF crate is unchanged from that tested revision. After the final rebase, workspace fmt and Clippy pass. Full cargo xtask ci timed out while compiling workspace tests, so the full test suite and remaining CI stages did not complete. The earlier macOS Clippy blocker has been fixed upstream. --- Cargo.lock | 2 + ROADMAP.md | 4 + crates/pdf/Cargo.toml | 6 + crates/pdf/src/export.rs | 6 +- crates/pdf/src/export/exporter.rs | 14 +- crates/pdf/src/export/nodes.rs | 17 ++- crates/pdf/src/export/paint.rs | 2 +- crates/pdf/src/export/writer.rs | 4 +- crates/pdf/src/forms.rs | 12 +- crates/pdf/src/lib.rs | 1 + crates/pdf/src/surface.rs | 140 +++++++++++++++++++ crates/pdf/tests/gradient_forms.rs | 215 +++++++++++++++++++++++++++++ docs/file-format-parity.md | 9 ++ docs/gaps.md | 7 + docs/roadmap.md | 2 + docs/target-app-parity.md | 4 +- 16 files changed, 426 insertions(+), 19 deletions(-) create mode 100644 crates/pdf/src/surface.rs create mode 100644 crates/pdf/tests/gradient_forms.rs diff --git a/Cargo.lock b/Cargo.lock index 93ae3be0f..b7ef1c02a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6643,6 +6643,8 @@ dependencies = [ "kurbo 0.13.1", "log", "md-5", + "objc2-core-foundation", + "objc2-core-graphics", "roxmltree 0.21.2", "serde", "serde_json", diff --git a/ROADMAP.md b/ROADMAP.md index 1bedf0b95..c685b9206 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,5 +1,7 @@ # VectorCraft Roadmap +> **Gradient-form update:** 2026-11-08 · minor · PDF export fix rebased onto current main and checked with macOS Quartz; headline scores and estimates unchanged. + - Contour add-on (beyond Illustrator): drag a selected shape's edge outward or inward for a live, editable offset copy, with a floating bar for distance and Sharp, Round or Square corners; Bake turns it into paths. Scores and estimates unchanged. - Compound Shapes add-on (beyond Illustrator): a menu for Make, Release and Expand Compound Shape and a setting, off by default, that makes the Pathfinder panel's Shape Mode buttons create compound shapes on a click. Scores and estimates unchanged. @@ -137,6 +139,8 @@ Ranked; detail in [docs/roadmap.md](docs/roadmap.md) and [docs/gaps.md](docs/gap ## Progress log - 2026-11-05: Window › Toolbars › Keep Tools Beside the Canvas, off by default, docks the Tools panel between left-side panels and the canvas instead of at the window's left edge, and the canvas's left edge then also docks a floating Tools panel (`window.toolsBesideCanvas`, #5395). Workspaces keep the setting; workspaces saved without it keep Tools at the window's edge. An optional VectorCraft layout choice; headline numbers unchanged. +- 2026-11-08: PDF form-gradient placement reverified on current main: three Quartz regressions fail without the fix and pass with it. The fix follows the exporter module split and keeps upstream layer-box repairs in the form's new space, with a translated-layer regression. Headline numbers unchanged. +- 2026-10-26: PDF form-gradient placement checked with macOS Quartz on the shared writer: linear and radial gradients follow transparency groups, masks, PDF layers and overprinting art. Headline numbers unchanged. - 2026-11-05: Release Type on a Path joins the Type Tools add-on (Object › Plug-ins › Type Tools): each selected type on a path splits into point type with the same text and attributes, on one straight line starting where its first character sat, and the plain path it was set on, with its exact shape and place in the stacking order and no paint; or keeps only the path or only the text, optionally leaving the original. A dialog with Preview, one undo step (`addon.type.releasePath`, #5532). A VectorCraft addition: headline numbers unchanged. - 2026-11-04: New Document: the units chosen in Preset Details now belong to the chosen document, as the pane's options are listed for the selected preset; the chosen card shows them, every other Recent and Saved card keeps the units its document or preset was made in, and the built-in tabs still follow the chosen units, so switching between inches and millimetres no longer restyles every recent template. Scores and estimates unchanged. - 2026-11-02: Mouse wheel over a closed font-family field in Character, Properties or Control changes selected text through the family list using saved font filters (excluding search text), one undo step per notch, stopping at the ends (#5253). Scores and estimates unchanged. diff --git a/crates/pdf/Cargo.toml b/crates/pdf/Cargo.toml index 66ff8b6e3..a5a3b8386 100644 --- a/crates/pdf/Cargo.toml +++ b/crates/pdf/Cargo.toml @@ -52,3 +52,9 @@ vectorcraft-text = { workspace = true, features = ["test-fonts"] } serde_json = { workspace = true } # `draw_text`, to write test files with live text as other writers do. krilla = { workspace = true, features = ["simple-text"] } + +# An independent raster oracle for gradient placement in PDF forms. These are test-only macOS +# bindings; the exporter stays portable Rust and does not depend on an OS PDF renderer. +[target.'cfg(target_os = "macos")'.dev-dependencies] +objc2-core-foundation = { version = "0.3", default-features = false, features = ["std", "CFData"] } +objc2-core-graphics = { version = "0.3", default-features = false, features = ["std", "CGBitmapContext", "CGColorSpace", "CGContext", "CGDataProvider", "CGImage", "CGPDFDocument", "CGPDFPage"] } diff --git a/crates/pdf/src/export.rs b/crates/pdf/src/export.rs index 47184d520..c159c1f19 100644 --- a/crates/pdf/src/export.rs +++ b/crates/pdf/src/export.rs @@ -9,12 +9,11 @@ use craft_pdf::write::{self, Metadata}; use krilla::color::separation::{Color as SepColor, SeparationColorant, SeparationSpace}; use krilla::color::{cmyk, luma, rgb}; use krilla::configure::{Archival, PdfVersion}; -use krilla::geom::{Path, PathBuilder, Size, Transform}; +use krilla::geom::{Path, PathBuilder, Size}; use krilla::image::Image; use krilla::num::NormalizedF32; use krilla::page::PageSettings; use krilla::paint::{Fill, LinearGradient, RadialGradient, SpreadMethod, Stop, Stroke, StrokeDash}; -use krilla::surface::Surface; use krilla::text::{Glyph, GlyphId, KrillaGlyph}; use kurbo::{PathEl, Shape, Vec2}; use vectorcraft_color::{BlendMode, Color, GradientKind, Paint}; @@ -28,6 +27,7 @@ use crate::forms::Mark; use crate::lab_spot::{find, rfind}; use crate::marks::PageBoxes; use crate::output::ColorOut; +use crate::surface::Surface; use crate::{Compatibility, ExportReport, PdfError, PdfOptions, Standard}; mod entry; @@ -131,7 +131,7 @@ mod tests { fn writable_bounds_values_the_transform_and_values_through_it() { let mut pdf = krilla::Document::new(); let mut page = pdf.start_page_with(PageSettings::new(Size::from_wh(100.0, 100.0).unwrap())); - let mut s = page.surface(); + let mut s = Surface::page(page.surface(), 100.0); let r = Rect::new(0.0, 0.0, 10.0, 10.0); assert!(writable(&s, Affine::IDENTITY, r)); assert!(!writable(&s, Affine::IDENTITY, Rect::new(-2_147_483_649.0, 0.0, 10.0, 10.0))); diff --git a/crates/pdf/src/export/exporter.rs b/crates/pdf/src/export/exporter.rs index eb312021e..1901ab9ea 100644 --- a/crates/pdf/src/export/exporter.rs +++ b/crates/pdf/src/export/exporter.rs @@ -1,9 +1,11 @@ use super::entry::RgbInCmyk; use super::{ Affine, Arc, BezPath, BlendMode, Color, Compatibility, Document, Exporter, Fill, FillRule, HashMap, HashSet, Node, NormalizedF32, Path, - PathBuilder, PathEl, Rect, Shape, Standard, Surface, Transform, cmyk, krilla, luma, rgb, + PathBuilder, PathEl, Rect, Shape, Standard, Surface, cmyk, krilla, luma, rgb, }; +pub(super) use crate::surface::xf; + /// How deep inline graphics nest before they are left out. pub(super) const MAX_INLINE_DEPTH: u32 = 4; @@ -92,11 +94,6 @@ pub(super) const MAX_GRADIENT_PX: f64 = 4096.0; /// with overflow checks it panics on -2,147,483,648. pub(super) const MAX_COORD: f64 = 1e9; -pub(super) fn xf(a: Affine) -> Transform { - let c = a.as_coeffs(); - Transform::from_row(c[0] as f32, c[1] as f32, c[2] as f32, c[3] as f32, c[4] as f32, c[5] as f32) -} - /// The current transform of surface `s` in the content stream it draws into, without that stream's /// root transform (a page's y flip). pub(super) fn ctm(s: &Surface) -> Affine { @@ -199,9 +196,12 @@ pub(super) fn cover(s: &mut Surface, page: Rect, level: u8, opacity: f32) { /// group of that opacity that, unlike the writer's opacity groups, is not isolated. /// The cover is white, so a reader that takes every soft mask as a luminosity mask shows the same. pub(super) fn constant_mask(s: &mut Surface, page: Rect, alpha: f32) -> krilla::mask::Mask { + let transform = s.group_transform(); let mut sb = s.stream_builder(); - let mut ms = sb.surface(); + let mut ms = Surface::stream(sb.surface()); + ms.push_transform(&transform); cover(&mut ms, page, 255, alpha); + ms.pop(); ms.finish(); krilla::mask::Mask::new(sb.finish(), krilla::mask::MaskType::Alpha) } diff --git a/crates/pdf/src/export/nodes.rs b/crates/pdf/src/export/nodes.rs index dbf4b0d87..2d9a9cc17 100644 --- a/crates/pdf/src/export/nodes.rs +++ b/crates/pdf/src/export/nodes.rs @@ -22,8 +22,10 @@ impl Exporter<'_> { /// Opacity mask → luminosity soft mask. Outside the art the backdrop is black (clip) or white; /// invert is a white Difference rect on top (luminance is linear, so luma(1 − c) = 1 − luma(c)). fn soft_mask(&mut self, s: &mut Surface, m: &vectorcraft_doc::OpacityMask, page: Rect) -> Option { + let transform = s.group_transform(); let mut sb = s.stream_builder(); - let mut ms = sb.surface(); + let mut ms = Surface::stream(sb.surface()); + ms.push_transform(&transform); // An opaque backdrop when it matters: white outside the art (no clip), or black for the // inverting Difference pass below to turn white. if !m.clip || m.invert { @@ -39,6 +41,8 @@ impl Exporter<'_> { self.image_left_out = image_left_out.map(|outer| outer || incomplete); (self.knockout, self.overprint) = (knockout, overprint); if incomplete { + ms.pop(); + ms.finish(); return None; } if m.invert { @@ -46,6 +50,7 @@ impl Exporter<'_> { cover(&mut ms, page, 255, 1.0); ms.pop(); } + ms.pop(); ms.finish(); Some(krilla::mask::Mask::new(sb.finish(), krilla::mask::MaskType::Luminosity)) } @@ -96,21 +101,27 @@ impl Exporter<'_> { s.opacity = 1.0; s }; + let transform = s.group_transform(); let mut sb = s.stream_builder(); - let mut ms = sb.surface(); + let mut ms = Surface::stream(sb.surface()); + ms.push_transform(&transform); cover(&mut ms, page, 255, 1.0); let alpha = { + let transform = ms.group_transform(); let mut ab = ms.stream_builder(); - let mut als = ab.surface(); + let mut als = Surface::stream(ab.surface()); + als.push_transform(&transform); let overprint = std::mem::take(&mut self.overprint); self.measured_node(&mut als, &shape, page, false, Some(measured)); self.overprint = overprint; + als.pop(); als.finish(); krilla::mask::Mask::new(ab.finish(), krilla::mask::MaskType::Alpha) }; ms.push_mask(alpha); cover(&mut ms, page, 0, 1.0); ms.pop(); + ms.pop(); ms.finish(); krilla::mask::Mask::new(sb.finish(), krilla::mask::MaskType::Luminosity) } diff --git a/crates/pdf/src/export/paint.rs b/crates/pdf/src/export/paint.rs index 17a3a10bf..bb0a660f0 100644 --- a/crates/pdf/src/export/paint.rs +++ b/crates/pdf/src/export/paint.rs @@ -351,7 +351,7 @@ impl Exporter<'_> { let cell = Rect::new(0.0, 0.0, pw, ph); let tiles = def.instances_in(Affine::IDENTITY, cell); let mut sb = s.stream_builder(); - let mut cs = sb.surface(); + let mut cs = Surface::stream(sb.surface()); let reaches_out = tiles.iter().any(|t| t.visual_bounds().is_some_and(|b| cell.intersect(b) != b)); let clip = reaches_out.then(|| to_path(&cs, &cell.to_path(0.1))).flatten(); if let Some(c) = &clip { diff --git a/crates/pdf/src/export/writer.rs b/crates/pdf/src/export/writer.rs index d7e652f2e..027025ad2 100644 --- a/crates/pdf/src/export/writer.rs +++ b/crates/pdf/src/export/writer.rs @@ -2,7 +2,7 @@ use super::entry::RgbInCmyk; use super::exporter::{constant_mask, cover, to_path, writable, xf}; use super::{ Affine, Arc, Archival, ColorOut, Compatibility, Document, Exporter, HashSet, Image, Metadata, Node, PageSettings, PdfError, PdfVersion, Rect, - Shape, Size, Standard, Vec2, find, krilla, rfind, write, + Shape, Size, Standard, Surface, Vec2, find, krilla, rfind, write, }; /// A PDF being written, shared by export and print: the file's configuration and metadata, then @@ -125,7 +125,7 @@ impl Writer { let size = Size::from_wh(w.max(least) as f32, h.max(least) as f32).ok_or_else(|| PdfError::BadSetting(format!("page size {w} × {h}")))?; let at = |b: Rect| krilla::geom::Rect::from_ltrb(b.x0 as f32, b.y0 as f32, b.x1 as f32, b.y1 as f32); let mut page = self.pdf.document_mut().start_page_with(PageSettings::new(size).with_trim_box(at(sheet.trim)).with_bleed_box(at(sheet.bleed))); - let mut s = page.surface(); + let mut s = Surface::page(page.surface(), size.height()); let paper = Rect::new(0.0, 0.0, w, h); if sheet.negative { cover(&mut s, paper, 255, 1.0); diff --git a/crates/pdf/src/forms.rs b/crates/pdf/src/forms.rs index 4a1d06147..b771e88b7 100644 --- a/crates/pdf/src/forms.rs +++ b/crates/pdf/src/forms.rs @@ -13,7 +13,6 @@ use std::io::Read; use craft_pdf::write::backend as krilla; use krilla::geom::PathBuilder; use krilla::graphic::Graphic; -use krilla::surface::Surface; use std::sync::Arc; use vectorcraft_doc::{Document, Node, NodeKind}; @@ -21,6 +20,7 @@ use vectorcraft_doc::{Document, Node, NodeKind}; use crate::encrypt::{Lexer, Obj, StreamSpans, Tok, int, stream_spans}; use crate::output::{deflate, text_string}; use crate::patch::{Patch, Xref}; +use crate::surface::{Surface, affine}; use crate::{PdfError, PdfSettings, Standard}; /// What a marked form is. @@ -87,6 +87,9 @@ pub(crate) fn form(s: &mut Surface, mark: Mark, draw: impl FnOnce(&mut Surface)) /// form's drawing space), rounded outwards to whole units: [`finish`] gives the form a box that /// covers the paper and the art. pub(crate) fn layer_form(s: &mut Surface, i: usize, paper: vectorcraft_geom::Rect, draw: impl FnOnce(&mut Surface)) { + // The art and its box move into page space together; keeping the old local paper here + // would clip layers on translated artboards or give the importer a spurious clipping group. + let paper = affine(s.group_transform()).transform_rect_bbox(paper); let corner = |x: f64, y: f64| (x as f32, y as f32); marked(s, Mark::Layer(i), [corner(paper.x0.floor(), paper.y0.floor()), corner(paper.x1.ceil(), paper.y1.ceil())], draw); } @@ -94,8 +97,9 @@ pub(crate) fn layer_form(s: &mut Surface, i: usize, paper: vectorcraft_geom::Rec /// Draw `draw` as a form of its own, marked `mark`: an empty clip from the mark's first point /// through `rest`. fn marked(s: &mut Surface, mark: Mark, rest: [(f32, f32); 2], draw: impl FnOnce(&mut Surface)) { + let transform = s.group_transform(); let mut sb = s.stream_builder(); - let mut fs = sb.surface(); + let mut fs = Surface::stream(sb.surface()); let (x, y) = mark.at(); let mut pb = PathBuilder::new(); pb.move_to(x, y); @@ -108,10 +112,14 @@ fn marked(s: &mut Surface, mark: Mark, rest: [(f32, f32); 2], draw: impl FnOnce( fs.push_clip_path(&p, &krilla::paint::FillRule::NonZero); fs.pop(); } + fs.push_transform(&transform); draw(&mut fs); + fs.pop(); fs.finish(); let stream = sb.finish(); + s.push_transform(&transform.invert().unwrap_or_default()); s.draw_graphic(Graphic::new(stream, false)); + s.pop(); } // ---------- finishing the written file ---------- diff --git a/crates/pdf/src/lib.rs b/crates/pdf/src/lib.rs index ca7e44ae7..5c9701e7e 100644 --- a/crates/pdf/src/lib.rs +++ b/crates/pdf/src/lib.rs @@ -70,6 +70,7 @@ mod presets; mod print; mod settings; mod streams; +mod surface; mod syntax; pub use editing::{EDITING_FILE, Editing, LEGACY_EDITING_FILE, editing, editing_with}; diff --git a/crates/pdf/src/surface.rs b/crates/pdf/src/surface.rs new file mode 100644 index 000000000..4229781cd --- /dev/null +++ b/crates/pdf/src/surface.rs @@ -0,0 +1,140 @@ +//! Keep form XObjects in page space. Quartz resolves shading patterns inside a form against the +//! page's initial space, whereas other readers also apply the transform at the form's invocation. +//! Moving that transform inside the form makes both interpretations agree (including gradients +//! in opacity groups, masks, layers and overprinting art), without rasterizing the art. +//! +//! krilla's page surface has an implicit y flip which `ctm()` does not include. Remember that +//! root separately; a form's surface has an identity root. A group still counts as one push/pop +//! for the drawing code, even though moving its transform takes three krilla pushes. + +use std::ops::{Deref, DerefMut}; + +use craft_pdf::write::backend as krilla; +use krilla::blend::BlendMode; +use krilla::geom::{Path, Transform}; +use krilla::mask::Mask; +use krilla::num::NormalizedF32; +use krilla::paint::FillRule; +use vectorcraft_geom::Affine; + +pub(crate) fn xf(a: Affine) -> Transform { + let c = a.as_coeffs(); + Transform::from_row(c[0] as f32, c[1] as f32, c[2] as f32, c[3] as f32, c[4] as f32, c[5] as f32) +} + +pub(crate) fn affine(t: Transform) -> Affine { + Affine::new([t.sx(), t.ky(), t.kx(), t.sy(), t.tx(), t.ty()].map(f64::from)) +} + +struct Push { + count: usize, + root: Affine, +} + +/// A drawing surface whose form groups keep gradients and masks in the same page space. +pub(crate) struct Surface<'a> { + inner: krilla::surface::Surface<'a>, + root: Affine, + pushes: Vec, +} + +impl<'a> Surface<'a> { + /// Wrap a page while recording the writer's implicit Y flip outside its reported CTM. + pub fn page(inner: krilla::surface::Surface<'a>, height: f32) -> Self { + Self { inner, root: Affine::new([1.0, 0.0, 0.0, -1.0, 0.0, height.into()]), pushes: vec![] } + } + + /// Wrap a form or pattern stream, which starts without a page's implicit Y flip. + pub fn stream(inner: krilla::surface::Surface<'a>) -> Self { + Self { inner, root: Affine::IDENTITY, pushes: vec![] } + } + + /// Current drawing space → page space, for both a form's art and its mask. A singular + /// enclosing transform draws no area: keep that form local rather than trying to invert it. + pub fn group_transform(&self) -> Transform { + let t = xf(self.root * affine(self.inner.ctm())); + if t.invert().is_some() { t } else { Transform::identity() } + } + + /// Move the enclosing transform inside a form and record its three pushes as one group. + fn group(&mut self, push: impl FnOnce(&mut krilla::surface::Surface)) { + let t = self.group_transform(); + self.inner.push_transform(&t.invert().unwrap_or_default()); + push(&mut self.inner); + self.inner.push_transform(&t); + self.pushes.push(Push { count: 3, root: self.root }); + self.root = Affine::IDENTITY; + } + + /// Apply opacity in page space when it creates a form; full opacity is a plain push. + pub fn push_opacity(&mut self, opacity: NormalizedF32) { + if opacity == NormalizedF32::ONE { + // krilla does not create a form for full opacity. + self.inner.push_opacity(opacity); + self.plain_push(); + } else { + self.group(|s| s.push_opacity(opacity)); + } + } + + pub fn push_isolated(&mut self) { + self.group(|s| s.push_isolated()); + } + + /// The mask's stream is already in [`Self::group_transform`]'s space, like the group's art. + pub fn push_mask(&mut self, mask: Mask) { + self.group(|s| s.push_mask(mask)); + } + + fn plain_push(&mut self) { + self.pushes.push(Push { count: 1, root: self.root }); + } + + pub fn push_transform(&mut self, transform: &Transform) { + self.inner.push_transform(transform); + self.plain_push(); + } + + pub fn push_clip_path(&mut self, path: &Path, rule: &FillRule) { + self.inner.push_clip_path(path, rule); + self.plain_push(); + } + + pub fn push_blend_mode(&mut self, mode: BlendMode) { + self.inner.push_blend_mode(mode); + self.plain_push(); + } + + /// Restore the enclosing coordinate space, undoing every writer push for the last group. + /// An unmatched pop is logged rather than forwarded to the writer's stack. + pub fn pop(&mut self) { + let Some(push) = self.pushes.pop() else { + log::error!("PDF surface pop without a matching push"); + return; + }; + for _ in 0..push.count { + self.inner.pop(); + } + self.root = push.root; + } + + pub fn finish(self) { + self.inner.finish(); + } +} + +// Drawing operations use krilla directly; every push/pop above keeps our coordinate state in +// step. Stream builders are wrapped with `Surface::stream` when their surfaces are created. +impl<'a> Deref for Surface<'a> { + type Target = krilla::surface::Surface<'a>; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl DerefMut for Surface<'_> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} diff --git a/crates/pdf/tests/gradient_forms.rs b/crates/pdf/tests/gradient_forms.rs new file mode 100644 index 000000000..9f06c9b84 --- /dev/null +++ b/crates/pdf/tests/gradient_forms.rs @@ -0,0 +1,215 @@ +//! Gradients in forms must follow the art, including on pages with a nonzero origin. Quartz +//! exposed a failure the import/render round trip did not: it resolved a form's shading pattern +//! in page space, leaving off-centre gradients painted entirely in their last stop's colour. +//! These fixtures are original, generated geometry; no user artwork or external files are used. +#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] + +use std::sync::Arc; + +use vectorcraft_color::{BlendMode, Color, Gradient, GradientGeom, GradientKind, GradientPaint, GradientStop, Paint}; +use vectorcraft_doc::{Appearance, ColorMode, Document, Knockout, Node, OpacityMask}; +use vectorcraft_geom::{Affine, Point, Rect, shapes}; +use vectorcraft_pdf::{PdfOptions, export, import}; +use vectorcraft_testkit::raster::{assert_similar, render_artboard}; + +#[derive(Clone, Copy, Debug)] +enum Form { + Page, + Opacity, + Isolated, + Nested, + PassThrough, + Mask, + Knockout, + Layer, + Overprint, +} + +const FORMS: [Form; 9] = + [Form::Page, Form::Opacity, Form::Isolated, Form::Nested, Form::PassThrough, Form::Mask, Form::Knockout, Form::Layer, Form::Overprint]; + +fn rect(d: &mut Document, bounds: Rect, paint: Paint) -> Node { + Node::path(d.alloc_id(), shapes::rectangle(bounds), Appearance::basic(paint, Paint::None, 0.0)) +} + +fn group(d: &mut Document, n: Node) -> Node { + Node::group(d.alloc_id(), vec![Arc::new(n)]) +} + +/// Generate off-centre gradient art in each form type, moving the art and artboard together. +fn fixture(form: Form, kind: GradientKind, mode: ColorMode, origin: Point) -> (Document, PdfOptions) { + let mut d = Document::new_with_mode(120.0, 180.0, mode); + let color = |k| if mode == ColorMode::Cmyk { Color::cmyk(0.0, 0.0, 0.0, k) } else { Color::gray(k) }; + let mut gradient = GradientPaint::new(Gradient { + kind, + stops: vec![GradientStop::new(0.0, color(1.0)), GradientStop::new(0.6, color(0.4)), GradientStop::new(1.0, color(0.0))], + ..Default::default() + }); + let (start, end) = match kind { + GradientKind::Radial => (Point::new(32.0, 35.0), Point::new(60.0, 35.0)), + _ => (Point::new(10.0, 10.0), Point::new(70.0, 70.0)), + }; + gradient.geom = Some(GradientGeom { start, end, aspect: 0.8, focal: None }); + let mut n = rect(&mut d, Rect::new(10.0, 10.0, 70.0, 70.0), Paint::Gradient(Box::new(gradient))); + let mut opts = PdfOptions::default(); + match form { + Form::Page => d.page_isolate = true, + Form::Opacity => n.opacity = 0.6, + Form::Isolated => { + n = group(&mut d, n); + n.isolate = true; + } + Form::Nested => { + n.opacity = 0.75; + n = group(&mut d, n); + n.opacity = 0.8; + } + Form::PassThrough => { + n.blend = BlendMode::Multiply; + n = group(&mut d, n); + n.opacity = 0.6; + } + Form::Mask => { + let mask = rect(&mut d, Rect::new(10.0, 10.0, 44.0, 70.0), Paint::solid(Color::WHITE)); + n.extras_mut().mask = Some(Box::new(OpacityMask::new(mask, true))); + } + Form::Knockout => { + let top = rect(&mut d, Rect::new(50.0, 50.0, 80.0, 80.0), Paint::solid(Color::WHITE)); + n = Node::group(d.alloc_id(), vec![Arc::new(n), Arc::new(top)]); + n.knockout = Knockout::On; + } + Form::Layer => opts.settings.create_layers = true, + Form::Overprint => n.appearance.fill_mut().unwrap().overprint = true, + } + n.transform(Affine::translate(origin.to_vec2()), false); + d.artboards[0].rect = d.artboards[0].rect + origin.to_vec2(); + let layer = d.default_layer().unwrap(); + d.insert(Some(layer), 0, n).unwrap(); + (d, opts) +} + +/// Round trips preserve gradient placement for the form types the importer supports. +#[test] +fn gradients_in_forms_keep_their_position_when_read_back() { + // The importer has a pre-existing mismatch on the knockout's synthesized nested masks; + // Quartz below checks its actual PDF output, along with the other forms. + for form in FORMS.into_iter().filter(|f| !matches!(f, Form::Knockout)) { + for kind in [GradientKind::Linear, GradientKind::Radial] { + for origin in [Point::ORIGIN, Point::new(125.0, -80.0)] { + let (d, opts) = fixture(form, kind, ColorMode::Rgb, origin); + let back = import(&export(&d, &opts).unwrap()).unwrap(); + assert_similar(&render_artboard(&back), &render_artboard(&d), 5.0, 0.01); + } + } + } +} + +/// Layer form bounds stay page-sized after translation, without extra imported clipping groups. +#[test] +fn translated_layer_forms_cover_the_paper_in_their_new_space() { + // The layer-box repair added upstream must move with the gradient fix's form transform. + // A local-space paper box would otherwise introduce a clipping group on import. + for origin in [Point::ORIGIN, Point::new(125.0, -80.0), Point::new(-250.0, 450.0)] { + let (d, mut opts) = fixture(Form::Layer, GradientKind::Radial, ColorMode::Rgb, origin); + opts.settings.compression.compress_text = false; + let bytes = export(&d, &opts).unwrap(); + let text = String::from_utf8_lossy(&bytes); + let boxes: Vec> = text + .split("/Subtype/Form") + .skip(1) + .map(|form| { + form.split("/BBox[").nth(1).unwrap().split(']').next().unwrap().split_ascii_whitespace().map(|v| v.parse().unwrap()).collect() + }) + .collect(); + assert_eq!(boxes, [vec![0.0, 0.0, 120.0, 180.0]], "{origin:?}: the layer covers exactly the page"); + let back = import(&bytes).unwrap(); + let children = back.layers[0].children().unwrap(); + assert_eq!(children.len(), 1); + assert!(children[0].path_data().is_some(), "{origin:?}: no clipping group wraps the layer's art"); + } +} + +// Test the renderer that exhibited the fault, not just our own importer (which accepted the +// old files). OS bindings are confined to this integration-test module. +#[cfg(target_os = "macos")] +#[allow(unsafe_code)] +mod quartz { + use objc2_core_foundation::CFData; + use objc2_core_graphics::{CGBitmapContextCreate, CGColorSpace, CGContext, CGDataProvider, CGImageAlphaInfo, CGPDFDocument}; + use vectorcraft_testkit::raster::Image; + + use super::*; + + /// Render the first PDF page through Quartz as an oracle independent of our importer. + fn render(bytes: &[u8]) -> Image { + let data = CFData::from_bytes(bytes); + let provider = CGDataProvider::with_cf_data(Some(&data)).unwrap(); + let doc = CGPDFDocument::with_provider(Some(&provider)).unwrap(); + let page = CGPDFDocument::page(Some(&doc), 1).unwrap(); + let space = CGColorSpace::new_device_rgb().unwrap(); + let (width, height) = (120, 180); + let mut rgba = vec![255u8; width * height * 4]; + // SAFETY: the buffer holds every RGBA row, stays allocated and is not accessed until the + // context is dropped. All inputs are the small, generated fixtures above. + let context = unsafe { + CGBitmapContextCreate(rgba.as_mut_ptr().cast(), width, height, 8, width * 4, Some(&space), CGImageAlphaInfo::PremultipliedLast.0) + } + .unwrap(); + CGContext::draw_pdf_page(Some(&context), Some(&page)); + drop(context); + Image { width: width as u32, height: height as u32, rgba } + } + + /// Quartz places RGB and CMYK radial centres, edges and masks with their translated art. + #[test] + fn quartz_keeps_off_centre_radials_in_transparency_and_layer_forms() { + for mode in [ColorMode::Rgb, ColorMode::Cmyk] { + for form in FORMS { + for origin in [Point::ORIGIN, Point::new(125.0, -80.0)] { + let (d, opts) = fixture(form, GradientKind::Radial, mode, origin); + let img = render(&export(&d, &opts).unwrap()); + let centre = img.over_white(32, 35)[0]; + let edge = img.over_white(32, 55)[0]; + assert!(centre < 150 && edge.saturating_sub(centre) > 40, "{mode:?} {form:?} {origin:?}: centre {centre}, edge {edge}"); + assert_eq!(img.over_white(90, 100), [255; 3], "art stays in its bounds"); + if matches!(form, Form::Mask) { + assert_eq!(img.over_white(46, 35), [255; 3], "the mask moves with the gradient"); + } + } + } + } + } + + /// Quartz retains the linear ramp's direction inside forms on a translated artboard. + #[test] + fn quartz_keeps_linear_gradient_directions_in_forms() { + for form in FORMS.into_iter().filter(|f| !matches!(f, Form::Mask | Form::Knockout)) { + let (d, opts) = fixture(form, GradientKind::Linear, ColorMode::Rgb, Point::new(-30.0, 45.0)); + let img = render(&export(&d, &opts).unwrap()); + let (start, end) = (img.over_white(20, 20)[0], img.over_white(60, 60)[0]); + assert!(end.saturating_sub(start) > 60, "{form:?}: start {start}, end {end}"); + } + } + + /// A gradient's stop-opacity mask remains aligned with its colour through nested forms. + #[test] + fn quartz_keeps_gradient_stop_opacity_aligned_inside_nested_forms() { + let (mut d, opts) = fixture(Form::Nested, GradientKind::Radial, ColorMode::Rgb, Point::ORIGIN); + let mut leaves = vec![]; + d.walk(|n| { + if n.path_data().is_some() { + leaves.push(n.id); + } + }); + for id in leaves { + let fill = d.node_mut(id).unwrap().appearance.fill_mut().unwrap(); + let Paint::Gradient(g) = &mut fill.paint else { panic!("gradient fixture") }; + for stop in &mut g.gradient.stops { + stop.opacity = 0.6 * (1.0 - stop.offset); + } + } + let img = render(&export(&d, &opts).unwrap()); + let (centre, edge) = (img.over_white(32, 35)[0], img.over_white(32, 55)[0]); + assert!(centre > 145 && centre < 205 && edge > 240, "centre {centre}, edge {edge}"); + } +} diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index 9b86156bf..136722fab 100644 --- a/docs/file-format-parity.md +++ b/docs/file-format-parity.md @@ -308,6 +308,15 @@ Names in the editing data that aren't UTF-8 (layers, objects, artboards, colors, | Affinity `.af`, `.afdesign`, `.afpub` | no | yes (beyond Illustrator) | no (#665) | 51 pinned files, [affinity-validation.md](affinity-validation.md) | | Clipboard: PDF, SVG, PNG, EMF, text | yes | yes | yes | copied files paste as art; cells copied from a spreadsheet (tab-separated text) paste as a table of point type over gridlines, at most 1000 rows, 256 columns and 20,000 cells (#5296) | +## PDF form gradients + +PDF export's linear and radial gradients stay aligned inside transparency groups, opacity masks, +PDF layers and overprinting art. The original synthetic tests in +`crates/pdf/tests/gradient_forms.rs` check the output with macOS Quartz and an import/render +round trip. The three Quartz checks fail on main `65adb7b21` and pass with the form-space fix; +translated layer bounds also have a regression for the current writer's layer-box repair. +Stop opacity on translated artboards remains outside this regression coverage. + ## PDF editable imports - [x] Place from bytes or unlinked paths, and PDF clipboard paste, keep the selected page's live text. Synthetic two-page regressions in `crates/engine/src/cmd/fileio/tests_pdffidelity.rs` cover both sources, page selection and explicit outlines. The single-page importer honors the same text choice as Open; existing PDF fallback rules are unchanged. diff --git a/docs/gaps.md b/docs/gaps.md index 467946e93..7fcf7586e 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -5,6 +5,13 @@ ## Feature submission evidence **Feature evidence last updated:** 2026-11-02. + +**PDF form gradients — 2026-11-08:** original synthetic Quartz checks reproduce the placement, direction and nested +stop-opacity failures on main `65adb7b21` and pass with the rebased fix. The import/render round trip +also passes; a translated-layer regression covers the interaction with the upstream layer-box +repair (`crates/pdf/tests/gradient_forms.rs`). Stop opacity on translated artboards remains outside +this coverage. No ranked gap or estimate is closed by this narrow export fix. + | 2026-11-06 | minor | Opening a file that takes longer than 0.4 s shows a progress window: the file name, a moving bar (reading reports no steps), how many more files wait, Cancel (Esc; the file doesn't open) and Continue in Background (`file.cancelOpen`, `file.openInBackground`); it never opens over another dialog or progress window (part of #5930); scores and estimates unchanged | Raster rendering fits and preflattens curved stroke outlines more tightly only below four times the raster tolerance, retaining thin-band coverage without changing stroke weight or vector export geometry. Current cached character/object outlines, cropped long strokes, gradient clips and effect rasters share the helper; cropped-region reuse and guarded flattening remain. The unchanged `fractional_curved_strokes_retain_coverage_between_notches` regression covers fractional weights, rotation, phase, opacity and f32-rounded endpoints. This is a quality/cost tradeoff, not a speed claim. Scores and estimates unchanged. diff --git a/docs/roadmap.md b/docs/roadmap.md index a5aaadc66..7bb2321e7 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -195,6 +195,7 @@ separate. Scores and hours unchanged. [Behavior and command details](development | 2026-11-08 | minor | M7: spell check engine, `text.spelling.check` and `text.spelling.suggest` on craft-proof (#4936); the dialog, Auto Spell Check and the custom dictionary remain; scores and estimates unchanged. | | 2026-11-08 | minor | Floating tab groups collapse into the side icon rail and opened rail panels fold back into it (#5925, in part); tab groups remain intact. Scores and estimates unchanged. | | 2026-11-09 | minor | Numeric fields step on wheel hover without taking focus; keyboard focus and existing rounding rules remain (#5949). Scores and estimates unchanged. | +| 2026-11-08 | minor | M4: PDF form-gradient fix follows the exporter module split and preserves page-sized layer bounds on translated artboards; Quartz and round-trip checks pass. Estimates unchanged. | | 2026-11-07 | minor | M2: with the Pen or Curvature tool, Edit › Copy and Cut take the anchors and segments a Cmd/Ctrl-lent Direct Selection picked, not the whole path (#6123); estimates unchanged | | 2026-11-07 | minor | Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged. | | 2026-11-06 | minor | M2: a shape just drawn moves by its centre point (Show Center) and turns from the rotate zone just outside a bounding-box corner with its tool still active, as the Selection tool's gestures do (#5477); estimates unchanged. | @@ -210,6 +211,7 @@ separate. Scores and hours unchanged. [Behavior and command details](development | 2026-11-03 | minor | M7: retain composition trimming in rotated character placement and rotation metadata; estimates unchanged. | | 2026-10-29 | minor | M6: Pathfinder: live compound shapes (#4522): the panel menu's Make, Release and Expand Compound Shape, Alt-clicking a Shape Mode, a selected member's Shape Mode and the Expand button act; scores unchanged | | 2026-10-26 | minor | M4/G9: EPS Save and Save As: an opened EPS saves back to its own path as EPS, keeping the chosen artboard's page frame and transparent margins and the complete editable VectorCraft document; Save's TIFF preview is capped at 1024 px (Export keeps 2048); partly read, preview-only or damaged-editing-data sources still need confirmation before they are overwritten; it does not write Illustrator's own editing data (#868), and G9's real-file import fidelity is unchanged; scores and estimates unchanged | +| 2026-10-26 | minor | M4: PDF form-gradient placement checked with Quartz on the shared writer; estimates unchanged. | | 2026-10-25 | minor | M10/G1: a selected Grid Repeat shows handles: the one below the grid adds or removes rows, the one right of it columns, the one past its bottom-right corner both, and the sliders between the first two columns and rows set the horizontal and vertical spacing; each drag is one undo step and Escape cancels it (#2691); estimates unchanged | | 2026-10-24 | minor | Docs catch-up: Current focus's three G11 lines merged into one (#864 and #885 remain; #937, #983/#1037 and #834 fixed) and its two stale G2 lines (filters left, the Effect Gallery to build) removed; G2 stays the gallery's thumbnails and in-dialog preview, 1–2 h. Estimates unchanged. | | 2026-10-24 | minor | M10/G5: brush libraries (Window › Brush Libraries, the Brushes panel's Brush Libraries Menu, four generated libraries, Save Brushes, Other Library…); estimates unchanged | diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index cdffff919..af5c9f2f3 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -226,7 +226,7 @@ dominate; 3D, graphs and automation are specialist). They sum to 105. Feature de | Image Trace, graphs, image tools | 3 | 75% | graph Column designs, Image Trace Create Strokes depth (Create Object Mosaic is done; junction-returning piece survival has source-generated regressions in `image-trace-quality.md`; large images trace with a warning; Auto Color keeps rare distinct colours) | 5–8 | estimated (raised from 72%); quick presets reveal adjustment controls, Properties reopens them | | Layers, artboards, document setup | 5 | 78% | Selected-artboard deletion is consistent across panel actions (all-selected retains artboard 1); history prunes removed indices while preserving valid multiselection; remaining: 30.0's artboard locking and colours and managing artboards from the canvas, Layers panel row context menus and Collect for Export (click-to-fit objects is done, #1227) | 5–8 | estimated (lowered from 82%: target moved); Artboard deletion keys keep artwork and the first of an all-selected set; Artboard Options' Show Center Mark and Show Cross Hairs draw on the canvas only (exports and print leave them out), regression-tested; Artboard Options' Show Video Safe Areas and Video Ruler Pixel Aspect Ratio (per artboard, native format) are done ([video display options](#evidence-what-ships-today-by-area)), score unchanged | | View & navigation | 3 | 70% | multiple windows and the rest of Arrange (Cascade, Float in Window, Float All in Windows) | 9–14 | estimated; Window › Arrange › Tile and Consolidate All Windows done (score/hours unchanged); document-tab artwork hover/copy and lossless transfer tested (2026-10-22; score/hours unchanged); cached canvas reprojection preserves scale during resize, rotation and DPI changes (regression and contributor original-file evidence below; scores/hours unchanged); cached canvas dimensions match the renderer limit before cache-key creation, view construction and worker submission (regression evidence below) | -| File formats | 6 | 75% | `.ai` files open with a flattened or wrong layer structure in real cases (#951), no writing of Illustrator's editing data (#868), FreeHand/CorelDRAW/DWG/HEIC import, 3D export; see [file-format-parity.md](file-format-parity.md); EPS legacy mesh/opacity-mask and raster comparison follow-up verified | 10–20 | estimated (lowered from 85%: open issues); DXF control-point SPLINE import closure regression-tested (#1299 follow-up after #1320); PDF writer metadata, reader streaming and immutable backend checks in `crates/pdf/src/tests.rs`, `crates/pdf/src/tests/reader_streaming.rs` and `xtask/src/pdf_deps.rs`; PDF Place and clipboard paste retain live text (synthetic regressions); EPS ordinary Save retains the page frame and editable document (2026-10-26); `.ai`/EPS editing data checked against a user's files (2026-10-30): case setting (`ate::tests`, `tests_native::type_from_the_text_document_keeps_its_case_setting`), placed-file placements (`tests_native::a_small_placed_file_goes_where_it_draws_closer_to_the_page`), pages cropped to the art (`tests_native::type_on_the_copies_of_a_page_cropped_to_the_art_is_read_once`), mesh corner weights and state (`ai::mesh::tests::corners_with_handle_weights_of_zero_are_read`, `ai::tests::a_gradient_mesh_takes_the_transparency_written_after_it`), type on a path spacing (`tests_native::type_round_a_circle_is_spaced_as_the_file_lays_it_out`; the distance from the path under Align to Path is measured from the top of each character's em box, half the height of the top of its H or the bottom of its font's bounding box (`tests_native::aligned_type_lies_as_far_from_its_path_as_the_file_lays_it_out`), and the fit under Gravity does not reproduce the file's layout), blend state (`ai::tests::a_blends_transparency_follows_its_record`); scores and estimates unchanged. | +| File formats | 6 | 75% | `.ai` files open with a flattened or wrong layer structure in real cases (#951), no writing of Illustrator's editing data (#868), FreeHand/CorelDRAW/DWG/HEIC import, 3D export; see [file-format-parity.md](file-format-parity.md); EPS legacy mesh/opacity-mask and raster comparison follow-up verified | 10–20 | estimated (lowered from 85%: open issues); DXF control-point SPLINE import closure regression-tested (#1299 follow-up after #1320); PDF writer metadata, reader streaming and immutable backend checks in `crates/pdf/src/tests.rs`, `crates/pdf/src/tests/reader_streaming.rs` and `xtask/src/pdf_deps.rs`; PDF Place and clipboard paste retain live text (synthetic regressions); PDF form-gradient placement and direction independently checked with macOS Quartz (`crates/pdf/tests/gradient_forms.rs`); EPS ordinary Save retains the page frame and editable document (2026-10-26); `.ai`/EPS editing data checked against a user's files (2026-10-30): case setting (`ate::tests`, `tests_native::type_from_the_text_document_keeps_its_case_setting`), placed-file placements (`tests_native::a_small_placed_file_goes_where_it_draws_closer_to_the_page`), pages cropped to the art (`tests_native::type_on_the_copies_of_a_page_cropped_to_the_art_is_read_once`), mesh corner weights and state (`ai::mesh::tests::corners_with_handle_weights_of_zero_are_read`, `ai::tests::a_gradient_mesh_takes_the_transparency_written_after_it`), type on a path spacing (`tests_native::type_round_a_circle_is_spaced_as_the_file_lays_it_out`; the distance from the path under Align to Path is measured from the top of each character's em box, half the height of the top of its H or the bottom of its font's bounding box (`tests_native::aligned_type_lies_as_far_from_its_path_as_the_file_lays_it_out`), and the fit under Gravity does not reproduce the file's layout), blend state (`ai::tests::a_blends_transparency_follows_its_record`); scores and estimates unchanged. | | File formats | 6 | 75% | `.ai` files open with a flattened or wrong layer structure in real cases (#951), a saved `.ai`'s editing copy without live effects on objects other than paths, compound paths and groups that qualify (at most a fill and a stroke, a non-zero fill rule when filled, no large canvas) and whose appearance and effects the art-style writer supports, and with live objects as their art (#868), FreeHand/CorelDRAW/DWG/HEIC import, textures, map art and shading in 3D export; see [file-format-parity.md](file-format-parity.md); EPS legacy mesh/opacity-mask and raster comparison follow-up verified | 10–20 | estimated (lowered from 85%: open issues); DXF control-point SPLINE import closure regression-tested (#1299 follow-up after #1320); PDF writer metadata, reader streaming and immutable backend checks in `crates/pdf/src/tests.rs`, `crates/pdf/src/tests/reader_streaming.rs` and `xtask/src/pdf_deps.rs`; PDF Place and clipboard paste retain live text (synthetic regressions); EPS ordinary Save retains the page frame and editable document (2026-10-26); `.ai`/EPS editing data checked against a user's files (2026-10-30): case setting (`ate::tests`, `tests_native::type_from_the_text_document_keeps_its_case_setting`), placed-file placements (`tests_native::a_small_placed_file_goes_where_it_draws_closer_to_the_page`), pages cropped to the art (`tests_native::type_on_the_copies_of_a_page_cropped_to_the_art_is_read_once`), mesh corner weights and state (`ai::mesh::tests::corners_with_handle_weights_of_zero_are_read`, `ai::tests::a_gradient_mesh_takes_the_transparency_written_after_it`), type on a path spacing (`tests_native::type_round_a_circle_is_spaced_as_the_file_lays_it_out`), blend state (`ai::tests::a_blends_transparency_follows_its_record`); scores and estimates unchanged. | | File formats | 6 | 75% | `.ai` files open with a flattened or wrong layer structure in real cases (#951), a saved `.ai`'s editing copy without live effects on objects other than paths, compound paths and groups that qualify (at most a fill and a stroke, a non-zero fill rule when filled, no large canvas) and whose appearance and effects the art-style writer supports, and with live objects as their art (#868), FreeHand/CorelDRAW/DWG/HEIC import, 3D export; see [file-format-parity.md](file-format-parity.md); EPS legacy mesh/opacity-mask and raster comparison follow-up verified | 10–20 | estimated (lowered from 85%: open issues); DXF control-point SPLINE import closure regression-tested (#1299 follow-up after #1320); PDF writer metadata, reader streaming and immutable backend checks in `crates/pdf/src/tests.rs`, `crates/pdf/src/tests/reader_streaming.rs` and `xtask/src/pdf_deps.rs`; PDF Place and clipboard paste retain live text (synthetic regressions); SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; EPS ordinary Save retains the page frame and editable document (2026-10-26); `.ai`/EPS editing data checked against a user's files (2026-10-30): case setting (`ate::tests`, `tests_native::type_from_the_text_document_keeps_its_case_setting`), placed-file placements (`tests_native::a_small_placed_file_goes_where_it_draws_closer_to_the_page`), pages cropped to the art (`tests_native::type_on_the_copies_of_a_page_cropped_to_the_art_is_read_once`), mesh corner weights and state (`ai::mesh::tests::corners_with_handle_weights_of_zero_are_read`, `ai::tests::a_gradient_mesh_takes_the_transparency_written_after_it`), type on a path spacing (`tests_native::type_round_a_circle_is_spaced_as_the_file_lays_it_out`), blend state (`ai::tests::a_blends_transparency_follows_its_record`); scores and estimates unchanged. | | View & navigation | 3 | 70% | multiple windows and the rest of Arrange (Cascade, Float in Window, Float All in Windows) | 9–14 | estimated; Window › Arrange › Tile and Consolidate All Windows done (score/hours unchanged); document-tab artwork hover/copy and lossless transfer tested (2026-10-22; score/hours unchanged); cached canvas reprojection preserves scale during resize, rotation and DPI changes (regression and contributor original-file evidence below; scores/hours unchanged) | @@ -1909,6 +1909,7 @@ Edit › Copy and Cut put the selected ruler guides on the clipboard, with the s | 2026-11-07 | minor | File formats and Type core: `.ai`/EPS type from the text document lays its lines out closer to the file's page: auto leading per line at the paragraph's share, the paragraph's hyphenation options, possibly more of the last lines the file sets in an area frame of its own, and a line a soft return ends justified (#6137); evidence added; scores unchanged. | | 2026-11-06 | minor | `.ai` and EPS import: Art brushes in the older form of their objects come into the Brushes panel; evidence updated; scores unchanged. | | 2026-11-08 | minor | Panel numeric wheel turns do not seed canvas zoom glides; corrected the disabled-field scroll description (#5949). Scores and estimates unchanged. | +| 2026-11-08 | minor | File formats: rebased PDF form-gradient fix, current-main Quartz evidence and translated-layer bounds regression; score 75% and hours unchanged (estimated). | | 2026-11-07 | minor | Numeric fields step on wheel hover without taking focus; keyboard focus and existing rounding rules remain (#5949). Scores and estimates unchanged. | | 2026-11-08 | minor | Consolidated the four current Guides feature-area rows using existing global/artboard ruler mode, video ruler and status-bar evidence; remaining gaps, score and estimates unchanged. The earlier detailed missing-items table remains historical. | | 2026-11-07 | minor | Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged. | @@ -1998,6 +1999,7 @@ Edit › Copy and Cut put the selected ruler guides on the clipboard, with the s | 2026-10-29 | minor | Object › Path › Smooth… with its slider and Auto-Smooth (#4115) added to the evidence; score and hours unchanged. | | 2026-10-26 | minor | File formats: EPS Save and Save As: an opened EPS saves back to its own path as EPS, keeping the chosen artboard's page frame and transparent margins and the complete editable VectorCraft document; Save's TIFF preview is capped at 1024 px (Export keeps 2048); partly read, preview-only or damaged-editing-data sources still need confirmation before they are overwritten; evidence added ([file-format-parity.md](file-format-parity.md#eps-save-and-source-protection)); writing Illustrator's editing data (#868) and the `.ai` structure issues stay in the missing items; score and hours unchanged (estimated) | | 2026-10-26 | minor | View & navigation: New View… / Edit Views… taken off the missing items (all 25 saved views in the View menu, an Edit Views… list dialog, views checked on open); evidence added; score and hours unchanged. | +| 2026-10-26 | minor | File formats: rebased PDF form-gradient fix and independent Quartz evidence; score 75% and hours unchanged (estimated). | | 2026-10-25 | minor | Type core: a double-click on a threaded port breaks the thread there (`text.thread.break`); evidence added; scores unchanged. | | 2026-10-25 | minor | Repeat: a selected Grid Repeat shows handles: the one below the grid adds or removes rows, the one right of it columns, the one past its bottom-right corner both, and the sliders between the first two columns and rows set the horizontal and vertical spacing; each drag is one undo step and Escape cancels it (#2691); evidence added; score and hours unchanged (estimated). | | 2026-10-24 | minor | Path operations: the Shape Builder's edge-source grid holds a bounded number of cell references (coincident segments no longer quadratic in memory) and files a segment once per cell; Path operations 80%, Shape Builder 70% unchanged (estimated).; area row and evidence list updated; the budget counts an item's distinct cells, so a degenerate grid's pieces don't send it to the scanned list. | From 6063e662bc5c4e1219f76f5a65623aaef9adf22c Mon Sep 17 00:00:00 2001 From: Patrick Nadeau Date: Sun, 8 Nov 2026 18:27:00 -0500 Subject: [PATCH 04/38] M4: correct PDF gradient progress dates after review --- ROADMAP.md | 4 ++-- docs/file-format-parity.md | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index c685b9206..9250e67b6 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -138,9 +138,8 @@ Ranked; detail in [docs/roadmap.md](docs/roadmap.md) and [docs/gaps.md](docs/gap ## Progress log -- 2026-11-05: Window › Toolbars › Keep Tools Beside the Canvas, off by default, docks the Tools panel between left-side panels and the canvas instead of at the window's left edge, and the canvas's left edge then also docks a floating Tools panel (`window.toolsBesideCanvas`, #5395). Workspaces keep the setting; workspaces saved without it keep Tools at the window's edge. An optional VectorCraft layout choice; headline numbers unchanged. - 2026-11-08: PDF form-gradient placement reverified on current main: three Quartz regressions fail without the fix and pass with it. The fix follows the exporter module split and keeps upstream layer-box repairs in the form's new space, with a translated-layer regression. Headline numbers unchanged. -- 2026-10-26: PDF form-gradient placement checked with macOS Quartz on the shared writer: linear and radial gradients follow transparency groups, masks, PDF layers and overprinting art. Headline numbers unchanged. +- 2026-11-05: Window › Toolbars › Keep Tools Beside the Canvas, off by default, docks the Tools panel between left-side panels and the canvas instead of at the window's left edge, and the canvas's left edge then also docks a floating Tools panel (`window.toolsBesideCanvas`, #5395). Workspaces keep the setting; workspaces saved without it keep Tools at the window's edge. An optional VectorCraft layout choice; headline numbers unchanged. - 2026-11-05: Release Type on a Path joins the Type Tools add-on (Object › Plug-ins › Type Tools): each selected type on a path splits into point type with the same text and attributes, on one straight line starting where its first character sat, and the plain path it was set on, with its exact shape and place in the stacking order and no paint; or keeps only the path or only the text, optionally leaving the original. A dialog with Preview, one undo step (`addon.type.releasePath`, #5532). A VectorCraft addition: headline numbers unchanged. - 2026-11-04: New Document: the units chosen in Preset Details now belong to the chosen document, as the pane's options are listed for the selected preset; the chosen card shows them, every other Recent and Saved card keeps the units its document or preset was made in, and the built-in tabs still follow the chosen units, so switching between inches and millimetres no longer restyles every recent template. Scores and estimates unchanged. - 2026-11-02: Mouse wheel over a closed font-family field in Character, Properties or Control changes selected text through the family list using saved font filters (excluding search text), one undo step per notch, stopping at the ends (#5253). Scores and estimates unchanged. @@ -157,6 +156,7 @@ Ranked; detail in [docs/roadmap.md](docs/roadmap.md) and [docs/gaps.md](docs/gap - 2026-10-31: Export for Screens works as the reference app's dialog: Artboards and Assets tabs with a grid or list of thumbnails, Clear Selection and Prefix, Create Sub-folders by Scale or Format (on by default), Export PDFs As Single File (the chosen artboards as one PDF's pages, `pdfs: "single"`) or Multiple Files, iOS and Android rows, format presets in a ☰ menu, and a count of the files the export writes (#4895). About 55 community PRs and fixes landed the same day: Find Font, Replace Font, Outline Stroke and Expand leave hidden and locked art alone (with a warning); DXF export writes LINE and ELLIPSE entities and outlines only the strokes a CAD line can't draw; Blob Brush Angle and Roundness; Shift-constrained Perspective Selection; Grid Repeat Options Preview; a gradient mesh from a file budgeted at about a million quads; width-profile strokes keep their shape zoomed out; `.ai`/EPS import fidelity (area type, envelopes, transparency in dictionaries, character styles); PDF import finds hyphenated and CMap-suffixed font names; Windows renders follow the process's core affinity; Enter or Escape in a floating panel's field no longer reaches the canvas (#4803); Properties shows Blend Options for any selected blend (#4817); Artwork Cleanup finds doubled type (#4835) and joins stacked lines into one object (Join Point Text, #4826). Headline numbers unchanged. - 2026-10-31: Arrange Tools, a built-in add-on (Object › Plug-ins › Arrange Tools): Arrange in Grid lays the selected objects out in rows and columns, a row or a column with gaps, from the selection's top-left or centred on the artboard; Swap Objects swaps two objects' places (and, with Swap Objects and Sizes, their sizes); Replace with Top Object puts a copy of the topmost selected object in place of each of the others, kept, stretched or fitted in proportion; Set Size of Each gives every selected object an exact width and/or height about a reference point; Zoom to Selection fits the view to the selection. Each is one undo step with a previewed dialog where it has options. A VectorCraft addition: headline numbers unchanged. - 2026-10-30: Gradient panel presets: a Presets menu applies one of the built-in gradient libraries' presets (grouped by theme) or a saved one to the gradient behind the active proxy, keeping its placement, as one undo step; the panel menu saves the current gradient as a preset, kept with the preferences, and deletes saved ones (`gradient.presets.list/apply/save/delete`). +- 2026-10-26: PDF form-gradient placement checked with macOS Quartz on the shared writer: linear and radial gradients follow transparency groups, masks, PDF layers and overprinting art. Headline numbers unchanged. - 2026-10-23: Pathfinder › Trap: the Pathfinder panel's Trap… and Effect › Pathfinder › Trap… add a thin overprinting strip of the lighter colour inside the darker one where two solid colours meet, so misregistered plates leave no gap ([trapping](https://helpx.adobe.com/illustrator/using/trapping.html)), with Thickness, Height/Width, Tint Reduction, Traps with Process Color and Reverse Traps. Illustrator effects 49/50 (SVG Filters remains); Live vector effects hours 6–10 → 4–8; headline numbers unchanged. - 2026-10-23: OpenType SVG colour fonts draw as vector art: each glyph's SVG document is read by the SVG importer, in the text's colours where it asks for them, and draws, exports and outlines as COLR glyphs do; all colour font formats are now drawn. - 2026-10-23: COLR colour fonts (v0 and v1, as in Noto Color Emoji's vector build, Segoe UI Emoji, Nabla) draw as vector art: their layers' colours, gradients, clips and blend modes on the canvas and in SVG, EPS, legacy `.ai` and PDF, and as editable paths with Create Outlines; glyphs using sweep gradients or compositing modes are drawn as images of the glyph. diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index 136722fab..ae797aaee 100644 --- a/docs/file-format-parity.md +++ b/docs/file-format-parity.md @@ -398,6 +398,7 @@ Stop opacity on translated artboards remains outside this regression coverage. | 2026-10-29 | minor | Affinity import: attached Recolor Adjustment (RcRA) layers import as color overlays with HSL lightness (#4420); scores unchanged. | | 2026-10-30 | minor | EMF export: a pattern fill whose tiles stay apart (grid and brick tilings, art inside its tile) is written as one shape for each part of the tile, clipped once to what it fills, instead of every tile in a clip of its own; the picture is the same. A rotated two-colour stripe fill goes from 473 shapes and 246 clips to 2 shapes and 1 clip (122 KB to 61 KB; unrotated 99 KB to 33 KB). Tiles that overlap, hex tilings and tiles with strokes, images or nested clips are written as before (#3911). Tests in `crates/metafile/src/tests.rs` and `scene::merge_tests`; scores and hours unchanged | | 2026-10-26 | minor | EPS Save and Save As: an opened EPS saves back to its own path as EPS, keeping the chosen artboard's page frame and transparent margins and the complete editable VectorCraft document; Save's TIFF preview is capped at 1024 px (Export keeps 2048); partly read, preview-only or damaged-editing-data sources still need confirmation before they are overwritten; Save As leaves out the export-only crop and selection options. Synthetic regressions in `tests::save` (engine and UI) and `dialogs::save_options::tests`, listed under [EPS Save and source protection](#eps-save-and-source-protection); it does not write Illustrator's own editing data (#868), and G9's real-file import fidelity is unchanged; scores and hours unchanged | +| 2026-10-26 | minor | PDF form-gradient placement checked with Quartz on the shared writer; scores and estimates unchanged. | | 2026-10-25 | minor | `.ai` import: the editing data is read up to 512 MiB on 64-bit builds (the limit was 256 MiB), so a file whose embedded rasters unpack to 300 MiB or more keeps its layers, groups and text instead of opening as its PDF part (part of #3510); 32-bit and web builds keep 256 MiB. Own-fixture test `import::ai::tests::editing_data_past_256_mib_is_read`; scores and hours unchanged | | 2026-10-25 | minor | `.ai` import: the `--` placeholder of a radial gradient's highlight (`-- -- angle length Bh`, a gradient without one) and the `XC` operator after a linked image's file name no longer send the whole file to its PDF part, so files with them keep their layers, groups and live text (#4176). A `--` anywhere else is still an operator the reader doesn't know. Own-fixture tests in `import::ai::tests`; scores and hours unchanged | | 2026-10-24 | minor | EPS, EMF/WMF and legacy `.ai`: type with ruby (ふりがな) is written with the reading's glyphs where the canvas draws them (they were dropped); legacy `.ai` writes such type as outlines, since a text object can't hold ruby. Tests `eps::tests::ruby_is_written_with_its_type`, `tests_legacy_ai::type_with_ruby_is_outlines_with_the_reading`, `metafile::tests::ruby_is_written_with_its_type`; scores and hours unchanged | From ad7cf2898c4b051b9bf884b5ee86f37304e8160d Mon Sep 17 00:00:00 2001 From: Patrick Nadeau Date: Sun, 8 Nov 2026 19:13:06 -0500 Subject: [PATCH 05/38] M4: keep gradient evidence with October history --- docs/gaps.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/gaps.md b/docs/gaps.md index 7fcf7586e..d2b40b253 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -878,6 +878,7 @@ trap, and the defaults (0.25 pt, 100%, 40%) are VectorCraft's own, the pages giv | 2026-10-29 | minor | G8: a blend makes at most 100,000 steps in all, the steps of blends nested in its keys and steps included; past it each pair of keys keeps the same share of its steps (#3579). No entry closed, estimates unchanged. | | 2026-10-26 | minor | G9: EPS save-back: an opened EPS saves back to its own path as EPS, keeping the chosen artboard's page frame and transparent margins and the complete editable VectorCraft document; Save's TIFF preview is capped at 1024 px (Export keeps 2048); partly read, preview-only or damaged-editing-data sources still need confirmation before they are overwritten. This narrows G9's save-back workflow only; its import-fidelity items (layer and group structure, guides, open speed, live objects) and estimate are unchanged, and writing Illustrator's own editing data stays out of scope (#868) | | 2026-10-26 | minor | G7: View › New View… / Edit Views… ([public source](https://helpx.adobe.com/illustrator/using/viewing-artwork.html)): the View menu lists every one of a document's 25 saved views (it listed 10), Edit Views… lists them to rename or delete in one undo step (`view.saved.edit {edits}`), and a file's views are checked when it opens; New View/Edit Views leave G7's missing list; estimate unchanged | +| 2026-10-26 | minor | PDF form-gradient placement checked with Quartz on the shared writer; broader gaps unchanged. | | 2026-10-25 | minor | G4: a double-click on a threaded port with the Selection tool breaks the thread there (`text.thread.break {id, port}`): the story flows into the frames before the break, the frames after it are left empty and stay threaded with each other when two or more; a double-click on a port with no thread does nothing; estimate unchanged | | 2026-10-31 | minor | G1: Grid Repeat Options has a Preview checkbox (#2651); estimate unchanged. | | 2026-10-25 | minor | G1: a selected Grid Repeat shows handles: the one below the grid adds or removes rows, the one right of it columns, the one past its bottom-right corner both, and the sliders between the first two columns and rows set the horizontal and vertical spacing; each drag is one undo step and Escape cancels it (#2691); bounding-box handles keep priority where they meet; Radial Repeat handles remain; estimates unchanged. | From 3befda6e12c86ccd72693665753b55523f45b310 Mon Sep 17 00:00:00 2001 From: Patrick Nadeau Date: Mon, 9 Nov 2026 08:35:33 -0500 Subject: [PATCH 06/38] M4: limit gradient documentation to PDF behavior --- ROADMAP.md | 4 ---- docs/file-format-parity.md | 1 - docs/gaps.md | 8 -------- docs/roadmap.md | 2 -- docs/target-app-parity.md | 4 +--- 5 files changed, 1 insertion(+), 18 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 9250e67b6..1bedf0b95 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,7 +1,5 @@ # VectorCraft Roadmap -> **Gradient-form update:** 2026-11-08 · minor · PDF export fix rebased onto current main and checked with macOS Quartz; headline scores and estimates unchanged. - - Contour add-on (beyond Illustrator): drag a selected shape's edge outward or inward for a live, editable offset copy, with a floating bar for distance and Sharp, Round or Square corners; Bake turns it into paths. Scores and estimates unchanged. - Compound Shapes add-on (beyond Illustrator): a menu for Make, Release and Expand Compound Shape and a setting, off by default, that makes the Pathfinder panel's Shape Mode buttons create compound shapes on a click. Scores and estimates unchanged. @@ -138,7 +136,6 @@ Ranked; detail in [docs/roadmap.md](docs/roadmap.md) and [docs/gaps.md](docs/gap ## Progress log -- 2026-11-08: PDF form-gradient placement reverified on current main: three Quartz regressions fail without the fix and pass with it. The fix follows the exporter module split and keeps upstream layer-box repairs in the form's new space, with a translated-layer regression. Headline numbers unchanged. - 2026-11-05: Window › Toolbars › Keep Tools Beside the Canvas, off by default, docks the Tools panel between left-side panels and the canvas instead of at the window's left edge, and the canvas's left edge then also docks a floating Tools panel (`window.toolsBesideCanvas`, #5395). Workspaces keep the setting; workspaces saved without it keep Tools at the window's edge. An optional VectorCraft layout choice; headline numbers unchanged. - 2026-11-05: Release Type on a Path joins the Type Tools add-on (Object › Plug-ins › Type Tools): each selected type on a path splits into point type with the same text and attributes, on one straight line starting where its first character sat, and the plain path it was set on, with its exact shape and place in the stacking order and no paint; or keeps only the path or only the text, optionally leaving the original. A dialog with Preview, one undo step (`addon.type.releasePath`, #5532). A VectorCraft addition: headline numbers unchanged. - 2026-11-04: New Document: the units chosen in Preset Details now belong to the chosen document, as the pane's options are listed for the selected preset; the chosen card shows them, every other Recent and Saved card keeps the units its document or preset was made in, and the built-in tabs still follow the chosen units, so switching between inches and millimetres no longer restyles every recent template. Scores and estimates unchanged. @@ -156,7 +153,6 @@ Ranked; detail in [docs/roadmap.md](docs/roadmap.md) and [docs/gaps.md](docs/gap - 2026-10-31: Export for Screens works as the reference app's dialog: Artboards and Assets tabs with a grid or list of thumbnails, Clear Selection and Prefix, Create Sub-folders by Scale or Format (on by default), Export PDFs As Single File (the chosen artboards as one PDF's pages, `pdfs: "single"`) or Multiple Files, iOS and Android rows, format presets in a ☰ menu, and a count of the files the export writes (#4895). About 55 community PRs and fixes landed the same day: Find Font, Replace Font, Outline Stroke and Expand leave hidden and locked art alone (with a warning); DXF export writes LINE and ELLIPSE entities and outlines only the strokes a CAD line can't draw; Blob Brush Angle and Roundness; Shift-constrained Perspective Selection; Grid Repeat Options Preview; a gradient mesh from a file budgeted at about a million quads; width-profile strokes keep their shape zoomed out; `.ai`/EPS import fidelity (area type, envelopes, transparency in dictionaries, character styles); PDF import finds hyphenated and CMap-suffixed font names; Windows renders follow the process's core affinity; Enter or Escape in a floating panel's field no longer reaches the canvas (#4803); Properties shows Blend Options for any selected blend (#4817); Artwork Cleanup finds doubled type (#4835) and joins stacked lines into one object (Join Point Text, #4826). Headline numbers unchanged. - 2026-10-31: Arrange Tools, a built-in add-on (Object › Plug-ins › Arrange Tools): Arrange in Grid lays the selected objects out in rows and columns, a row or a column with gaps, from the selection's top-left or centred on the artboard; Swap Objects swaps two objects' places (and, with Swap Objects and Sizes, their sizes); Replace with Top Object puts a copy of the topmost selected object in place of each of the others, kept, stretched or fitted in proportion; Set Size of Each gives every selected object an exact width and/or height about a reference point; Zoom to Selection fits the view to the selection. Each is one undo step with a previewed dialog where it has options. A VectorCraft addition: headline numbers unchanged. - 2026-10-30: Gradient panel presets: a Presets menu applies one of the built-in gradient libraries' presets (grouped by theme) or a saved one to the gradient behind the active proxy, keeping its placement, as one undo step; the panel menu saves the current gradient as a preset, kept with the preferences, and deletes saved ones (`gradient.presets.list/apply/save/delete`). -- 2026-10-26: PDF form-gradient placement checked with macOS Quartz on the shared writer: linear and radial gradients follow transparency groups, masks, PDF layers and overprinting art. Headline numbers unchanged. - 2026-10-23: Pathfinder › Trap: the Pathfinder panel's Trap… and Effect › Pathfinder › Trap… add a thin overprinting strip of the lighter colour inside the darker one where two solid colours meet, so misregistered plates leave no gap ([trapping](https://helpx.adobe.com/illustrator/using/trapping.html)), with Thickness, Height/Width, Tint Reduction, Traps with Process Color and Reverse Traps. Illustrator effects 49/50 (SVG Filters remains); Live vector effects hours 6–10 → 4–8; headline numbers unchanged. - 2026-10-23: OpenType SVG colour fonts draw as vector art: each glyph's SVG document is read by the SVG importer, in the text's colours where it asks for them, and draws, exports and outlines as COLR glyphs do; all colour font formats are now drawn. - 2026-10-23: COLR colour fonts (v0 and v1, as in Noto Color Emoji's vector build, Segoe UI Emoji, Nabla) draw as vector art: their layers' colours, gradients, clips and blend modes on the canvas and in SVG, EPS, legacy `.ai` and PDF, and as editable paths with Create Outlines; glyphs using sweep gradients or compositing modes are drawn as images of the glyph. diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index ae797aaee..136722fab 100644 --- a/docs/file-format-parity.md +++ b/docs/file-format-parity.md @@ -398,7 +398,6 @@ Stop opacity on translated artboards remains outside this regression coverage. | 2026-10-29 | minor | Affinity import: attached Recolor Adjustment (RcRA) layers import as color overlays with HSL lightness (#4420); scores unchanged. | | 2026-10-30 | minor | EMF export: a pattern fill whose tiles stay apart (grid and brick tilings, art inside its tile) is written as one shape for each part of the tile, clipped once to what it fills, instead of every tile in a clip of its own; the picture is the same. A rotated two-colour stripe fill goes from 473 shapes and 246 clips to 2 shapes and 1 clip (122 KB to 61 KB; unrotated 99 KB to 33 KB). Tiles that overlap, hex tilings and tiles with strokes, images or nested clips are written as before (#3911). Tests in `crates/metafile/src/tests.rs` and `scene::merge_tests`; scores and hours unchanged | | 2026-10-26 | minor | EPS Save and Save As: an opened EPS saves back to its own path as EPS, keeping the chosen artboard's page frame and transparent margins and the complete editable VectorCraft document; Save's TIFF preview is capped at 1024 px (Export keeps 2048); partly read, preview-only or damaged-editing-data sources still need confirmation before they are overwritten; Save As leaves out the export-only crop and selection options. Synthetic regressions in `tests::save` (engine and UI) and `dialogs::save_options::tests`, listed under [EPS Save and source protection](#eps-save-and-source-protection); it does not write Illustrator's own editing data (#868), and G9's real-file import fidelity is unchanged; scores and hours unchanged | -| 2026-10-26 | minor | PDF form-gradient placement checked with Quartz on the shared writer; scores and estimates unchanged. | | 2026-10-25 | minor | `.ai` import: the editing data is read up to 512 MiB on 64-bit builds (the limit was 256 MiB), so a file whose embedded rasters unpack to 300 MiB or more keeps its layers, groups and text instead of opening as its PDF part (part of #3510); 32-bit and web builds keep 256 MiB. Own-fixture test `import::ai::tests::editing_data_past_256_mib_is_read`; scores and hours unchanged | | 2026-10-25 | minor | `.ai` import: the `--` placeholder of a radial gradient's highlight (`-- -- angle length Bh`, a gradient without one) and the `XC` operator after a linked image's file name no longer send the whole file to its PDF part, so files with them keep their layers, groups and live text (#4176). A `--` anywhere else is still an operator the reader doesn't know. Own-fixture tests in `import::ai::tests`; scores and hours unchanged | | 2026-10-24 | minor | EPS, EMF/WMF and legacy `.ai`: type with ruby (ふりがな) is written with the reading's glyphs where the canvas draws them (they were dropped); legacy `.ai` writes such type as outlines, since a text object can't hold ruby. Tests `eps::tests::ruby_is_written_with_its_type`, `tests_legacy_ai::type_with_ruby_is_outlines_with_the_reading`, `metafile::tests::ruby_is_written_with_its_type`; scores and hours unchanged | diff --git a/docs/gaps.md b/docs/gaps.md index d2b40b253..467946e93 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -5,13 +5,6 @@ ## Feature submission evidence **Feature evidence last updated:** 2026-11-02. - -**PDF form gradients — 2026-11-08:** original synthetic Quartz checks reproduce the placement, direction and nested -stop-opacity failures on main `65adb7b21` and pass with the rebased fix. The import/render round trip -also passes; a translated-layer regression covers the interaction with the upstream layer-box -repair (`crates/pdf/tests/gradient_forms.rs`). Stop opacity on translated artboards remains outside -this coverage. No ranked gap or estimate is closed by this narrow export fix. - | 2026-11-06 | minor | Opening a file that takes longer than 0.4 s shows a progress window: the file name, a moving bar (reading reports no steps), how many more files wait, Cancel (Esc; the file doesn't open) and Continue in Background (`file.cancelOpen`, `file.openInBackground`); it never opens over another dialog or progress window (part of #5930); scores and estimates unchanged | Raster rendering fits and preflattens curved stroke outlines more tightly only below four times the raster tolerance, retaining thin-band coverage without changing stroke weight or vector export geometry. Current cached character/object outlines, cropped long strokes, gradient clips and effect rasters share the helper; cropped-region reuse and guarded flattening remain. The unchanged `fractional_curved_strokes_retain_coverage_between_notches` regression covers fractional weights, rotation, phase, opacity and f32-rounded endpoints. This is a quality/cost tradeoff, not a speed claim. Scores and estimates unchanged. @@ -878,7 +871,6 @@ trap, and the defaults (0.25 pt, 100%, 40%) are VectorCraft's own, the pages giv | 2026-10-29 | minor | G8: a blend makes at most 100,000 steps in all, the steps of blends nested in its keys and steps included; past it each pair of keys keeps the same share of its steps (#3579). No entry closed, estimates unchanged. | | 2026-10-26 | minor | G9: EPS save-back: an opened EPS saves back to its own path as EPS, keeping the chosen artboard's page frame and transparent margins and the complete editable VectorCraft document; Save's TIFF preview is capped at 1024 px (Export keeps 2048); partly read, preview-only or damaged-editing-data sources still need confirmation before they are overwritten. This narrows G9's save-back workflow only; its import-fidelity items (layer and group structure, guides, open speed, live objects) and estimate are unchanged, and writing Illustrator's own editing data stays out of scope (#868) | | 2026-10-26 | minor | G7: View › New View… / Edit Views… ([public source](https://helpx.adobe.com/illustrator/using/viewing-artwork.html)): the View menu lists every one of a document's 25 saved views (it listed 10), Edit Views… lists them to rename or delete in one undo step (`view.saved.edit {edits}`), and a file's views are checked when it opens; New View/Edit Views leave G7's missing list; estimate unchanged | -| 2026-10-26 | minor | PDF form-gradient placement checked with Quartz on the shared writer; broader gaps unchanged. | | 2026-10-25 | minor | G4: a double-click on a threaded port with the Selection tool breaks the thread there (`text.thread.break {id, port}`): the story flows into the frames before the break, the frames after it are left empty and stay threaded with each other when two or more; a double-click on a port with no thread does nothing; estimate unchanged | | 2026-10-31 | minor | G1: Grid Repeat Options has a Preview checkbox (#2651); estimate unchanged. | | 2026-10-25 | minor | G1: a selected Grid Repeat shows handles: the one below the grid adds or removes rows, the one right of it columns, the one past its bottom-right corner both, and the sliders between the first two columns and rows set the horizontal and vertical spacing; each drag is one undo step and Escape cancels it (#2691); bounding-box handles keep priority where they meet; Radial Repeat handles remain; estimates unchanged. | diff --git a/docs/roadmap.md b/docs/roadmap.md index 7bb2321e7..a5aaadc66 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -195,7 +195,6 @@ separate. Scores and hours unchanged. [Behavior and command details](development | 2026-11-08 | minor | M7: spell check engine, `text.spelling.check` and `text.spelling.suggest` on craft-proof (#4936); the dialog, Auto Spell Check and the custom dictionary remain; scores and estimates unchanged. | | 2026-11-08 | minor | Floating tab groups collapse into the side icon rail and opened rail panels fold back into it (#5925, in part); tab groups remain intact. Scores and estimates unchanged. | | 2026-11-09 | minor | Numeric fields step on wheel hover without taking focus; keyboard focus and existing rounding rules remain (#5949). Scores and estimates unchanged. | -| 2026-11-08 | minor | M4: PDF form-gradient fix follows the exporter module split and preserves page-sized layer bounds on translated artboards; Quartz and round-trip checks pass. Estimates unchanged. | | 2026-11-07 | minor | M2: with the Pen or Curvature tool, Edit › Copy and Cut take the anchors and segments a Cmd/Ctrl-lent Direct Selection picked, not the whole path (#6123); estimates unchanged | | 2026-11-07 | minor | Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged. | | 2026-11-06 | minor | M2: a shape just drawn moves by its centre point (Show Center) and turns from the rotate zone just outside a bounding-box corner with its tool still active, as the Selection tool's gestures do (#5477); estimates unchanged. | @@ -211,7 +210,6 @@ separate. Scores and hours unchanged. [Behavior and command details](development | 2026-11-03 | minor | M7: retain composition trimming in rotated character placement and rotation metadata; estimates unchanged. | | 2026-10-29 | minor | M6: Pathfinder: live compound shapes (#4522): the panel menu's Make, Release and Expand Compound Shape, Alt-clicking a Shape Mode, a selected member's Shape Mode and the Expand button act; scores unchanged | | 2026-10-26 | minor | M4/G9: EPS Save and Save As: an opened EPS saves back to its own path as EPS, keeping the chosen artboard's page frame and transparent margins and the complete editable VectorCraft document; Save's TIFF preview is capped at 1024 px (Export keeps 2048); partly read, preview-only or damaged-editing-data sources still need confirmation before they are overwritten; it does not write Illustrator's own editing data (#868), and G9's real-file import fidelity is unchanged; scores and estimates unchanged | -| 2026-10-26 | minor | M4: PDF form-gradient placement checked with Quartz on the shared writer; estimates unchanged. | | 2026-10-25 | minor | M10/G1: a selected Grid Repeat shows handles: the one below the grid adds or removes rows, the one right of it columns, the one past its bottom-right corner both, and the sliders between the first two columns and rows set the horizontal and vertical spacing; each drag is one undo step and Escape cancels it (#2691); estimates unchanged | | 2026-10-24 | minor | Docs catch-up: Current focus's three G11 lines merged into one (#864 and #885 remain; #937, #983/#1037 and #834 fixed) and its two stale G2 lines (filters left, the Effect Gallery to build) removed; G2 stays the gallery's thumbnails and in-dialog preview, 1–2 h. Estimates unchanged. | | 2026-10-24 | minor | M10/G5: brush libraries (Window › Brush Libraries, the Brushes panel's Brush Libraries Menu, four generated libraries, Save Brushes, Other Library…); estimates unchanged | diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index af5c9f2f3..cdffff919 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -226,7 +226,7 @@ dominate; 3D, graphs and automation are specialist). They sum to 105. Feature de | Image Trace, graphs, image tools | 3 | 75% | graph Column designs, Image Trace Create Strokes depth (Create Object Mosaic is done; junction-returning piece survival has source-generated regressions in `image-trace-quality.md`; large images trace with a warning; Auto Color keeps rare distinct colours) | 5–8 | estimated (raised from 72%); quick presets reveal adjustment controls, Properties reopens them | | Layers, artboards, document setup | 5 | 78% | Selected-artboard deletion is consistent across panel actions (all-selected retains artboard 1); history prunes removed indices while preserving valid multiselection; remaining: 30.0's artboard locking and colours and managing artboards from the canvas, Layers panel row context menus and Collect for Export (click-to-fit objects is done, #1227) | 5–8 | estimated (lowered from 82%: target moved); Artboard deletion keys keep artwork and the first of an all-selected set; Artboard Options' Show Center Mark and Show Cross Hairs draw on the canvas only (exports and print leave them out), regression-tested; Artboard Options' Show Video Safe Areas and Video Ruler Pixel Aspect Ratio (per artboard, native format) are done ([video display options](#evidence-what-ships-today-by-area)), score unchanged | | View & navigation | 3 | 70% | multiple windows and the rest of Arrange (Cascade, Float in Window, Float All in Windows) | 9–14 | estimated; Window › Arrange › Tile and Consolidate All Windows done (score/hours unchanged); document-tab artwork hover/copy and lossless transfer tested (2026-10-22; score/hours unchanged); cached canvas reprojection preserves scale during resize, rotation and DPI changes (regression and contributor original-file evidence below; scores/hours unchanged); cached canvas dimensions match the renderer limit before cache-key creation, view construction and worker submission (regression evidence below) | -| File formats | 6 | 75% | `.ai` files open with a flattened or wrong layer structure in real cases (#951), no writing of Illustrator's editing data (#868), FreeHand/CorelDRAW/DWG/HEIC import, 3D export; see [file-format-parity.md](file-format-parity.md); EPS legacy mesh/opacity-mask and raster comparison follow-up verified | 10–20 | estimated (lowered from 85%: open issues); DXF control-point SPLINE import closure regression-tested (#1299 follow-up after #1320); PDF writer metadata, reader streaming and immutable backend checks in `crates/pdf/src/tests.rs`, `crates/pdf/src/tests/reader_streaming.rs` and `xtask/src/pdf_deps.rs`; PDF Place and clipboard paste retain live text (synthetic regressions); PDF form-gradient placement and direction independently checked with macOS Quartz (`crates/pdf/tests/gradient_forms.rs`); EPS ordinary Save retains the page frame and editable document (2026-10-26); `.ai`/EPS editing data checked against a user's files (2026-10-30): case setting (`ate::tests`, `tests_native::type_from_the_text_document_keeps_its_case_setting`), placed-file placements (`tests_native::a_small_placed_file_goes_where_it_draws_closer_to_the_page`), pages cropped to the art (`tests_native::type_on_the_copies_of_a_page_cropped_to_the_art_is_read_once`), mesh corner weights and state (`ai::mesh::tests::corners_with_handle_weights_of_zero_are_read`, `ai::tests::a_gradient_mesh_takes_the_transparency_written_after_it`), type on a path spacing (`tests_native::type_round_a_circle_is_spaced_as_the_file_lays_it_out`; the distance from the path under Align to Path is measured from the top of each character's em box, half the height of the top of its H or the bottom of its font's bounding box (`tests_native::aligned_type_lies_as_far_from_its_path_as_the_file_lays_it_out`), and the fit under Gravity does not reproduce the file's layout), blend state (`ai::tests::a_blends_transparency_follows_its_record`); scores and estimates unchanged. | +| File formats | 6 | 75% | `.ai` files open with a flattened or wrong layer structure in real cases (#951), no writing of Illustrator's editing data (#868), FreeHand/CorelDRAW/DWG/HEIC import, 3D export; see [file-format-parity.md](file-format-parity.md); EPS legacy mesh/opacity-mask and raster comparison follow-up verified | 10–20 | estimated (lowered from 85%: open issues); DXF control-point SPLINE import closure regression-tested (#1299 follow-up after #1320); PDF writer metadata, reader streaming and immutable backend checks in `crates/pdf/src/tests.rs`, `crates/pdf/src/tests/reader_streaming.rs` and `xtask/src/pdf_deps.rs`; PDF Place and clipboard paste retain live text (synthetic regressions); EPS ordinary Save retains the page frame and editable document (2026-10-26); `.ai`/EPS editing data checked against a user's files (2026-10-30): case setting (`ate::tests`, `tests_native::type_from_the_text_document_keeps_its_case_setting`), placed-file placements (`tests_native::a_small_placed_file_goes_where_it_draws_closer_to_the_page`), pages cropped to the art (`tests_native::type_on_the_copies_of_a_page_cropped_to_the_art_is_read_once`), mesh corner weights and state (`ai::mesh::tests::corners_with_handle_weights_of_zero_are_read`, `ai::tests::a_gradient_mesh_takes_the_transparency_written_after_it`), type on a path spacing (`tests_native::type_round_a_circle_is_spaced_as_the_file_lays_it_out`; the distance from the path under Align to Path is measured from the top of each character's em box, half the height of the top of its H or the bottom of its font's bounding box (`tests_native::aligned_type_lies_as_far_from_its_path_as_the_file_lays_it_out`), and the fit under Gravity does not reproduce the file's layout), blend state (`ai::tests::a_blends_transparency_follows_its_record`); scores and estimates unchanged. | | File formats | 6 | 75% | `.ai` files open with a flattened or wrong layer structure in real cases (#951), a saved `.ai`'s editing copy without live effects on objects other than paths, compound paths and groups that qualify (at most a fill and a stroke, a non-zero fill rule when filled, no large canvas) and whose appearance and effects the art-style writer supports, and with live objects as their art (#868), FreeHand/CorelDRAW/DWG/HEIC import, textures, map art and shading in 3D export; see [file-format-parity.md](file-format-parity.md); EPS legacy mesh/opacity-mask and raster comparison follow-up verified | 10–20 | estimated (lowered from 85%: open issues); DXF control-point SPLINE import closure regression-tested (#1299 follow-up after #1320); PDF writer metadata, reader streaming and immutable backend checks in `crates/pdf/src/tests.rs`, `crates/pdf/src/tests/reader_streaming.rs` and `xtask/src/pdf_deps.rs`; PDF Place and clipboard paste retain live text (synthetic regressions); EPS ordinary Save retains the page frame and editable document (2026-10-26); `.ai`/EPS editing data checked against a user's files (2026-10-30): case setting (`ate::tests`, `tests_native::type_from_the_text_document_keeps_its_case_setting`), placed-file placements (`tests_native::a_small_placed_file_goes_where_it_draws_closer_to_the_page`), pages cropped to the art (`tests_native::type_on_the_copies_of_a_page_cropped_to_the_art_is_read_once`), mesh corner weights and state (`ai::mesh::tests::corners_with_handle_weights_of_zero_are_read`, `ai::tests::a_gradient_mesh_takes_the_transparency_written_after_it`), type on a path spacing (`tests_native::type_round_a_circle_is_spaced_as_the_file_lays_it_out`), blend state (`ai::tests::a_blends_transparency_follows_its_record`); scores and estimates unchanged. | | File formats | 6 | 75% | `.ai` files open with a flattened or wrong layer structure in real cases (#951), a saved `.ai`'s editing copy without live effects on objects other than paths, compound paths and groups that qualify (at most a fill and a stroke, a non-zero fill rule when filled, no large canvas) and whose appearance and effects the art-style writer supports, and with live objects as their art (#868), FreeHand/CorelDRAW/DWG/HEIC import, 3D export; see [file-format-parity.md](file-format-parity.md); EPS legacy mesh/opacity-mask and raster comparison follow-up verified | 10–20 | estimated (lowered from 85%: open issues); DXF control-point SPLINE import closure regression-tested (#1299 follow-up after #1320); PDF writer metadata, reader streaming and immutable backend checks in `crates/pdf/src/tests.rs`, `crates/pdf/src/tests/reader_streaming.rs` and `xtask/src/pdf_deps.rs`; PDF Place and clipboard paste retain live text (synthetic regressions); SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; EPS ordinary Save retains the page frame and editable document (2026-10-26); `.ai`/EPS editing data checked against a user's files (2026-10-30): case setting (`ate::tests`, `tests_native::type_from_the_text_document_keeps_its_case_setting`), placed-file placements (`tests_native::a_small_placed_file_goes_where_it_draws_closer_to_the_page`), pages cropped to the art (`tests_native::type_on_the_copies_of_a_page_cropped_to_the_art_is_read_once`), mesh corner weights and state (`ai::mesh::tests::corners_with_handle_weights_of_zero_are_read`, `ai::tests::a_gradient_mesh_takes_the_transparency_written_after_it`), type on a path spacing (`tests_native::type_round_a_circle_is_spaced_as_the_file_lays_it_out`), blend state (`ai::tests::a_blends_transparency_follows_its_record`); scores and estimates unchanged. | | View & navigation | 3 | 70% | multiple windows and the rest of Arrange (Cascade, Float in Window, Float All in Windows) | 9–14 | estimated; Window › Arrange › Tile and Consolidate All Windows done (score/hours unchanged); document-tab artwork hover/copy and lossless transfer tested (2026-10-22; score/hours unchanged); cached canvas reprojection preserves scale during resize, rotation and DPI changes (regression and contributor original-file evidence below; scores/hours unchanged) | @@ -1909,7 +1909,6 @@ Edit › Copy and Cut put the selected ruler guides on the clipboard, with the s | 2026-11-07 | minor | File formats and Type core: `.ai`/EPS type from the text document lays its lines out closer to the file's page: auto leading per line at the paragraph's share, the paragraph's hyphenation options, possibly more of the last lines the file sets in an area frame of its own, and a line a soft return ends justified (#6137); evidence added; scores unchanged. | | 2026-11-06 | minor | `.ai` and EPS import: Art brushes in the older form of their objects come into the Brushes panel; evidence updated; scores unchanged. | | 2026-11-08 | minor | Panel numeric wheel turns do not seed canvas zoom glides; corrected the disabled-field scroll description (#5949). Scores and estimates unchanged. | -| 2026-11-08 | minor | File formats: rebased PDF form-gradient fix, current-main Quartz evidence and translated-layer bounds regression; score 75% and hours unchanged (estimated). | | 2026-11-07 | minor | Numeric fields step on wheel hover without taking focus; keyboard focus and existing rounding rules remain (#5949). Scores and estimates unchanged. | | 2026-11-08 | minor | Consolidated the four current Guides feature-area rows using existing global/artboard ruler mode, video ruler and status-bar evidence; remaining gaps, score and estimates unchanged. The earlier detailed missing-items table remains historical. | | 2026-11-07 | minor | Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged. | @@ -1999,7 +1998,6 @@ Edit › Copy and Cut put the selected ruler guides on the clipboard, with the s | 2026-10-29 | minor | Object › Path › Smooth… with its slider and Auto-Smooth (#4115) added to the evidence; score and hours unchanged. | | 2026-10-26 | minor | File formats: EPS Save and Save As: an opened EPS saves back to its own path as EPS, keeping the chosen artboard's page frame and transparent margins and the complete editable VectorCraft document; Save's TIFF preview is capped at 1024 px (Export keeps 2048); partly read, preview-only or damaged-editing-data sources still need confirmation before they are overwritten; evidence added ([file-format-parity.md](file-format-parity.md#eps-save-and-source-protection)); writing Illustrator's editing data (#868) and the `.ai` structure issues stay in the missing items; score and hours unchanged (estimated) | | 2026-10-26 | minor | View & navigation: New View… / Edit Views… taken off the missing items (all 25 saved views in the View menu, an Edit Views… list dialog, views checked on open); evidence added; score and hours unchanged. | -| 2026-10-26 | minor | File formats: rebased PDF form-gradient fix and independent Quartz evidence; score 75% and hours unchanged (estimated). | | 2026-10-25 | minor | Type core: a double-click on a threaded port breaks the thread there (`text.thread.break`); evidence added; scores unchanged. | | 2026-10-25 | minor | Repeat: a selected Grid Repeat shows handles: the one below the grid adds or removes rows, the one right of it columns, the one past its bottom-right corner both, and the sliders between the first two columns and rows set the horizontal and vertical spacing; each drag is one undo step and Escape cancels it (#2691); evidence added; score and hours unchanged (estimated). | | 2026-10-24 | minor | Path operations: the Shape Builder's edge-source grid holds a bounded number of cell references (coincident segments no longer quadratic in memory) and files a segment once per cell; Path operations 80%, Shape Builder 70% unchanged (estimated).; area row and evidence list updated; the budget counts an item's distinct cells, so a degenerate grid's pieces don't send it to the scanned list. | From 99a6e4032319d5c59a686c94319ff914f0f974dc Mon Sep 17 00:00:00 2001 From: Christian Lappin <36496308+christianlappin@users.noreply.github.com> Date: Mon, 9 Nov 2026 12:50:22 -0500 Subject: [PATCH 07/38] test(layers): synchronize thumbnail queue assertion (#6318) --- crates/ui-egui/src/panels/layers/thumbs.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/ui-egui/src/panels/layers/thumbs.rs b/crates/ui-egui/src/panels/layers/thumbs.rs index 4fcdc03e4..2a6d010b9 100644 --- a/crates/ui-egui/src/panels/layers/thumbs.rs +++ b/crates/ui-egui/src/panels/layers/thumbs.rs @@ -957,10 +957,15 @@ mod tests { assert!(thumbs.texture(&ctx, &src_b, n, 24).is_some(), "the shown document's thumbnails are shown"); } // Shown again, document 1's rows are asked for again and come back. + // Hold the worker so texture() cannot collect a completed row before the count. + let release = hold(&thumbs); for n in &a_rows { assert!(thumbs.texture(&ctx, &src_a, n, 24).is_none()); } assert_eq!(thumbs.pending.keys().filter(|k| k.0 == 1).count(), 3); + release.send(()).expect("the held job is waiting"); + let held = thumbs.worker.as_ref().and_then(ThumbWorker::wait).expect("the held job finishes"); + assert_eq!(held.key.0, (0, NodeId(0))); for _ in &a_rows { receive_checked(&ctx, &mut thumbs, 1, &a, &a_rows); } From 78a72a523bda31467e20bede28da81341798740a Mon Sep 17 00:00:00 2001 From: Christian Lappin <36496308+christianlappin@users.noreply.github.com> Date: Mon, 9 Nov 2026 13:04:03 -0500 Subject: [PATCH 08/38] test(text): preserve duplicate glyph occurrence order (#6323) --- crates/text/src/tests/harakat.rs | 44 +++++++++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/crates/text/src/tests/harakat.rs b/crates/text/src/tests/harakat.rs index c097fc48b..e590f103c 100644 --- a/crates/text/src/tests/harakat.rs +++ b/crates/text/src/tests/harakat.rs @@ -512,6 +512,43 @@ fn assert_authored_ownership(t: &TextObject, text: &str, l: &TextLayout, what: & } } +/// Repeated glyphs in one cluster can belong to differently painted runs. Match each authored +/// occurrence in order, without needing an installed font, and still reject a changed owner. +#[test] +fn duplicate_authored_glyphs_keep_occurrence_order() { + let mut text = TextObject::point(Point::ZERO, "\u{0651}", style(FAMILY)); + text.runs.push(vectorcraft_doc::TextRun::new("\u{0651}", CharStyle { fill: red(), ..style(FAMILY) })); + let glyph = |run| PositionedGlyph { + outline: Default::default(), + run, + byte: 0, + origin: Point::ZERO, + advance: 0.0, + len: 4, + angle: 0.0, + line: 0, + font_id: 0, + gid: 7, + xf: Affine::IDENTITY, + rtl: true, + scale: 1.0, + image: None, + colour: None, + bend: None, + unturned: Affine::IDENTITY, + pivot: Point::ZERO, + carets: Vec::new(), + width: 0.0, + width_start: 0.0, + }; + let before = TextLayout { glyphs: vec![glyph(0), glyph(1)], ..TextLayout::default() }; + assert_eq!(assert_kashida_ownership(&text, &before, &before, &red(), "duplicate glyphs", false), 0); + + let mut wrong = before.clone(); + wrong.glyphs[0].run = 1; + assert!(std::panic::catch_unwind(|| assert_kashida_ownership(&text, &before, &wrong, &red(), "changed owner", false)).is_err()); +} + /// Compare authored glyph run indices before/after Insert Kashidas (matched by byte and gid; /// layout order can differ between point and justified area), and assert every inserted tatweel /// uses the base letter's paint at the cluster start—not `mark_fill`. When `require_mark_last`, @@ -525,16 +562,17 @@ fn assert_kashida_ownership( what: &str, require_mark_last: bool, ) -> usize { - let mut before_runs: std::collections::HashMap<(usize, u32), Vec> = std::collections::HashMap::new(); + let mut before_runs: std::collections::HashMap<(usize, u32), std::collections::VecDeque> = std::collections::HashMap::new(); for g in before.glyphs.iter().filter(|g| g.len > 0) { - before_runs.entry((g.byte, g.gid)).or_default().push(g.run); + before_runs.entry((g.byte, g.gid)).or_default().push_back(g.run); } let mut after_n = 0; for g in with_kashida.glyphs.iter().filter(|g| g.len > 0) { after_n += 1; let runs = before_runs.get_mut(&(g.byte, g.gid)).unwrap_or_else(|| panic!("{what}: authored glyph {} at {} missing before kashida", g.gid, g.byte)); - let want_run = runs.pop().unwrap_or_else(|| panic!("{what}: no remaining before-run for {} at {}", g.gid, g.byte)); + // Duplicate glyphs at the same cluster byte retain their occurrence order. + let want_run = runs.pop_front().unwrap_or_else(|| panic!("{what}: no remaining before-run for {} at {}", g.gid, g.byte)); assert_eq!(g.run, want_run, "{what}: authored run at {} gid {}", g.byte, g.gid); let want_fill = coloured.runs.get(want_run).map(|r| &r.style.fill).unwrap_or_else(|| panic!("{what}: missing run {want_run}")); assert_eq!(&run_of(coloured, g).style.fill, want_fill, "{what}: authored fill at {}", g.byte); From 1c8503f3e211e40bad47f15b01a1f913dc5d2ff6 Mon Sep 17 00:00:00 2001 From: Christian Lappin <36496308+christianlappin@users.noreply.github.com> Date: Mon, 9 Nov 2026 13:50:36 -0500 Subject: [PATCH 09/38] fix(mcp): route Rotate View gestures to the canvas (#6322) --- crates/ui-egui/src/canvas.rs | 1 + crates/ui-egui/src/canvas/input.rs | 8 +-- crates/ui-egui/src/canvas/view.rs | 10 ++++ crates/ui-egui/src/control.rs | 65 ++++++++++++++++++++++- crates/ui-egui/src/tests/rotateview.rs | 73 ++++++++++++++++++++++++++ docs/mcp.md | 14 ++++- 6 files changed, 162 insertions(+), 9 deletions(-) diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 8ab3cc068..c769f32b1 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -77,6 +77,7 @@ use tool_overlays::{ print_tiling_overlay, rotate_view_dial, shaper_widget_overlay, slice_overlay, thread_overlays, }; pub(crate) use view::animated_zoom; +pub(crate) use view::drag_rotation; pub use view::fit; use view::scroll_bars; #[cfg(test)] diff --git a/crates/ui-egui/src/canvas/input.rs b/crates/ui-egui/src/canvas/input.rs index 5fbc2b903..43e5df0bc 100644 --- a/crates/ui-egui/src/canvas/input.rs +++ b/crates/ui-egui/src/canvas/input.rs @@ -337,14 +337,8 @@ fn drag_held(app: &mut VectorcraftApp, ui: &Ui, f: &Pointing, d: Drag, p: Pos2) } } Drag::RotateView { start_angle, start_rot, snaps } => { - let c = rect.center(); - let a = (p.y - c.y).atan2(p.x - c.x) as f64; - let mut deg = start_rot + (a - start_angle).to_degrees(); - if snaps && m.shift { - deg = (deg / 15.0).round() * 15.0; - } if let Some(vm) = app.view_mut() { - vm.rotation = vectorcraft_geom::normalize_deg(deg); + vm.rotation = super::view::drag_rotation(rect.center(), p, start_angle, start_rot, snaps && m.shift); } } Drag::ZoomBox { start } => { diff --git a/crates/ui-egui/src/canvas/view.rs b/crates/ui-egui/src/canvas/view.rs index 39bc80545..f4a470a4b 100644 --- a/crates/ui-egui/src/canvas/view.rs +++ b/crates/ui-egui/src/canvas/view.rs @@ -1,6 +1,16 @@ use super::drag::{drag_id, drop_press}; use super::{CornerRadius, Drag, Point, Pos2, Rect, SCROLL_BAR, Sense, Stroke, Tokens, Ui, VectorcraftApp, View, Xf, pos2, vec2}; +/// The mouse and control-channel Rotate View gestures use the same angle and Shift snapping. +pub(crate) fn drag_rotation(center: Pos2, point: Pos2, start_angle: f64, start_rotation: f64, snap: bool) -> f64 { + let angle = (point.y - center.y).atan2(point.x - center.x) as f64; + let mut degrees = start_rotation + (angle - start_angle).to_degrees(); + if snap { + degrees = (degrees / 15.0).round() * 15.0; + } + vectorcraft_geom::normalize_deg(degrees) +} + /// Fit the view (View → Fit Artboard / Fit All / Actual Size, Arrange Tools › Zoom to Selection). pub fn fit(app: &mut VectorcraftApp, how: &str) { let Some(rect) = app.canvas_rect else { diff --git a/crates/ui-egui/src/control.rs b/crates/ui-egui/src/control.rs index 6af51d95a..bb2918d13 100644 --- a/crates/ui-egui/src/control.rs +++ b/crates/ui-egui/src/control.rs @@ -608,6 +608,63 @@ pub(crate) fn save_screenshot_with_limit( #[cfg(test)] mod tests; +/// The window and coordinate mapping where a control-channel Rotate View drag began. +#[derive(Clone, Copy)] +struct RotateViewPress { + window: (u64, u32), + tool_resets: u64, + xf: Xf, + start_angle: f64, + start_rotation: f64, +} + +/// Rotate View belongs to the canvas, not the engine's placeholder tool. Keep its press mapping +/// across requests so document-coordinate drags do not move again as the view turns underneath. +fn rotate_view_pointer( + app: &mut VectorcraftApp, + ctx: &egui::Context, + kind: PointerKind, + position: vectorcraft_geom::Point, + screen: bool, + shift: bool, +) -> Result<(), String> { + let id = egui::Id::unique("control.rotateView"); + match kind { + PointerKind::Up => { + ctx.data_mut(|d| d.remove::(id)); + return Ok(()); + } + PointerKind::Move | PointerKind::DoubleClick => return Ok(()), + _ => {} + } + let window = app.session.active().zip(crate::doc_windows::current(app)).map(|(d, w)| (d.uid, w.1)).ok_or("no document")?; + let tool_resets = app.session.tool_resets(); + let mut press = if kind == PointerKind::Down { + let rect = app.canvas_rect.ok_or("no canvas yet")?; + let view = app.view().ok_or("no document")?; + RotateViewPress { window, tool_resets, xf: Xf::new(rect, view), start_angle: 0.0, start_rotation: view.rotation } + } else { + ctx.data(|d| d.get_temp::(id)).ok_or("Rotate View needs a down event before a drag")? + }; + // A gesture cannot turn a different document/view or resume after another tool was chosen. + if (press.window, press.tool_resets) != (window, tool_resets) { + ctx.data_mut(|d| d.remove::(id)); + return Err("the Rotate View gesture's window or tool changed".into()); + } + let point = if screen { egui::pos2(position.x as f32, position.y as f32) } else { press.xf.to_screen(position) }; + let center = press.xf.rect.center(); + if !point.is_finite() || !center.is_finite() { + return Err("Rotate View needs finite canvas coordinates".into()); + } + if kind == PointerKind::Down { + press.start_angle = (point.y - center.y).atan2(point.x - center.x) as f64; + ctx.data_mut(|d| d.insert_temp(id, press)); + } else { + app.view_mut().ok_or("no document")?.rotation = crate::canvas::drag_rotation(center, point, press.start_angle, press.start_rotation, shift); + } + Ok(()) +} + /// `ui.pointer`: pointer events through the tools. fn pointer_events(app: &mut VectorcraftApp, ctx: &egui::Context, req: &ControlRequest) -> Outcome { let p = &req.params; @@ -634,6 +691,13 @@ fn pointer_events(app: &mut VectorcraftApp, ctx: &egui::Context, req: &ControlRe let x = e.get("x").and_then(Value::as_f64).unwrap_or(0.0); let y = e.get("y").and_then(Value::as_f64).unwrap_or(0.0); let screen = e.get("space").and_then(Value::as_str) == Some("screen"); + let mods = e.get("mods").and_then(|m| serde_json::from_value(m.clone()).ok()).unwrap_or(base_mods); + if app.session.tool_id() == "rotateView" { + if let Err(e) = rotate_view_pointer(app, ctx, kind, vectorcraft_geom::Point::new(x, y), screen, mods.shift) { + return err(e); + } + continue; + } let pos = if screen { match xf { Some(xf) => xf.to_doc(egui::pos2(x as f32, y as f32)), @@ -642,7 +706,6 @@ fn pointer_events(app: &mut VectorcraftApp, ctx: &egui::Context, req: &ControlRe } else { vectorcraft_geom::Point::new(x, y) }; - let mods = e.get("mods").and_then(|m| serde_json::from_value(m.clone()).ok()).unwrap_or(base_mods); // A tilt given in screen axes leans the same way on the page as on the screen. let pen = PointerEvent::json_pen(e).map(|q| match xf { Some(xf) if screen => crate::canvas::pose_on_page(q, &xf), diff --git a/crates/ui-egui/src/tests/rotateview.rs b/crates/ui-egui/src/tests/rotateview.rs index 8f477894a..a709a767e 100644 --- a/crates/ui-egui/src/tests/rotateview.rs +++ b/crates/ui-egui/src/tests/rotateview.rs @@ -55,6 +55,79 @@ fn view_rotate_turns_the_view_to_any_angle() { assert_eq!(empty.run("view.rotate", json!({"angle": 30})).unwrap_err(), "no document"); } +/// MCP's `pointer_gesture` reaches `ui.pointer`: view tools must turn the canvas there too, +/// rather than silently sending the gesture to the engine's no-op tool. +#[test] +fn control_rotate_view_gesture_turns_the_canvas_and_reset_restores_it() { + let mut app = with_document(); + let ctx = egui::Context::default(); + app.canvas_rect = Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(800.0, 600.0))); + let view = app.view_mut().unwrap(); + view.center = vectorcraft_geom::Point::new(200.0, 150.0); + view.zoom = 1.0; + let revision = app.session.active().unwrap().revision; + let send = super::synthetic::control; + send(&mut app, &ctx, "ui.tool.select", json!({"tool": "rotateView"})); + send(&mut app, &ctx, "ui.pointer", json!({"events": [{"kind": "down", "x": 300, "y": 150}]})); + send( + &mut app, + &ctx, + "ui.pointer", + json!({"events": [ + {"kind": "drag", "x": 200, "y": 250}, + {"kind": "up", "x": 200, "y": 250} + ]}), + ); + let view = app.view().unwrap(); + assert!((view.rotation - 90.0).abs() < 1e-3, "{}", view.rotation); + assert_eq!(view.center, vectorcraft_geom::Point::new(200.0, 150.0)); + assert_eq!(view.zoom, 1.0); + assert_eq!(app.session.active().unwrap().revision, revision, "navigation does not edit the document"); + send(&mut app, &ctx, "engine.execute", json!({"command": "view.rotateReset", "params": {}})); + assert_eq!(app.view().unwrap().rotation, 0.0); +} + +#[test] +fn control_rotate_view_keeps_press_coordinates_and_shift_snapping_across_requests() { + let mut app = with_document(); + let ctx = egui::Context::default(); + app.canvas_rect = Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(800.0, 600.0))); + let view = app.view_mut().unwrap(); + view.center = vectorcraft_geom::Point::new(200.0, 150.0); + view.zoom = 1.0; + view.rotation = 30.0; + let send = super::synthetic::control; + send(&mut app, &ctx, "ui.tool.select", json!({"tool": "rotateView"})); + send(&mut app, &ctx, "ui.pointer", json!({"events": [{"kind": "down", "x": 300, "y": 150}]})); + let angle = 20_f64.to_radians(); + let (x, y) = (200.0 + 100.0 * angle.cos(), 150.0 + 100.0 * angle.sin()); + for _ in 0..2 { + send(&mut app, &ctx, "ui.pointer", json!({"mods": {"shift": true}, "events": [{"kind": "drag", "x": x, "y": y}]})); + assert_eq!(app.view().unwrap().rotation, 45.0, "the same document point does not turn again as the view changes"); + } + send(&mut app, &ctx, "ui.pointer", json!({"events": [{"kind": "up", "x": x, "y": y}]})); + assert_eq!(app.view().unwrap().rotation, 45.0); +} + +#[test] +fn control_rotate_view_rejects_nonfinite_positions_and_a_changed_document() { + let mut app = with_document(); + let ctx = egui::Context::default(); + app.canvas_rect = Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(800.0, 600.0))); + let send = super::synthetic::control; + send(&mut app, &ctx, "ui.tool.select", json!({"tool": "rotateView"})); + let (req, _) = crate::control::ControlRequest::new("ui.pointer", json!({"events": [{"kind": "down", "x": 1e300, "y": 0}]})); + let crate::control::Outcome::Done(reply) = crate::control::handle(&mut app, &ctx, &req) else { panic!("not done") }; + assert_eq!(reply["ok"], false); + assert_eq!(app.view().unwrap().rotation, 0.0); + send(&mut app, &ctx, "ui.pointer", json!({"events": [{"kind": "down", "x": 300, "y": 150}]})); + app.run("file.new", json!({})).unwrap(); + let (req, _) = crate::control::ControlRequest::new("ui.pointer", json!({"events": [{"kind": "drag", "x": 200, "y": 250}]})); + let crate::control::Outcome::Done(reply) = crate::control::handle(&mut app, &ctx, &req) else { panic!("not done") }; + assert_eq!(reply["ok"], false); + assert_eq!(app.view().unwrap().rotation, 0.0); +} + #[test] fn the_view_menu_lists_the_status_bar_angles_and_checks_the_current_one() { let mut app = with_document(); diff --git a/docs/mcp.md b/docs/mcp.md index fb9b3cfc5..d8a5f3759 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -4009,7 +4009,19 @@ every document they read except a native file and the native document that an SV `view.rotate {angle}` (desktop app) turns the view to `angle` degrees about the center of the canvas; a positive angle turns the art clockwise on screen. View › Rotate View and the status bar's angle menu run it. It returns `{angle}`, the -angle it set, normalized to (-180, 180], and `view.rotateReset` turns the view back to 0. +angle it set, normalized to (-180, 180], and `view.rotateReset` turns the view back to 0, returning `null` on success. + +Select the Rotate View Tool with `select_tool` and the tool id `rotateView`. Its `pointer_gesture` +drags turn the canvas about its center; Shift snaps to 15°. Document coordinates for a rotation +gesture use the view at the initial down event, including when the gesture spans several calls. +Reset Rotate View is the command `view.rotateReset`: + +```json +{"name":"select_tool","arguments":{"tool":"rotateView"}} +{"name":"pointer_gesture","arguments":{"events":[{"kind":"down","x":300,"y":150},{"kind":"drag","x":200,"y":250},{"kind":"up","x":200,"y":250}]}} +{"name":"command_run","arguments":{"id":"view.rotateReset","params":{}}} +``` + `view.rotateToSelection` (desktop app, View › Rotate View to Selection) turns the view about the center of the canvas to the angle the selected objects share (`selectionRotation` in `document.inspect`, 0 when their angles differ), so their bounding box is square to the window. It returns `{angle}`, and with nothing selected it returns "nothing selected". From 7d73882d3a1729d236360aa9caf9a50abc006379 Mon Sep 17 00:00:00 2001 From: Julius van der Vaart Date: Mon, 9 Nov 2026 20:47:16 +0100 Subject: [PATCH 10/38] Stroke: Line Arrow arrowhead (#5421) Add Line Arrow, a 41st generated arrowhead: two arms as thick as the line (at 100 % scale) meeting at right angles in a sharp tip, cut square at their ends. It is saved by the new name `LineArrow`, appended so existing names stay stable, and listed third in the arrowhead menus after Arrow (open). Its arms spread four weights either side of the line, twice as wide as the other heads, so Arrowhead now states its own reach and half width. Visual bounds and clicks on the canvas (head_reach, head_contains) use them instead of one fixed 4.6 x 2 box; every other head keeps the sizes it had. Tests: the outline test checks each head against its own reach and half width; a new test checks the line arrow's tip, corners, open middle and arm thickness; outset_covers_arrowheads checks that the wider head grows the outset; the saved-name list includes LineArrow. --- crates/doc/src/appearance.rs | 1 - crates/doc/src/appearance/layers.rs | 9 ++---- crates/doc/src/appearance/stroke_style.rs | 27 +++++++++++++++- crates/doc/src/appearance/tests.rs | 13 ++++++-- crates/doc/src/reach.rs | 12 +++---- crates/effects/src/stroke/arrow.rs | 18 ++++++++--- crates/effects/src/stroke/tests_arrows.rs | 39 ++++++++++++++++++++--- crates/engine/src/cmd/stroke.rs | 2 +- docs/mcp.md | 7 ++-- docs/target-app-parity.md | 5 +-- 10 files changed, 101 insertions(+), 32 deletions(-) diff --git a/crates/doc/src/appearance.rs b/crates/doc/src/appearance.rs index 3ac145fd2..2e19ad44a 100644 --- a/crates/doc/src/appearance.rs +++ b/crates/doc/src/appearance.rs @@ -8,7 +8,6 @@ mod layers; mod profile; mod stroke_style; -pub(crate) use layers::HEAD_REACH; pub use layers::{Effect, FillLayer, StrokeGradientMode, StrokeLayer, stroke_paint_bounds}; pub use profile::{AngleProfile, PressureProfile, ProfilePreset, SavedProfile, WidthProfile}; pub use stroke_style::{ArrowAlign, Arrowhead, Dash, LineCap, LineJoin, StrokeAlign}; diff --git a/crates/doc/src/appearance/layers.rs b/crates/doc/src/appearance/layers.rs index 880f3490d..923275999 100644 --- a/crates/doc/src/appearance/layers.rs +++ b/crates/doc/src/appearance/layers.rs @@ -139,10 +139,6 @@ impl StrokeGradientMode { } } -/// How far any arrowhead reaches from its tip, in units of its weight (the open arrow's arms are -/// cut square a little behind its 4 × 4 box). -pub(crate) const HEAD_REACH: f64 = 4.6; - fn ten() -> f64 { 10.0 } @@ -188,7 +184,8 @@ impl StrokeLayer { } } /// Weight of the start (`end == false`) or end arrowhead: stroke weight × its scale, at least - /// a quarter point. A head of weight `hw` fits a box about `4·hw` long and wide. + /// a quarter point. A head of weight `hw` fits a box about `4·hw` long and wide (see + /// [`Arrowhead::reach`] and [`Arrowhead::half_width`]). pub fn arrow_weight(&self, end: bool) -> f64 { let pct = if end { self.arrow_scale.1 } else { self.arrow_scale.0 }; (self.width * pct / 100.0).max(0.25) @@ -197,7 +194,7 @@ impl StrokeLayer { /// without one): the head's diagonal, or with [`ArrowAlign::Extend`] its length, plus the cap. pub fn head_reach(&self, end: bool) -> f64 { let head = if end { self.end_arrow } else { self.start_arrow }; - head.map_or(0.0, |_| HEAD_REACH * self.arrow_weight(end) + self.width / 2.0) + head.map_or(0.0, |k| k.reach() * self.arrow_weight(end) + self.width / 2.0) } /// How far the arrowheads can reach from the path's end points (0 without heads). pub fn arrow_reach(&self) -> f64 { diff --git a/crates/doc/src/appearance/stroke_style.rs b/crates/doc/src/appearance/stroke_style.rs index acd4d8189..1bc88d2d0 100644 --- a/crates/doc/src/appearance/stroke_style.rs +++ b/crates/doc/src/appearance/stroke_style.rs @@ -49,6 +49,10 @@ impl Dash { } } +/// How far the arrowheads drawn in the 4 × 4 box reach from their tip, in units of their weight +/// (the open arrow's arms are cut square a little behind the box). +const HEAD_REACH: f64 = 4.6; + /// An arrowhead shape. Saved by variant name: new shapes are appended, never renamed or reordered. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum Arrowhead { @@ -94,13 +98,17 @@ pub enum Arrowhead { Kite, TriangleBar, Oval, + /// Two arms of the line's own weight meeting at right angles in a sharp tip, cut square at + /// their ends. Twice as wide as the other heads. + LineArrow, } impl Arrowhead { /// Every arrowhead, in menu order (arrows, then outlined shapes, then bars and marks). - pub const ALL: [Arrowhead; 40] = [ + pub const ALL: [Arrowhead; 41] = [ Arrowhead::Arrow, Arrowhead::ArrowOpen, + Arrowhead::LineArrow, Arrowhead::Barbed, Arrowhead::Concave, Arrowhead::DoubleArrow, @@ -141,6 +149,22 @@ impl Arrowhead { Arrowhead::Fork, ]; + /// How far any part of the head can be from its tip, in units of its weight. + pub fn reach(self) -> f64 { + match self { + // Its farthest corners are 4 + ½√2 back and 4 − ½√2 out. + Arrowhead::LineArrow => 5.75, + _ => HEAD_REACH, + } + } + /// How far the head spreads either side of its line, in units of its weight. + pub fn half_width(self) -> f64 { + match self { + Arrowhead::LineArrow => 4.0, + _ => 2.0, + } + } + /// The name the arrowhead menus show. pub fn label(self) -> &'static str { match self { @@ -184,6 +208,7 @@ impl Arrowhead { Arrowhead::Kite => "Kite", Arrowhead::TriangleBar => "Triangle to Bar", Arrowhead::Oval => "Oval", + Arrowhead::LineArrow => "Line Arrow", } } } diff --git a/crates/doc/src/appearance/tests.rs b/crates/doc/src/appearance/tests.rs index c297b3475..0a1b3e1a5 100644 --- a/crates/doc/src/appearance/tests.rs +++ b/crates/doc/src/appearance/tests.rs @@ -219,7 +219,7 @@ fn profile_presets_round_trip_their_ids() { #[test] fn arrowhead_names_are_stable_and_every_one_is_listed_once() { // Saved files store these names: they may only ever be appended to. - const SAVED: [&str; 40] = [ + const SAVED: [&str; 41] = [ "Triangle", "TriangleOpen", "Circle", @@ -260,6 +260,7 @@ fn arrowhead_names_are_stable_and_every_one_is_listed_once() { "Kite", "TriangleBar", "Oval", + "LineArrow", ]; for name in SAVED { let a: Arrowhead = serde_json::from_value(serde_json::json!(name)).unwrap(); @@ -293,9 +294,15 @@ fn outset_covers_arrowheads() { st.arrow_scale = (100.0, 200.0); assert_eq!(st.arrow_weight(false), 4.0); assert_eq!(st.arrow_weight(true), 8.0); - assert_eq!((st.head_reach(false), st.head_reach(true)), (0.0, HEAD_REACH * 8.0 + 2.0)); + let reach = Arrowhead::Triangle.reach(); + assert_eq!((st.head_reach(false), st.head_reach(true)), (0.0, reach * 8.0 + 2.0)); // A 32 pt head whose tip sits up to its length (plus the cap) past the end point. - assert_eq!(a.outset(), HEAD_REACH * 8.0 + 2.0); + assert_eq!(a.outset(), reach * 8.0 + 2.0); + // The line arrow's arms spread twice as wide, so it reaches further from its tip. + a.stroke_mut().unwrap().end_arrow = Some(Arrowhead::LineArrow); + assert!(Arrowhead::LineArrow.reach() > reach); + assert_eq!(a.outset(), Arrowhead::LineArrow.reach() * 8.0 + 2.0); + a.stroke_mut().unwrap().end_arrow = Some(Arrowhead::Triangle); a.stroke_mut().unwrap().width = 0.01; assert_eq!(a.stroke().unwrap().arrow_weight(true), 0.25, "heads keep a minimum size"); } diff --git a/crates/doc/src/reach.rs b/crates/doc/src/reach.rs index 0c4ef497f..58ebaafc6 100644 --- a/crates/doc/src/reach.rs +++ b/crates/doc/src/reach.rs @@ -7,7 +7,6 @@ use vectorcraft_geom::hit::fill_contains; use vectorcraft_geom::kurbo::{ParamCurveArclen, ParamCurveNearest}; use vectorcraft_geom::{BezPath, FillRule, ParamCurve, PathSeg, Point, Rect, Shape, Vec2}; -use crate::appearance::HEAD_REACH; use crate::{Appearance, ArrowAlign, Dash, LineCap, LineJoin, StrokeAlign, StrokeLayer, WidthProfile}; /// Arc-length accuracy for placing a point along a profile (document points). @@ -283,18 +282,19 @@ impl StrokeLayer { } /// Is `p` within `tol` of where the start (`end == false`) or end arrowhead at `at` (the path - /// leaving along `out`) can paint? Heads are at most about 4.6 weights long and 4 wide. With + /// leaving along `out`) can paint? A head reaches [`crate::Arrowhead::reach`] weights from its tip and + /// [`crate::Arrowhead::half_width`] either side of its line (4.6 and 2 for most). With /// [`ArrowAlign::Extend`] a head points along `out` with its tip up to its length (or the cap) /// past the end; with [`ArrowAlign::Tip`] its tip is on the end and it lies back along the /// chord the path bends through under it, so anywhere within its length of the end. fn head_contains(&self, at: Point, out: Vec2, end: bool, p: Point, tol: f64) -> bool { - let hw = self.arrow_weight(end); - let v = p - at; + let Some(kind) = (if end { self.end_arrow } else { self.start_arrow }) else { return false }; + let (hw, v) = (self.arrow_weight(end), p - at); match self.arrow_align { - ArrowAlign::Tip => v.hypot() <= HEAD_REACH * hw + tol, + ArrowAlign::Tip => v.hypot() <= kind.reach() * hw + tol, ArrowAlign::Extend => { let along = v.dot(out); - v.cross(out).abs() <= 2.0 * hw + tol && along <= 4.0 * hw + self.width / 2.0 + tol && along >= -HEAD_REACH * hw - tol + v.cross(out).abs() <= kind.half_width() * hw + tol && along <= 4.0 * hw + self.width / 2.0 + tol && along >= -kind.reach() * hw - tol } } } diff --git a/crates/effects/src/stroke/arrow.rs b/crates/effects/src/stroke/arrow.rs index 1a5c90095..ac64cb30d 100644 --- a/crates/effects/src/stroke/arrow.rs +++ b/crates/effects/src/stroke/arrow.rs @@ -1,5 +1,5 @@ //! Arrowheads. A head of weight `hw` (stroke weight × scale) fits a box `4·hw` long and `4·hw` -//! wide. Hollow kinds are rings (`hw/2` walls) or, for the open arrow, a chevron with `hw` arms. +//! wide; the line arrow's arms reach twice as far out ([`Arrowhead::half_width`]). Hollow kinds are rings (`hw/2` walls) or, for the open arrow, a chevron with `hw` arms. //! The stroke ends under the head at the head's *inset* from the tip: halfway into solid heads, //! in the back wall of hollow ones (with the cap kept out of the hole) and at the chevron's inner //! corner. With [`ArrowAlign::Tip`] the tip sits on the end point and the stroke is shortened by @@ -8,7 +8,7 @@ use std::borrow::Cow; -use std::f64::consts::{FRAC_PI_2, PI}; +use std::f64::consts::{FRAC_1_SQRT_2, FRAC_PI_2, PI}; use kurbo::{Affine, BezPath, ParamCurve, ParamCurveArclen, PathSeg, Point, Shape, Vec2}; use vectorcraft_doc::{ArrowAlign, Arrowhead, StrokeLayer}; @@ -261,7 +261,7 @@ fn local_stop(kind: Arrowhead) -> Stop { Arrowhead::Fork => Stop::At(2.7), Arrowhead::HalfArrowLeft | Arrowhead::HalfArrowRight | Arrowhead::Feather => Stop::At(1.3), Arrowhead::Swallowtail => Stop::At(1.4), - Arrowhead::Chevron => Stop::At(1.0), + Arrowhead::Chevron | Arrowhead::LineArrow => Stop::At(1.0), Arrowhead::Slash => Stop::At(1.2), Arrowhead::DoubleSlash => Stop::At(1.05), Arrowhead::DoubleArrow => Stop::At(1.7), @@ -273,8 +273,9 @@ fn local_stop(kind: Arrowhead) -> Stop { } /// The outline of a head added after the first ten, in local units: x from the tip (0) back -/// along the line (to at most 4), y across it (−2 to 2, positive to the right of the direction -/// the head points). Every shape is generated here. +/// along the line (to at most 4; the line arrow's square-cut arms 4.71), y across it (−2 to 2, +/// the line arrow −4 to 4; positive to the right of the direction the head points). Every shape +/// is generated here. fn local(kind: Arrowhead) -> BezPath { let mut p = BezPath::new(); let poly = |p: &mut BezPath, pts: &[(f64, f64)]| polygon(p, &pts.iter().map(|&q| Point::from(q)).collect::>()); @@ -363,6 +364,13 @@ fn local(kind: Arrowhead) -> BezPath { poly(&mut p, &[(0.0, 2.0), (0.6, 2.0), (0.6, 0.25), (4.0, 1.8), (4.0, -1.8), (0.6, -0.25), (0.6, -2.0), (0.0, -2.0)]) } Arrowhead::Oval => p.extend(kurbo::Ellipse::new((2.0, 0.0), (2.0, 1.2), 0.0).path_elements(LOCAL_TOL)), + Arrowhead::LineArrow => { + // Arms one unit thick (the line's weight at 100 %) at right angles to each other, from + // a sharp tip to their outer corners at (4, ±4), cut square across: a unit across an + // arm at 45° is ½√2 along each axis, and the arms' inner edges meet √2 behind the tip. + let t = FRAC_1_SQRT_2; + poly(&mut p, &[(0.0, 0.0), (4.0, 4.0), (4.0 + t, 4.0 - t), (2.0 * t, 0.0), (4.0 + t, t - 4.0), (4.0, -4.0)]); + } // The first ten are drawn by `shape`. Arrowhead::Triangle | Arrowhead::TriangleOpen diff --git a/crates/effects/src/stroke/tests_arrows.rs b/crates/effects/src/stroke/tests_arrows.rs index 39d6d9d57..432804899 100644 --- a/crates/effects/src/stroke/tests_arrows.rs +++ b/crates/effects/src/stroke/tests_arrows.rs @@ -1,6 +1,6 @@ -//! Every arrowhead's outline: drawn, inside its box (4 × 4 weights; the open arrow's square-cut -//! arms reach a little further back), without self-intersections, and covering the end of the -//! stroke under it. +//! Every arrowhead's outline: drawn, inside its reach and half width (4 × 4 weights for most; the +//! open arrow's square-cut arms reach a little further back, the line arrow's twice as far out), +//! without self-intersections, and covering the end of the stroke under it. use kurbo::{BezPath, PathEl, Point, Shape}; use vectorcraft_color::{Color, Paint}; @@ -53,7 +53,13 @@ fn every_outline_is_drawn_inside_its_box_without_self_intersections() { let h = head(kind); assert!(h.outline.area().abs() > 4.0, "{kind:?} is drawn"); let b = h.outline.bounding_box(); - assert!(b.x0 >= 91.0 - 1e-6 && b.x1 <= 100.0 + 1e-6 && b.y0 >= -4.0 - 1e-6 && b.y1 <= 4.0 + 1e-6, "{kind:?} leaves its box: {b:?}"); + let (reach, side) = (2.0 * kind.reach(), 2.0 * kind.half_width()); + assert!(b.x1 <= 100.0 + 1e-6 && b.y0 >= -side - 1e-6 && b.y1 <= side + 1e-6, "{kind:?} leaves its box: {b:?}"); + if kind.half_width() == 2.0 { + assert!(b.x0 >= 91.0 - 1e-6, "{kind:?} is longer than the 4 × 4 box: {b:?}"); + } + let far = contours(&h.outline).iter().flatten().map(|p| p.distance(h.tip)).fold(0.0, f64::max); + assert!(far <= reach + 1e-6, "{kind:?} reaches {far} from its tip, past {reach}"); let edges: Vec<(Point, Point)> = contours(&h.outline).iter().flat_map(|c| (0..c.len()).map(|i| (c[i], c[(i + 1) % c.len()]))).collect(); assert!(edges.len() >= 3, "{kind:?}"); for (i, a) in edges.iter().enumerate() { @@ -74,6 +80,31 @@ fn the_stroke_ends_inside_every_head() { } } +#[test] +fn the_line_arrow_is_a_right_angle_of_two_arms_as_thick_as_the_line() { + // Weight 2 at 100 %: a local unit (x back from the tip, y across) is 2 pt. + let h = head(Arrowhead::LineArrow); + let at = |x: f64, y: f64| Point::new(100.0 - 2.0 * x, 2.0 * y); + // A sharp tip on the end point; outer corners 4 units back and out, square-cut ends. + let pts: Vec = contours(&h.outline).concat(); + for (corner, name) in [(at(0.0, 0.0), "tip"), (at(4.0, 4.0), "right corner"), (at(4.0, -4.0), "left corner")] { + assert!(pts.iter().any(|p| p.distance(corner) < 1e-9), "{name} {corner:?} in {pts:?}"); + } + // Open between the arms, solid at the tip. + assert_eq!(h.outline.winding(at(2.5, 0.0)), 0); + assert_ne!(h.outline.winding(at(0.7, 0.0)), 0); + // Halfway along each arm, 45° off the line, it is one unit (the line's weight) across. + let s = std::f64::consts::FRAC_1_SQRT_2; + for side in [1.0, -1.0] { + let (mx, my) = (2.0 + 0.5 * s, side * (2.0 - 0.5 * s)); + let (nx, ny) = (-side * s, s); + for (d, inside) in [(0.45, true), (-0.45, true), (0.55, false), (-0.55, false)] { + let w = h.outline.winding(at(mx + nx * d, my + ny * d)); + assert_eq!(w != 0, inside, "arm {side} at {d} across"); + } + } +} + #[test] fn half_arrows_barb_on_their_own_side() { // Pointing along +x on a y-down page, left is −y. diff --git a/crates/engine/src/cmd/stroke.rs b/crates/engine/src/cmd/stroke.rs index 1fb3373c7..e97659bfc 100644 --- a/crates/engine/src/cmd/stroke.rs +++ b/crates/engine/src/cmd/stroke.rs @@ -20,7 +20,7 @@ pub fn specs() -> Vec { "Stroke Options", ["Window", "Stroke"], None, - "{weight?: pt, cap?: butt|round|square, join?: miter|round|bevel, miterLimit?, align?: center|inside|outside, dash?: [d,g,…]|null (a 0 dash with a round or projecting cap draws dots or squares; a new pattern keeps the current offset and alignment), dashOffset? (exact dashes only), alignDashes?: bool (true: dashes fitted to corners and path ends, every run between them holding whole periods with a dash centred on each corner and end; false, the default for a new pattern: exact lengths), startArrow?, endArrow?: Arrow|ArrowOpen|Barbed|Concave|DoubleArrow|HalfArrowLeft|HalfArrowRight|Chevron|Feather|Swallowtail|Triangle|TriangleOpen|TriangleReverse|TriangleBar|Kite|Leaf|Drop|Circle|CircleOpen|HalfCircle|Oval|Target|Square|SquareOpen|Tag|TagOpen|Diamond|DiamondOpen|Hexagon|HexagonOpen|Star|Cross|Plus|Bar|DoubleBar|DotOnBar|Slash|DoubleSlash|Bracket|Fork|null (HalfArrowLeft/Right: one barb, left or right of the direction the head points), arrowAlign?: \"extend\" (tip past the end point, default)|\"tip\" (tip on the end point; the stroke is shortened), profile?: \"uniform\"|\"lens\"|\"taperStart\"|\"taperEnd\"|\"pinch\"|\"teardrop\"|\"wave\"|a saved profile's name (stroke.widthProfile.list), widthPoints?, gradientMode? (as document.inspect reports them in strokeOptions, so those read back unchanged; ignored here: a custom profile is set with the Width tool, gradientMode with paint.editGradient), item?: stroke item index|null (omitted: the Appearance panel's active item if it is a stroke, else the top stroke, created when missing), ids?|id?, target?: \"object\"|\"contents\", aligned?: bool (index and item are rows of the first target's stack: each other target edits its equal one, as the Appearance panel does for a mixed selection)} Without a stroke item, type takes weight, cap, join, miterLimit, align (around its glyph outlines; the type stays live) and the dash options as its characters' stroke (every run; text.setRangeStyle `strokeOptions` styles a range), and images and symbol instances (also in groups) are left alone. With nothing selected (and no ids) they set up the next object drawn (appearance.newArt; weight always does) → {objects: how many objects took the stroke options}", + "{weight?: pt, cap?: butt|round|square, join?: miter|round|bevel, miterLimit?, align?: center|inside|outside, dash?: [d,g,…]|null (a 0 dash with a round or projecting cap draws dots or squares; a new pattern keeps the current offset and alignment), dashOffset? (exact dashes only), alignDashes?: bool (true: dashes fitted to corners and path ends, every run between them holding whole periods with a dash centred on each corner and end; false, the default for a new pattern: exact lengths), startArrow?, endArrow?: Arrow|ArrowOpen|LineArrow|Barbed|Concave|DoubleArrow|HalfArrowLeft|HalfArrowRight|Chevron|Feather|Swallowtail|Triangle|TriangleOpen|TriangleReverse|TriangleBar|Kite|Leaf|Drop|Circle|CircleOpen|HalfCircle|Oval|Target|Square|SquareOpen|Tag|TagOpen|Diamond|DiamondOpen|Hexagon|HexagonOpen|Star|Cross|Plus|Bar|DoubleBar|DotOnBar|Slash|DoubleSlash|Bracket|Fork|null (HalfArrowLeft/Right: one barb, left or right of the direction the head points), arrowAlign?: \"extend\" (tip past the end point, default)|\"tip\" (tip on the end point; the stroke is shortened), profile?: \"uniform\"|\"lens\"|\"taperStart\"|\"taperEnd\"|\"pinch\"|\"teardrop\"|\"wave\"|a saved profile's name (stroke.widthProfile.list), widthPoints?, gradientMode? (as document.inspect reports them in strokeOptions, so those read back unchanged; ignored here: a custom profile is set with the Width tool, gradientMode with paint.editGradient), item?: stroke item index|null (omitted: the Appearance panel's active item if it is a stroke, else the top stroke, created when missing), ids?|id?, target?: \"object\"|\"contents\", aligned?: bool (index and item are rows of the first target's stack: each other target edits its equal one, as the Appearance panel does for a mixed selection)} Without a stroke item, type takes weight, cap, join, miterLimit, align (around its glyph outlines; the type stays live) and the dash options as its characters' stroke (every run; text.setRangeStyle `strokeOptions` styles a range), and images and symbol instances (also in groups) are left alone. With nothing selected (and no ids) they set up the next object drawn (appearance.newArt; weight always does) → {objects: how many objects took the stroke options}", has_doc, stroke_set ), diff --git a/docs/mcp.md b/docs/mcp.md index 8f605eee0..574a0e811 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -3177,11 +3177,12 @@ and `stroke.widthProfile.reset` removes every saved one. `stroke.set {profile}` ## Arrowheads -`stroke.set {startArrow, endArrow}` takes any of 40 generated heads by name (the params doc lists them; `null` for -none): arrows (`Arrow`, `Barbed`, `Concave`, `DoubleArrow`, `HalfArrowLeft`/`Right`, `Chevron`, `Feather`, +`stroke.set {startArrow, endArrow}` takes any of 41 generated heads by name (the params doc lists them; `null` for +none): arrows (`Arrow`, `LineArrow`, `Barbed`, `Concave`, `DoubleArrow`, `HalfArrowLeft`/`Right`, `Chevron`, `Feather`, `Swallowtail`…), filled and open shapes (`Triangle`, `Circle`, `Oval`, `Target`, `Tag`, `Diamond`, `Hexagon`, `Star`, the `…Open` rings…) and marks (`Bar`, `DoubleBar`, `DotOnBar`, `Slash`, `DoubleSlash`, `Bracket`, `Fork`, `Cross`, -`Plus`). Each head fits a box about four times its weight (stroke weight × `arrowScale`) long and wide. +`Plus`). Each head fits a box about four times its weight (stroke weight × `arrowScale`) long and wide; `LineArrow`, two arms +of the line's weight at right angles, spreads twice as wide. ## Stroke bounds and clicks diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 2effc1142..e5e7cb97d 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1,6 +1,6 @@ # Parity with Adobe Illustrator -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-09 · **Change:** minor (Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; G5: UI Frames built-in symbol library with default 9-slice; bundled content ~32%; previously minor: G5: dynamic symbol part on-canvas opacity/blend/gradient/pattern; strokes remaining no longer lists Overlap/auto corners; symbols ~88%; previously minor: interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; previously minor: harakat paint fallback clarified; previously minor: current Guides feature-area rows consolidated using existing ruler evidence; scores and estimates unchanged; previously minor: Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; previously minor: G5 docs: Art brush Overlap and Pattern brush auto corners removed from strokes remaining; symbols remaining lists dynamic on-canvas beyond solid and guides outside the art; previously minor: custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: EPS import: embedded fonts (composite, Type 42, CID-keyed, Type 1, CFF FontSet) draw their glyphs; spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; `.ai`/EPS type keeps its tab stops, and right-aligned lines with tabs lay out as the file's page, #6147; `.ai`/EPS type from the text document lays its lines out closer to the file's page, within limits, #6137; `.ai` and EPS import: older Art brush objects read; numeric fields step on wheel hover without taking keyboard focus, and a panel wheel turn cannot seed a canvas zoom glide, #5949; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen; Tools stays beside the canvas with left-side panels; new gradients follow document color mode; Selection QoL: marquee modes and visible feedback, #5313; SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; the canvas keeps selection outlines and anchors between frames; Indian-language hyphenation review and license allowlist, #4947; Stroke gradients: an arrowhead aligned to the tip is painted to the corners of its base, #5964; Drop Shadow and glow dialogs: spinner arrows, #5367, #5368; Stroke Weight Up and Down shortcuts, #5797; Hindi catalog and contributor loanword preferences; full wording review in progress; Properties in Edit Artboards: a Transform section with the reference point above the Artboards section, #5219; the Control bar's Stroke: and Opacity: links scrub their fields, #4088; an image zoomed in on is separated, colorized or proofed only where it shows (#4155); a document's first frame renders on the render worker, so opening a heavy file no longer freezes the window, #4155; Control bar Paragraph section with type selected, #5372; translating unchanged type reuses its glyph and character-stroke outlines; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026; Units › East Asian Type acts, with Q and Ha units; scores unchanged) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-09 · **Change:** minor (Line Arrow arrowhead (#5421); scores and estimates unchanged; Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; G5: UI Frames built-in symbol library with default 9-slice; bundled content ~32%; previously minor: G5: dynamic symbol part on-canvas opacity/blend/gradient/pattern; strokes remaining no longer lists Overlap/auto corners; symbols ~88%; previously minor: interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; previously minor: harakat paint fallback clarified; previously minor: current Guides feature-area rows consolidated using existing ruler evidence; scores and estimates unchanged; previously minor: Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; previously minor: G5 docs: Art brush Overlap and Pattern brush auto corners removed from strokes remaining; symbols remaining lists dynamic on-canvas beyond solid and guides outside the art; previously minor: custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: EPS import: embedded fonts (composite, Type 42, CID-keyed, Type 1, CFF FontSet) draw their glyphs; spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; `.ai`/EPS type keeps its tab stops, and right-aligned lines with tabs lay out as the file's page, #6147; `.ai`/EPS type from the text document lays its lines out closer to the file's page, within limits, #6137; `.ai` and EPS import: older Art brush objects read; numeric fields step on wheel hover without taking keyboard focus, and a panel wheel turn cannot seed a canvas zoom glide, #5949; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen; Tools stays beside the canvas with left-side panels; new gradients follow document color mode; Selection QoL: marquee modes and visible feedback, #5313; SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; the canvas keeps selection outlines and anchors between frames; Indian-language hyphenation review and license allowlist, #4947; Stroke gradients: an arrowhead aligned to the tip is painted to the corners of its base, #5964; Drop Shadow and glow dialogs: spinner arrows, #5367, #5368; Stroke Weight Up and Down shortcuts, #5797; Hindi catalog and contributor loanword preferences; full wording review in progress; Properties in Edit Artboards: a Transform section with the reference point above the Artboards section, #5219; the Control bar's Stroke: and Opacity: links scrub their fields, #4088; an image zoomed in on is separated, colorized or proofed only where it shows (#4155); a document's first frame renders on the render worker, so opening a heavy file no longer freezes the window, #4155; Control bar Paragraph section with type selected, #5372; translating unchanged type reuses its glyph and character-stroke outlines; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026; Units › East Asian Type acts, with Q and Ha units; scores unchanged) ## Feature submission evidence @@ -41,6 +41,7 @@ Artboard Options forwards the active Artboard tool's Scale Artwork with Artboard | Date | Change | Summary | |---|---|---| +| 2026-11-09 | minor | Stroke panel: Line Arrow, a 41st generated arrowhead: two arms as thick as the line at right angles, a sharp tip and square-cut ends, twice as wide as the other heads; bounds and clicks follow each head's own reach (`stroke.set` `LineArrow`, #5421); scores and estimates unchanged | | 2026-11-07 | minor | File formats and Type core (#6147): `.ai`/EPS type keeps its paragraphs' tab stops (positions and alignments; leaders aren't read; at most 1,024 a paragraph and 2^20 a story), and right-aligned area type sets the text after a line's last tab against the right edge, the text before it at its stops; centred and justified lines are aligned as a whole as before; evidence added. Scores and estimates unchanged. | | 2026-11-07 | minor | File formats and Type core (#6137): `.ai`/EPS type from the text document keeps auto leading per line, its hyphenation options, may show more of the last lines the file sets in an area frame of its own, when growing the frame by its lines' deepest descent plus 0.05 pt shows them, and a line a soft return ends is justified; evidence added. Limits: frames grow only for area type of one frame, horizontal, by its lines' deepest descent plus 0.05 pt; type in a font that isn't installed, and hyphenation in a language that isn't imported, still lay out differently. Scores and estimates unchanged. | | 2026-11-08 | minor | Style panels: a double-click on a user character style's row opens its Character Style Options (`ui.charStyleOptions`), and on a paragraph style's row its Paragraph Style Options (rename); the Character Style Options dialog keeps a fixed width and scrolls its sections in a shorter window, so OK and Cancel stay on screen. Tests `panels::text_styles::tests::double_clicking_a_character_style_row_opens_its_options` and `dialogs::char_style_options::tests::the_sections_scroll_in_a_window_shorter_than_the_general_list`. Scores and estimates unchanged. | @@ -1231,7 +1232,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur Cmd/Ctrl-click on the Gradient panel's Gradient Fill box or on the Tools panel's Gradient button resets the gradient of the active Fill or Stroke to the linear White, Black gradient, as one undo step ([Default keyboard shortcuts](https://web.archive.org/web/20260726060015/https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html)). With a gradient stop or a freeform gradient point selected, Alt-clicking a color swatch in the Swatches panel recolors it instead of the inactive Fill or Stroke. - **Freeform gradient colors in their own model:** Object ▸ Expand and Object ▸ Create Gradient Mesh (`object.mesh.create`) mix a freeform gradient's points in the color model they share: points of C50 M20 Y0 K10 give mesh points of C50 M20 Y0 K10, and RGB points keep their values in a working RGB space other than sRGB. The canvas, the gradient chips and the SVG, PDF, EPS and EMF writers mix the points in that model too. PDF and EPS write a gradient whose points are all CMYK, or all Gray in a CMYK file, as a CMYK image, which PDF's Output settings convert as they convert any CMYK image. Points in different models mix in display RGB (on the canvas in a CMYK document that shows transparency, as the inks of each point), and a CMYK document stores the result of Expand as CMYK through the color settings. Points colored along CMYK stops (`paint.editGradient {stops}`, or a CMYK gradient switched to freeform) are CMYK, and so are a point `paint.freeform.splitLine` adds between CMYK points and a point `paint.freeform.addPoint` adds without `color` to CMYK points. Public reference: [Gradients](https://web.archive.org/web/20250714042311/https://helpx.adobe.com/illustrator/using/gradients.html) and [Meshes](https://web.archive.org/web/20250812201019/https://helpx.adobe.com/illustrator/using/meshes.html), which do not state how colors in different models mix; the rule is VectorCraft's own. - - **Strokes:** one stroke-geometry module shared by the canvas, SVG, PDF and Outline Stroke; dotted lines (zero-length dashes draw dots or squares); arrowheads with hollow outlines, tip-on-end or extend-past-end alignment and one opacity for line and head. SVG and PDF export arrowheads, width profiles, brushes and aligned strokes exactly as the canvas draws them; Outline Stroke and the live Outline Stroke effect outline every stroke item as drawn (dashes, heads, profiles, alignment); dashes can be fitted to corners and path ends; width profiles carry through dashes, and their corners take the join. The Stroke panel edits type's character strokes, follows the stroke units (its weight presets are round values in that unit: `0.25 mm`, `0.0625 in`, `1 px`, #426; fields show four decimals in mm, cm and inches, three in points), shows mixed values blank and opens from the Control bar. A width profile library, thirty more arrowheads, and Width Point Edit with multi-select and discontinuous points. Visual bounds and clicks cover the whole painted stroke. Scale Strokes & Effects, Scale Corners and Use Preview Bounds work everywhere, and new art inherits stroke options, a graphic style or the whole last appearance. + - **Strokes:** one stroke-geometry module shared by the canvas, SVG, PDF and Outline Stroke; dotted lines (zero-length dashes draw dots or squares); arrowheads with hollow outlines, tip-on-end or extend-past-end alignment and one opacity for line and head. SVG and PDF export arrowheads, width profiles, brushes and aligned strokes exactly as the canvas draws them; Outline Stroke and the live Outline Stroke effect outline every stroke item as drawn (dashes, heads, profiles, alignment); dashes can be fitted to corners and path ends; width profiles carry through dashes, and their corners take the join. The Stroke panel edits type's character strokes, follows the stroke units (its weight presets are round values in that unit: `0.25 mm`, `0.0625 in`, `1 px`, #426; fields show four decimals in mm, cm and inches, three in points), shows mixed values blank and opens from the Control bar. A width profile library, thirty-one more arrowheads (among them Line Arrow: two arms of the line's own weight at right angles, cut square, #5421), and Width Point Edit with multi-select and discontinuous points. Visual bounds and clicks cover the whole painted stroke. Scale Strokes & Effects, Scale Corners and Use Preview Bounds work everywhere, and new art inherits stroke options, a graphic style or the whole last appearance. - **Brushes (part of #852):** double-clicking a brush in the Brushes panel (or the panel menu's Brush Options…) opens Calligraphic Brush Options (Name; Angle, Roundness and Size, each Fixed, Random, Pressure, Tilt, Bearing or Rotation with its Variation; live previews of the nib and of a light–heavy–light stroke) or Bristle Brush Options (Shape and the bristle sliders); OK edits the brush and every stroke painted with it. The Paintbrush records the pen pressure along each stroke (kept with the path, saved, and joined when a stroke continues a path); a Calligraphic value on Pressure goes from value − variation at the lightest pressure to value + variation at the heaviest, on the canvas and in SVG, PDF and the other exports. Brushes saved before keep their look. Scatter Brush Options (Size, Spacing, Scatter and Rotation, each Fixed, Random between two values, Pressure from the first at the lightest pressure to the second at the heaviest, Tilt, Bearing or Rotation; Rotation relative to Page or Path), Art Brush Options (Width, Scale Proportionately, Stretch to Fit Stroke Length or Stretch Between Guides with the guides' start and end, the four Directions, Flip Along and Across, Overlap) and Pattern Brush Options (Scale, Spacing, the five tiles shown, Flip Along and Across, Stretch to fit, Add space to fit or Approximate path) each have Colorization (None, Tints, Tints and Shades, Hue Shift with its Key Color picked from the art's colours) and a live stroke preview; values from files and commands are clamped to their ranges. Art Brush Options › Overlap places art on each run between sharp corners so folds do not overlap (`ArtBrush.overlap`; `art_brush_overlap_splits_at_corners`). Still to come: Stylus Wheel, pen pressure on macOS where the windowing layer reports none, and tilt, bearing and rotation for Art, Pattern and Bristle brushes and on native Wayland and the web. Under Tints and Tints and Shades, art in black ink alone goes by its ink amount, and a process or gray stroke color's tints and shades stay in that color's model; these are VectorCraft's own rules. Type in a scatter brush's art takes the brush's colorization as its paths do: Tints, Tints and Shades and Hue Shift recolor the characters' solid fills and strokes in each copy the brush draws, and Brush Options' Key Color list, whose first color is Hue Shift's default key, counts the characters' fills and their strokes wider than 0. The Brushes page says that a scatter, art or pattern brush takes its colors from the current stroke color and the brush's colorization method () and does not mention type there; recoloring the characters' paints like the art's fills and strokes is VectorCraft's own rule. The Symbol Stainer mixes the characters' strokes of type toward the stain as it mixes their fills. Evidence: `tests::scatter_colorization_recolors_type`, `tests::art_colors_list_the_most_used_first`, `tests::stain_mixes_type_strokes_too` (vectorcraft-brush). From 0153a5d33e9f0ebe20e0565206f309450663ea2d Mon Sep 17 00:00:00 2001 From: Julius van der Vaart Date: Mon, 9 Nov 2026 21:17:41 +0100 Subject: [PATCH 11/38] Stroke: document where the stroke stops under a local arrowhead (#5421) --- crates/effects/src/stroke/arrow.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/effects/src/stroke/arrow.rs b/crates/effects/src/stroke/arrow.rs index ac64cb30d..603f46e28 100644 --- a/crates/effects/src/stroke/arrow.rs +++ b/crates/effects/src/stroke/arrow.rs @@ -252,6 +252,7 @@ const HEX: f64 = 2.0 * 1.732_050_807_568_877_2; /// Flattening tolerance of the curved local heads, in units. const LOCAL_TOL: f64 = 1e-3; +/// Where the stroke ends under a head drawn in local units ([`local`]), in those units. fn local_stop(kind: Arrowhead) -> Stop { match kind { Arrowhead::Target | Arrowhead::TagOpen | Arrowhead::DiamondOpen => Stop::Wall(4.0), From ef25dffe013d0a5c4f7dd4b71e5425ef6b70eb01 Mon Sep 17 00:00:00 2001 From: Christian Lappin <36496308+christianlappin@users.noreply.github.com> Date: Mon, 9 Nov 2026 15:31:35 -0500 Subject: [PATCH 12/38] fix(mcp): validate and cancel Rotate View gestures (#6322) --- crates/mcp/src/tests.rs | 54 ++++++++++++ crates/ui-egui/src/app.rs | 1 + crates/ui-egui/src/app/commands.rs | 4 + crates/ui-egui/src/control.rs | 109 ++++++++++++++++++------- crates/ui-egui/src/lib.rs | 2 + crates/ui-egui/src/tests/rotateview.rs | 87 +++++++++++++++++++- docs/mcp.md | 2 + 7 files changed, 228 insertions(+), 31 deletions(-) diff --git a/crates/mcp/src/tests.rs b/crates/mcp/src/tests.rs index e986deda0..b12f44680 100644 --- a/crates/mcp/src/tests.rs +++ b/crates/mcp/src/tests.rs @@ -1463,6 +1463,60 @@ fn pointer_gesture_forwards_backquote_on_each_event() { assert_eq!(rect["bounds"]["x"], 100.0, "{doc}"); } +/// The documented Rotate View calls reach the desktop control methods, preserving document +/// coordinates and modifiers, and a successful null reset remains a successful MCP response. +#[test] +fn rotate_view_calls_forward_to_desktop_control() { + use std::cell::RefCell; + use std::rc::Rc; + + struct Recording(Rc>>); + impl Backend for Recording { + fn call(&mut self, method: &str, params: Value) -> Result { + self.0.borrow_mut().push((method.into(), params)); + Ok(Value::Null) + } + fn has_ui(&self) -> bool { + true + } + fn describe(&self) -> String { + "desktop control contract".into() + } + } + + let seen = Rc::new(RefCell::new(Vec::new())); + let mut s = Server::new(Box::new(Recording(seen.clone()))); + let selected = call(&mut s, 1, "select_tool", json!({"tool": "rotateView"})); + let turned = call( + &mut s, + 2, + "pointer_gesture", + json!({"events": [ + {"kind": "down", "x": 300, "y": 150}, {"kind": "drag", "x": 200, "y": 250}, {"kind": "up", "x": 200, "y": 250} + ], "mods": {"shift": true}}), + ); + let reset = call(&mut s, 3, "command_run", json!({"id": "view.rotateReset", "params": {}})); + for reply in [&selected, &turned, &reset] { + assert_eq!(reply["isError"], false, "{reply}"); + } + assert_eq!(text_of(&reset), "null"); + assert_eq!( + *seen.borrow(), + vec![ + ("ui.tool.select".into(), json!({"tool": "rotateView"})), + ( + "ui.pointer".into(), + json!({"events": [ + {"kind": "down", "x": 300, "y": 150, "space": "doc"}, + {"kind": "drag", "x": 200, "y": 250, "space": "doc"}, + {"kind": "up", "x": 200, "y": 250, "space": "doc"} + ], "mods": {"shift": true}}) + ), + ("engine.execute".into(), json!({"command": "view.rotateReset", "params": {}})) + ] + ); +} + /// A `backquote` that is not a boolean is an error, and none of the gesture's events runs. #[test] fn pointer_gesture_rejects_a_backquote_that_is_not_a_boolean() { diff --git a/crates/ui-egui/src/app.rs b/crates/ui-egui/src/app.rs index d02c59728..32e020523 100644 --- a/crates/ui-egui/src/app.rs +++ b/crates/ui-egui/src/app.rs @@ -123,6 +123,7 @@ impl VectorcraftApp { updates_rt: Default::default(), frame_rate: Default::default(), canvas_rect: None, + control_rotate_press: None, hover_doc: None, titlebar_from_env: false, custom_titlebar: false, diff --git a/crates/ui-egui/src/app/commands.rs b/crates/ui-egui/src/app/commands.rs index d5ca8a196..9e698b1bf 100644 --- a/crates/ui-egui/src/app/commands.rs +++ b/crates/ui-egui/src/app/commands.rs @@ -404,6 +404,10 @@ impl VectorcraftApp { if let Err(e) = switched { self.ui.status = e.to_string(); } + if before != self.session.tool_id() { + // A control gesture cannot resume after another tool has owned the pointer. + self.control_rotate_press = None; + } // A newly chosen Artboard tool starts on the active artboard (the view's). if before != "artboard" && self.session.tool_id() == "artboard" diff --git a/crates/ui-egui/src/control.rs b/crates/ui-egui/src/control.rs index bb2918d13..4b2b6f867 100644 --- a/crates/ui-egui/src/control.rs +++ b/crates/ui-egui/src/control.rs @@ -610,9 +610,9 @@ mod tests; /// The window and coordinate mapping where a control-channel Rotate View drag began. #[derive(Clone, Copy)] -struct RotateViewPress { +pub(crate) struct RotateViewPress { window: (u64, u32), - tool_resets: u64, + document_resets: u64, xf: Xf, start_angle: f64, start_rotation: f64, @@ -622,53 +622,102 @@ struct RotateViewPress { /// across requests so document-coordinate drags do not move again as the view turns underneath. fn rotate_view_pointer( app: &mut VectorcraftApp, - ctx: &egui::Context, kind: PointerKind, position: vectorcraft_geom::Point, screen: bool, shift: bool, ) -> Result<(), String> { - let id = egui::Id::unique("control.rotateView"); - match kind { - PointerKind::Up => { - ctx.data_mut(|d| d.remove::(id)); - return Ok(()); - } - PointerKind::Move | PointerKind::DoubleClick => return Ok(()), - _ => {} - } let window = app.session.active().zip(crate::doc_windows::current(app)).map(|(d, w)| (d.uid, w.1)).ok_or("no document")?; - let tool_resets = app.session.tool_resets(); - let mut press = if kind == PointerKind::Down { - let rect = app.canvas_rect.ok_or("no canvas yet")?; - let view = app.view().ok_or("no document")?; - RotateViewPress { window, tool_resets, xf: Xf::new(rect, view), start_angle: 0.0, start_rotation: view.rotation } - } else { - ctx.data(|d| d.get_temp::(id)).ok_or("Rotate View needs a down event before a drag")? + let document_resets = app.session.tool_resets(); + let stored = if kind == PointerKind::Down { None } else { app.control_rotate_press }; + if kind == PointerKind::Drag && stored.is_none() { + return Err("Rotate View needs a down event before a drag".into()); + } + let mut press = match stored { + Some(press) => press, + None => { + let rect = app.canvas_rect.ok_or("no canvas yet")?; + let view = app.view().ok_or("no document")?; + RotateViewPress { window, document_resets, xf: Xf::new(rect, view), start_angle: 0.0, start_rotation: view.rotation } + } }; - // A gesture cannot turn a different document/view or resume after another tool was chosen. - if (press.window, press.tool_resets) != (window, tool_resets) { - ctx.data_mut(|d| d.remove::(id)); - return Err("the Rotate View gesture's window or tool changed".into()); + // A gesture cannot turn a different document/view or resume after its document was reset. + if (press.window, press.document_resets) != (window, document_resets) { + return Err("the Rotate View gesture's window or document changed".into()); } let point = if screen { egui::pos2(position.x as f32, position.y as f32) } else { press.xf.to_screen(position) }; let center = press.xf.rect.center(); if !point.is_finite() || !center.is_finite() { return Err("Rotate View needs finite canvas coordinates".into()); } - if kind == PointerKind::Down { - press.start_angle = (point.y - center.y).atan2(point.x - center.x) as f64; - ctx.data_mut(|d| d.insert_temp(id, press)); - } else { - app.view_mut().ok_or("no document")?.rotation = crate::canvas::drag_rotation(center, point, press.start_angle, press.start_rotation, shift); + match kind { + PointerKind::Down => { + press.start_angle = (point.y - center.y).atan2(point.x - center.x) as f64; + app.control_rotate_press = Some(press); + } + PointerKind::Drag => { + app.view_mut().ok_or("no document")?.rotation = + crate::canvas::drag_rotation(center, point, press.start_angle, press.start_rotation, shift); + } + PointerKind::Up => { + app.control_rotate_press = None; + } + PointerKind::Move | PointerKind::DoubleClick => {} + } + Ok(()) +} + +/// A Rotate View packet accepts only the documented boolean modifier fields. +fn validate_rotate_view_mods(value: Option<&Value>) -> Result<(), String> { + let Some(value) = value else { return Ok(()) }; + let Some(fields) = value.as_object() else { return Err("Rotate View `mods` must be an object of booleans".into()) }; + if fields.iter().any(|(key, value)| !matches!(key.as_str(), "shift" | "alt" | "cmd" | "ctrl" | "space") || !value.is_boolean()) { + return Err("Rotate View `mods` accepts only boolean shift, alt, cmd, ctrl and space".into()); + } + Ok(()) +} + +/// Reject malformed coordinates and modifiers before any event can change the angle or press. +fn validate_rotate_view_events(params: &Value, events: &[Value]) -> Result<(), String> { + validate_rotate_view_mods(params.get("mods"))?; + for event in events { + for axis in ["x", "y"] { + if event.get(axis).and_then(Value::as_f64).filter(|value| value.is_finite()).is_none() { + return Err(format!("Rotate View `{axis}` must be a finite number")); + } + } + validate_rotate_view_mods(event.get("mods"))?; + if event.get("space").is_some_and(|space| !matches!(space.as_str(), Some("doc" | "screen"))) { + return Err("Rotate View `space` must be doc or screen".into()); + } } Ok(()) } /// `ui.pointer`: pointer events through the tools. fn pointer_events(app: &mut VectorcraftApp, ctx: &egui::Context, req: &ControlRequest) -> Outcome { + let rotating = app.session.tool_id() == "rotateView"; + let before = if rotating { app.view().map(|view| view.rotation) } else { None }; + let outcome = pointer_events_inner(app, ctx, req); + if rotating && matches!(&outcome, Outcome::Done(reply) if reply.get("ok").and_then(Value::as_bool) == Some(false)) { + // A failed packet must neither leave a partial turn nor allow a later drag to resume it. + app.control_rotate_press = None; + if let (Some(before), Some(view)) = (before, app.view_mut()) { + view.rotation = before; + } + ctx.request_repaint(); + } + outcome +} + +/// Apply a pointer packet after Rotate View validation; other tools retain their existing routing. +fn pointer_events_inner(app: &mut VectorcraftApp, ctx: &egui::Context, req: &ControlRequest) -> Outcome { let p = &req.params; let Some(events) = p.get("events").and_then(Value::as_array) else { return err("missing `events`") }; + let rotating = app.session.tool_id() == "rotateView"; + if rotating && let Err(error) = validate_rotate_view_events(p, events) { + return err(error); + } let base_mods: Mods = p.get("mods").and_then(|m| serde_json::from_value(m.clone()).ok()).unwrap_or_default(); let view = app.view_info(); let xf = app.canvas_rect.zip(app.view().copied()).map(|(rect, v)| Xf::new(rect, &v)); @@ -692,8 +741,8 @@ fn pointer_events(app: &mut VectorcraftApp, ctx: &egui::Context, req: &ControlRe let y = e.get("y").and_then(Value::as_f64).unwrap_or(0.0); let screen = e.get("space").and_then(Value::as_str) == Some("screen"); let mods = e.get("mods").and_then(|m| serde_json::from_value(m.clone()).ok()).unwrap_or(base_mods); - if app.session.tool_id() == "rotateView" { - if let Err(e) = rotate_view_pointer(app, ctx, kind, vectorcraft_geom::Point::new(x, y), screen, mods.shift) { + if rotating { + if let Err(e) = rotate_view_pointer(app, kind, vectorcraft_geom::Point::new(x, y), screen, mods.shift) { return err(e); } continue; diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 03acb7597..52ad116ba 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -186,6 +186,8 @@ pub struct VectorcraftApp { frame_rate: frame_rate::FrameRate, /// Canvas rect of the last frame (screen points), for control-channel coordinate mapping. pub canvas_rect: Option, + /// An unfinished control-channel Rotate View gesture, kept only for this app instance. + pub(crate) control_rotate_press: Option, /// Hover position in document coordinates. pub hover_doc: Option, /// System clipboard: SVG to publish next frame, the last SVG we published (so pasting it back diff --git a/crates/ui-egui/src/tests/rotateview.rs b/crates/ui-egui/src/tests/rotateview.rs index a709a767e..296ab7674 100644 --- a/crates/ui-egui/src/tests/rotateview.rs +++ b/crates/ui-egui/src/tests/rotateview.rs @@ -30,6 +30,13 @@ fn with_document() -> VectorcraftApp { app } +/// Send a control request and keep its success/error value for protocol assertions. +fn control_response(app: &mut VectorcraftApp, ctx: &egui::Context, method: &str, params: Value) -> Value { + let (request, _) = crate::control::ControlRequest::new(method, params); + let crate::control::Outcome::Done(reply) = crate::control::handle(app, ctx, &request) else { panic!("not done") }; + reply +} + /// Returns the labels of the View › Rotate View items that are checked. fn checked(app: &VectorcraftApp) -> Vec<&'static str> { ANGLES.iter().filter(|(_, a)| menus::checked(app, "view.rotate", &json!({ "angle": a })) == Some(true)).map(|(l, _)| *l).collect() @@ -69,6 +76,8 @@ fn control_rotate_view_gesture_turns_the_canvas_and_reset_restores_it() { let send = super::synthetic::control; send(&mut app, &ctx, "ui.tool.select", json!({"tool": "rotateView"})); send(&mut app, &ctx, "ui.pointer", json!({"events": [{"kind": "down", "x": 300, "y": 150}]})); + let mut frame = ctx.run_ui(Default::default(), |_| {}); + frame.textures_delta.clear(); send( &mut app, &ctx, @@ -83,10 +92,13 @@ fn control_rotate_view_gesture_turns_the_canvas_and_reset_restores_it() { assert_eq!(view.center, vectorcraft_geom::Point::new(200.0, 150.0)); assert_eq!(view.zoom, 1.0); assert_eq!(app.session.active().unwrap().revision, revision, "navigation does not edit the document"); - send(&mut app, &ctx, "engine.execute", json!({"command": "view.rotateReset", "params": {}})); + let reset = control_response(&mut app, &ctx, "engine.execute", json!({"command": "view.rotateReset", "params": {}})); + assert_eq!(reset["ok"], true); + assert!(reset["result"].is_null()); assert_eq!(app.view().unwrap().rotation, 0.0); } +/// Repeated document points stay fixed across requests, including Shift snapping from a turned view. #[test] fn control_rotate_view_keeps_press_coordinates_and_shift_snapping_across_requests() { let mut app = with_document(); @@ -109,6 +121,7 @@ fn control_rotate_view_keeps_press_coordinates_and_shift_snapping_across_request assert_eq!(app.view().unwrap().rotation, 45.0); } +/// Invalid coordinates and document changes cannot turn a different view. #[test] fn control_rotate_view_rejects_nonfinite_positions_and_a_changed_document() { let mut app = with_document(); @@ -128,6 +141,78 @@ fn control_rotate_view_rejects_nonfinite_positions_and_a_changed_document() { assert_eq!(app.view().unwrap().rotation, 0.0); } +/// An invalid packet leaves the angle unchanged and requires a fresh press, including errors +/// after an otherwise valid drag and malformed releases or hovers. +#[test] +fn control_rotate_view_rejects_malformed_packets_without_leaving_a_press() { + let drag = json!({"kind": "drag", "x": 200, "y": 250}); + for packet in [ + json!({"events": [drag.clone(), {"kind": "drag", "y": 250}]}), + json!({"events": [drag.clone(), {"kind": "drag", "x": "200", "y": 250}]}), + json!({"events": [drag.clone(), {"kind": "drag", "x": 200, "y": 250, "mods": {"shift": "yes"}}]}), + json!({"mods": "shift", "events": [drag.clone()]}), + json!({"events": [drag.clone(), {"kind": "drag", "x": 200, "y": 250, "mods": {"unknown": true}}]}), + json!({"events": [drag.clone(), {"kind": "drag", "x": 1e300, "y": 250}]}), + json!({"events": [drag.clone(), {"kind": "up", "x": 1e300, "y": 250}]}), + json!({"events": [drag.clone(), {"kind": "move", "x": 1e300, "y": 250}]}), + json!({"events": [drag.clone(), {"kind": "doubleclick", "x": 1e300, "y": 250}]}), + json!({"events": [drag.clone(), {"kind": "wiggle", "x": 200, "y": 250}]}), + json!({"events": [drag.clone(), {"kind": "drag", "x": 200, "y": 250, "space": "window"}]}), + json!({"events": null}), + ] { + let mut app = with_document(); + let ctx = egui::Context::default(); + app.canvas_rect = Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(800.0, 600.0))); + let view = app.view_mut().unwrap(); + view.center = vectorcraft_geom::Point::new(200.0, 150.0); + view.zoom = 1.0; + super::synthetic::control(&mut app, &ctx, "ui.tool.select", json!({"tool": "rotateView"})); + super::synthetic::control(&mut app, &ctx, "ui.pointer", json!({"events": [{"kind": "down", "x": 300, "y": 150}]})); + let reply = control_response(&mut app, &ctx, "ui.pointer", packet.clone()); + assert_eq!(reply["ok"], false, "{packet}: {reply}"); + assert_eq!(app.view().unwrap().rotation, 0.0, "{packet}"); + let resume = control_response(&mut app, &ctx, "ui.pointer", json!({"events": [drag.clone()]})); + assert_eq!(resume["ok"], false, "a failed packet cancels its press: {packet}"); + assert_eq!(app.view().unwrap().rotation, 0.0); + } +} + +/// Screen-space gestures survive frames, stop on release or a tool change, and start afresh +/// from the current angle. A press at the pivot uses the same finite convention as the mouse. +#[test] +fn control_rotate_view_screen_events_require_a_live_press() { + let mut app = with_document(); + let ctx = egui::Context::default(); + app.canvas_rect = Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(800.0, 600.0))); + let view = app.view_mut().unwrap(); + view.center = vectorcraft_geom::Point::new(1200.0, 900.0); + view.zoom = 2.0; + let send = super::synthetic::control; + send(&mut app, &ctx, "ui.tool.select", json!({"tool": "rotateView"})); + let down = json!({"events": [{"kind": "down", "x": 500, "y": 300, "space": "screen"}]}); + let drag = json!({"events": [{"kind": "drag", "x": 400, "y": 400, "space": "screen"}]}); + assert_eq!(control_response(&mut app, &ctx, "ui.pointer", drag.clone())["ok"], false); + send(&mut app, &ctx, "ui.pointer", down.clone()); + let mut frame = ctx.run_ui(Default::default(), |_| {}); + frame.textures_delta.clear(); + send(&mut app, &ctx, "ui.pointer", drag.clone()); + assert!((app.view().unwrap().rotation - 90.0).abs() < 1e-3); + send(&mut app, &ctx, "ui.pointer", json!({"events": [{"kind": "up", "x": 400, "y": 400, "space": "screen"}]})); + assert_eq!(control_response(&mut app, &ctx, "ui.pointer", drag.clone())["ok"], false, "release ends the press"); + send(&mut app, &ctx, "ui.pointer", down.clone()); + send(&mut app, &ctx, "ui.tool.select", json!({"tool": "selection"})); + send(&mut app, &ctx, "ui.tool.select", json!({"tool": "rotateView"})); + assert_eq!(control_response(&mut app, &ctx, "ui.pointer", drag.clone())["ok"], false, "a tool change ends the press"); + send(&mut app, &ctx, "ui.pointer", down); + send(&mut app, &ctx, "ui.pointer", drag); + assert!((app.view().unwrap().rotation.abs() - 180.0).abs() < 1e-3, "a new press starts at the current angle"); + let before = app.view().unwrap().rotation; + send(&mut app, &ctx, "ui.pointer", json!({"events": [{"kind": "down", "x": 400, "y": 300, "space": "screen"}]})); + send(&mut app, &ctx, "ui.pointer", json!({"events": [{"kind": "drag", "x": 500, "y": 300, "space": "screen"}]})); + assert!(app.view().unwrap().rotation.is_finite()); + assert!((app.view().unwrap().rotation - before).abs() < 1e-3); +} + #[test] fn the_view_menu_lists_the_status_bar_angles_and_checks_the_current_one() { let mut app = with_document(); diff --git a/docs/mcp.md b/docs/mcp.md index d8a5f3759..28c62b279 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -4014,6 +4014,8 @@ angle it set, normalized to (-180, 180], and `view.rotateReset` turns the view b Select the Rotate View Tool with `select_tool` and the tool id `rotateView`. Its `pointer_gesture` drags turn the canvas about its center; Shift snaps to 15°. Document coordinates for a rotation gesture use the view at the initial down event, including when the gesture spans several calls. +The control channel rejects malformed rotation packets without changing the angle and cancels +the current press. Start a new down event after an error. Reset Rotate View is the command `view.rotateReset`: ```json From f94b0f46d30f6be87a24b64c578f5e77d645bfd0 Mon Sep 17 00:00:00 2001 From: Christian Lappin <36496308+christianlappin@users.noreply.github.com> Date: Mon, 9 Nov 2026 17:13:40 -0500 Subject: [PATCH 13/38] docs(tests): describe thumbnail queue regression (#6318) --- crates/ui-egui/src/panels/layers/thumbs.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/ui-egui/src/panels/layers/thumbs.rs b/crates/ui-egui/src/panels/layers/thumbs.rs index 2a6d010b9..2f1587813 100644 --- a/crates/ui-egui/src/panels/layers/thumbs.rs +++ b/crates/ui-egui/src/panels/layers/thumbs.rs @@ -928,6 +928,7 @@ mod tests { thumbs.receive(ctx, done); } + /// Prioritize the shown document’s thumbnails, requeue them when revisited, and retain cached rows. #[test] fn the_shown_documents_thumbnails_are_made_before_the_queued_ones_of_another() { let ctx = egui::Context::default(); From c67a6241286de0da5377d69fa1b043a78420058c Mon Sep 17 00:00:00 2001 From: jmverges Date: Tue, 10 Nov 2026 02:40:37 +0000 Subject: [PATCH 14/38] .ai import: a note for type in an envelope (part of #3179) An envelope of type keeps its drawn art with its type hidden, but nothing in the import notes said so, while the other envelopes that keep their drawn art do. Opening such a file now notes that the type in an envelope comes in as the envelope's drawn art, not as editable type, and that the type is kept hidden. The type is still not editable in the envelope. Co-Authored-By: Claude Opus 5.5 --- crates/eps/src/import/ai.rs | 2 ++ crates/eps/src/import/ai/envelope.rs | 2 ++ crates/eps/src/import/ai/read/containers.rs | 1 + crates/eps/src/import/tests_native.rs | 3 ++- docs/file-format-parity.md | 3 ++- docs/target-app-parity.md | 2 +- 6 files changed, 10 insertions(+), 3 deletions(-) diff --git a/crates/eps/src/import/ai.rs b/crates/eps/src/import/ai.rs index b353a0088..5f4a5b631 100644 --- a/crates/eps/src/import/ai.rs +++ b/crates/eps/src/import/ai.rs @@ -37,6 +37,8 @@ use crate::ps; pub(crate) use envelope::DRAWN as ENVELOPE_DRAWN; #[cfg(test)] pub(crate) use envelope::MAX_CHECKED; +#[cfg(test)] +pub(crate) use envelope::TYPE_DRAWN as ENVELOPE_TYPE_DRAWN; pub(crate) use envelope::{WARPED_TYPE_DRAWN, WarpedType}; pub(crate) use frames::text_frames; pub use obj::{NameEncoding, Names}; diff --git a/crates/eps/src/import/ai/envelope.rs b/crates/eps/src/import/ai/envelope.rs index a421bbd6a..fa1f61e69 100644 --- a/crates/eps/src/import/ai/envelope.rs +++ b/crates/eps/src/import/ai/envelope.rs @@ -41,6 +41,8 @@ use crate::import::native::compare::{MAX_DIFFERENCE, MAX_SOLID_DIFFERENCE, compa pub(super) const PLUGIN: &str = "Adobe Deform Plugin"; // brand-ok: the plugin name the files use /// The note for an envelope that keeps its drawn art. pub(crate) const DRAWN: &str = "an envelope VectorCraft couldn't read as a live envelope comes in as its drawn art"; +/// The note for an envelope of type, which keeps its drawn art with its type hidden. +pub(crate) const TYPE_DRAWN: &str = "type in an envelope comes in as the envelope's drawn art, not as editable type: the type is kept, hidden"; /// The note for warped type that keeps its drawn art, before how many. pub(crate) const WARPED_TYPE_DRAWN: &str = "warped text objects that came in as drawn art, not as editable type"; /// The most envelopes waiting for their drawn art at once. diff --git a/crates/eps/src/import/ai/read/containers.rs b/crates/eps/src/import/ai/read/containers.rs index 35c0f46ae..3de4daadf 100644 --- a/crates/eps/src/import/ai/read/containers.rs +++ b/crates/eps/src/import/ai/read/containers.rs @@ -519,6 +519,7 @@ impl<'a> Reader<'a> { // An envelope of type keeps its drawn art and its type, hidden, as above: the // type is filled in after reading, which the envelope can't wait for. if typed { + self.warn(crate::import::ai::envelope::TYPE_DRAWN); return Ok(()); } if self.envelopes.len() >= crate::import::ai::envelope::MAX_PENDING { diff --git a/crates/eps/src/import/tests_native.rs b/crates/eps/src/import/tests_native.rs index 13f979f9f..931717655 100644 --- a/crates/eps/src/import/tests_native.rs +++ b/crates/eps/src/import/tests_native.rs @@ -5222,7 +5222,8 @@ fn envelope_options_keep_the_source_text_hidden_under_its_outlined_appearance() } }); assert_eq!(texts, [("CHONKYCATS".to_string(), false)], "{:?}", r.warnings); - assert!(super::tests::program_warnings(&r.warnings).is_empty(), "{:?}", r.warnings); + // The notes say the type isn't editable. + assert_eq!(super::tests::program_warnings(&r.warnings), [crate::import::ai::ENVELOPE_TYPE_DRAWN], "{:?}", r.warnings); } /// A live envelope that draws a little apart from its drawn art, in layers that already differ diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index a04c8926e..702164071 100644 --- a/docs/file-format-parity.md +++ b/docs/file-format-parity.md @@ -1,6 +1,6 @@ # File format parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-09 · **Change:** minor (PDF import: Separation images keep their ink, #6314; PDF editable import preserves authored ActualText once, including empty/nested spans and authored ordering (#6165); cross-line/style layout remains approximate; scores and estimates unchanged; PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; PDF import: image masks open colorized by the color they are painted in, #6313; EPS and `.ai` editing data: a ligature the page names by a private-use glyph matches its text object; EPS import: CIDFonts only named, over a Unicode CMap, set as live type; EPS import: CMap mappings to base-font codes and glyph names; EPS import: CID-keyed fonts in FontSets (CFF) draw their glyphs; EPS import: `kshow` procedures kern live type; EPS import: embedded Type 1 fonts' glyphs and widths; EPS import: CID-keyed fonts draw their glyphs; EPS import: Type 42 fonts' characters and widths from the font they carry, show operators' spacing kept as tracking; EPS import: an image decoder's own buffers stop counting against the memory budget once it is decoded; EPS import: sampled functions interpolate between their samples; `.ai` and EPS type from the text document keeps its tab stops; it keeps auto leading and hyphenation options, and may show more of the file's last lines in an area frame; `.ai` and EPS import: Art brushes in the older form of their objects come into the Brushes panel; EPS import: gradients drawn as abutting strips come in as one gradient, without seams; EPS import: composite fonts draw their Type 42 glyphs; SVG import: filter and mask regions clip, #5876, #5875; SVG import: Gaussian blurs keep `color-interpolation-filters`, #5877; PDF and `.ai` export: objects' URLs as links, #5785; SVG import: `color-interpolation="linearRGB"` gradients, #5878; `.ai` import: paths with a Calligraphic brush open brushed, #372) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-10 · **Change:** minor (`.ai` and EPS import: a note for type in an envelope, #3179; PDF import: Separation images keep their ink, #6314; PDF editable import preserves authored ActualText once, including empty/nested spans and authored ordering (#6165); cross-line/style layout remains approximate; scores and estimates unchanged; PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; PDF import: image masks open colorized by the color they are painted in, #6313; EPS and `.ai` editing data: a ligature the page names by a private-use glyph matches its text object; EPS import: CIDFonts only named, over a Unicode CMap, set as live type; EPS import: CMap mappings to base-font codes and glyph names; EPS import: CID-keyed fonts in FontSets (CFF) draw their glyphs; EPS import: `kshow` procedures kern live type; EPS import: embedded Type 1 fonts' glyphs and widths; EPS import: CID-keyed fonts draw their glyphs; EPS import: Type 42 fonts' characters and widths from the font they carry, show operators' spacing kept as tracking; EPS import: an image decoder's own buffers stop counting against the memory budget once it is decoded; EPS import: sampled functions interpolate between their samples; `.ai` and EPS type from the text document keeps its tab stops; it keeps auto leading and hyphenation options, and may show more of the file's last lines in an area frame; `.ai` and EPS import: Art brushes in the older form of their objects come into the Brushes panel; EPS import: gradients drawn as abutting strips come in as one gradient, without seams; EPS import: composite fonts draw their Type 42 glyphs; SVG import: filter and mask regions clip, #5876, #5875; SVG import: Gaussian blurs keep `color-interpolation-filters`, #5877; PDF and `.ai` export: objects' URLs as links, #5785; SVG import: `color-interpolation="linearRGB"` gradients, #5878; `.ai` import: paths with a Calligraphic brush open brushed, #372) · **Target:** Adobe Illustrator 2026 (30.x) Every format Illustrator opens, places, saves or exports, against VectorCraft's format table (`FORMATS`, `OPEN_EXTS`, `PLACE_EXTS` in `crates/engine/src/cmd/fileio/formats.rs`). Illustrator's list comes from its public @@ -324,6 +324,7 @@ Names in the editing data that aren't UTF-8 (layers, objects, artboards, colors, | Date | Change | Summary | |---|---|---| +| 2026-11-10 | minor | `.ai` and EPS import: an envelope of type, which keeps its drawn art with its type hidden, now says so in the import notes ("type in an envelope comes in as the envelope's drawn art, not as editable type: the type is kept, hidden"); nothing said it before, as the other envelopes that keep their drawn art do. The type is still not editable in the envelope (part of #3179). Test `tests_native::envelope_options_keep_the_source_text_hidden_under_its_outlined_appearance` | | 2026-11-09 | minor | PDF import: an image in a Separation color space (one ink, ISO 32000-1 §8.6.6.4) opens as a grayscale image of its tints (white where it lays no ink, black at 100 %, its Decode array applied; 1, 2, 4, 8 and 16 bits a sample, or one gray channel from its JPEG, CCITT, JBIG2 or JPEG 2000 decoder) colorized by its ink at 100 %: a spot ink's swatch, a process plate's CMYK color, or Registration. It used to open as an RGB image, losing the ink, and Fill and Recolor Artwork didn't reach its color. Its soft mask or mask becomes its opacity mask, as a CMYK image's does. The ink is found by the image's color space (kind, colorant name and tint transform), once per image. As with any colorized image, where it lays no ink the art under it shows, where the RGB image was opaque white. The ink `None`, which paints nothing, an ink whose color can't be told, an image inside an alpha soft mask, inline images and DeviceN images open as before; samples that can't be read (data cut short, a decoder's output of another layout) open in RGB with a warning, and the ink gets no swatch. Tests `colorize::a_separation_image_opens_colorized_by_its_ink`, `colorize::a_separation_image_reads_its_samples_and_decode_array`, `colorize::a_separation_image_of_a_process_plate_or_of_no_ink`, `colorize::a_separation_image_keeps_its_soft_mask`, `colorize::a_separation_image_drawn_twice_is_colorized_twice`, `colorize::a_separation_jpeg_opens_colorized_by_its_ink`, `colorize::a_separation_image_that_cant_be_read_stays_rgb` (#6314) | | 2026-11-09 | minor | PDF import: an image mask (`/ImageMask true`, ISO 32000-1 §8.9.6.2) painted in a solid color opens as a grayscale image of what it paints (black, the rest white) colorized by that color, as the `.ai` reader opens a 1-bit image mask (#5790). It used to open as an RGB image with the color in its pixels: Fill and Recolor Artwork didn't reach the color, a spot color lost its ink, and the same mask painted again in another color took the first color, in an uncolored pattern's cell too. Its color now counts toward the document's color mode, as a fill's does. A spot color is found by its color space, as a fill's is when the page's walk names no ink: two spaces of other inks with the same alternate and tint transform can't be told apart, and the color comes in as RGB. Inside an alpha soft mask, or painted with a pattern, a mask opens as before. Tests `colorize::an_image_mask_opens_colorized_by_its_spot_color`, `colorize::an_image_mask_drawn_in_two_colors_keeps_both`, `colorize::an_uncolored_pattern_drawing_an_image_mask_keeps_each_color` (#6313) | | 2026-11-09 | minor | PDF editable import preserves authored ActualText once, including empty/nested spans and authored ordering (#6165); cross-line/style layout remains approximate; scores and estimates unchanged. Synthetic evidence: `crates/pdf/src/tests/actual_text.rs`. | diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 7610075f6..4aa6b7302 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1382,7 +1382,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur A `.ai`, or a PDF saved with Illustrator's editing data, whose editing data can't be decompressed opens from its PDF part with a warning that its layers weren't read and why, and an export or Save As to its own path asks first (`acknowledgeLoss`), as for a file whose layers the reader can't read (`crates/eps/src/import/tests_native.rs`, `crates/engine/src/cmd/fileio/tests_aiimport.rs`). A `.ai` or EPS opened from its editing data with one object on a shown layer that the reader can't read (an unknown operator inside a group, a pattern fill whose pattern it doesn't have, a symbol instance, a placed file without a usable preview) keeps its layers and groups: in that object's place, its layer gets a group of the page's objects inside the object's box, named after it, with a warning that lists what came in as the page's art and an export or Save As to its own path asking first (`acknowledgeLoss`). The file still opens from its page or PDF part when no box can be found, when live art or another unread object shares the page art inside it, when the object spans two artboards or lies on an artboard that has no page, when no page art lies inside its box, when that page art is inside a page group with a clip, mask, transparency or appearance, when the object holds a frame of threaded type, when the page art uses a pattern, image or swatch that the page lacks or that differs from the editing data's, or when the page art doesn't carry the object's transparency (#4806; `crates/eps/src/import/tests_native.rs`, `crates/engine/src/cmd/fileio/tests_aiimport.rs`). A `.ai` or EPS opened from its editing data with one object on a shown layer that the reader can't read (an unknown operator inside a group, a pattern fill whose pattern it doesn't have, a symbol instance, a placed file without a usable preview) keeps its layers and groups: in that object's place, its layer gets a group of the page's objects inside the object's box, named after it, with a warning that lists what came in as the page's art and an export or Save As to its own path asking first (`acknowledgeLoss`). The file still opens from its page or PDF part when no box can be found, when live art or another unread object shares the page art inside it, when the object spans two artboards, or when the page art doesn't carry the object's transparency (#4806; `crates/eps/src/import/tests_native.rs`, `crates/engine/src/cmd/fileio/tests_aiimport.rs`). - In a `.ai` opened from its editing data, an envelope made with a warp, a mesh or a top object opens as a live envelope of its contents, with its warp style, bend and distortions, its mesh points and handles or its top object's outline, its Envelope Options, its Fidelity and whether its contents are being edited. When Distort Appearance is on and the size the file saves for the contents matches their bounds with strokes, the contents map from that stroked box, until an edit in Edit Contents changes how far the strokes reach; VectorCraft's new envelopes map from the geometric box, as do contents with no width or no height, such as a single straight line. An envelope that can't be read, or that VectorCraft draws differently from the file's drawn art, keeps that drawn art, with a note. An envelope of type keeps its drawn art, with its type hidden (`crates/eps/src/import/ai/tests.rs`, `crates/eps/src/import/tests_native.rs`). + In a `.ai` opened from its editing data, an envelope made with a warp, a mesh or a top object opens as a live envelope of its contents, with its warp style, bend and distortions, its mesh points and handles or its top object's outline, its Envelope Options, its Fidelity and whether its contents are being edited. When Distort Appearance is on and the size the file saves for the contents matches their bounds with strokes, the contents map from that stroked box, until an edit in Edit Contents changes how far the strokes reach; VectorCraft's new envelopes map from the geometric box, as do contents with no width or no height, such as a single straight line. An envelope that can't be read, or that VectorCraft draws differently from the file's drawn art, keeps that drawn art, with a note. An envelope of type keeps its drawn art, with its type hidden and a note that says the type isn't editable (#3179; `crates/eps/src/import/ai/tests.rs`, `crates/eps/src/import/tests_native.rs`). In a `.ai` opened from its editing data, Convert to Shape on a whole path or type object opens as a live Rectangle, Rounded Rectangle or Ellipse effect with its Absolute or Relative size, its corner radius, its place among the object's effects and whether it is hidden. A relative size in the file is added to each side. A relative Ellipse, or a setting VectorCraft doesn't read, keeps the drawn art (`crates/eps/src/import/ai/style/tests.rs`, `crates/eps/src/import/ai/tests.rs`, `crates/eps/src/import/tests_native.rs`). In a `.ai` opened from its editing data, Bas Relief in an art style takes its Detail, Smoothness and Light direction from the style's settings block, and an object other than type with it opens as itself with the live effect. A settings block that VectorCraft can't read (malformed, another effect's, a value out of range or a key it doesn't know) keeps the object's drawn look (part of #637; [raster effects reference](https://helpx.adobe.com/illustrator/desktop/special-effects-styles/apply-filter-effects/raster-effects-reference.html), [Photoshop File Formats Specification](https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/), [scripting the filter gallery](https://community.adobe.com/questions-712/scripting-support-for-the-filter-gallery-1064646); `crates/eps/src/import/ai/style/tests.rs`, `crates/eps/src/import/ai/tests.rs`). PDF export and `.ai` saves write a page with a side longer than 14,400 pt with a `UserUnit` (PDF 1.6), its longer side divided by 14,400 and rounded up to the next thousandth, so that no side is longer than 14,400 units (ISO 32000-1, Annex C.2); in PDF 1.3 to 1.5 files and PDF/X files such a page is written in points. From b5f62c75b98fd8127dcd79a809e27bebde766c89 Mon Sep 17 00:00:00 2001 From: jmverges Date: Tue, 10 Nov 2026 03:15:38 +0000 Subject: [PATCH 15/38] .ai import: the envelope type note doesn't promise the type is kept A text slot the text document gives nothing is left out, with a note of its own, so the note for type in an envelope no longer says the type is kept hidden. Co-Authored-By: Claude Opus 5.5 --- crates/eps/src/import/ai/envelope.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/eps/src/import/ai/envelope.rs b/crates/eps/src/import/ai/envelope.rs index fa1f61e69..ef9297143 100644 --- a/crates/eps/src/import/ai/envelope.rs +++ b/crates/eps/src/import/ai/envelope.rs @@ -41,8 +41,9 @@ use crate::import::native::compare::{MAX_DIFFERENCE, MAX_SOLID_DIFFERENCE, compa pub(super) const PLUGIN: &str = "Adobe Deform Plugin"; // brand-ok: the plugin name the files use /// The note for an envelope that keeps its drawn art. pub(crate) const DRAWN: &str = "an envelope VectorCraft couldn't read as a live envelope comes in as its drawn art"; -/// The note for an envelope of type, which keeps its drawn art with its type hidden. -pub(crate) const TYPE_DRAWN: &str = "type in an envelope comes in as the envelope's drawn art, not as editable type: the type is kept, hidden"; +/// The note for an envelope of type, which keeps its drawn art with its type hidden (unless the +/// text document gives the type nothing, which a note of its own then says). +pub(crate) const TYPE_DRAWN: &str = "type in an envelope comes in as the envelope's drawn art, not as editable type"; /// The note for warped type that keeps its drawn art, before how many. pub(crate) const WARPED_TYPE_DRAWN: &str = "warped text objects that came in as drawn art, not as editable type"; /// The most envelopes waiting for their drawn art at once. From f68338482fe23149cc7328664b1ce9292a0ee33f Mon Sep 17 00:00:00 2001 From: Ahmed Khan Date: Mon, 9 Nov 2026 21:13:07 -0800 Subject: [PATCH 16/38] A full flush refused with ENOTTY falls back to a plain fsync (#1620) A folder shared into a macOS VM with virtiofs (a Tart VM's --dir share) refuses F_FULLFSYNC with ENOTTY (os error 25) rather than ENOTSUP, so full_flush_unsupported did not match it and every save there failed. It now matches ENOTSUP or ENOTTY, as craft-libs' atomic crate already does. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VE6j3WRdUkYkmdJG6Q3Me5 --- crates/format/src/atomic.rs | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/crates/format/src/atomic.rs b/crates/format/src/atomic.rs index c208f1b52..863c6fdd3 100644 --- a/crates/format/src/atomic.rs +++ b/crates/format/src/atomic.rs @@ -10,7 +10,8 @@ //! is left as it was and the save fails. //! //! On macOS `File::sync_all` is a full flush (`F_FULLFSYNC`), which SMB shares refuse with -//! `ENOTSUP` while a plain `fsync` works: [`sync_file`] falls back to the plain one there (#1620). +//! `ENOTSUP` and virtiofs shares (folders shared into a macOS VM) with `ENOTTY`, while a plain +//! `fsync` works: [`sync_file`] falls back to the plain one there (#1620). use std::fs::{File, OpenOptions}; use std::io::{self, Write as _}; @@ -180,8 +181,9 @@ fn copy_new(from: &Path, path: &Path) -> io::Result<()> { } /// Flush `file` to stable storage (`sync_all`). On Apple platforms that is a full flush -/// (`F_FULLFSYNC`); where the volume refuses it with `ENOTSUP` (SMB shares, #1620) this falls back -/// to a plain `fsync`. Any other error, and a failed `fsync`, is returned as it is. +/// (`F_FULLFSYNC`); where the volume refuses it with `ENOTSUP` (SMB shares) or `ENOTTY` (virtiofs +/// shares, #1620) this falls back to a plain `fsync`. Any other error, and a failed `fsync`, is +/// returned as it is. pub fn sync_file(file: &File) -> io::Result<()> { match full_sync(file) { Err(e) if full_flush_unsupported(&e) => plain_fsync(file), @@ -190,9 +192,10 @@ pub fn sync_file(file: &File) -> io::Result<()> { } /// Whether `e` is a volume refusing a full flush. Never off Apple platforms: there `sync_all` is -/// already a plain `fsync`. Checked by its code because std maps `ENOTSUP` to `Uncategorized`. +/// already a plain `fsync`. Checked by its code because std maps `ENOTSUP` and `ENOTTY` to +/// `Uncategorized`. fn full_flush_unsupported(e: &io::Error) -> bool { - cfg!(target_vendor = "apple") && e.raw_os_error() == Some(ENOTSUP) + cfg!(target_vendor = "apple") && matches!(e.raw_os_error(), Some(ENOTSUP | ENOTTY)) } #[cfg(target_vendor = "apple")] @@ -232,6 +235,9 @@ fn full_sync(file: &File) -> io::Result<()> { const ENOTSUP: i32 = 45; #[cfg(not(target_vendor = "apple"))] const ENOTSUP: i32 = 95; +/// What macOS answers when a virtiofs share (a folder shared into a VM) is asked for a full flush. +/// The same number on every Unix. +const ENOTTY: i32 = 25; #[cfg(test)] thread_local! { @@ -414,6 +420,17 @@ mod tests { let _ = std::fs::remove_dir_all(&d); } + /// A save to a virtiofs share (#1620: a folder shared into a macOS VM): macOS refuses the full + /// flush there with `ENOTTY` (os error 25), which falls back to a plain `fsync` as `ENOTSUP` does. + /// Elsewhere neither is a refused full flush, and other errors never are. + #[test] + fn a_full_flush_refused_with_enotty_falls_back_like_enotsup() { + let apple = cfg!(target_vendor = "apple"); + assert_eq!(full_flush_unsupported(&io::Error::from_raw_os_error(25)), apple, "ENOTTY"); + assert_eq!(full_flush_unsupported(&io::Error::from_raw_os_error(ENOTSUP)), apple, "ENOTSUP"); + assert!(!full_flush_unsupported(&io::Error::other("the connection was lost"))); + } + fn denied() -> io::Result { Err(io::Error::new(io::ErrorKind::PermissionDenied, "refused by the share")) } From c8d7f2cce061205e06ea315b12cff994e03384be Mon Sep 17 00:00:00 2001 From: Julius van der Vaart Date: Tue, 10 Nov 2026 06:42:19 +0100 Subject: [PATCH 17/38] Stroke: Line Arrow's arms take the head's weight, at least 0.25 pt (#5421) --- crates/doc/src/appearance/stroke_style.rs | 4 ++-- docs/mcp.md | 5 +++-- docs/target-app-parity.md | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/crates/doc/src/appearance/stroke_style.rs b/crates/doc/src/appearance/stroke_style.rs index 1bc88d2d0..b100a8a7b 100644 --- a/crates/doc/src/appearance/stroke_style.rs +++ b/crates/doc/src/appearance/stroke_style.rs @@ -98,8 +98,8 @@ pub enum Arrowhead { Kite, TriangleBar, Oval, - /// Two arms of the line's own weight meeting at right angles in a sharp tip, cut square at - /// their ends. Twice as wide as the other heads. + /// Two arms of the head's weight (the line's at 100 % scale) meeting at right angles in a + /// sharp tip, cut square at their ends. Twice as wide as the other heads. LineArrow, } diff --git a/docs/mcp.md b/docs/mcp.md index df33d3a26..e7c9b6025 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -3240,8 +3240,9 @@ and `stroke.widthProfile.reset` removes every saved one. `stroke.set {profile}` none): arrows (`Arrow`, `LineArrow`, `Barbed`, `Concave`, `DoubleArrow`, `HalfArrowLeft`/`Right`, `Chevron`, `Feather`, `Swallowtail`…), filled and open shapes (`Triangle`, `Circle`, `Oval`, `Target`, `Tag`, `Diamond`, `Hexagon`, `Star`, the `…Open` rings…) and marks (`Bar`, `DoubleBar`, `DotOnBar`, `Slash`, `DoubleSlash`, `Bracket`, `Fork`, `Cross`, -`Plus`). Each head fits a box about four times its weight (stroke weight × `arrowScale`) long and wide; `LineArrow`, two arms -of the line's weight at right angles, spreads twice as wide. +`Plus`). Each head fits a box about four times its weight long and wide. A head's weight is the stroke weight scaled by +`arrowScale`, at least 0.25 pt. `LineArrow` has two arms of the head's weight (the line's at 100 %) at right angles and +spreads twice as wide. ## Stroke bounds and clicks diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 170402aea..638508877 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -42,7 +42,7 @@ Artboard Options forwards the active Artboard tool's Scale Artwork with Artboard | Date | Change | Summary | |---|---|---| | 2026-11-10 | minor | Type advanced: right-click suggestions, Ignore All and Add to Dictionary on a word Auto Spell Check underlined, #4936; evidence added; the dialogs remain; scores and hours unchanged. | -| 2026-11-09 | minor | Stroke panel: Line Arrow, a 41st generated arrowhead: two arms as thick as the line at right angles, a sharp tip and square-cut ends, twice as wide as the other heads; bounds and clicks follow each head's own reach (`stroke.set` `LineArrow`, #5421); scores and estimates unchanged | +| 2026-11-09 | minor | Stroke panel: Line Arrow, a 41st generated arrowhead: two arms of the head's weight (the line's at 100 % arrowhead scale, at least 0.25 pt) at right angles, a sharp tip and square-cut ends, twice as wide as the other heads; bounds and clicks follow each head's own reach (`stroke.set` `LineArrow`, #5421); scores and estimates unchanged | | 2026-11-09 | minor | Canvas: a 3D effect (Extrude & Bevel, Inflate, Rotate, Revolve) changed on type or a group draws again where its art was and is, measured by that art; the canvas measures through its renderer and draws the part with the art it made to measure it. Exports and Object › Rasterize take the solid of 3D type. Fit to Artwork Bounds and per-artboard SVG export measure every artboard through one renderer, building each solid once. A 3D effect on a fill or stroke that builds nothing (Revolve after Roughen) no longer overflows the stack on the canvas, in Object › Rasterize and in raster and EPS exports: the fill or stroke draws as without it. Tests `tests::changed::a_3d_effect_changed_on_type_or_a_group_changes_where_its_solid_was`, `fx::tests::a_renderer_measures_the_art_on_each_artboard_building_each_solid_once`, `canvas::tests::an_edit_to_a_3d_effect_redraws_with_the_art_measured`, `fx::tests::a_3d_effect_that_builds_nothing_draws_the_item_as_without_it`. Scores and estimates unchanged. | | 2026-11-07 | minor | File formats and Type core (#6147): `.ai`/EPS type keeps its paragraphs' tab stops (positions and alignments; leaders aren't read; at most 1,024 a paragraph and 2^20 a story), and right-aligned area type sets the text after a line's last tab against the right edge, the text before it at its stops; centred and justified lines are aligned as a whole as before; evidence added. Scores and estimates unchanged. | | 2026-11-07 | minor | File formats and Type core (#6137): `.ai`/EPS type from the text document keeps auto leading per line, its hyphenation options, may show more of the last lines the file sets in an area frame of its own, when growing the frame by its lines' deepest descent plus 0.05 pt shows them, and a line a soft return ends is justified; evidence added. Limits: frames grow only for area type of one frame, horizontal, by its lines' deepest descent plus 0.05 pt; type in a font that isn't installed, and hyphenation in a language that isn't imported, still lay out differently. Scores and estimates unchanged. | @@ -1248,7 +1248,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur Cmd/Ctrl-click on the Gradient panel's Gradient Fill box or on the Tools panel's Gradient button resets the gradient of the active Fill or Stroke to the linear White, Black gradient, as one undo step ([Default keyboard shortcuts](https://web.archive.org/web/20260726060015/https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html)). With a gradient stop or a freeform gradient point selected, Alt-clicking a color swatch in the Swatches panel recolors it instead of the inactive Fill or Stroke. - **Freeform gradient colors in their own model:** Object ▸ Expand and Object ▸ Create Gradient Mesh (`object.mesh.create`) mix a freeform gradient's points in the color model they share: points of C50 M20 Y0 K10 give mesh points of C50 M20 Y0 K10, and RGB points keep their values in a working RGB space other than sRGB. The canvas, the gradient chips and the SVG, PDF, EPS and EMF writers mix the points in that model too. PDF and EPS write a gradient whose points are all CMYK, or all Gray in a CMYK file, as a CMYK image, which PDF's Output settings convert as they convert any CMYK image. Points in different models mix in display RGB (on the canvas in a CMYK document that shows transparency, as the inks of each point), and a CMYK document stores the result of Expand as CMYK through the color settings. Points colored along CMYK stops (`paint.editGradient {stops}`, or a CMYK gradient switched to freeform) are CMYK, and so are a point `paint.freeform.splitLine` adds between CMYK points and a point `paint.freeform.addPoint` adds without `color` to CMYK points. Public reference: [Gradients](https://web.archive.org/web/20250714042311/https://helpx.adobe.com/illustrator/using/gradients.html) and [Meshes](https://web.archive.org/web/20250812201019/https://helpx.adobe.com/illustrator/using/meshes.html), which do not state how colors in different models mix; the rule is VectorCraft's own. - - **Strokes:** one stroke-geometry module shared by the canvas, SVG, PDF and Outline Stroke; dotted lines (zero-length dashes draw dots or squares); arrowheads with hollow outlines, tip-on-end or extend-past-end alignment and one opacity for line and head. SVG and PDF export arrowheads, width profiles, brushes and aligned strokes exactly as the canvas draws them; Outline Stroke and the live Outline Stroke effect outline every stroke item as drawn (dashes, heads, profiles, alignment); dashes can be fitted to corners and path ends; width profiles carry through dashes, and their corners take the join. The Stroke panel edits type's character strokes, follows the stroke units (its weight presets are round values in that unit: `0.25 mm`, `0.0625 in`, `1 px`, #426; fields show four decimals in mm, cm and inches, three in points), shows mixed values blank and opens from the Control bar. A width profile library, thirty-one more arrowheads (among them Line Arrow: two arms of the line's own weight at right angles, cut square, #5421), and Width Point Edit with multi-select and discontinuous points. Visual bounds and clicks cover the whole painted stroke. Scale Strokes & Effects, Scale Corners and Use Preview Bounds work everywhere, and new art inherits stroke options, a graphic style or the whole last appearance. + - **Strokes:** one stroke-geometry module shared by the canvas, SVG, PDF and Outline Stroke; dotted lines (zero-length dashes draw dots or squares); arrowheads with hollow outlines, tip-on-end or extend-past-end alignment and one opacity for line and head. SVG and PDF export arrowheads, width profiles, brushes and aligned strokes exactly as the canvas draws them; Outline Stroke and the live Outline Stroke effect outline every stroke item as drawn (dashes, heads, profiles, alignment); dashes can be fitted to corners and path ends; width profiles carry through dashes, and their corners take the join. The Stroke panel edits type's character strokes, follows the stroke units (its weight presets are round values in that unit: `0.25 mm`, `0.0625 in`, `1 px`, #426; fields show four decimals in mm, cm and inches, three in points), shows mixed values blank and opens from the Control bar. A width profile library, thirty-one more arrowheads (among them Line Arrow: two arms of the head's weight (the line's at 100 % scale, at least 0.25 pt) at right angles, cut square, #5421), and Width Point Edit with multi-select and discontinuous points. Visual bounds and clicks cover the whole painted stroke. Scale Strokes & Effects, Scale Corners and Use Preview Bounds work everywhere, and new art inherits stroke options, a graphic style or the whole last appearance. - **Brushes (part of #852):** double-clicking a brush in the Brushes panel (or the panel menu's Brush Options…) opens Calligraphic Brush Options (Name; Angle, Roundness and Size, each Fixed, Random, Pressure, Tilt, Bearing or Rotation with its Variation; live previews of the nib and of a light–heavy–light stroke) or Bristle Brush Options (Shape and the bristle sliders); OK edits the brush and every stroke painted with it. The Paintbrush records the pen pressure along each stroke (kept with the path, saved, and joined when a stroke continues a path); a Calligraphic value on Pressure goes from value − variation at the lightest pressure to value + variation at the heaviest, on the canvas and in SVG, PDF and the other exports. Brushes saved before keep their look. Scatter Brush Options (Size, Spacing, Scatter and Rotation, each Fixed, Random between two values, Pressure from the first at the lightest pressure to the second at the heaviest, Tilt, Bearing or Rotation; Rotation relative to Page or Path), Art Brush Options (Width, Scale Proportionately, Stretch to Fit Stroke Length or Stretch Between Guides with the guides' start and end, the four Directions, Flip Along and Across, Overlap) and Pattern Brush Options (Scale, Spacing, the five tiles shown, Flip Along and Across, Stretch to fit, Add space to fit or Approximate path) each have Colorization (None, Tints, Tints and Shades, Hue Shift with its Key Color picked from the art's colours) and a live stroke preview; values from files and commands are clamped to their ranges. Art Brush Options › Overlap places art on each run between sharp corners so folds do not overlap (`ArtBrush.overlap`; `art_brush_overlap_splits_at_corners`). Still to come: Stylus Wheel, pen pressure on macOS where the windowing layer reports none, and tilt, bearing and rotation for Art, Pattern and Bristle brushes and on native Wayland and the web. Under Tints and Tints and Shades, art in black ink alone goes by its ink amount, and a process or gray stroke color's tints and shades stay in that color's model; these are VectorCraft's own rules. Type in a scatter brush's art takes the brush's colorization as its paths do: Tints, Tints and Shades and Hue Shift recolor the characters' solid fills and strokes in each copy the brush draws, and Brush Options' Key Color list, whose first color is Hue Shift's default key, counts the characters' fills and their strokes wider than 0. The Brushes page says that a scatter, art or pattern brush takes its colors from the current stroke color and the brush's colorization method () and does not mention type there; recoloring the characters' paints like the art's fills and strokes is VectorCraft's own rule. The Symbol Stainer mixes the characters' strokes of type toward the stain as it mixes their fills. Evidence: `tests::scatter_colorization_recolors_type`, `tests::art_colors_list_the_most_used_first`, `tests::stain_mixes_type_strokes_too` (vectorcraft-brush). From 89482ba940f6b7d7e0a96efbfcfe6a9e0a94b922 Mon Sep 17 00:00:00 2001 From: Julius van der Vaart Date: Tue, 10 Nov 2026 06:48:16 +0100 Subject: [PATCH 18/38] Stroke: Line Arrow's arms match the line at 100 % only from 0.25 pt (#5421) --- crates/doc/src/appearance/stroke_style.rs | 5 +++-- crates/effects/src/stroke/arrow.rs | 7 ++++--- docs/mcp.md | 4 ++-- docs/target-app-parity.md | 4 ++-- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/crates/doc/src/appearance/stroke_style.rs b/crates/doc/src/appearance/stroke_style.rs index b100a8a7b..46a0c30c3 100644 --- a/crates/doc/src/appearance/stroke_style.rs +++ b/crates/doc/src/appearance/stroke_style.rs @@ -98,8 +98,9 @@ pub enum Arrowhead { Kite, TriangleBar, Oval, - /// Two arms of the head's weight (the line's at 100 % scale) meeting at right angles in a - /// sharp tip, cut square at their ends. Twice as wide as the other heads. + /// Two arms of the head's weight (the line's weight at 100 % scale, for lines of 0.25 pt or + /// more) meeting at right angles in a sharp tip, cut square at their ends. Twice as wide as + /// the other heads. LineArrow, } diff --git a/crates/effects/src/stroke/arrow.rs b/crates/effects/src/stroke/arrow.rs index 603f46e28..c96dcb477 100644 --- a/crates/effects/src/stroke/arrow.rs +++ b/crates/effects/src/stroke/arrow.rs @@ -366,9 +366,10 @@ fn local(kind: Arrowhead) -> BezPath { } Arrowhead::Oval => p.extend(kurbo::Ellipse::new((2.0, 0.0), (2.0, 1.2), 0.0).path_elements(LOCAL_TOL)), Arrowhead::LineArrow => { - // Arms one unit thick (the line's weight at 100 %) at right angles to each other, from - // a sharp tip to their outer corners at (4, ±4), cut square across: a unit across an - // arm at 45° is ½√2 along each axis, and the arms' inner edges meet √2 behind the tip. + // Arms one unit thick (the head's weight: the line's at 100 % for lines of 0.25 pt or + // more) at right angles to each other, from a sharp tip to their outer corners at + // (4, ±4), cut square across: a unit across an arm at 45° is ½√2 along each axis, and + // the arms' inner edges meet √2 behind the tip. let t = FRAC_1_SQRT_2; poly(&mut p, &[(0.0, 0.0), (4.0, 4.0), (4.0 + t, 4.0 - t), (2.0 * t, 0.0), (4.0 + t, t - 4.0), (4.0, -4.0)]); } diff --git a/docs/mcp.md b/docs/mcp.md index e7c9b6025..77638be63 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -3241,8 +3241,8 @@ none): arrows (`Arrow`, `LineArrow`, `Barbed`, `Concave`, `DoubleArrow`, `HalfAr `Swallowtail`…), filled and open shapes (`Triangle`, `Circle`, `Oval`, `Target`, `Tag`, `Diamond`, `Hexagon`, `Star`, the `…Open` rings…) and marks (`Bar`, `DoubleBar`, `DotOnBar`, `Slash`, `DoubleSlash`, `Bracket`, `Fork`, `Cross`, `Plus`). Each head fits a box about four times its weight long and wide. A head's weight is the stroke weight scaled by -`arrowScale`, at least 0.25 pt. `LineArrow` has two arms of the head's weight (the line's at 100 %) at right angles and -spreads twice as wide. +`arrowScale`, at least 0.25 pt. `LineArrow` has two arms of the head's weight (the line's weight at 100 % for lines of +0.25 pt or more) at right angles and spreads twice as wide. ## Stroke bounds and clicks diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 638508877..d7d2e6fc5 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -42,7 +42,7 @@ Artboard Options forwards the active Artboard tool's Scale Artwork with Artboard | Date | Change | Summary | |---|---|---| | 2026-11-10 | minor | Type advanced: right-click suggestions, Ignore All and Add to Dictionary on a word Auto Spell Check underlined, #4936; evidence added; the dialogs remain; scores and hours unchanged. | -| 2026-11-09 | minor | Stroke panel: Line Arrow, a 41st generated arrowhead: two arms of the head's weight (the line's at 100 % arrowhead scale, at least 0.25 pt) at right angles, a sharp tip and square-cut ends, twice as wide as the other heads; bounds and clicks follow each head's own reach (`stroke.set` `LineArrow`, #5421); scores and estimates unchanged | +| 2026-11-09 | minor | Stroke panel: Line Arrow, a 41st generated arrowhead: two arms of the head's weight (stroke weight × arrowhead scale, at least 0.25 pt: the line's weight at 100 % for lines of 0.25 pt or more) at right angles, a sharp tip and square-cut ends, twice as wide as the other heads; bounds and clicks follow each head's own reach (`stroke.set` `LineArrow`, #5421); scores and estimates unchanged | | 2026-11-09 | minor | Canvas: a 3D effect (Extrude & Bevel, Inflate, Rotate, Revolve) changed on type or a group draws again where its art was and is, measured by that art; the canvas measures through its renderer and draws the part with the art it made to measure it. Exports and Object › Rasterize take the solid of 3D type. Fit to Artwork Bounds and per-artboard SVG export measure every artboard through one renderer, building each solid once. A 3D effect on a fill or stroke that builds nothing (Revolve after Roughen) no longer overflows the stack on the canvas, in Object › Rasterize and in raster and EPS exports: the fill or stroke draws as without it. Tests `tests::changed::a_3d_effect_changed_on_type_or_a_group_changes_where_its_solid_was`, `fx::tests::a_renderer_measures_the_art_on_each_artboard_building_each_solid_once`, `canvas::tests::an_edit_to_a_3d_effect_redraws_with_the_art_measured`, `fx::tests::a_3d_effect_that_builds_nothing_draws_the_item_as_without_it`. Scores and estimates unchanged. | | 2026-11-07 | minor | File formats and Type core (#6147): `.ai`/EPS type keeps its paragraphs' tab stops (positions and alignments; leaders aren't read; at most 1,024 a paragraph and 2^20 a story), and right-aligned area type sets the text after a line's last tab against the right edge, the text before it at its stops; centred and justified lines are aligned as a whole as before; evidence added. Scores and estimates unchanged. | | 2026-11-07 | minor | File formats and Type core (#6137): `.ai`/EPS type from the text document keeps auto leading per line, its hyphenation options, may show more of the last lines the file sets in an area frame of its own, when growing the frame by its lines' deepest descent plus 0.05 pt shows them, and a line a soft return ends is justified; evidence added. Limits: frames grow only for area type of one frame, horizontal, by its lines' deepest descent plus 0.05 pt; type in a font that isn't installed, and hyphenation in a language that isn't imported, still lay out differently. Scores and estimates unchanged. | @@ -1248,7 +1248,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur Cmd/Ctrl-click on the Gradient panel's Gradient Fill box or on the Tools panel's Gradient button resets the gradient of the active Fill or Stroke to the linear White, Black gradient, as one undo step ([Default keyboard shortcuts](https://web.archive.org/web/20260726060015/https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html)). With a gradient stop or a freeform gradient point selected, Alt-clicking a color swatch in the Swatches panel recolors it instead of the inactive Fill or Stroke. - **Freeform gradient colors in their own model:** Object ▸ Expand and Object ▸ Create Gradient Mesh (`object.mesh.create`) mix a freeform gradient's points in the color model they share: points of C50 M20 Y0 K10 give mesh points of C50 M20 Y0 K10, and RGB points keep their values in a working RGB space other than sRGB. The canvas, the gradient chips and the SVG, PDF, EPS and EMF writers mix the points in that model too. PDF and EPS write a gradient whose points are all CMYK, or all Gray in a CMYK file, as a CMYK image, which PDF's Output settings convert as they convert any CMYK image. Points in different models mix in display RGB (on the canvas in a CMYK document that shows transparency, as the inks of each point), and a CMYK document stores the result of Expand as CMYK through the color settings. Points colored along CMYK stops (`paint.editGradient {stops}`, or a CMYK gradient switched to freeform) are CMYK, and so are a point `paint.freeform.splitLine` adds between CMYK points and a point `paint.freeform.addPoint` adds without `color` to CMYK points. Public reference: [Gradients](https://web.archive.org/web/20250714042311/https://helpx.adobe.com/illustrator/using/gradients.html) and [Meshes](https://web.archive.org/web/20250812201019/https://helpx.adobe.com/illustrator/using/meshes.html), which do not state how colors in different models mix; the rule is VectorCraft's own. - - **Strokes:** one stroke-geometry module shared by the canvas, SVG, PDF and Outline Stroke; dotted lines (zero-length dashes draw dots or squares); arrowheads with hollow outlines, tip-on-end or extend-past-end alignment and one opacity for line and head. SVG and PDF export arrowheads, width profiles, brushes and aligned strokes exactly as the canvas draws them; Outline Stroke and the live Outline Stroke effect outline every stroke item as drawn (dashes, heads, profiles, alignment); dashes can be fitted to corners and path ends; width profiles carry through dashes, and their corners take the join. The Stroke panel edits type's character strokes, follows the stroke units (its weight presets are round values in that unit: `0.25 mm`, `0.0625 in`, `1 px`, #426; fields show four decimals in mm, cm and inches, three in points), shows mixed values blank and opens from the Control bar. A width profile library, thirty-one more arrowheads (among them Line Arrow: two arms of the head's weight (the line's at 100 % scale, at least 0.25 pt) at right angles, cut square, #5421), and Width Point Edit with multi-select and discontinuous points. Visual bounds and clicks cover the whole painted stroke. Scale Strokes & Effects, Scale Corners and Use Preview Bounds work everywhere, and new art inherits stroke options, a graphic style or the whole last appearance. + - **Strokes:** one stroke-geometry module shared by the canvas, SVG, PDF and Outline Stroke; dotted lines (zero-length dashes draw dots or squares); arrowheads with hollow outlines, tip-on-end or extend-past-end alignment and one opacity for line and head. SVG and PDF export arrowheads, width profiles, brushes and aligned strokes exactly as the canvas draws them; Outline Stroke and the live Outline Stroke effect outline every stroke item as drawn (dashes, heads, profiles, alignment); dashes can be fitted to corners and path ends; width profiles carry through dashes, and their corners take the join. The Stroke panel edits type's character strokes, follows the stroke units (its weight presets are round values in that unit: `0.25 mm`, `0.0625 in`, `1 px`, #426; fields show four decimals in mm, cm and inches, three in points), shows mixed values blank and opens from the Control bar. A width profile library, thirty-one more arrowheads (among them Line Arrow: two arms of the head's weight (stroke weight × arrowhead scale, at least 0.25 pt: the line's weight at 100 % for lines of 0.25 pt or more) at right angles, cut square, #5421), and Width Point Edit with multi-select and discontinuous points. Visual bounds and clicks cover the whole painted stroke. Scale Strokes & Effects, Scale Corners and Use Preview Bounds work everywhere, and new art inherits stroke options, a graphic style or the whole last appearance. - **Brushes (part of #852):** double-clicking a brush in the Brushes panel (or the panel menu's Brush Options…) opens Calligraphic Brush Options (Name; Angle, Roundness and Size, each Fixed, Random, Pressure, Tilt, Bearing or Rotation with its Variation; live previews of the nib and of a light–heavy–light stroke) or Bristle Brush Options (Shape and the bristle sliders); OK edits the brush and every stroke painted with it. The Paintbrush records the pen pressure along each stroke (kept with the path, saved, and joined when a stroke continues a path); a Calligraphic value on Pressure goes from value − variation at the lightest pressure to value + variation at the heaviest, on the canvas and in SVG, PDF and the other exports. Brushes saved before keep their look. Scatter Brush Options (Size, Spacing, Scatter and Rotation, each Fixed, Random between two values, Pressure from the first at the lightest pressure to the second at the heaviest, Tilt, Bearing or Rotation; Rotation relative to Page or Path), Art Brush Options (Width, Scale Proportionately, Stretch to Fit Stroke Length or Stretch Between Guides with the guides' start and end, the four Directions, Flip Along and Across, Overlap) and Pattern Brush Options (Scale, Spacing, the five tiles shown, Flip Along and Across, Stretch to fit, Add space to fit or Approximate path) each have Colorization (None, Tints, Tints and Shades, Hue Shift with its Key Color picked from the art's colours) and a live stroke preview; values from files and commands are clamped to their ranges. Art Brush Options › Overlap places art on each run between sharp corners so folds do not overlap (`ArtBrush.overlap`; `art_brush_overlap_splits_at_corners`). Still to come: Stylus Wheel, pen pressure on macOS where the windowing layer reports none, and tilt, bearing and rotation for Art, Pattern and Bristle brushes and on native Wayland and the web. Under Tints and Tints and Shades, art in black ink alone goes by its ink amount, and a process or gray stroke color's tints and shades stay in that color's model; these are VectorCraft's own rules. Type in a scatter brush's art takes the brush's colorization as its paths do: Tints, Tints and Shades and Hue Shift recolor the characters' solid fills and strokes in each copy the brush draws, and Brush Options' Key Color list, whose first color is Hue Shift's default key, counts the characters' fills and their strokes wider than 0. The Brushes page says that a scatter, art or pattern brush takes its colors from the current stroke color and the brush's colorization method () and does not mention type there; recoloring the characters' paints like the art's fills and strokes is VectorCraft's own rule. The Symbol Stainer mixes the characters' strokes of type toward the stain as it mixes their fills. Evidence: `tests::scatter_colorization_recolors_type`, `tests::art_colors_list_the_most_used_first`, `tests::stain_mixes_type_strokes_too` (vectorcraft-brush). From db419f1aade0b18d0926e30f4d7b3c649fd50ae7 Mon Sep 17 00:00:00 2001 From: makesomethingshit Date: Tue, 10 Nov 2026 15:41:18 +0900 Subject: [PATCH 19/38] Hide selection marks during Vector Halftone preview --- crates/ui-egui/src/canvas/overlays.rs | 40 ++++++++------- crates/ui-egui/src/canvas/tests.rs | 67 ++++++++++++++++++++++++++ crates/ui-egui/src/dialogs/halftone.rs | 6 +++ docs/roadmap.md | 5 +- docs/target-app-parity.md | 6 ++- docs/ui-parity.md | 4 ++ 6 files changed, 108 insertions(+), 20 deletions(-) diff --git a/crates/ui-egui/src/canvas/overlays.rs b/crates/ui-egui/src/canvas/overlays.rs index fb9872a77..f3d79443b 100644 --- a/crates/ui-egui/src/canvas/overlays.rs +++ b/crates/ui-egui/src/canvas/overlays.rs @@ -140,7 +140,8 @@ pub(super) fn selection_overlay(app: &mut VectorcraftApp, p: &egui::Painter, xf: None => b, }); let st = app.session.active(); - let big = st.is_some_and(|st| too_many_anchors(st.selection.objects.iter().filter_map(|id| st.doc.node(*id)))); + let show_marks = !crate::dialogs::halftone::previewing(app); + let big = show_marks && st.is_some_and(|st| too_many_anchors(st.selection.objects.iter().filter_map(|id| st.doc.node(*id)))); let big_bounds = if big { app.selection_bounds() } else { None }; let app = &*app; let Some(st) = app.session.active() else { return }; @@ -152,24 +153,27 @@ pub(super) fn selection_overlay(app: &mut VectorcraftApp, p: &egui::Painter, xf: let options = mark_options(app, curve_points_from_tool); let grow = options.look.grow; let anchor = |direct: bool| grow + if direct { 5.0 } else { 4.0 }; - // Too many paths to outline: the selection's bounds stand for them. - if let Some((b, id)) = big_bounds.zip(st.selection.objects.first()) { - stroke_path(p, &vectorcraft_geom::shapes::rectangle(b).to_bezpath(), xf, Stroke::new(1.0, c32(st.doc.layer_color(*id)))); + // The temporary halftone selects its dots; their marks obscure the art (#6369). + if show_marks { + // Too many paths to outline: the selection's bounds stand for them. + if let Some((b, id)) = big_bounds.zip(st.selection.objects.first()) { + stroke_path(p, &vectorcraft_geom::shapes::rectangle(b).to_bezpath(), xf, Stroke::new(1.0, c32(st.doc.layer_color(*id)))); + } + // The key object (Align to Key Object): its outline drawn thicker, or its bounds when it has + // too many paths to outline. + let key = st.selection.key.and_then(|k| Some((k, st.doc.node(k)?))); + let (key_bounds, key_outline) = match key { + Some((k, n)) if too_many_anchors([n]) => (Some((k, n)), None), + Some((k, _)) => (None, Some(k)), + None => (None, None), + }; + if let Some((k, n)) = key_bounds + && let Some(b) = n.geometric_bounds_with(&symbols) + { + stroke_path(p, &vectorcraft_geom::shapes::rectangle(b).to_bezpath(), xf, Stroke::new(KEY_OUTLINE, c32(st.doc.layer_color(k)))); + } + marks.paint(st, p, xf, &options, key_outline, if big { &[] } else { &st.selection.objects }); } - // The key object (Align to Key Object): its outline drawn thicker, or its bounds when it has - // too many paths to outline. - let key = st.selection.key.and_then(|k| Some((k, st.doc.node(k)?))); - let (key_bounds, key_outline) = match key { - Some((k, n)) if too_many_anchors([n]) => (Some((k, n)), None), - Some((k, _)) => (None, Some(k)), - None => (None, None), - }; - if let Some((k, n)) = key_bounds - && let Some(b) = n.geometric_bounds_with(&symbols) - { - stroke_path(p, &vectorcraft_geom::shapes::rectangle(b).to_bezpath(), xf, Stroke::new(KEY_OUTLINE, c32(st.doc.layer_color(k)))); - } - marks.paint(st, p, xf, &options, key_outline, if big { &[] } else { &st.selection.objects }); // The spine of each selected blend (or of the blend a selected key object belongs to). let mut spines = vec![]; for id in &st.selection.objects { diff --git a/crates/ui-egui/src/canvas/tests.rs b/crates/ui-egui/src/canvas/tests.rs index 42778c729..cdc91a97b 100644 --- a/crates/ui-egui/src/canvas/tests.rs +++ b/crates/ui-egui/src/canvas/tests.rs @@ -1546,6 +1546,73 @@ fn cmd_with_another_tool_drags_with_the_selection_tool_used_last() { assert_eq!(cmd_drag(&mut app, inside, inside), "selection"); } +/// #6369: only the selection box remains over the temporary halftone, and the original +/// selection's marks come back when Preview is turned off or the dialog is cancelled. +#[test] +fn halftone_preview_hides_dot_outlines_and_keeps_selection_bounds() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 400, "height": 300})).unwrap(); + app.ui.task_bar = false; + app.ui.view.rulers = false; + app.ui.view.bounding_box = true; + let original = app.run("shape.ellipse", json!({"x": 50, "y": 50, "width": 120, "height": 100})).unwrap()["id"].as_u64().unwrap(); + let original = vectorcraft_doc::NodeId(original); + app.run("paint.setFill", json!({"color": "#000000"})).unwrap(); + let canvas = egui::Context::default(); + let dialog = egui::Context::default(); + let dialog_frame = |app: &mut VectorcraftApp| { + let raw = egui::RawInput { screen_rect: Some(egui::Rect::from_min_size(Pos2::ZERO, vec2(800.0, 600.0))), ..Default::default() }; + let mut out = dialog.run_ui(raw, |ui| crate::dialogs::show(app, ui.ctx())); + out.textures_delta.clear(); + }; + let has_interior_marks = |app: &mut VectorcraftApp| { + frame(app, &canvas, vec![]); + let xf = Xf::new(app.canvas_rect.unwrap(), app.view().unwrap()); + let corners = app.selection_box().unwrap().corners().map(|p| xf.to_screen(p)); + let inside = egui::Rect::from_points(&corners).shrink(10.0); + let st = app.session.active().unwrap(); + let color = c32(st.doc.layer_color(st.selection.objects[0])); + let points = drawn_in(app, &canvas, color, 1.0); + assert!(corners.iter().all(|p| points.contains(p)), "the outer selection box is still drawn"); + points.iter().any(|p| inside.contains(*p)) + }; + assert!(has_interior_marks(&mut app), "the selected ellipse shows its outline"); + + app.run("ui.menuDialog", json!({"command": "object.vectorHalftone"})).unwrap(); + dialog_frame(&mut app); + assert!(app.session.in_interaction()); + assert_ne!(app.session.active().unwrap().selection.objects, vec![original], "the halftone is previewed and selected"); + assert!(!has_interior_marks(&mut app), "selection marks cover the halftone dots"); + + let set_preview = |app: &mut VectorcraftApp, on| { + crate::dialogs::set_field(app.ui.dialog.as_mut().unwrap(), "preview".into(), json!(on)); + dialog_frame(app); + }; + set_preview(&mut app, false); + assert_eq!(app.session.active().unwrap().selection.objects, vec![original]); + assert!(has_interior_marks(&mut app), "turning Preview off restores the outline"); + + set_preview(&mut app, true); + assert!(!has_interior_marks(&mut app), "turning Preview back on hides the dot marks again"); + crate::dialogs::cancel(&mut app); + assert_eq!(app.session.active().unwrap().selection.objects, vec![original]); + assert!(has_interior_marks(&mut app), "Cancel restores the outline"); + + app.run("ui.menuDialog", json!({"command": "object.move"})).unwrap(); + crate::dialogs::set_field(app.ui.dialog.as_mut().unwrap(), "preview".into(), json!(true)); + dialog_frame(&mut app); + assert!(app.session.in_interaction()); + assert!(has_interior_marks(&mut app), "another dialog's preview keeps selection marks"); + crate::dialogs::cancel(&mut app); + + app.run("ui.menuDialog", json!({"command": "object.vectorHalftone"})).unwrap(); + dialog_frame(&mut app); + assert!(!has_interior_marks(&mut app)); + crate::dialogs::confirm(&mut app).unwrap(); + assert!(app.ui.dialog.is_none() && !app.session.in_interaction()); + assert!(has_interior_marks(&mut app), "OK restores ordinary selection marks on the applied dots"); +} + /// #525: a selection with more anchors than the overlay draws (a traced photo) shows its bounds, /// not every outline and anchor, which made a mesh larger than the GPU takes; an ordinary one /// still shows its outline and anchors. diff --git a/crates/ui-egui/src/dialogs/halftone.rs b/crates/ui-egui/src/dialogs/halftone.rs index 653b16fc9..5084754fc 100644 --- a/crates/ui-egui/src/dialogs/halftone.rs +++ b/crates/ui-egui/src/dialogs/halftone.rs @@ -29,6 +29,12 @@ pub fn fields() -> Value { json!({"shape": "circle", "frequency": 20, "angle": 45, "mode": "mono", "color": "#000000", "invert": false, "clip": true, "keepOriginal": false, "preview": true}) } +/// Whether the open Object → Vector Halftone dialog is showing its temporary dot group. +pub(crate) fn previewing(app: &VectorcraftApp) -> bool { + app.ui.dialog.as_ref().is_some_and(|d| d.kind == KIND) + && app.session.active().and_then(|st| st.interaction.as_ref()?.preview.as_ref()).is_some_and(|(cmd, _)| cmd == CMD) +} + fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { screen_fields(ui, d); form::check(ui, d, "keepOriginal", tl!("Keep Original")); diff --git a/docs/roadmap.md b/docs/roadmap.md index a0361d72a..b196718c9 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -2,6 +2,8 @@ > **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-10 · **Change:** minor (right-click on a word Auto Spell Check underlined: its suggestions, Ignore All and Add to Dictionary, #4936; previously minor: G2: Effect Gallery thumbnails and in-dialog preview are in; Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: PDF editable import preserves authored ActualText once, including empty/nested spans and authored ordering (#6165); cross-line/style layout remains approximate; scores and estimates unchanged; PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: EPS import: embedded fonts (composite, Type 42, CID-keyed, Type 1, CFF FontSet) draw their glyphs; spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; M4/M7: `.ai`/EPS type keeps its tab stops, and right-aligned lines with tabs lay out as the file's page, #6147; M4/M7: `.ai`/EPS type from the text document lays its lines out closer to the file's page, within limits, #6137; numeric fields step on wheel hover without taking keyboard focus, #5949; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen; Selection QoL: marquee modes and visible feedback, #5313; SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; Indian-language hyphenation review and license allowlist, #4947; Hindi catalog and contributor loanword preferences; full wording review in progress; Effect dialogs: a colour swatch beside a hex colour opens the Color Picker, #5365; shape tools move the selection from its centre point and turn it from the zone outside a box corner, #5477; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x) +> Vector Halftone preview update (2026-11-10): temporary dot selection marks are hidden, and the bounding box stays available (#6369). + > Numeric spinner update (2026-11-10): held mouse arrows repeat past the click timeout; scores and estimates unchanged (#6307). ## Feature submission evidence @@ -153,7 +155,7 @@ native wording review, layout checks and a web Devanagari font remain. [Notes](l | M11 | Artboards & views (selected-set deletion shared by Artboards menu, trash and Properties, retaining artboard 1 when all are selected; Artboard deletion keys keep the first of an all-selected set and protect artwork; history prunes invalid indices while preserving valid multiselection (history tests and full workspace CI pass); click-to-fit objects with the Artboard tool (#1227), artboard escape routing and document setup modal handoff done; artboard panel/tool with Move Artwork for X/Y fields and Artboard Options, Trim View, middle-button pan, print tiling and the status bar's artboard navigator with synchronized Artboard tool selection done; Window › Arrange › Tile and Consolidate All Windows done; multiple windows, presentation polish) (cached canvas reprojection preserves scale during resize, rotation and DPI changes; regression-tested and contributor-confirmed 2026-10-19; estimates unchanged) (cached canvas dimensions match the renderer limit before cache-key creation, view construction and worker submission; regression-tested; estimates unchanged) | 🟡; Artboard Options shares existing presets and orientation controls and can fit artwork without moving it; artwork fitting includes all drawn document art across artboards and off-board; artwork bounds are cached by active document and revision, and selection uses the shared bounds cache across artboards | 15–25 | | M10 | Brushes, symbols, patterns, Repeat | 🟡 pattern swatches (5 tile types, Pattern Options, editing mode, SVG ``/PDF export) and live Repeat (radial/grid/mirror) done; interactive selected mirror axis implemented (focused and native checks passed; full CI blocked by an unrelated text test); symbol and brush libraries done; brushes/symbols in progress | 23–37 | | M11 | Artboards & views (selected-set deletion shared by Artboards menu, trash and Properties, retaining artboard 1 when all are selected; Artboard deletion keys keep the first of an all-selected set and protect artwork; history prunes invalid indices while preserving valid multiselection (history tests and full workspace CI pass); click-to-fit objects with the Artboard tool (#1227), artboard escape routing and document setup modal handoff done; artboard panel/tool with Move Artwork for X/Y fields and Artboard Options, Trim View, middle-button pan, print tiling and the status bar's artboard navigator with synchronized Artboard tool selection done; Window › Arrange › Tile and Consolidate All Windows done; multiple windows, presentation polish) (cached canvas reprojection preserves scale during resize, rotation and DPI changes; regression-tested and contributor-confirmed 2026-10-19; estimates unchanged) (cached canvas dimensions match the renderer limit before cache-key creation, view construction and worker submission; regression-tested; estimates unchanged); status-bar pointer coordinates follow the active ruler origin and General units | 🟡; Artboard Options shares existing presets and orientation controls and can fit artwork without moving it; artwork fitting includes all drawn document art across artboards and off-board; artwork bounds are cached by active document and revision, and selection uses the shared bounds cache across artboards | 15–25 | -| M12 | Advanced color & art (CMYK/ICC, separations, Gradient Mesh, Live Paint, Image Trace, Graphs) | 🟡 Gradient Mesh, Live Paint, Image Trace (junction-returning pieces retain their positioned boundaries; a simpler contour walk with identical output; an edge's sub-pixel shift is the same from either side; notches read only their own piece; collapsed loops keep few segments; large images trace with a warning; Auto Color keeps rare distinct colours; quick presets trace at once and the Image Trace button opens the adjustment controls; Properties reopens them; tiny fitted contours retain islands and holes; Create Strokes: exact crop EDT, Guo–Hall thinning and maximal-disc graph simplification; 15 presets, the 14 documented ones each with an enhanced version; Ignore Color, Shapes, Auto Grouping and Gradients options; measured in `image-trace-quality.md`; closed outlines retain their interior after straightening; the Flat Logo mode places edges to a fraction of a pixel, restores sharp corners, snaps lines and circles, and refuses gradients; live trace result bounds and selection highlights exclude the retained bitmap except in source-image views), Recolor Artwork, colour management (ICC, soft proofing, separations preview), Graphs (all 9 tools, Graph Data/Type, group-selected series colours persist through regenerate/save/open) done; Graph Design and Marker designs, Transpose and Switch x/y, `\|` label line breaks, the pie options, drop shadows, the legend across the top, overlapping widths, First Row/Column in Front, filled lines, kept label formatting and the Graph Data worksheet done; graph Column designs pending | 6–10 | +| M12 | Advanced color & art (CMYK/ICC, separations, Gradient Mesh, Live Paint, Image Trace, Graphs) | 🟡 Gradient Mesh, Live Paint, Image Trace (junction-returning pieces retain their positioned boundaries; a simpler contour walk with identical output; an edge's sub-pixel shift is the same from either side; notches read only their own piece; collapsed loops keep few segments; large images trace with a warning; Auto Color keeps rare distinct colours; quick presets trace at once and the Image Trace button opens the adjustment controls; Properties reopens them; tiny fitted contours retain islands and holes; Create Strokes: exact crop EDT, Guo–Hall thinning and maximal-disc graph simplification; 15 presets, the 14 documented ones each with an enhanced version; Ignore Color, Shapes, Auto Grouping and Gradients options; measured in `image-trace-quality.md`; closed outlines retain their interior after straightening; the Flat Logo mode places edges to a fraction of a pixel, restores sharp corners, snaps lines and circles, and refuses gradients; live trace result bounds and selection highlights exclude the retained bitmap except in source-image views), Recolor Artwork, colour management (ICC, soft proofing, separations preview), Graphs (all 9 tools, Graph Data/Type, group-selected series colours persist through regenerate/save/open) done; Graph Design and Marker designs, Transpose and Switch x/y, `\|` label line breaks, the pie options, drop shadows, the legend across the top, overlapping widths, First Row/Column in Front, filled lines, kept label formatting and the Graph Data worksheet done; graph Column designs pending; Vector Halftone Preview keeps the selection box without drawing dot selection marks (#6369) | 6–10 | | M13 | Automation (Actions ✅ record/playback, persisted; variables, scripting, batch) | 🟡 MCP `add_text` exposes existing vertical point, area and path type; broader scripting and batch work remains | 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) (status messages and readouts use stable responsive allocations sized for localized labels and ordinary coordinates; sample widths cached by language/unit/DPI; regression-tested and contributor-confirmed 2026-10-19; estimates unchanged) | 🟡 Interim macOS cursor delivery (#2292) uses existing artwork and host-owned AppKit cursors; headless coverage only, native menu/focus/handback and display validation pending. Dock groups collapse to their tabs by a double-click on a tab (UI.8); Preferences › General › Constrain Angle acts for the arrow keys, Shift drags and new objects (#394, 2026-10-21); numeric-field pointer selection and focus regression-tested (#1326, in part; short movements still select all); RPM runtime capabilities and glibc floor checked (#1275; real-distro install/start-up pending), Eyedropper cursor with precise-setting checks (#1269), Preferences, shortcut editor (chords with Shift and a punctuation key fire as typed; other apps' text shortcut sets imported), workspaces (each keeps its layout changes until Reset , UI.7), Window › Help Bar shown or hidden apart from the status bar and kept in saved workspaces, panels docked by dragging into any dock group (as tabs or a top/bottom split) or into the icon column, and closed from a dock tab's context menu (UI.6), a custom title bar on Windows/Linux with a Home button, content-sized dialogs, menus that scroll when longer than the window and macOS native menu dismissal done (M14.8), and native macOS save sheets with a Format menu (#866); titled PDF exports set `DisplayDocTitle` for viewers and accessibility checkers (#3646); accessibility and broader Windows/Linux packaging/QA pending; floating tab groups collapse into the side icon rail and opened rail panels fold back into it (#5925, in part), with pointer and native-window regression coverage; estimates unchanged; numeric spinner arrows keep repeating while held with the mouse (#6307) | 18–28 | @@ -204,6 +206,7 @@ separate. Scores and hours unchanged. [Behavior and command details](development | 2026-11-08 | minor | Floating tab groups collapse into the side icon rail and opened rail panels fold back into it (#5925, in part); tab groups remain intact. Scores and estimates unchanged. | | 2026-11-10 | minor | Held numeric spinner arrows keep stepping past the click timeout and stop on release (#6307). Scores and estimates unchanged. | | 2026-11-09 | minor | Numeric fields step on wheel hover without taking focus; keyboard focus and existing rounding rules remain (#5949). Scores and estimates unchanged. | +| 2026-11-10 | minor | Vector Halftone Preview leaves the dots unobscured and keeps the selection box (#6369). Scores and estimates unchanged. | | 2026-11-07 | minor | M2: with the Pen or Curvature tool, Edit › Copy and Cut take the anchors and segments a Cmd/Ctrl-lent Direct Selection picked, not the whole path (#6123); estimates unchanged | | 2026-11-07 | minor | Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged. | | 2026-11-06 | minor | M2: a shape just drawn moves by its centre point (Show Center) and turns from the rotate zone just outside a bounding-box corner with its tool still active, as the Selection tool's gestures do (#5477); estimates unchanged. | diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 0c65afc74..943275476 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -2,6 +2,8 @@ > **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-10 · **Change:** minor (right-click on a word Auto Spell Check underlined: its suggestions, Ignore All and Add to Dictionary, #4936; previously minor: Canvas: a 3D effect changed on type or a group redraws where its solid was; Fit to Artwork Bounds and per-artboard SVG export build each solid once for all artboards; previously minor: Effect Gallery: a thumbnail for each filter and a preview area inside the dialog, on an original drawing; Raster effects 52% → 55%, hours 1–2 → none; headline numbers unchanged; Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: PDF editable import preserves authored ActualText once, including empty/nested spans and authored ordering (#6165); cross-line/style layout remains approximate; scores and estimates unchanged; PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; G5: UI Frames built-in symbol library with default 9-slice; bundled content ~32%; previously minor: G5: dynamic symbol part on-canvas opacity/blend/gradient/pattern; strokes remaining no longer lists Overlap/auto corners; symbols ~88%; previously minor: interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; previously minor: harakat paint fallback clarified; previously minor: current Guides feature-area rows consolidated using existing ruler evidence; scores and estimates unchanged; previously minor: Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; previously minor: G5 docs: Art brush Overlap and Pattern brush auto corners removed from strokes remaining; symbols remaining lists dynamic on-canvas beyond solid and guides outside the art; previously minor: custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: EPS import: embedded fonts (composite, Type 42, CID-keyed, Type 1, CFF FontSet) draw their glyphs; spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; `.ai`/EPS type keeps its tab stops, and right-aligned lines with tabs lay out as the file's page, #6147; `.ai`/EPS type from the text document lays its lines out closer to the file's page, within limits, #6137; `.ai` and EPS import: older Art brush objects read; numeric fields step on wheel hover without taking keyboard focus, and a panel wheel turn cannot seed a canvas zoom glide, #5949; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen; Tools stays beside the canvas with left-side panels; new gradients follow document color mode; Selection QoL: marquee modes and visible feedback, #5313; SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; the canvas keeps selection outlines and anchors between frames; Indian-language hyphenation review and license allowlist, #4947; Stroke gradients: an arrowhead aligned to the tip is painted to the corners of its base, #5964; Drop Shadow and glow dialogs: spinner arrows, #5367, #5368; Stroke Weight Up and Down shortcuts, #5797; Hindi catalog and contributor loanword preferences; full wording review in progress; Properties in Edit Artboards: a Transform section with the reference point above the Artboards section, #5219; the Control bar's Stroke: and Opacity: links scrub their fields, #4088; an image zoomed in on is separated, colorized or proofed only where it shows (#4155); a document's first frame renders on the render worker, so opening a heavy file no longer freezes the window, #4155; Control bar Paragraph section with type selected, #5372; translating unchanged type reuses its glyph and character-stroke outlines; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026; Units › East Asian Type acts, with Q and Ha units; scores unchanged) +> Vector Halftone preview update (2026-11-10): temporary dot selection marks are hidden, and the bounding box stays available (#6369). + ## Feature submission evidence EPS embedded fonts, 2026-11-09 (#6023–#6067): the fonts an EPS carries draw their glyphs instead of being set as type in an installed font: composite (Type 0) fonts with the 8/8 mapping, plain Type 42 fonts (characters from their glyph names or character map, widths from the font), CID-keyed fonts over CMaps (Type 0 and Type 2 CIDFonts, CIDFonts in a FontSet's Compact Font Format data, base-font CMaps, named CIDFonts over predefined Unicode CMaps set as type) and embedded Type 1 fonts; the show operators' spacing along the baseline is kept as tracking, `kshow` included. Each step is charged to the interpreter's budgets. Hand-written test files only (no corpus file uses these fonts); scores and estimates unchanged. Evidence: `crates/eps/src/import/tests_composite.rs`, `tests_type42.rs`, `tests_cid.rs`, `tests_type1.rs`; details in [file-format-parity.md](file-format-parity.md). @@ -389,7 +391,7 @@ what keeps it from beta, not from alpha. | Dimension | % | One agent (h) | Kind | Doc | |---|---:|---:|---|---| | Features (depth, weighted) | 75% | 220–360 | estimated | this file | -| UI/UX fidelity (look ~80%, interaction ~45%) | 55% | 70–110 | estimated | [ui-parity.md](ui-parity.md); Tools sits inside left-side panels beside the canvas (layout choice; no rescoring); held spinner arrows keep stepping (#6307) | +| UI/UX fidelity (look ~80%, interaction ~45%) | 55% | 70–110 | estimated | [ui-parity.md](ui-parity.md); Tools sits inside left-side panels beside the canvas (layout choice; no rescoring); held spinner arrows keep stepping (#6307); Vector Halftone Preview keeps the selection box without the dot marks (#6369) | | File formats | 75% | 15–30 | estimated | [file-format-parity.md](file-format-parity.md) | | Hardware (GPU, pen, touch, displays) | 35% | 25–45 | estimated | [hardware-parity.md](hardware-parity.md) | | Localization (the 12 key languages, equally weighted) | 74% | 50–85 | measured from catalogs | [localization-parity.md](localization-parity.md) | @@ -819,6 +821,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - In isolation mode the Control bar has an Exit Isolation Mode button (`object.exitIsolation`), except while a pattern is edited. Outside isolation mode, with one group, compound path, path, image, mesh or placed file selected, it has an Isolate Selected Object button (`object.isolate`), which also isolates a path inside a group that Direct Selection selected. - While an opacity mask is edited, Exit Isolation Mode (Esc, the isolation bar's back arrow, the Control bar's button, and the Layers panel menu's and canvas context menu's Exit Isolation Mode) leaves mask editing as Stop Editing Opacity Mask does, in one undo step: the mask keeps the edits, and the masked object is selected. While a pattern is edited, the Exit Isolation Mode item of the Layers panel menu and of the canvas context menu saves the pattern and leaves pattern editing, as Done does. - Numeric spinner arrows keep stepping while held with the mouse past the click timeout and stop on release (#6307; `a_held_spinner_arrow_repeats`). + - Object › Vector Halftone keeps the selection box and hides selection marks on the temporary dots during Preview; turning Preview off or cancelling restores the original selection marks (#6369). - Tab shows and hides the panels (in the Type tool it types a tab) without moving the keyboard focus onto a field, so the next shortcut or letter still works; in dialogs and panel fields it moves between fields, one press each — a numeric field's scrub label is not a tab stop of its own, and the Transform panel's, Properties panel's and Control bar's W/H link comes after the fields it sits between (`one_tab_moves_from_a_field_to_the_next`). A field's up/down spinner, the chevron that opens its presets (Stroke Weight, font size, Rotate, Export for Screens' scale) and the Transparency row's opacity slider chevron are not tab stops either. Alt+↓ with no other modifier in a field with presets opens them with the keyboard focus on the chosen one, instead of stepping the field down; Enter picks the focused preset without pressing a dialog's OK, and after a pick or Escape the field has the keyboard focus again (`tab_goes_from_field_to_field_past_the_spinner_and_the_preset_chevron`, `tab_goes_from_the_opacity_field_past_its_slider_chevron`, `alt_down_opens_a_fields_presets_for_the_keyboard`, `escape_closes_a_fields_presets_and_returns_to_the_field`). - Shift+Tab (`window.panels`) shows and hides the dock and the floating panels and leaves the toolbar and the Control bar as they are; hiding the panels with Tab or Shift+Tab also closes a panel popped out of the icon column. - The second Tab (`window.dock`) shows again only the bars the first Tab hid, after a restart too: a Control bar that was off stays off, a toolbar hidden with Window › Tools stays hidden, and panels shown with Shift+Tab in between stay shown. @@ -2002,6 +2005,7 @@ Edit › Copy and Cut put the selected ruler guides on the clipboard, with the s | 2026-10-30 | minor | Color Effects add-on: live Duotone (one to four inks, spot tones kept) and Print Ink Fixes; scores and estimates unchanged | | 2026-10-30 | minor | 3D Extrude & Bevel, Inflate and Rotate apply to type, which stays editable (#4715); scores and estimates unchanged | | 2026-10-30 | minor | Color Effects add-on: live Vector Halftone; scores and estimates unchanged | +| 2026-11-10 | minor | Vector Halftone Preview leaves the dots unobscured and keeps the selection box (#6369). Scores and estimates unchanged. | | 2026-10-30 | minor | Path Precision Tools add-on: the Parametric Shape Tool; evidence added; scores unchanged. | | 2026-10-30 | minor | Presentation Mode fits the active artboard and hides selections, guides, grids and artboard edges; scores and estimates unchanged | | 2026-10-30 | minor | Color Effects add-on: Select by Color Kind; scores and estimates unchanged | diff --git a/docs/ui-parity.md b/docs/ui-parity.md index 103b0d10b..3ae04ecd0 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -2,6 +2,8 @@ > **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-09 · **Change:** minor (interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; numeric fields step on wheel hover without taking keyboard focus, #5949; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen; Selection QoL: marquee modes and visible feedback, #5313; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; Drop Shadow and glow dialogs: spinner arrows, #5367, #5368; Stroke Weight Up and Down shortcuts, #5797; Layers: Rename (`ui.rename`) opens the name field of the selected layer, group or object, #5324; Properties in Edit Artboards: Transform section above the Artboards section, #5219; scrubby labels: the Control bar's Stroke: and Opacity: links and the Properties panel's Stroke link scrub their fields, #4088; Properties: Quick Actions has Repeat Options for one selected repeat, #5181; Direct Selection: Shift-click picks several blend spine points, which drag together, #5285; Direct Selection: Cmd/Ctrl held lends the Selection tool and shows its bounding box, #3901; Join tool trims two crossing open paths and joins them at the crossing, #5844; Control bar: Paragraph: link and Align left/center/right while type is selected, #5372; shape tools: a press on the selection's centre point moves it and one in the rotate zone outside a box corner turns it, #5477; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; Units › East Asian Type acts, with Q and Ha units; scores unchanged) +> Vector Halftone preview update (2026-11-10): temporary dot selection marks are hidden, and the bounding box stays available (#6369). + > Numeric spinner update (2026-11-10): held mouse arrows repeat past the click timeout; scores and estimates unchanged (#6307). ## Feature submission evidence @@ -246,6 +248,7 @@ tools, never checked side by side) isn't captured row by row. | Moving a rounded rectangle with Craquelure at high DPI | partial | Distance searches optimized; moved-viewport pixels unchanged for three texture effects; `texture_drag_perf` probe available. Idle Windows mouse-drag QA remains (#1257) | | Bounding-box handles: Shift proportional, Alt from centre, rotate outside the corners | done | | | Rotated objects keep a rotated bounding box and their angle; Reset Bounding Box | done | | +| Vector Halftone Preview keeps the selection box and hides selection marks on the temporary dots | done | #6369; Preview off and Cancel restore the original marks. | | Free Transform, Scale and Reflect along a rotated box | missing | | | Free Transform widget: constrain, free distort, perspective distort; the key-held variants | done | | | Transform tools: click sets the reference point (snaps to anchors/centres), Alt-click opens the dialog, Alt-release copies | done | with anchors selected with Direct Selection, the tools, the dialogs and Move transform those anchors alone, about the centre of their box by default (#1408; the default point and whole-object copies are our choices) | @@ -561,6 +564,7 @@ separate. Scores and hours unchanged. [Behavior and command details](development | 2026-10-31 | minor | Artwork Cleanup add-on (a VectorCraft addition): its text tools in the panel and Object › Plug-ins › Artwork Cleanup; scores and estimates unchanged | | 2026-10-30 | minor | Color Effects add-on: live Duotone (one to four inks, spot tones kept) and Print Ink Fixes; scores and estimates unchanged | | 2026-10-30 | minor | Color Effects add-on: live Vector Halftone; scores and estimates unchanged | +| 2026-11-10 | minor | Vector Halftone Preview leaves the dots unobscured and keeps the selection box (#6369). Scores and estimates unchanged. | | 2026-10-30 | minor | Path Precision Tools add-on: the Parametric Shape Tool (ten re-editable shape kinds), its panel and Convert to Parametric Shape; scores and estimates unchanged | | 2026-10-30 | minor | Smart Pen add-on: an Alt-drag away from paths selects points with a marquee; scores and estimates unchanged | | 2026-10-30 | minor | Color Effects add-on: Select by Color Kind; scores and estimates unchanged | From e3a17716edcf42a937f25b56db3068bad7a8b330 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabio=20William=20Concei=C3=A7=C3=A3o?= Date: Tue, 10 Nov 2026 08:47:45 +0000 Subject: [PATCH 20/38] G5: built-in Map Markers symbol library --- crates/engine/src/cmd/symbollib.rs | 28 ++++++++++++++++ crates/engine/src/tests/symbollib.rs | 32 ++++++++++++++++++- crates/ui-egui/src/i18n/bg.tsv | 5 +++ crates/ui-egui/src/i18n/bs.tsv | 5 +++ crates/ui-egui/src/i18n/ca.tsv | 5 +++ crates/ui-egui/src/i18n/cs.tsv | 5 +++ crates/ui-egui/src/i18n/da.tsv | 5 +++ crates/ui-egui/src/i18n/de.tsv | 5 +++ crates/ui-egui/src/i18n/el.tsv | 5 +++ crates/ui-egui/src/i18n/es.tsv | 5 +++ crates/ui-egui/src/i18n/fi.tsv | 5 +++ crates/ui-egui/src/i18n/fr.tsv | 5 +++ crates/ui-egui/src/i18n/hi.tsv | 5 +++ crates/ui-egui/src/i18n/hu.tsv | 5 +++ crates/ui-egui/src/i18n/id.tsv | 5 +++ crates/ui-egui/src/i18n/it.tsv | 5 +++ crates/ui-egui/src/i18n/ja.tsv | 5 +++ crates/ui-egui/src/i18n/ka.tsv | 5 +++ crates/ui-egui/src/i18n/ko.tsv | 5 +++ crates/ui-egui/src/i18n/nl.tsv | 5 +++ crates/ui-egui/src/i18n/pl.tsv | 5 +++ crates/ui-egui/src/i18n/pt-br.tsv | 5 +++ crates/ui-egui/src/i18n/ru.tsv | 5 +++ crates/ui-egui/src/i18n/sv.tsv | 5 +++ crates/ui-egui/src/i18n/tr.tsv | 5 +++ crates/ui-egui/src/i18n/uk.tsv | 5 +++ crates/ui-egui/src/i18n/vi.tsv | 5 +++ crates/ui-egui/src/i18n/zh-hans.tsv | 5 +++ crates/ui-egui/src/i18n/zh-hant.tsv | 5 +++ .../src/panels/tests_symbol_libraries.rs | 2 +- docs/gaps.md | 14 ++++---- docs/target-app-parity.md | 15 +++++---- 32 files changed, 211 insertions(+), 15 deletions(-) diff --git a/crates/engine/src/cmd/symbollib.rs b/crates/engine/src/cmd/symbollib.rs index 1e74eb456..fe3f1149d 100644 --- a/crates/engine/src/cmd/symbollib.rs +++ b/crates/engine/src/cmd/symbollib.rs @@ -157,6 +157,7 @@ pub struct BuiltinSymbolLibrary { pub const BUILTIN: &[BuiltinSymbolLibrary] = &[ BuiltinSymbolLibrary { id: "basic-shapes", name: "Basic Shapes", make: basic_shapes }, BuiltinSymbolLibrary { id: "ui-frames", name: "UI Frames", make: ui_frames }, + BuiltinSymbolLibrary { id: "map-markers", name: "Map Markers", make: map_markers }, ]; /// Built-in library `id`, made once. @@ -221,6 +222,33 @@ fn ui_frames() -> SymbolLibrary { SymbolLibrary { name: "UI Frames".into(), doc } } +/// Map Markers: pin, flag, crosshair and checkpoint for map/HUD art (original, no 9-slice). +fn map_markers() -> SymbolLibrary { + let c = Point::ZERO; + let pin = [(0.0, 22.0), (-9.0, 4.0), (-12.0, -6.0), (-8.0, -16.0), (0.0, -20.0), (8.0, -16.0), (12.0, -6.0), (9.0, 4.0)]; + let flag = [(-16.0, 22.0), (-12.0, 22.0), (-12.0, -8.0), (18.0, -14.0), (-12.0, -20.0), (-12.0, -22.0), (-16.0, -22.0)]; + let pin: Vec = pin.iter().map(|&(x, y)| Point::new(x, y)).collect(); + let flag: Vec = flag.iter().map(|&(x, y)| Point::new(x, y)).collect(); + let markers = [ + ("Pin", vectorcraft_geom::PathData::single(vectorcraft_geom::SubPath::polyline(&pin, true)), "#e74c3c"), + ("Flag", vectorcraft_geom::PathData::single(vectorcraft_geom::SubPath::polyline(&flag, true)), "#3498db"), + ("Crosshair", shapes::star(c, 20.0, 5.0, 4, 0.0), "#2c3e50"), + ("Checkpoint", shapes::polygon(c, 18.0, 4, 0.0), "#27ae60"), + ]; + let mut doc = Document::new(100.0, 100.0); + for (name, path, hex) in markers { + let fill = Color::from_hex(hex).unwrap_or(Color::BLACK); + let edge = fill.lerp(&Color::BLACK, 0.35); + let id = doc.alloc_id(); + let art = Node::path(id, path, Appearance::basic(Paint::solid(fill), Paint::solid(edge), 1.25)); + let Ok((art, size, _)) = normalize(art) else { continue }; + doc.symbols.push(Symbol { dynamic: false, name: name.into(), art: Arc::new(art) }); + set_natural_size(&mut doc, name, Some(size)); + } + doc.title = "Map Markers".into(); + SymbolLibrary { name: "Map Markers".into(), doc } +} + /// An instance of symbol `name` (what the resource gathering follows). fn instance(name: &str) -> Node { Node::new(NodeId(0), NodeKind::SymbolInstance { symbol: name.into(), xf: Affine::IDENTITY }) diff --git a/crates/engine/src/tests/symbollib.rs b/crates/engine/src/tests/symbollib.rs index 42909da1c..410bd473d 100644 --- a/crates/engine/src/tests/symbollib.rs +++ b/crates/engine/src/tests/symbollib.rs @@ -84,12 +84,16 @@ fn the_builtin_library_is_original_art_generated_in_code() { let mut s = Session::new(); let list = run(&mut s, "symbol.libraries", json!({})); let libs = list["libraries"].as_array().unwrap(); - assert_eq!(libs.len(), 2, "{libs:?}"); + assert_eq!(libs.len(), 3, "{libs:?}"); assert_eq!((libs[0]["id"].as_str(), libs[0]["category"].as_str()), (Some("basic-shapes"), Some("builtIn"))); assert_eq!( (libs[1]["id"].as_str(), libs[1]["name"].as_str(), libs[1]["category"].as_str()), (Some("ui-frames"), Some("UI Frames"), Some("builtIn")) ); + assert_eq!( + (libs[2]["id"].as_str(), libs[2]["name"].as_str(), libs[2]["category"].as_str()), + (Some("map-markers"), Some("Map Markers"), Some("builtIn")) + ); let lib = run(&mut s, "symbol.library", json!({"library": "basic shapes"})); let names: Vec<&str> = lib["symbols"].as_array().unwrap().iter().map(|x| x["name"].as_str().unwrap()).collect(); assert_eq!(names, ["Circle", "Square", "Rounded Square", "Triangle", "Diamond", "Hexagon", "Star", "Burst", "Arrow"]); @@ -125,6 +129,32 @@ fn ui_frames_symbols_carry_default_nine_slice_when_added() { )); } +#[test] +fn map_markers_symbols_add_without_nine_slice() { + let mut s = session(); + let lib = run(&mut s, "symbol.library", json!({"library": "map-markers"})); + let names: Vec<&str> = lib["symbols"].as_array().unwrap().iter().map(|x| x["name"].as_str().unwrap()).collect(); + assert_eq!(names, ["Pin", "Flag", "Crosshair", "Checkpoint"]); + let mut placed = Vec::new(); + for (i, name) in names.iter().enumerate() { + let r = run( + &mut s, + "symbol.addFromLibrary", + json!({"library": "map-markers", "name": name, "place": true, "x": 100.0 + 80.0 * i as f64, "y": 100.0}), + ); + assert_eq!(r["added"], json!([name])); + placed.push(r["placed"].as_u64().unwrap()); + } + let d = doc(&s); + for name in &names { + assert_eq!(d.symbol_nine_slice(name), None, "{name} should not carry 9-slice guides"); + } + assert!(matches!( + d.node(NodeId(placed[0])).map(|n| &n.kind), + Some(NodeKind::SymbolInstance { symbol, .. }) if symbol == "Pin" + )); +} + #[test] fn a_saved_library_reopens_and_its_symbols_join_another_document_in_one_step() { let dir = folder("user"); diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index e1121be66..92c0eb459 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -5844,6 +5844,11 @@ axis Y Axis Y ос Button Бутон Panel Панел UI Frames UI рамки + Map Markers Маркери на карта + Pin Карфица + Flag Флаг + Crosshair Мерник + Checkpoint Контролна точка Open Symbol Library Отваряне на библиотека със символи Save Symbol Library Запазване на библиотека със символи Save Symbol Library… Запази библиотека със символи… diff --git a/crates/ui-egui/src/i18n/bs.tsv b/crates/ui-egui/src/i18n/bs.tsv index d4749aba4..35a50dd07 100644 --- a/crates/ui-egui/src/i18n/bs.tsv +++ b/crates/ui-egui/src/i18n/bs.tsv @@ -5888,6 +5888,11 @@ axis Y Axis Y osa Button Dugme Panel Panel UI Frames UI okviri + Map Markers Markeri mape + Pin Pin + Flag Zastava + Crosshair Nišan + Checkpoint Kontrolna tačka Open Symbol Library Otvori biblioteku simbola Save Symbol Library Sačuvaj biblioteku simbola Save Symbol Library… Sačuvaj biblioteku simbola… diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index 91169aaa4..0a05f93bc 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -443,6 +443,11 @@ antiAlias Sharp Nítid Button Botó Panel Panell UI Frames Marcs d’IU + Map Markers Marcadors de mapa + Pin Xinxeta + Flag Bandera + Crosshair Punt de mira + Checkpoint Punt de control Batch Lot Batch… Lot… Bearing Orientació diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 91d86612c..4db1434f7 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -6052,6 +6052,11 @@ axis Y Axis osa Y Button Tlačítko Panel Panel UI Frames Rámce UI + Map Markers Mapové značky + Pin Špendlík + Flag Vlajka + Crosshair Zaměřovač + Checkpoint Kontrolní bod Open Symbol Library Otevřít knihovnu symbolů Save Symbol Library Uložit knihovnu symbolů Save Symbol Library… Uložit knihovnu symbolů… diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index 6348e68a4..e0143dd66 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -5859,6 +5859,11 @@ axis Y Axis Y-akse Button Knap Panel Panel UI Frames UI-rammer + Map Markers Kortmarkører + Pin Nål + Flag Flag + Crosshair Sigtekorn + Checkpoint Kontrolpunkt Open Symbol Library Åbn symbolbibliotek Save Symbol Library Gem symbolbibliotek Save Symbol Library… Gem symbolbibliotek… diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 0ffe6eeb9..fda23a01f 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -5635,6 +5635,11 @@ axis Y Axis Y-Achse Button Schaltfläche Panel Bedienfeld UI Frames UI-Rahmen + Map Markers Kartenmarkierungen + Pin Stecknadel + Flag Flagge + Crosshair Fadenkreuz + Checkpoint Kontrollpunkt Open Symbol Library Symbol-Bibliothek öffnen Save Symbol Library Symbol-Bibliothek speichern Save Symbol Library… Symbol-Bibliothek speichern… diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 6dc7a40ea..387de896c 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -6003,6 +6003,11 @@ axis Y Axis Άξονας Y Button Κουμπί Panel Πίνακας UI Frames Πλαίσια UI + Map Markers Δείκτες χάρτη + Pin Καρφίτσα + Flag Σημαία + Crosshair Σταυρόνημα + Checkpoint Σημείο ελέγχου Open Symbol Library Άνοιγμα βιβλιοθήκης συμβόλων Save Symbol Library Αποθήκευση βιβλιοθήκης συμβόλων Save Symbol Library… Αποθήκευση βιβλιοθήκης συμβόλων… diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 0f3947277..7850df8cb 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -5994,6 +5994,11 @@ axis Y Axis Eje Y Button Botón Panel Panel UI Frames Marcos de IU + Map Markers Marcadores de mapa + Pin Chincheta + Flag Bandera + Crosshair Punto de mira + Checkpoint Punto de control Open Symbol Library Abrir biblioteca de símbolos Save Symbol Library Guardar biblioteca de símbolos Save Symbol Library… Guardar biblioteca de símbolos… diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index e2ab48874..b2567df75 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -6082,6 +6082,11 @@ axis Y Axis Y-akseli Button Painike Panel Paneeli UI Frames UI-kehykset + Map Markers Karttamerkit + Pin Nasta + Flag Lippu + Crosshair Tähtäin + Checkpoint Tarkastuskohta Open Symbol Library Avaa symbolikirjasto Save Symbol Library Tallenna symbolikirjasto Save Symbol Library… Tallenna symbolikirjasto… diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index c2c333b6b..d4ed847db 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -6047,6 +6047,11 @@ axis Y Axis Axe Y Button Bouton Panel Panneau UI Frames Cadres d’IU + Map Markers Marqueurs de carte + Pin Épingle + Flag Drapeau + Crosshair Réticule + Checkpoint Point de contrôle Open Symbol Library Ouvrir une bibliothèque de symboles Save Symbol Library Enregistrer la bibliothèque de symboles Save Symbol Library… Enregistrer la bibliothèque de symboles… diff --git a/crates/ui-egui/src/i18n/hi.tsv b/crates/ui-egui/src/i18n/hi.tsv index d39ab857f..f1922221f 100644 --- a/crates/ui-egui/src/i18n/hi.tsv +++ b/crates/ui-egui/src/i18n/hi.tsv @@ -514,6 +514,11 @@ antiAlias Sharp शार्प Button बटन Panel पैनल UI Frames UI फ़्रेम + Map Markers मानचित्र चिह्न + Pin पिन + Flag झंडा + Crosshair क्रॉसहेयर + Checkpoint चेकपॉइंट Batch बैच Batch Trace Folder फ़ोल्डर बैच ट्रेस करें Batch… बैच… diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 2b70f11db..463bac7cd 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -6035,6 +6035,11 @@ axis Y Axis Y tengely Button Gomb Panel Panel UI Frames UI keretek + Map Markers Térképjelölők + Pin Gombostű + Flag Zászló + Crosshair Célkereszt + Checkpoint Ellenőrzőpont Open Symbol Library Szimbólumkönyvtár megnyitása Save Symbol Library Szimbólumkönyvtár mentése Save Symbol Library… Szimbólumkönyvtár mentése… diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index ed0ce6d4e..4b1502dd9 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -5859,6 +5859,11 @@ axis Y Axis Sumbu Y Button Tombol Panel Panel UI Frames Bingkai UI + Map Markers Penanda Peta + Pin Pin + Flag Bendera + Crosshair Bidik silang + Checkpoint Pos pemeriksaan Open Symbol Library Buka pustaka simbol Save Symbol Library Simpan pustaka simbol Save Symbol Library… Simpan pustaka simbol… diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 975800f1f..854d92b40 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -5669,6 +5669,11 @@ axis Y Axis Asse Y Button Pulsante Panel Pannello UI Frames Cornici UI + Map Markers Marcatori mappa + Pin Spillo + Flag Bandiera + Crosshair Mirino + Checkpoint Punto di controllo Open Symbol Library Apri libreria simboli Save Symbol Library Salva libreria simboli Save Symbol Library… Salva libreria simboli… diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 4e0df1a47..c35ad35cc 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -5734,6 +5734,11 @@ axis Y Axis Y 軸 Button ボタン Panel パネル UI Frames UIフレーム + Map Markers マップマーカー + Pin ピン + Flag 旗 + Crosshair クロスヘア + Checkpoint チェックポイント Open Symbol Library シンボルライブラリを開く Save Symbol Library シンボルライブラリを保存 Save Symbol Library… シンボルライブラリを保存… diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index 0ff9696f7..4c3759852 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -5975,6 +5975,11 @@ axis Y Axis Y ღერძი Button ღილაკი Panel პანელი UI Frames UI ჩარჩოები + Map Markers რუკის ნიშნულები + Pin ქინძისთავი + Flag დროშა + Crosshair სამიზნე + Checkpoint საკონტროლო წერტილი Open Symbol Library სიმბოლოების ბიბლიოთეკის გახსნა Save Symbol Library სიმბოლოების ბიბლიოთეკის შენახვა Save Symbol Library… სიმბოლოების ბიბლიოთეკის შენახვა… diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index b68b4b308..885b90381 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -5630,6 +5630,11 @@ axis Y Axis Y축 Button 버튼 Panel 패널 UI Frames UI 프레임 + Map Markers 지도 마커 + Pin 핀 + Flag 깃발 + Crosshair 십자선 + Checkpoint 체크포인트 Open Symbol Library 심볼 라이브러리 열기 Save Symbol Library 심볼 라이브러리 저장 Save Symbol Library… 심볼 라이브러리 저장… diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index 005972780..f37225fef 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -5910,6 +5910,11 @@ axis Y Axis Y-as Button Knop Panel Paneel UI Frames UI-kaders + Map Markers Kaartmarkeringen + Pin Speld + Flag Vlag + Crosshair Vizier + Checkpoint Controlepunt Open Symbol Library Symboolbibliotheek openen Save Symbol Library Symboolbibliotheek opslaan Save Symbol Library… Symboolbibliotheek opslaan… diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index feb3d8308..8840b28eb 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -5906,6 +5906,11 @@ axis Y Axis Oś Y Button Przycisk Panel Panel UI Frames Ramki UI + Map Markers Znaczniki mapy + Pin Pinezka + Flag Flaga + Crosshair Celownik + Checkpoint Punkt kontrolny Open Symbol Library Otwórz bibliotekę symboli Save Symbol Library Zapisz bibliotekę symboli Save Symbol Library… Zapisz bibliotekę symboli… diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index e0e83aaec..d0a1f5a81 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -5967,6 +5967,11 @@ axis Y Axis Eixo Y Button Botão Panel Painel UI Frames Molduras de IU + Map Markers Marcadores de mapa + Pin Alfinete + Flag Bandeira + Crosshair Mira + Checkpoint Ponto de verificação Open Symbol Library Abrir Biblioteca de Símbolos Save Symbol Library Salvar Biblioteca de Símbolos Save Symbol Library… Salvar Biblioteca de Símbolos… diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 7ccd9c605..5b6f91220 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -5663,6 +5663,11 @@ axis Y Axis Ось Y Button Кнопка Panel Панель UI Frames UI-рамки + Map Markers Маркеры карты + Pin Булавка + Flag Флаг + Crosshair Прицел + Checkpoint Контрольная точка Open Symbol Library Открыть библиотеку символов Save Symbol Library Сохранить библиотеку символов Save Symbol Library… Сохранить библиотеку символов… diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index a732ff6c0..d637a33e0 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -6075,6 +6075,11 @@ axis Y Axis Y-axel Button Knapp Panel Panel UI Frames UI-ramar + Map Markers Kartmarkörer + Pin Nål + Flag Flagga + Crosshair Hårkors + Checkpoint Kontrollpunkt Open Symbol Library Öppna symbolbibliotek Save Symbol Library Spara symbolbibliotek Save Symbol Library… Spara symbolbibliotek… diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index dad8615f5..e6f96e7a8 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -6008,6 +6008,11 @@ axis Y Axis Y ekseni Button Düğme Panel Panel UI Frames UI çerçeveleri + Map Markers Harita İşaretleri + Pin İğne + Flag Bayrak + Crosshair Nişangah + Checkpoint Kontrol noktası Open Symbol Library Sembol Kitaplığını Aç Save Symbol Library Sembol Kitaplığını Kaydet Save Symbol Library… Sembol Kitaplığını Kaydet… diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 3fcc681b5..2d0fe9859 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -5649,6 +5649,11 @@ axis Y Axis Вісь Y Button Кнопка Panel Панель UI Frames UI-рамки + Map Markers Маркери карти + Pin Шпилька + Flag Прапор + Crosshair Приціл + Checkpoint Контрольна точка Open Symbol Library Відкрити бібліотеку символів Save Symbol Library Зберегти бібліотеку символів Save Symbol Library… Зберегти бібліотеку символів… diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index 5151b4a57..762b7a9f7 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -5650,6 +5650,11 @@ axis Y Axis Trục Y Button Nút Panel Bảng UI Frames Khung UI + Map Markers Đánh dấu bản đồ + Pin Ghim + Flag Cờ + Crosshair Tâm ngắm + Checkpoint Điểm kiểm tra Open Symbol Library Mở thư viện ký hiệu Save Symbol Library Lưu thư viện ký hiệu Save Symbol Library… Lưu thư viện ký hiệu… diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index e527dbb1f..749efbdbe 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -5890,6 +5890,11 @@ axis Y Axis Y 轴 Button 按钮 Panel 面板 UI Frames UI 框架 + Map Markers 地图标记 + Pin 图钉 + Flag 旗帜 + Crosshair 准星 + Checkpoint 检查点 Open Symbol Library 打开符号库 Save Symbol Library 保存符号库 Save Symbol Library… 保存符号库… diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 16e6898bf..182dd95f3 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -5687,6 +5687,11 @@ axis Y Axis Y 軸 Button 按鈕 Panel 面板 UI Frames UI 框架 + Map Markers 地圖標記 + Pin 圖釘 + Flag 旗幟 + Crosshair 準星 + Checkpoint 檢查點 Open Symbol Library 開啟符號資料庫 Save Symbol Library 儲存符號資料庫 Save Symbol Library… 儲存符號資料庫… diff --git a/crates/ui-egui/src/panels/tests_symbol_libraries.rs b/crates/ui-egui/src/panels/tests_symbol_libraries.rs index 6482bf312..7dee49df2 100644 --- a/crates/ui-egui/src/panels/tests_symbol_libraries.rs +++ b/crates/ui-egui/src/panels/tests_symbol_libraries.rs @@ -113,7 +113,7 @@ fn the_window_menu_and_the_symbols_panel_open_symbol_libraries() { let under = |e: &crate::menus::MenuEntry| e.path == ["Window", "Symbol Libraries"]; let opened: Vec<_> = entries.iter().filter(|e| under(e) && e.command.as_deref() == Some("window.symbolLibrary")).map(|e| e.params["library"].clone()).collect(); - assert_eq!(opened, [json!("basic-shapes"), json!("ui-frames")]); + assert_eq!(opened, [json!("basic-shapes"), json!("ui-frames"), json!("map-markers")]); let has = |cmd: &str| entries.iter().find(|e| under(e) && e.command.as_deref() == Some(cmd)).map(|e| e.enabled); assert_eq!(has("window.symbolLibrary.other"), Some(true)); assert_eq!(has("ui.saveSymbolLibrary"), Some(false), "no symbols to save yet"); diff --git a/docs/gaps.md b/docs/gaps.md index 0b910710f..2910585f6 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -1,6 +1,6 @@ # Gaps -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-10 · **Change:** minor (right-click on type: Spelling › Auto Spell Check, with its check mark, #4936; previously minor: right-click on a word Auto Spell Check underlined: its suggestions, Ignore All and Add to Dictionary, #4936; previously minor: G2: Effect Gallery thumbnails and in-dialog preview; estimate 1–2 h → 0; Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: PDF editable import preserves authored ActualText once, including empty/nested spans and authored ordering (#6165); cross-line/style layout remains approximate; scores and estimates unchanged; PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; G5: built-in UI Frames symbol library with default 9-slice; estimate 11–21 h; previously minor: G5: dynamic symbol part on-canvas takes opacity, blend, gradient and pattern; docs Overlap/auto-corners sync; estimate 12–22 h; previously minor: interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; previously minor: Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; previously minor: G5 docs: Art brush Overlap, Pattern brush auto corners and Global Edit Shift-click leave-out moved from Missing to Done (code already shipped); previously minor: custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: EPS import: embedded fonts (composite, Type 42, CID-keyed, Type 1, CFF FontSet) draw their glyphs; spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; G9: EPS gradients drawn as abutting strips import as one gradient, without seams; Tools stays beside the canvas with left-side panels; new gradients follow document color mode; Selection QoL: marquee modes and visible feedback, #5313; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; Indian-language hyphenation review and license allowlist, #4947; opening a slow file shows a progress window, part of #5930; Hindi catalog and contributor loanword preferences; agent wording pass complete; wider native proofreading welcome; an image zoomed in on is separated, colorized or proofed only where it shows (#4155); scores and estimates unchanged; a document's first frame renders on the render worker, so opening a heavy file no longer freezes the window; scores and estimates unchanged; large images in CMYK documents are shrunk before they are separated into ink planes (#4155); scores and estimates unchanged; `.ai` paths with a Calligraphic brush open brushed, with the 6D Art Pen's channels, and Windows reads the pen's tilt and barrel rotation, #372; scores and estimates unchanged; Compound Shapes add-on, beyond Illustrator; scores and estimates unchanged; Contour add-on, beyond Illustrator; scores and estimates unchanged; a slow first frame is waited for instead of restarting without the graphics adapter, and the menu bar no longer waits for the installed fonts' scan; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-10 · **Change:** minor (G5: built-in Map Markers symbol library; estimate 11–21 → 10–20 h; previously minor: right-click on type: Spelling › Auto Spell Check, with its check mark, #4936; previously minor: right-click on a word Auto Spell Check underlined: its suggestions, Ignore All and Add to Dictionary, #4936; previously minor: G2: Effect Gallery thumbnails and in-dialog preview; estimate 1–2 h → 0; Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: PDF editable import preserves authored ActualText once, including empty/nested spans and authored ordering (#6165); cross-line/style layout remains approximate; scores and estimates unchanged; PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; G5: built-in UI Frames symbol library with default 9-slice; estimate 11–21 h; previously minor: G5: dynamic symbol part on-canvas takes opacity, blend, gradient and pattern; docs Overlap/auto-corners sync; estimate 12–22 h; previously minor: interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; previously minor: Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; previously minor: G5 docs: Art brush Overlap, Pattern brush auto corners and Global Edit Shift-click leave-out moved from Missing to Done (code already shipped); previously minor: custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: EPS import: embedded fonts (composite, Type 42, CID-keyed, Type 1, CFF FontSet) draw their glyphs; spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; G9: EPS gradients drawn as abutting strips import as one gradient, without seams; Tools stays beside the canvas with left-side panels; new gradients follow document color mode; Selection QoL: marquee modes and visible feedback, #5313; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; Indian-language hyphenation review and license allowlist, #4947; opening a slow file shows a progress window, part of #5930; Hindi catalog and contributor loanword preferences; agent wording pass complete; wider native proofreading welcome; an image zoomed in on is separated, colorized or proofed only where it shows (#4155); scores and estimates unchanged; a document's first frame renders on the render worker, so opening a heavy file no longer freezes the window; scores and estimates unchanged; large images in CMYK documents are shrunk before they are separated into ink planes (#4155); scores and estimates unchanged; `.ai` paths with a Calligraphic brush open brushed, with the 6D Art Pen's channels, and Windows reads the pen's tilt and barrel rotation, #372; scores and estimates unchanged; Compound Shapes add-on, beyond Illustrator; scores and estimates unchanged; Contour add-on, beyond Illustrator; scores and estimates unchanged; a slow first frame is waited for instead of restarting without the graphics adapter, and the menu bar no longer waits for the installed fonts' scan; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x) ## Feature submission evidence @@ -97,7 +97,7 @@ stage ([ROADMAP.md](../ROADMAP.md)). | 8 | [G2 Photoshop-style raster effects and the Effect Gallery](#g2-photoshop-style-raster-effects-and-the-effect-gallery) | 0 | no | [effects-parity.md](effects-parity.md) | | 9 | [G13 Illustrator 2026 (30.x) additions](#g13-illustrator-2026-30x-additions) | 13–23 | no | [target-app-parity.md](target-app-parity.md) | | 10 | [G14 Pen hardware beyond Windows pressure](#g14-pen-hardware-beyond-windows-pressure) | 4–10 | Wayland pressure | [hardware-parity.md](hardware-parity.md) | -| 11 | [G5 Brushes, symbols and libraries](#g5-brushes-symbols-and-libraries) | 11–21 | no | [target-app-parity.md](target-app-parity.md) | +| 11 | [G5 Brushes, symbols and libraries](#g5-brushes-symbols-and-libraries) | 10–20 | no | [target-app-parity.md](target-app-parity.md) | | 12 | [G3 3D and Materials](#g3-3d-and-materials) | 25–45 | no | [effects-parity.md](effects-parity.md) | | 13 | [G7 Views and windows](#g7-views-and-windows) | 15–25 | no | [ui-parity.md](ui-parity.md) | | 14 | [G6 Automation and scripting](#g6-automation-and-scripting) | 4–8 | no | [target-app-parity.md](target-app-parity.md) | @@ -628,8 +628,9 @@ macOS cursor delivery (#2292) is narrowed to a native host adapter using the exi symbol static drops them; stored in the native format. Color eyedropper for scatter, art and pattern brushes (Hue Shift colorization; samples canvas art); symbol libraries ([public source](https://helpx.adobe.com/illustrator/desktop/manage-objects/traces-mockups-symbols/create-or-import-symbol-libraries.html)): Window › Symbol Libraries and the Symbols panel's Symbol - Libraries Menu open each library in a library panel of its own (two original libraries generated in code, - Basic Shapes and UI Frames with default 9-slice guides on Panel, Button and Bar; User Defined; Other + Libraries Menu open each library in a library panel of its own (three original libraries generated in code, + Basic Shapes, UI Frames with default 9-slice guides on Panel, Button and Bar, and Map Markers with Pin, + Flag, Crosshair and Checkpoint; User Defined; Other Library… reads the symbols of any document VectorCraft opens); a click adds a symbol to the document's Symbols with its Symbol Options and the resources it uses, as one undo step, and dragging one onto the canvas also places it; Save Symbols / Save Symbol Library writes the @@ -653,10 +654,10 @@ macOS cursor delivery (#2292) is narrowed to a native host adapter using the exi (`transparency.set`) and a gradient or pattern fill or stroke (`paint.setFill` / `paint.setStroke`, Eyedropper), for that instance only. - **Missing:** more built-in brush and graphic-style libraries (and further symbol libraries beyond Basic - Shapes and UI Frames; original, generated in code, never Adobe's); dynamic symbols' `.ai` read and write; + Shapes, UI Frames and Map Markers; original, generated in code, never Adobe's); dynamic symbols' `.ai` read and write; Global Edit of groups holding type or images, rotated copies as matches and highlighting matches before the start; 9-slice guides outside the symbol's art (a dragged guide stops at the art's edge). -- **Estimate:** 11–21 h. +- **Estimate:** 10–20 h. ## G3 3D and Materials @@ -854,6 +855,7 @@ trap, and the defaults (0.25 pt, 100%, 40%) are VectorCraft's own, the pages giv | Date | Change | Summary | |---|---|---| +| 2026-11-10 | minor | G5: built-in Map Markers symbol library (Pin, Flag, Crosshair, Checkpoint; no default 9-slice); Missing trimmed; estimate 11–21 → 10–20 h. | | 2026-11-10 | minor | G4 type: a right-click on type has Spelling › Auto Spell Check, checked while it is on, #4936; the Check Spelling and Edit Custom Dictionary dialogs remain; scores and estimates unchanged. | | 2026-11-10 | minor | G4 type: a right-click on a word Auto Spell Check underlined lists its suggestions, Ignore All and Add to Dictionary, #4936; the Check Spelling and Edit Custom Dictionary dialogs remain; scores and estimates unchanged. | | 2026-11-10 | minor | G2: the Effect Gallery shows a thumbnail of each filter and a preview area inside the dialog (an original landscape drawing passed through the filter). Estimate 1–2 h → 0. The canvas preview and one-undo OK are unchanged. | diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index c76d781c8..1f80c9956 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1,6 +1,6 @@ # Parity with Adobe Illustrator -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-10 · **Change:** minor (right-click on type: Spelling › Auto Spell Check, with its check mark, #4936; previously minor: right-click on a word Auto Spell Check underlined: its suggestions, Ignore All and Add to Dictionary, #4936; previously minor: Canvas: a 3D effect changed on type or a group redraws where its solid was; Fit to Artwork Bounds and per-artboard SVG export build each solid once for all artboards; previously minor: Effect Gallery: a thumbnail for each filter and a preview area inside the dialog, on an original drawing; Raster effects 52% → 55%, hours 1–2 → none; headline numbers unchanged; Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: PDF editable import preserves authored ActualText once, including empty/nested spans and authored ordering (#6165); cross-line/style layout remains approximate; scores and estimates unchanged; PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; G5: UI Frames built-in symbol library with default 9-slice; bundled content ~32%; previously minor: G5: dynamic symbol part on-canvas opacity/blend/gradient/pattern; strokes remaining no longer lists Overlap/auto corners; symbols ~88%; previously minor: interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; previously minor: harakat paint fallback clarified; previously minor: current Guides feature-area rows consolidated using existing ruler evidence; scores and estimates unchanged; previously minor: Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; previously minor: G5 docs: Art brush Overlap and Pattern brush auto corners removed from strokes remaining; symbols remaining lists dynamic on-canvas beyond solid and guides outside the art; previously minor: custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: EPS import: embedded fonts (composite, Type 42, CID-keyed, Type 1, CFF FontSet) draw their glyphs; spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; `.ai`/EPS type keeps its tab stops, and right-aligned lines with tabs lay out as the file's page, #6147; `.ai`/EPS type from the text document lays its lines out closer to the file's page, within limits, #6137; `.ai` and EPS import: older Art brush objects read; numeric fields step on wheel hover without taking keyboard focus, and a panel wheel turn cannot seed a canvas zoom glide, #5949; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen; Tools stays beside the canvas with left-side panels; new gradients follow document color mode; Selection QoL: marquee modes and visible feedback, #5313; SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; the canvas keeps selection outlines and anchors between frames; Indian-language hyphenation review and license allowlist, #4947; Stroke gradients: an arrowhead aligned to the tip is painted to the corners of its base, #5964; Drop Shadow and glow dialogs: spinner arrows, #5367, #5368; Stroke Weight Up and Down shortcuts, #5797; Hindi catalog and contributor loanword preferences; full wording review in progress; Properties in Edit Artboards: a Transform section with the reference point above the Artboards section, #5219; the Control bar's Stroke: and Opacity: links scrub their fields, #4088; an image zoomed in on is separated, colorized or proofed only where it shows (#4155); a document's first frame renders on the render worker, so opening a heavy file no longer freezes the window, #4155; Control bar Paragraph section with type selected, #5372; translating unchanged type reuses its glyph and character-stroke outlines; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026; Units › East Asian Type acts, with Q and Ha units; scores unchanged) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-10 · **Change:** minor (G5: Map Markers built-in symbol library; bundled content ~32% → ~34%; previously minor: right-click on type: Spelling › Auto Spell Check, with its check mark, #4936; previously minor: right-click on a word Auto Spell Check underlined: its suggestions, Ignore All and Add to Dictionary, #4936; previously minor: Canvas: a 3D effect changed on type or a group redraws where its solid was; Fit to Artwork Bounds and per-artboard SVG export build each solid once for all artboards; previously minor: Effect Gallery: a thumbnail for each filter and a preview area inside the dialog, on an original drawing; Raster effects 52% → 55%, hours 1–2 → none; headline numbers unchanged; Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: PDF editable import preserves authored ActualText once, including empty/nested spans and authored ordering (#6165); cross-line/style layout remains approximate; scores and estimates unchanged; PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; G5: UI Frames built-in symbol library with default 9-slice; bundled content ~32%; previously minor: G5: dynamic symbol part on-canvas opacity/blend/gradient/pattern; strokes remaining no longer lists Overlap/auto corners; symbols ~88%; previously minor: interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; previously minor: harakat paint fallback clarified; previously minor: current Guides feature-area rows consolidated using existing ruler evidence; scores and estimates unchanged; previously minor: Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; previously minor: G5 docs: Art brush Overlap and Pattern brush auto corners removed from strokes remaining; symbols remaining lists dynamic on-canvas beyond solid and guides outside the art; previously minor: custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: EPS import: embedded fonts (composite, Type 42, CID-keyed, Type 1, CFF FontSet) draw their glyphs; spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; `.ai`/EPS type keeps its tab stops, and right-aligned lines with tabs lay out as the file's page, #6147; `.ai`/EPS type from the text document lays its lines out closer to the file's page, within limits, #6137; `.ai` and EPS import: older Art brush objects read; numeric fields step on wheel hover without taking keyboard focus, and a panel wheel turn cannot seed a canvas zoom glide, #5949; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen; Tools stays beside the canvas with left-side panels; new gradients follow document color mode; Selection QoL: marquee modes and visible feedback, #5313; SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; the canvas keeps selection outlines and anchors between frames; Indian-language hyphenation review and license allowlist, #4947; Stroke gradients: an arrowhead aligned to the tip is painted to the corners of its base, #5964; Drop Shadow and glow dialogs: spinner arrows, #5367, #5368; Stroke Weight Up and Down shortcuts, #5797; Hindi catalog and contributor loanword preferences; full wording review in progress; Properties in Edit Artboards: a Transform section with the reference point above the Artboards section, #5219; the Control bar's Stroke: and Opacity: links scrub their fields, #4088; an image zoomed in on is separated, colorized or proofed only where it shows (#4155); a document's first frame renders on the render worker, so opening a heavy file no longer freezes the window, #4155; Control bar Paragraph section with type selected, #5372; translating unchanged type reuses its glyph and character-stroke outlines; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026; Units › East Asian Type acts, with Q and Ha units; scores unchanged) ## Feature submission evidence @@ -214,16 +214,16 @@ dominate; 3D, graphs and automation are specialist). They sum to 105. Feature de | 3D and Materials (and Turntable) | 4 | ~25% | live Revolve with automatic smooth vector shading and default-on continuous circular caps, Extrude & Bevel (twist, taper, caps, five bevel shapes), Inflate and Rotate with Position presets (colored Inflate uses smooth membrane lighting; white-light Inflate retains its existing shading; Extrude & Bevel and Rotate retain planar shading; one to ten colored lights, shared neutral ambient, painter visibility); materials, light presets, shadows, ray tracing, the rest of 3D Classic (Revolve, Rotate, Map Art), Turntable views, textures, map art and shading in 3D export | 30–55 | estimated (Extrude & Bevel, Inflate and Rotate added 2026-10-12) | | Type core | 9 | 80% | Auto-leading style resets are implemented; Hyphenation… and Justification… options are done (#2310, #2755), and a paragraph's Auto Leading also sets the Character panel's Leading, the Alt+↑/↓ leading nudge and Fill with Placeholder Text; remaining: Optical Margin Alignment, Middle Eastern features beyond bidi layout, optional group font-size preservation; transformed character controls fixed (#1034), see [type-parity.md](type-parity.md) | 14–19 | estimated; eleven-language pattern hyphenation in both composers, with grapheme and shaping-cluster protection (#4947, `hyphen::tests` and `layout::wrapping_tests`); Sanskrit patterns pending an allowlisted source; recent-font groups in font pickers (#3515); full-name font matches regression-tested (#1258); mixed paragraph alignment indication implemented; headless regressions, full integration CI and visual review pass; localized legacy family-name fallback with loaded/catalog regressions (#2208); Fractional Widths per type object regression-tested; color emoji fonts with bitmap glyphs (`sbix`, `CBDT`) draw and export; threading ports: every selected area type and type on a path shows its in and out ports (empty, arrow, red "+"), and the Selection tool threads from either, the in port into a frame before it, and a double-click on a threaded port breaks the thread there (`threads::tests::break_splits_the_thread_and_the_story_flows_into_the_first_part`, `select::tests::a_double_click_on_a_threaded_port_breaks_the_thread`, `canvas::tests::a_double_click_on_a_threaded_out_port_breaks_the_thread`) (#2572; `select::tests::a_click_on_the_in_port_threads_into_a_frame_before_it`, `text::tests::selected_type_shows_its_ports_in_each_state`, `threads::tests::add_before_threads_the_story_from_the_new_frame_into_the_clicked_one`, live screenshot); the Glyphs panel highlights and scrolls to the character selected with the Type tool (#4908, `panels::glyphs` tests); the OpenType panel lists only `ss01`–`ss20` sets present in the selected font and uses its custom names when supplied; Type-range desktop Copy publishes styled art and keeps font family, size and color on canvas Paste after editing (#5636); paint-only Arabic harakat keep joining with conditional GDEF-mark ownership and Insert Kashidas base-letter paint (`tests::harakat`) | | Type advanced (CJK, spelling, Touch Type, Retype) | 4 | 50% | CJK vertical composition (ruby, mojikumi), variable font axes, tab leaders, spell check (engine, custom dictionary and Auto Spell Check with its right-click suggestions and Spelling › Auto Spell Check done, #4936; the dialogs remain), Touch Type, Retype | 20–28 | estimated; upright fallback for fullwidth `!` / `?` regression-tested; score and hours unchanged | -| Symbols, blends, envelopes, Repeat, perspective | 5 | 88% | remaining: dynamic symbols' `.ai` round trip, further built-in symbol libraries (Basic Shapes and UI Frames with default 9-slice are done), envelope Anti-Alias/Preserve Shape, 9-slice guides outside the art; Symbol Options › Registration, Export Type (Graphic / Movie Clip) and 9-slice guides (drawn and draggable during symbol editing, #5280) and Symbol Type (dynamic on-canvas fill/stroke including gradient/pattern, opacity and blend, #4938) are done | 4–7 | estimated; Blend panel Type and Ramp control step easing with a separate color Shift (#2539); Blend Options retains spacing and orientation; polynomial curves are original approximations; interactive selected mirror axis implemented; bounding-box handles take priority over the axis line (focused regression and native checks passed; full CI blocked by an unrelated text test); #2544 spine Anchor Point conversion and picking regression-tested | +| Symbols, blends, envelopes, Repeat, perspective | 5 | 88% | remaining: dynamic symbols' `.ai` round trip, further built-in symbol libraries (Basic Shapes, UI Frames with default 9-slice, and Map Markers are done), envelope Anti-Alias/Preserve Shape, 9-slice guides outside the art; Symbol Options › Registration, Export Type (Graphic / Movie Clip) and 9-slice guides (drawn and draggable during symbol editing, #5280) and Symbol Type (dynamic on-canvas fill/stroke including gradient/pattern, opacity and blend, #4938) are done | 4–7 | estimated; Blend panel Type and Ramp control step easing with a separate color Shift (#2539); Blend Options retains spacing and orientation; polynomial curves are original approximations; interactive selected mirror axis implemented; bounding-box handles take priority over the axis line (focused regression and native checks passed; full CI blocked by an unrelated text test); #2544 spine Anchor Point conversion and picking regression-tested | | Type advanced (CJK, spelling, Touch Type, Retype) | 4 | 50% | CJK vertical composition (ruby, mojikumi), tab leaders, spell check (engine, custom dictionary and Auto Spell Check with its right-click suggestions and Spelling › Auto Spell Check done, #4936; the dialogs remain), Touch Type, Retype | 20–28 | estimated; upright fallback for fullwidth `!` / `?` regression-tested; score and hours unchanged | -| Symbols, blends, envelopes, Repeat, perspective | 5 | 88% | remaining: dynamic symbols' `.ai` round trip, further built-in symbol libraries (Basic Shapes and UI Frames with default 9-slice are done), envelope Anti-Alias/Preserve Shape, 9-slice guides outside the art; Symbol Options › Registration, Export Type (Graphic / Movie Clip) and 9-slice guides (drawn and draggable during symbol editing, #5280) and Symbol Type (dynamic on-canvas fill/stroke including gradient/pattern, opacity and blend, #4938) are done | 4–7 | estimated; Blend panel Type and Ramp control step easing with a separate color Shift (#2539); Blend Options retains spacing and orientation; polynomial curves are original approximations; interactive selected mirror axis implemented; bounding-box handles take priority over the axis line (focused regression and native checks passed; full CI blocked by an unrelated text test); #2544 spine Anchor Point conversion and picking regression-tested | +| Symbols, blends, envelopes, Repeat, perspective | 5 | 88% | remaining: dynamic symbols' `.ai` round trip, further built-in symbol libraries (Basic Shapes, UI Frames with default 9-slice, and Map Markers are done), envelope Anti-Alias/Preserve Shape, 9-slice guides outside the art; Symbol Options › Registration, Export Type (Graphic / Movie Clip) and 9-slice guides (drawn and draggable during symbol editing, #5280) and Symbol Type (dynamic on-canvas fill/stroke including gradient/pattern, opacity and blend, #4938) are done | 4–7 | estimated; Blend panel Type and Ramp control step easing with a separate color Shift (#2539); Blend Options retains spacing and orientation; polynomial curves are original approximations; interactive selected mirror axis implemented; bounding-box handles take priority over the axis line (focused regression and native checks passed; full CI blocked by an unrelated text test); #2544 spine Anchor Point conversion and picking regression-tested | | Image Trace, graphs, image tools | 3 | 75% | graph Column designs, Image Trace Create Strokes depth (Create Object Mosaic is done; junction-returning piece survival has source-generated regressions in `image-trace-quality.md`; simpler contour walk, identical output; an edge's sub-pixel shift is the same from either side; notches read only their own piece; collapsed loops keep few segments, so Paths is monotone on the code-drawn sweep) | 5–8 | estimated (raised from 72%); quick presets reveal adjustment controls, Properties reopens them; Live trace result bounds and selection highlights exclude the retained bitmap except in source-image views (scores/hours unchanged) | | Live vector effects | 5 | 85% | Outline Object, SVG Filters | 4–8 | measured count 49/50 (Pathfinder 13/13 with Trap), depth estimated; Warp Options controls verified by focused checks and original-image review | | 3D and Materials (and Turntable) | 4 | ~25% | live Revolve with automatic smooth vector shading and default-on continuous circular caps, Extrude & Bevel (twist, taper, caps, five bevel shapes), Inflate and Rotate with Position presets (colored Inflate uses smooth membrane lighting; white-light Inflate retains its existing shading; Extrude & Bevel and Rotate retain planar shading; one to ten colored lights, shared neutral ambient, original lighting presets, painter visibility); materials, shadows, ray tracing, the rest of 3D Classic (Revolve, Rotate, Map Art), Turntable views, textures, map art and shading in 3D export | 30–55 | estimated (Extrude & Bevel, Inflate and Rotate added 2026-10-12) | | Type advanced (CJK, spelling, Touch Type, Retype) | 4 | 50% | CJK vertical composition (mojikumi), variable font axes, tab leaders, spell check (engine, custom dictionary and Auto Spell Check with its right-click suggestions and Spelling › Auto Spell Check done, #4936; the dialogs remain), Touch Type, Retype | 20–28 | estimated; No Break keeps marked characters on one line; upright fallback for fullwidth `!` / `?` regression-tested; score and hours unchanged | | Type advanced (CJK, spelling, Touch Type, Retype) | 4 | 50% | CJK vertical composition (mojikumi), tab leaders, spell check (engine, custom dictionary and Auto Spell Check with its right-click suggestions and Spelling › Auto Spell Check done, #4936; the dialogs remain), Touch Type, Retype | 20–28 | estimated; No Break keeps marked characters on one line; upright fallback for fullwidth `!` / `?` regression-tested; score and hours unchanged | | Raster effects (Effect Gallery) | 4 | 55% | — | — | estimated (raised from 52%: the Effect Gallery's filter thumbnails and in-dialog preview, an original landscape drawing passed through each filter, [Filter Gallery overview](https://helpx.adobe.com/photoshop/desktop/effects-filters/get-started-with-filters/filter-gallery.html), `dialogs::effect_gallery::tests::gallery_shows_thumbnails_and_an_in_dialog_preview`; the gallery itself shipped in #2883, six categories, per-filter settings, ordered stack, live canvas preview, one undo step, `stack_ok_is_one_undo_step`, `stack_preview_is_one_interaction`); measured count 57/57 (Artistic complete with Colored Pencil, Fresco, Paint Daubs, Palette Knife, Rough Pastels and Underpainting; Sketch complete with Torn Edges and Conté Crayon, `pixel::tests_sketch::conte_crayon_*`); texture redraw distances optimized (#1257), pixels verified; the score stays under 100% because the filters are original approximations; depth estimated | -| Symbols, blends, envelopes, Repeat, perspective | 5 | 88% | remaining: dynamic symbols' `.ai` round trip, further built-in symbol libraries (Basic Shapes and UI Frames with default 9-slice are done), envelope Anti-Alias/Preserve Shape, 9-slice guides outside the art; Symbol Options › Registration, Export Type and Symbol Type (dynamic on-canvas fill/stroke including gradient/pattern, opacity and blend, #4938) and on-canvas 9-slice guides (#5280) are done | 4–7 | estimated | +| Symbols, blends, envelopes, Repeat, perspective | 5 | 88% | remaining: dynamic symbols' `.ai` round trip, further built-in symbol libraries (Basic Shapes, UI Frames with default 9-slice, and Map Markers are done), envelope Anti-Alias/Preserve Shape, 9-slice guides outside the art; Symbol Options › Registration, Export Type and Symbol Type (dynamic on-canvas fill/stroke including gradient/pattern, opacity and blend, #4938) and on-canvas 9-slice guides (#5280) are done | 4–7 | estimated | | Image Trace, graphs, image tools | 3 | 75% | graph Column designs, Image Trace Create Strokes depth (Create Object Mosaic is done; junction-returning piece survival has source-generated regressions in `image-trace-quality.md`; large images trace with a warning; Auto Color keeps rare distinct colours) | 5–8 | estimated (raised from 72%); quick presets reveal adjustment controls, Properties reopens them | | Layers, artboards, document setup | 5 | 78% | Selected-artboard deletion is consistent across panel actions (all-selected retains artboard 1); history prunes removed indices while preserving valid multiselection; remaining: 30.0's artboard locking and colours and managing artboards from the canvas, Layers panel row context menus and Collect for Export (click-to-fit objects is done, #1227) | 5–8 | estimated (lowered from 82%: target moved); Artboard deletion keys keep artwork and the first of an all-selected set; Artboard Options' Show Center Mark and Show Cross Hairs draw on the canvas only (exports and print leave them out), regression-tested; Artboard Options' Show Video Safe Areas and Video Ruler Pixel Aspect Ratio (per artboard, native format) are done ([video display options](#evidence-what-ships-today-by-area)), score unchanged | | View & navigation | 3 | 70% | multiple windows and the rest of Arrange (Cascade, Float in Window, Float All in Windows) | 9–14 | estimated; Window › Arrange › Tile and Consolidate All Windows done (score/hours unchanged); document-tab artwork hover/copy and lossless transfer tested (2026-10-22; score/hours unchanged); cached canvas reprojection preserves scale during resize, rotation and DPI changes (regression and contributor original-file evidence below; scores/hours unchanged); cached canvas dimensions match the renderer limit before cache-key creation, view construction and worker submission (regression evidence below) | @@ -487,7 +487,7 @@ about 1–1.5 agent-hours each, so the other rows (estimated on the older scale) | 3D and Materials | 4 | ~25% | Initial Revolve, Extrude & Bevel, Inflate and Rotate, with simple lighting and four original lighting presets; materials, shadows, ray tracing, Revolve (Classic), Rotate (Classic), Map Art and more complete visibility remain | 25–45 | | Type advanced | 4 | 50% | CJK composition for vertical type (mojikumi beyond Line-end Punctuation Half Width — the other sets (and their paragraph-start bracket and indent rules), custom kinsoku sets (beyond Hard, Soft and None) and the Mojikumi Settings dialog —, manual tate-chu-yoko is done, its up/down settings remain; No Break is done), tab leaders, spell check (its engine, custom dictionary commands and Auto Spell Check are done, #4936: the Check Spelling and Edit Custom Dictionary dialogs remain; a right-click on an underlined word offers its suggestions, Ignore All and Add to Dictionary, and a right-click on type has Spelling › Auto Spell Check), Touch Type, Retype; vertical point/area/path type and Type Orientation have initial support (beyond parity, not scored: inline symbols in text) | 20–28 | | Type core | 9 | 80% | Middle Eastern features beyond bidi layout, Paragraph Direction, digit types and Kashidas (the Middle Eastern composers, editable bidirectional SVG text, a split caret where directions meet); paint-only harakat keep joining with conditional GDEF-mark colour ownership, but fonts without GDEF mark classes and some AAT/`morx` ligatures can still leave marks painted as their letters; Every-line Composer fidelity for justified and ragged text (available per object; mixed type sizes in a paragraph still break single-line in non-rectangular area type and with text wrap), Optical Margin Alignment, paragraph spacing limit for vertically justified area type, hidden characters, paragraph attributes in the Type tool's copy and paste (the characters keep their styles, the paragraphs take the destination's), mixed-value display in the Paragraph panel for a selection across differing paragraphs, partial selection of type-area anchors by marquee, optional font-size preservation during group scaling (scaled character controls fixed in #1034). Hyphenation and Justification options, Fractional Widths, System Layout, styled Type-range canvas Paste (#5636), and pattern hyphenation for eleven Indian languages with grapheme/cluster protection (#4947) are done; Sanskrit automatic patterns remain pending an allowlisted source | 14–19 | -| Symbols, blends, envelopes, Repeat, perspective | 5 | 90% | further built-in symbol libraries (Basic Shapes and UI Frames with default 9-slice are done), dynamic symbols' `.ai` round trip, envelope Anti-Alias/Preserve Shape output and warp point editing, brush interpolation in blends (M8 fidelity pass done); Symbol Options › Registration and Export Type and on-canvas dynamic part paint (gradient/pattern, opacity, blend) are done; symbol editing shows draggable 9-slice guides (#5280), while guides outside the artwork remain unsupported | 4–7 | +| Symbols, blends, envelopes, Repeat, perspective | 5 | 90% | further built-in symbol libraries (Basic Shapes, UI Frames with default 9-slice, and Map Markers are done), dynamic symbols' `.ai` round trip, envelope Anti-Alias/Preserve Shape output and warp point editing, brush interpolation in blends (M8 fidelity pass done); Symbol Options › Registration and Export Type and on-canvas dynamic part paint (gradient/pattern, opacity, blend) are done; symbol editing shows draggable 9-slice guides (#5280), while guides outside the artwork remain unsupported | 4–7 | | Image Trace, graphs, image tools | 3 | 72% | graph Column designs (Graph Design, Marker designs and series colours set with Group Selection are done), Create Object Mosaic, per-feature stroke/fill choice and varying widths in Image Trace's Create Strokes (Vector Halftone and the Image Trace View modes, Palette and Create Strokes are done) | 6–10 | | Layers, artboards, document setup | 5 | 82% | Layers panel: a confirmation before deleting layers that hold art, Collect for Export, row context menus, the Layer Options dialog for several rows with mixed values, Paste Remembers Layers for sublayers, drag-scrolling a long panel while dragging rows; artboard preset polish (artboard escape routing and document setup modal handoff done; Move Artwork for artboard X/Y edits, Rearrange All Artboards, Document Setup, New Document and the rest of the Layers panel are done) | 4–7 | | View & navigation | 3 | 70% | New View/Edit Views, multiple windows/arrange, Snap to Pixel/Glyph (print tiling is done) | 9–14 | @@ -530,7 +530,7 @@ since 2026-10-01. | **Interaction fidelity:** modifiers, cursors, small behaviours | ~30–40% | The dedicated pass hasn't started, and there has been no side-by-side session with Illustrator yet. A power user notices this first | | **Look & feel** | ~75–80% | Measured against Illustrator 2026 screenshots (2026-10-02); the panels added since haven't been re-measured | | **File interop** | ~85% | SVG/SVGZ, PDF and PDF-compatible `.ai` (and legacy version 7 `.ai` out, keeping layers, groups and type as text), EPS, DXF, EMF/WMF, raster formats, PSD export and PSD/PSB placement (merged image), Place and Links, Package, Print, clipboard flavours. Affinity documents (Affinity 1–3) open natively as editable art, checked against 51 pinned public files (33 current `.af`), with selected saved-thumbnail comparisons and every-board native/SVG/PDF/PSD checks for current files; Affinity export is missing and effects, adjustments and brushes don't import. Affinity isn't an Illustrator feature, so this doesn't raise the parity score. The editing copy inside an Illustrator EPS or `.ai` is read for its structure: layers, groups, names, hidden objects and type, colours, gradients, transparency, images and every artboard (see Out of scope for what it doesn't read yet); DWG has no open spec | -| **Bundled content:** brush, symbol, style and swatch libraries | ~32% | Ours are original and generated in code, and far fewer than Illustrator ships; symbol libraries have two (Basic Shapes, UI Frames with default 9-slice) and brush libraries four (Art Ribbons, Calligraphic Nibs, Pattern Borders, Scatter Shapes) beside User Defined and Other Library… | +| **Bundled content:** brush, symbol, style and swatch libraries | ~34% | Ours are original and generated in code, and far fewer than Illustrator ships; symbol libraries have three (Basic Shapes, UI Frames with default 9-slice, Map Markers) and brush libraries four (Art Ribbons, Calligraphic Nibs, Pattern Borders, Scatter Shapes) beside User Defined and Other Library… | | **Performance** | unverified | Multithreaded, off-thread rendering and caches are in place. The last budget run (2026-10-01, loaded machine) measured 290 ms for a 50k-path fit against a 16 ms budget. Re-run `vectorcraft-cli perf` on an idle machine | | **Robustness** | good, new | ~2,840 tests, property tests, no panics in shipped code (lints, the `guard` safety net, import fuzzing), Data Recovery. Missing: a corpus of real-world files, Windows/Linux/browser QA | | **Platforms and 1.0 polish** | ~60% | The macOS app and the web build work, and Finder opens every readable format with the macOS app (#295, #354); on macOS the menus are in the system menu bar, laid out as a Mac app's, with the in-window menus as a fallback (`--in-window-menus`; verified on Windows and by cross-compilation, not yet by hand on a Mac); hybrid-graphics laptops render on the power-saving GPU by default on Windows and macOS (#306), and Windows starts on Direct3D 12, falling back to OpenGL, and loads no Vulkan driver unless `WGPU_BACKEND` asks for it (#545: Vulkan flickered black on an Intel GPU, DX12 and OpenGL didn't; #806: a faulty Vulkan driver crashed the app before its window appeared; verified by unit tests and the reporter's runs with `WGPU_BACKEND`), and Linux on the GPU the desktop runs on, since a Wayland compositor may refuse frames from the other one; a window that fails on its GPU while starting up starts again on the next one instead of crashing (#502; the dual-GPU Wayland case is verified by unit tests and cross-compilation, not on such hardware); web drops land in the document they were dropped on (#359); desktop drops carry no pointer position (winit 0.30), so a dropped document opens as a tab of its own and a picture or text is placed in the middle of the view, while the web places anything dropped on the canvas and opens what is dropped off it, as Illustrator does (#472); a web page that loses its WebGPU device or WebGL context restarts its graphics with the same documents (#369); on Linux/Wayland the clipboard reads the compositor's own (data-control) and copied image files paste, but files can't be dropped on the window under native Wayland until eframe moves to winit 0.31 (#398; the Wayland side is verified by compilation only), so Preferences › Performance › Linux Display Server › X11 (XWayland) opens the window through XWayland, where drops work (#1151; verified by unit tests and cross-compilation, not on a Wayland desktop); on Linux the Paste menu checks the system clipboard on a background thread, so a clipboard owner that never answers no longer freezes the window, while Windows and macOS keep their quick in-line check (#510; the X11 hang verified by the contributor, here by headless tests); on KDE Plasma 6.3+ under Wayland, pens and pen displays reach the app only under XWayland (`WAYLAND_DISPLAY=`) or with KWin's `KWIN_WAYLAND_EMULATE_TABLET=1`, since winit 0.30 has no Wayland tablet input (#491, not verified on hardware); Windows/Linux packaging and accessibility are pending | @@ -777,7 +777,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - The macOS menu bar is built from the in-window menus (one model, `native_menu`) and laid out as a Mac app's: the VectorCraft menu holds About, Settings ▸ (each Preferences page, General… ⌘K), Language ▸, Appearance ▸ (UI brightness), Services, Hide VectorCraft ⌃⌘H (⌘H stays Hide Edges), Hide Others, Show All and Quit; Window gains Minimize ⌃⌘M, Zoom and Bring All to Front, and Help the system's search field. Its key equivalents go back to egui as key presses, so user shortcuts, focused text fields and the Type tool's input method behave as in the window, and nothing fires twice; enabled, check marks and labels update in place. `--in-window-menus` or `VECTORCRAFT_IN_WINDOW_MENUS=1` keeps the menus in the window; `ui.inspect` reports `nativeMenuBar`. - macOS: Finder opens every format File › Open reads with VectorCraft (double-click, Open With, a drop on the Dock icon, `open -a`), at launch or while it runs; the release and `cargo xtask bundle` apps share one `Info.plist` that declares those document types, owning only VectorCraft's own formats. The DMG opens on a designed Finder window, VectorCraft.app beside the `Applications` link over a background with the app icon (#493). - Swatch and graphic style libraries, Libraries panel files, presets and plug-ins open wherever they are dropped on the window, as File › Open opens them. - - **Symbol libraries** ([public source](https://helpx.adobe.com/illustrator/desktop/manage-objects/traces-mockups-symbols/create-or-import-symbol-libraries.html)): Window › Symbol Libraries and the Symbols panel's Symbol Libraries Menu open each library in a library panel of its own, as swatch and graphic style libraries do. The built-in libraries, Basic Shapes and UI Frames (Panel, Button and Bar with default 9-slice guides), are original art generated in code (`crates/engine/src/cmd/symbollib.rs`). A click on a symbol adds it to the document's Symbols as one undo step, with its Symbol Options (size, registration, export type, 9-slice guides) and every resource it uses (the symbols it nests, patterns, swatches, brushes, graphic styles, images), and makes it the current symbol; a symbol the document already has is not added again and another symbol of the same name keeps its name, the library's comes in numbered. Thumbnails, in the library panels and the Symbols panel, draw a symbol with the symbols it nests and the patterns and images it uses. Dragging a library symbol onto the canvas also places an instance there, in the same step ([placing from a library](https://helpx.adobe.com/illustrator/desktop/manage-objects/traces-mockups-symbols/create-and-place-symbols.html)). Save Symbols (the library menu) and Save Symbol Library… (the Symbols panel menu) write the document's symbols as a `.vectorcraft` document holding only them, into the user library folder (listed under User Defined) or a file; Other Library… reads the symbols of any document VectorCraft opens; a file it can't read is an error and opens no panel. Tests: `crates/engine/src/tests/symbollib.rs`, `crates/ui-egui/src/panels/tests_symbol_libraries.rs`, `panels::symbols::tests::thumbnails_show_the_symbols_a_symbol_nests`, `canvas::tests::a_library_symbol_dropped_on_the_canvas_is_added_and_placed_there_in_one_step`. + - **Symbol libraries** ([public source](https://helpx.adobe.com/illustrator/desktop/manage-objects/traces-mockups-symbols/create-or-import-symbol-libraries.html)): Window › Symbol Libraries and the Symbols panel's Symbol Libraries Menu open each library in a library panel of its own, as swatch and graphic style libraries do. The built-in libraries, Basic Shapes, UI Frames (Panel, Button and Bar with default 9-slice guides) and Map Markers (Pin, Flag, Crosshair and Checkpoint, no default 9-slice), are original art generated in code (`crates/engine/src/cmd/symbollib.rs`). A click on a symbol adds it to the document's Symbols as one undo step, with its Symbol Options (size, registration, export type, 9-slice guides) and every resource it uses (the symbols it nests, patterns, swatches, brushes, graphic styles, images), and makes it the current symbol; a symbol the document already has is not added again and another symbol of the same name keeps its name, the library's comes in numbered. Thumbnails, in the library panels and the Symbols panel, draw a symbol with the symbols it nests and the patterns and images it uses. Dragging a library symbol onto the canvas also places an instance there, in the same step ([placing from a library](https://helpx.adobe.com/illustrator/desktop/manage-objects/traces-mockups-symbols/create-and-place-symbols.html)). Save Symbols (the library menu) and Save Symbol Library… (the Symbols panel menu) write the document's symbols as a `.vectorcraft` document holding only them, into the user library folder (listed under User Defined) or a file; Other Library… reads the symbols of any document VectorCraft opens; a file it can't read is an error and opens no panel. Tests: `crates/engine/src/tests/symbollib.rs`, `crates/ui-egui/src/panels/tests_symbol_libraries.rs`, `panels::symbols::tests::thumbnails_show_the_symbols_a_symbol_nests`, `canvas::tests::a_library_symbol_dropped_on_the_canvas_is_added_and_placed_there_in_one_step`. - **9-slice scaling** ([public source](https://helpx.adobe.com/illustrator/desktop/manage-objects/traces-mockups-symbols/maintain-proportions-while-scaling-symbols.html)): a scaled instance of a symbol with Enable Guides for 9-Slice Scaling on stays an instance and keeps the parts of its art outside the guides at their size. New guides sit a quarter of the symbol's width and height in from each edge ([reference](https://help.adobe.com/archive/en/illustrator/cs6/illustrator_reference.pdf), Use 9-slice scaling section); saved guides keep their values. The rest is VectorCraft's own rule: the corners keep both sizes, the top and bottom edges stretch in width, the left and right edges in height and the center in both; an instance smaller than its two outer cells on an axis shrinks them in proportion and leaves the center empty; the cells are sized along the symbol's axes before the instance's rotation, shear or reflection. A path with solid paint that crosses a guide is cut there and stays one path, its stroke at its weight with Scale Strokes & Effects on or off; type, images, live objects and gradient or pattern paint that cross a guide are drawn in each cell they reach, clipped to it, except those that draw symbol art (a nested instance, type with an inline graphic, a placed document, a brushed stroke, or a pattern whose tile draws one of these), which are placed by the cell their center is in; an object in one cell is placed by that cell and stays as it is. The canvas, bounds, the Eyedropper, Break Link to Symbol and the PDF, EPS, DXF, EMF/WMF and SVG exports place the art the same way; SVG writes such an instance as its placed art instead of a ``, and DXF and WMF, which do not clip, cut every path that crosses a guide and place other such objects by the cell their center is in. Tests: `symbol_nine_slice::tests` in `crates/doc`, `crates/engine/src/tests/symbol_nine_slice.rs`, `a_scaled_nine_slice_instance_keeps_its_corners` (PDF), `a_scaled_nine_slice_instance_is_written_as_the_canvas_draws_it` (SVG), `a_scaled_nine_slice_instance_is_sampled_as_it_is_drawn` (Eyedropper). - **Symbol editing mode** ([Edit symbols](https://helpx.adobe.com/illustrator/desktop/manage-objects/traces-mockups-symbols/edit-symbols.html), [Symbols panel options](https://helpx.adobe.com/illustrator/desktop/manage-objects/traces-mockups-symbols/symbols-panel-options.html)): a double-click on a symbol instance, the Control bar's Edit Symbol button, a double-click on a symbol in the Symbols panel and the panel menu's Edit Symbol (`symbol.edit`) open the symbol's art on a temporary isolated layer, where the instance shows it (from the panel: centered on the active artboard, or in the view in a document without artboards), with the rest of the document dimmed. Each edit there changes every instance at once; the instances keep their own transforms and stay linked (Break Link to Symbol still expands one), and a dynamic symbol's instances keep their appearance changes to the objects the art still has. Escape, the isolation bar's back button, the Control bar's Exit Symbol Editing Mode and a double-click outside the art leave the mode (`symbol.exitEditing`); leaving without an edit adds no undo step. Saving leaves the editing layer out. Tests: `crates/engine/src/tests/symboledit.rs`, `select::tests::a_double_click_on_a_symbol_instance_edits_its_symbol`, `panels::symbols::tests::edit_symbol_from_the_panel_centers_the_art_on_the_active_artboard_or_the_view`. - **Dynamic symbol appearance follows what an object draws** (VectorCraft's own rule): `symbol.instance.setAppearance` refuses a fill or stroke for an object of the art that does not draw it (a guide, blend, envelope, Repeat, Intertwine group, gradient mesh or placed document; a nested symbol instance's stroke, and its fill unless a solid color or none, which stains its art; an image's fill or stroke without a Convert to Shape or Outline Object effect) with an error that names the key and the object, and the call changes nothing. Opacity, blend and `null`, which takes a stored change back, are taken for every object. Tests: `crates/engine/src/tests/dynamic_symbols.rs`. @@ -1930,6 +1930,7 @@ Edit › Copy and Cut put the selected ruler guides on the clipboard, with the s | Date | Change | Summary | |---|---|---| +| 2026-11-10 | minor | Bundled content: built-in Map Markers symbol library (Pin, Flag, Crosshair, Checkpoint); symbol libraries evidence updated; ~32% → ~34%; more built-in symbol libraries remain. | | 2026-11-10 | minor | Type advanced: Spelling › Auto Spell Check in the context menu of type, checked while it is on, #4936; evidence added; the dialogs remain; scores and hours unchanged. | | 2026-11-10 | minor | Type advanced: right-click suggestions, Ignore All and Add to Dictionary on a word Auto Spell Check underlined, #4936; evidence added; the dialogs remain; scores and hours unchanged. | | 2026-11-10 | minor | Raster effects: the Effect Gallery shows a thumbnail of each filter and a preview of the visible stack (or the filter being browsed) inside the dialog, both an original landscape drawing passed through the filter ([Filter Gallery overview](https://helpx.adobe.com/photoshop/desktop/effects-filters/get-started-with-filters/filter-gallery.html)); 52% → 55%, 1–2 h → none (estimated). The canvas preview and one-undo OK are unchanged. Headline numbers unchanged. | From db97802ce677e0fbc47f950dba02d57c6f336425 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabio=20William=20Concei=C3=A7=C3=A3o?= Date: Tue, 10 Nov 2026 08:58:20 +0000 Subject: [PATCH 21/38] G5: record Map Markers on the M10 milestone --- docs/roadmap.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/roadmap.md b/docs/roadmap.md index 115ba760d..7204ed34b 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,6 +1,6 @@ # Roadmap: milestones and what's next -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-10 · **Change:** minor (right-click on type: Spelling › Auto Spell Check, with its check mark, #4936; previously minor: right-click on a word Auto Spell Check underlined: its suggestions, Ignore All and Add to Dictionary, #4936; previously minor: G2: Effect Gallery thumbnails and in-dialog preview are in; Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: PDF editable import preserves authored ActualText once, including empty/nested spans and authored ordering (#6165); cross-line/style layout remains approximate; scores and estimates unchanged; PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: EPS import: embedded fonts (composite, Type 42, CID-keyed, Type 1, CFF FontSet) draw their glyphs; spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; M4/M7: `.ai`/EPS type keeps its tab stops, and right-aligned lines with tabs lay out as the file's page, #6147; M4/M7: `.ai`/EPS type from the text document lays its lines out closer to the file's page, within limits, #6137; numeric fields step on wheel hover without taking keyboard focus, #5949; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen; Selection QoL: marquee modes and visible feedback, #5313; SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; Indian-language hyphenation review and license allowlist, #4947; Hindi catalog and contributor loanword preferences; full wording review in progress; Effect dialogs: a colour swatch beside a hex colour opens the Color Picker, #5365; shape tools move the selection from its centre point and turn it from the zone outside a box corner, #5477; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-10 · **Change:** minor (M10/G5: Map Markers built-in symbol library; previously minor: right-click on type: Spelling › Auto Spell Check, with its check mark, #4936; previously minor: right-click on a word Auto Spell Check underlined: its suggestions, Ignore All and Add to Dictionary, #4936; previously minor: G2: Effect Gallery thumbnails and in-dialog preview are in; Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: PDF editable import preserves authored ActualText once, including empty/nested spans and authored ordering (#6165); cross-line/style layout remains approximate; scores and estimates unchanged; PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: EPS import: embedded fonts (composite, Type 42, CID-keyed, Type 1, CFF FontSet) draw their glyphs; spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; M4/M7: `.ai`/EPS type keeps its tab stops, and right-aligned lines with tabs lay out as the file's page, #6147; M4/M7: `.ai`/EPS type from the text document lays its lines out closer to the file's page, within limits, #6137; numeric fields step on wheel hover without taking keyboard focus, #5949; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen; Selection QoL: marquee modes and visible feedback, #5313; SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; Indian-language hyphenation review and license allowlist, #4947; Hindi catalog and contributor loanword preferences; full wording review in progress; Effect dialogs: a colour swatch beside a hex colour opens the Color Picker, #5365; shape tools move the selection from its centre point and turn it from the zone outside a box corner, #5477; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x) > Numeric spinner update (2026-11-10): held mouse arrows repeat past the click timeout; scores and estimates unchanged (#6307). @@ -144,11 +144,11 @@ native wording review, layout checks and a web Devanagari font remain. [Notes](l | M7 | Type (point/area/path, editing, styles, OpenType, threading, glyphs) | Spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof and the custom dictionary and Ignore All commands and Auto Spell Check with its right-click suggestions, Ignore All and Add to Dictionary, and Spelling › Auto Spell Check in the context menu of type (#4936; the dialogs remain); Right-aligned area type sets the text after a line's last tab against the right edge, the text before it at its stops (#6147); A line a soft return ends is justified as a wrapped line is (#6137); Indian-language pattern hyphenation preserves syllables in both composers (#4947); 🟡 Fullwidth `!` / `?` keep an upright fallback in vertical type; Document-point character controls after object/group scaling (#1034; optional font-size preservation remains separate), Character/Paragraph Styles (explicit Auto-leading resets and local leading overrides), per-paragraph paragraph attributes, mixed paragraph alignment indication (headless regressions, full integration CI and visual review pass), Area Type Options (with Width/Height, Auto Size and Shrink Text to Fit; overflow reported to agents and vertical Align), type areas resized by their handles and reshaped with Direct Selection, threaded text, Fit Headline, Glyphs, OpenType panel, Find Font (Document, Recent and System replacement sources, #5217), installed system fonts in every font menu (fonts registered with Windows outside its font folders, the XDG and Flatpak host font folders on Linux, and fonts installed while the app runs, listed when it comes back to the front; searchable; the Character and Properties menus show each family's sample of the selected text, preview it live under the pointer or ↑/↓, show recently applied families first (#3515), filter by script and kind, list a family's styles, and star favourites; the Glyphs panel and Find Font's Replace With use the same menu without the live preview) and found by name on open/import (full-name aliases and Missing Fonts searches verified, #1258; Windows font-service QA pending), variable fonts' named instances as styles, CJK names in the UI, input methods (marked text in place, candidate window at the caret; Japanese IME), Text Wrap (offset, invert, both sides of an object; follows edits), Type on a Path effects and Options with draggable start/centre/end brackets (slide, flip), tab stops + Tabs panel (Above Text floats it over the selected text, its ruler at the canvas zoom so stops line up with the columns, #729), inline symbols in text (beyond parity), Hebrew/Arabic bidirectional text, Paragraph Direction, Every-line/Single-line Composer per object (ragged text too) done; paint-only Arabic harakat keep joining with GDEF-mark ownership and Insert Kashidas tatweels on the base letter's paint (`tests::harakat`); tab leaders, spell check, vertical type pending | 45–60 | | 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; Anchor Point spine conversion/handle editing and selection priority (#2544), with #2425 Shift reshape preserved; 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, Color Adjustments and Document Raster Effects Settings; PDF export warns when the pixel cap lowers raster-effect resolution; Artistic › Colored Pencil, Cutout, Dry Brush, Film Grain, Fresco, Neon Glow, Paint Daubs, Palette Knife, Poster Edges, Rough Pastels, Smudge Stick, Sponge, Underpainting and Watercolor and Sketch › Halftone Pattern, Bas Relief, Charcoal, Conté Crayon, Photocopy, Stamp, Torn Edges and Water Paper done (57/57 Photoshop filters); Revolve with automatic smooth vector shading, default-on circular caps and continuous rims, Extrude & Bevel, Inflate and Rotate with one to ten colored directional lights and four original lighting presets; SVG Filters, remaining raster effects and 3D materials and shadows pending | 81–130 | -| M10 | Brushes, symbols, patterns, Repeat | 🟡 pattern swatches (5 tile types, Pattern Options, editing mode, SVG ``/PDF export) and live Repeat (radial/grid/mirror) done; interactive selected mirror axis implemented (focused and native checks passed; full CI blocked by an unrelated text test); symbol libraries done (Window › Symbol Libraries, a generated Basic Shapes library, User Defined, Other Library…, Save Symbols); brush libraries done (Window › Brush Libraries, four generated libraries, User Defined, Other Library…, Save Brushes); symbol editing shows draggable 9-slice guides (#5280), with guides outside the artwork still unsupported; brushes/symbols in progress | 23–37 | +| M10 | Brushes, symbols, patterns, Repeat | 🟡 pattern swatches (5 tile types, Pattern Options, editing mode, SVG ``/PDF export) and live Repeat (radial/grid/mirror) done; interactive selected mirror axis implemented (focused and native checks passed; full CI blocked by an unrelated text test); symbol libraries done (Window › Symbol Libraries, three generated libraries (Basic Shapes, UI Frames, Map Markers), User Defined, Other Library…, Save Symbols); brush libraries done (Window › Brush Libraries, four generated libraries, User Defined, Other Library…, Save Brushes); symbol editing shows draggable 9-slice guides (#5280), with guides outside the artwork still unsupported; brushes/symbols in progress | 23–37 | | M11 | Artboards & views (selected-set deletion shared by Artboards menu, trash and Properties, retaining artboard 1 when all are selected; Artboard deletion keys keep the first of an all-selected set and protect artwork; history prunes invalid indices while preserving valid multiselection (history tests and full workspace CI pass); click-to-fit objects with the Artboard tool (#1227), artboard escape routing and document setup modal handoff done; artboard panel/tool with Move Artwork for X/Y fields and Artboard Options, Trim View, middle-button pan, print tiling and the status bar's artboard navigator with synchronized Artboard tool selection done; multiple windows, presentation polish) (cached canvas reprojection preserves scale during resize, rotation and DPI changes; regression-tested and contributor-confirmed 2026-10-19; estimates unchanged) (cached canvas dimensions match the renderer limit before cache-key creation, view construction and worker submission; regression-tested; estimates unchanged) | 🟡; Artboard Options shares existing presets and orientation controls and can fit artwork without moving it; artwork fitting includes all drawn document art across artboards and off-board; artwork bounds are cached by active document and revision, and selection uses the shared bounds cache across artboards | 15–25 | | M10 | Brushes, symbols, patterns, Repeat | 🟡 pattern swatches (5 tile types, Pattern Options, editing mode, SVG ``/PDF export) and live Repeat (radial/grid/mirror) done; interactive selected mirror axis implemented (focused and native checks passed; full CI blocked by an unrelated text test); symbol and brush libraries done; symbol editing shows draggable 9-slice guides (#5280), with guides outside the artwork still unsupported; brushes/symbols in progress | 23–37 | | M11 | Artboards & views (selected-set deletion shared by Artboards menu, trash and Properties, retaining artboard 1 when all are selected; Artboard deletion keys keep the first of an all-selected set and protect artwork; history prunes invalid indices while preserving valid multiselection (history tests and full workspace CI pass); click-to-fit objects with the Artboard tool (#1227), artboard escape routing and document setup modal handoff done; artboard panel/tool with Move Artwork for X/Y fields and Artboard Options, Trim View, middle-button pan, print tiling and the status bar's artboard navigator with synchronized Artboard tool selection done; multiple windows, presentation polish) (cached canvas reprojection preserves scale during resize, rotation and DPI changes; regression-tested and contributor-confirmed 2026-10-19; estimates unchanged) (cached canvas dimensions match the renderer limit before cache-key creation, view construction and worker submission; regression-tested; estimates unchanged); status-bar pointer coordinates follow the active ruler origin and General units | 🟡; Artboard Options shares existing presets and orientation controls and can fit artwork without moving it; artwork fitting includes all drawn document art across artboards and off-board; artwork bounds are cached by active document and revision, and selection uses the shared bounds cache across artboards | 15–25 | -| M10 | Brushes, symbols, patterns, Repeat | 🟡 pattern swatches (5 tile types, Pattern Options, editing mode, SVG ``/PDF export) and live Repeat (radial/grid/mirror) done; interactive selected mirror axis implemented (focused and native checks passed; full CI blocked by an unrelated text test); symbol libraries done (Window › Symbol Libraries, a generated Basic Shapes library, User Defined, Other Library…, Save Symbols); brush libraries done (Window › Brush Libraries, four generated libraries, User Defined, Other Library…, Save Brushes); brushes/symbols in progress | 23–37 | +| M10 | Brushes, symbols, patterns, Repeat | 🟡 pattern swatches (5 tile types, Pattern Options, editing mode, SVG ``/PDF export) and live Repeat (radial/grid/mirror) done; interactive selected mirror axis implemented (focused and native checks passed; full CI blocked by an unrelated text test); symbol libraries done (Window › Symbol Libraries, three generated libraries (Basic Shapes, UI Frames, Map Markers), User Defined, Other Library…, Save Symbols); brush libraries done (Window › Brush Libraries, four generated libraries, User Defined, Other Library…, Save Brushes); brushes/symbols in progress | 23–37 | | M11 | Artboards & views (selected-set deletion shared by Artboards menu, trash and Properties, retaining artboard 1 when all are selected; Artboard deletion keys keep the first of an all-selected set and protect artwork; history prunes invalid indices while preserving valid multiselection (history tests and full workspace CI pass); click-to-fit objects with the Artboard tool (#1227), artboard escape routing and document setup modal handoff done; artboard panel/tool with Move Artwork for X/Y fields and Artboard Options, Trim View, middle-button pan, print tiling and the status bar's artboard navigator with synchronized Artboard tool selection done; Window › Arrange › Tile and Consolidate All Windows done; multiple windows, presentation polish) (cached canvas reprojection preserves scale during resize, rotation and DPI changes; regression-tested and contributor-confirmed 2026-10-19; estimates unchanged) (cached canvas dimensions match the renderer limit before cache-key creation, view construction and worker submission; regression-tested; estimates unchanged) | 🟡; Artboard Options shares existing presets and orientation controls and can fit artwork without moving it; artwork fitting includes all drawn document art across artboards and off-board; artwork bounds are cached by active document and revision, and selection uses the shared bounds cache across artboards | 15–25 | | M10 | Brushes, symbols, patterns, Repeat | 🟡 pattern swatches (5 tile types, Pattern Options, editing mode, SVG ``/PDF export) and live Repeat (radial/grid/mirror) done; interactive selected mirror axis implemented (focused and native checks passed; full CI blocked by an unrelated text test); symbol and brush libraries done; brushes/symbols in progress | 23–37 | | M11 | Artboards & views (selected-set deletion shared by Artboards menu, trash and Properties, retaining artboard 1 when all are selected; Artboard deletion keys keep the first of an all-selected set and protect artwork; history prunes invalid indices while preserving valid multiselection (history tests and full workspace CI pass); click-to-fit objects with the Artboard tool (#1227), artboard escape routing and document setup modal handoff done; artboard panel/tool with Move Artwork for X/Y fields and Artboard Options, Trim View, middle-button pan, print tiling and the status bar's artboard navigator with synchronized Artboard tool selection done; Window › Arrange › Tile and Consolidate All Windows done; multiple windows, presentation polish) (cached canvas reprojection preserves scale during resize, rotation and DPI changes; regression-tested and contributor-confirmed 2026-10-19; estimates unchanged) (cached canvas dimensions match the renderer limit before cache-key creation, view construction and worker submission; regression-tested; estimates unchanged); status-bar pointer coordinates follow the active ruler origin and General units | 🟡; Artboard Options shares existing presets and orientation controls and can fit artwork without moving it; artwork fitting includes all drawn document art across artboards and off-board; artwork bounds are cached by active document and revision, and selection uses the shared bounds cache across artboards | 15–25 | @@ -192,6 +192,7 @@ separate. Scores and hours unchanged. [Behavior and command details](development | Date | Change | Summary | |---|---|---| +| 2026-11-10 | minor | M10/G5: Map Markers built-in symbol library (Pin, Flag, Crosshair, Checkpoint); estimates unchanged | | 2026-11-10 | minor | M7: Spelling › Auto Spell Check in the context menu of type (#4936); the dialogs remain; scores and estimates unchanged. | | 2026-11-10 | minor | M7: right-click suggestions, Ignore All and Add to Dictionary on a word Auto Spell Check underlined (#4936); the dialogs remain; scores and estimates unchanged. | | 2026-11-10 | minor | Current focus: the Effect Gallery's filter thumbnails and in-dialog preview are in (G2). Scores and estimates of other areas unchanged. | From 3eea6f1475b7d85a43546696b4522fe57621046e Mon Sep 17 00:00:00 2001 From: makesomethingshit Date: Tue, 10 Nov 2026 20:54:35 +0900 Subject: [PATCH 22/38] #6369: Clarify halftone preview documentation --- crates/ui-egui/src/canvas/overlays.rs | 1 + docs/roadmap.md | 4 ++-- docs/target-app-parity.md | 2 +- docs/ui-parity.md | 4 ++-- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/crates/ui-egui/src/canvas/overlays.rs b/crates/ui-egui/src/canvas/overlays.rs index f3d79443b..fcda53def 100644 --- a/crates/ui-egui/src/canvas/overlays.rs +++ b/crates/ui-egui/src/canvas/overlays.rs @@ -106,6 +106,7 @@ pub(super) fn mark_options(app: &VectorcraftApp, curve_points_from_tool: bool) - } } +/// Draw selection marks and widgets, keeping the box but hiding dot marks during Halftone Preview. pub(super) fn selection_overlay(app: &mut VectorcraftApp, p: &egui::Painter, xf: &Xf, curve_points_from_tool: bool, marks: &mut SelectionMarks) { // The Selection tool's bounding box (rotated with the objects after a rotation), also shown by // the shape tools that keep a shape just drawn ready to adjust while they draw none (#5477). It diff --git a/docs/roadmap.md b/docs/roadmap.md index b196718c9..f79f9e0e9 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,9 +1,9 @@ # Roadmap: milestones and what's next > **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-10 · **Change:** minor (right-click on a word Auto Spell Check underlined: its suggestions, Ignore All and Add to Dictionary, #4936; previously minor: G2: Effect Gallery thumbnails and in-dialog preview are in; Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: PDF editable import preserves authored ActualText once, including empty/nested spans and authored ordering (#6165); cross-line/style layout remains approximate; scores and estimates unchanged; PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: EPS import: embedded fonts (composite, Type 42, CID-keyed, Type 1, CFF FontSet) draw their glyphs; spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; M4/M7: `.ai`/EPS type keeps its tab stops, and right-aligned lines with tabs lay out as the file's page, #6147; M4/M7: `.ai`/EPS type from the text document lays its lines out closer to the file's page, within limits, #6137; numeric fields step on wheel hover without taking keyboard focus, #5949; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen; Selection QoL: marquee modes and visible feedback, #5313; SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; Indian-language hyphenation review and license allowlist, #4947; Hindi catalog and contributor loanword preferences; full wording review in progress; Effect dialogs: a colour swatch beside a hex colour opens the Color Picker, #5365; shape tools move the selection from its centre point and turn it from the zone outside a box corner, #5477; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x) - +> > Vector Halftone preview update (2026-11-10): temporary dot selection marks are hidden, and the bounding box stays available (#6369). - +> > Numeric spinner update (2026-11-10): held mouse arrows repeat past the click timeout; scores and estimates unchanged (#6307). ## Feature submission evidence diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 943275476..2eb3cfa28 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1,7 +1,7 @@ # Parity with Adobe Illustrator > **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-10 · **Change:** minor (right-click on a word Auto Spell Check underlined: its suggestions, Ignore All and Add to Dictionary, #4936; previously minor: Canvas: a 3D effect changed on type or a group redraws where its solid was; Fit to Artwork Bounds and per-artboard SVG export build each solid once for all artboards; previously minor: Effect Gallery: a thumbnail for each filter and a preview area inside the dialog, on an original drawing; Raster effects 52% → 55%, hours 1–2 → none; headline numbers unchanged; Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: PDF editable import preserves authored ActualText once, including empty/nested spans and authored ordering (#6165); cross-line/style layout remains approximate; scores and estimates unchanged; PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; G5: UI Frames built-in symbol library with default 9-slice; bundled content ~32%; previously minor: G5: dynamic symbol part on-canvas opacity/blend/gradient/pattern; strokes remaining no longer lists Overlap/auto corners; symbols ~88%; previously minor: interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; previously minor: harakat paint fallback clarified; previously minor: current Guides feature-area rows consolidated using existing ruler evidence; scores and estimates unchanged; previously minor: Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; previously minor: G5 docs: Art brush Overlap and Pattern brush auto corners removed from strokes remaining; symbols remaining lists dynamic on-canvas beyond solid and guides outside the art; previously minor: custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: EPS import: embedded fonts (composite, Type 42, CID-keyed, Type 1, CFF FontSet) draw their glyphs; spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; `.ai`/EPS type keeps its tab stops, and right-aligned lines with tabs lay out as the file's page, #6147; `.ai`/EPS type from the text document lays its lines out closer to the file's page, within limits, #6137; `.ai` and EPS import: older Art brush objects read; numeric fields step on wheel hover without taking keyboard focus, and a panel wheel turn cannot seed a canvas zoom glide, #5949; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen; Tools stays beside the canvas with left-side panels; new gradients follow document color mode; Selection QoL: marquee modes and visible feedback, #5313; SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; the canvas keeps selection outlines and anchors between frames; Indian-language hyphenation review and license allowlist, #4947; Stroke gradients: an arrowhead aligned to the tip is painted to the corners of its base, #5964; Drop Shadow and glow dialogs: spinner arrows, #5367, #5368; Stroke Weight Up and Down shortcuts, #5797; Hindi catalog and contributor loanword preferences; full wording review in progress; Properties in Edit Artboards: a Transform section with the reference point above the Artboards section, #5219; the Control bar's Stroke: and Opacity: links scrub their fields, #4088; an image zoomed in on is separated, colorized or proofed only where it shows (#4155); a document's first frame renders on the render worker, so opening a heavy file no longer freezes the window, #4155; Control bar Paragraph section with type selected, #5372; translating unchanged type reuses its glyph and character-stroke outlines; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026; Units › East Asian Type acts, with Q and Ha units; scores unchanged) - +> > Vector Halftone preview update (2026-11-10): temporary dot selection marks are hidden, and the bounding box stays available (#6369). ## Feature submission evidence diff --git a/docs/ui-parity.md b/docs/ui-parity.md index 3ae04ecd0..a9b4e7615 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -1,9 +1,9 @@ # UI and interaction parity > **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-09 · **Change:** minor (interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; numeric fields step on wheel hover without taking keyboard focus, #5949; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen; Selection QoL: marquee modes and visible feedback, #5313; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; Drop Shadow and glow dialogs: spinner arrows, #5367, #5368; Stroke Weight Up and Down shortcuts, #5797; Layers: Rename (`ui.rename`) opens the name field of the selected layer, group or object, #5324; Properties in Edit Artboards: Transform section above the Artboards section, #5219; scrubby labels: the Control bar's Stroke: and Opacity: links and the Properties panel's Stroke link scrub their fields, #4088; Properties: Quick Actions has Repeat Options for one selected repeat, #5181; Direct Selection: Shift-click picks several blend spine points, which drag together, #5285; Direct Selection: Cmd/Ctrl held lends the Selection tool and shows its bounding box, #3901; Join tool trims two crossing open paths and joins them at the crossing, #5844; Control bar: Paragraph: link and Align left/center/right while type is selected, #5372; shape tools: a press on the selection's centre point moves it and one in the rotate zone outside a box corner turns it, #5477; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; Units › East Asian Type acts, with Q and Ha units; scores unchanged) - +> > Vector Halftone preview update (2026-11-10): temporary dot selection marks are hidden, and the bounding box stays available (#6369). - +> > Numeric spinner update (2026-11-10): held mouse arrows repeat past the click timeout; scores and estimates unchanged (#6307). ## Feature submission evidence From 243bdb3f636a4f2cbf9b449a5fb1022b2252b275 Mon Sep 17 00:00:00 2001 From: ruchirguitar <113586074+ruchirguitar@users.noreply.github.com> Date: Tue, 10 Nov 2026 18:10:55 +0530 Subject: [PATCH 23/38] Fix #5713: resizable icon flyouts and bottom-aligned panel footers (#5973) * Fix #5713: resize icon flyouts and anchor panel footers * Fix #5713: reconcile parity evidence with upstream docs * Fix #5713: keep Actions rows above the pinned footer * Fix #5713: align documentation dates and regression claims * Fix #5713: keep short Glyphs grids above the footer * Fix #5713: pin craft-ui host minimum-size correction --------- Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com> --- Cargo.lock | 6 +- Cargo.toml | 2 +- crates/ui-egui/src/panel_docking.rs | 32 ++++-- crates/ui-egui/src/panel_docking/tests.rs | 122 ++++++++++++++++++++- crates/ui-egui/src/panels/actions.rs | 64 ++++++++++- crates/ui-egui/src/panels/artboards.rs | 12 +- crates/ui-egui/src/panels/glyphs.rs | 14 ++- crates/ui-egui/src/panels/variables.rs | 84 +++++++------- crates/ui-egui/src/widgets.rs | 1 + crates/ui-egui/src/widgets/panel_chrome.rs | 92 ++++++++++++++++ docs/gaps.md | 5 + docs/roadmap.md | 9 +- docs/target-app-parity.md | 7 +- docs/ui-parity.md | 6 + 14 files changed, 386 insertions(+), 70 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 88a9abca0..c66578303 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -975,12 +975,12 @@ source = "git+https://github.com/storytold/craft-libs?rev=9be9a63782298ab59cd9b7 [[package]] name = "craft-fonts-build" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=9be9a63782298ab59cd9b7a42ee9cba4f42a5ccb#9be9a63782298ab59cd9b7a42ee9cba4f42a5ccb" +source = "git+https://github.com/storytold/craft-libs?rev=bb8320f5fa5d20fa78a026d0de06fe4e2f08a099#bb8320f5fa5d20fa78a026d0de06fe4e2f08a099" [[package]] name = "craft-i18n" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=9be9a63782298ab59cd9b7a42ee9cba4f42a5ccb#9be9a63782298ab59cd9b7a42ee9cba4f42a5ccb" +source = "git+https://github.com/storytold/craft-libs?rev=bb8320f5fa5d20fa78a026d0de06fe4e2f08a099#bb8320f5fa5d20fa78a026d0de06fe4e2f08a099" dependencies = [ "fixed_decimal", "icu_decimal", @@ -1049,7 +1049,7 @@ dependencies = [ [[package]] name = "craft-ui" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=9be9a63782298ab59cd9b7a42ee9cba4f42a5ccb#9be9a63782298ab59cd9b7a42ee9cba4f42a5ccb" +source = "git+https://github.com/storytold/craft-libs?rev=bb8320f5fa5d20fa78a026d0de06fe4e2f08a099#bb8320f5fa5d20fa78a026d0de06fe4e2f08a099" dependencies = [ "craft-fonts-build", "craft-i18n", diff --git a/Cargo.toml b/Cargo.toml index 2c0bf38ec..9b4b99c92 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ repository = "https://github.com/storytold/vectorcraft" craft-pdf = { git = "https://github.com/storytold/craft-libs", rev = "9be9a63782298ab59cd9b7a42ee9cba4f42a5ccb", default-features = false, features = ["interpret", "write"] } craft-print = { git = "https://github.com/storytold/craft-libs", rev = "9be9a63782298ab59cd9b7a42ee9cba4f42a5ccb" } craft-docpath = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "9be9a63782298ab59cd9b7a42ee9cba4f42a5ccb" } -craft-ui = { git = "https://github.com/storytold/craft-libs", rev = "9be9a63782298ab59cd9b7a42ee9cba4f42a5ccb", features = ["serde"] } +craft-ui = { git = "https://github.com/storytold/craft-libs", rev = "bb8320f5fa5d20fa78a026d0de06fe4e2f08a099", features = ["serde"] } craft-keymap = { git = "https://github.com/storytold/craft-libs", rev = "9be9a63782298ab59cd9b7a42ee9cba4f42a5ccb" } craft-proof = { git = "https://github.com/storytold/craft-libs", rev = "9be9a63782298ab59cd9b7a42ee9cba4f42a5ccb" } # Decides who draws the window frame (desktop vs. app) so the suite shares one native titlebar per diff --git a/crates/ui-egui/src/panel_docking.rs b/crates/ui-egui/src/panel_docking.rs index a74f47b6a..3447cd1e1 100644 --- a/crates/ui-egui/src/panel_docking.rs +++ b/crates/ui-egui/src/panel_docking.rs @@ -949,6 +949,7 @@ impl Transaction { } if let Some(saved) = &location.group { apply(&mut candidate, Action::ResizeGroup { panel: panel.clone(), height: saved.height })?; + apply(&mut candidate, Action::ResizeFlyoutWidth { panel: panel.clone(), width: saved.flyout_width })?; apply(&mut candidate, Action::SetMinimized { panel: panel.clone(), minimized: saved.minimized })?; } self.workspace.layout = candidate; @@ -1087,6 +1088,7 @@ fn known(action: &Action) -> bool { | Action::SetIconic { panel, .. } | Action::ResizeColumn { panel, .. } | Action::ResizeGroup { panel, .. } + | Action::ResizeFlyoutWidth { panel, .. } | Action::ResizeIconic { panel, .. } | Action::SetNative { panel, .. } | Action::SetMonitor { panel, .. } => registered(panel), @@ -1169,7 +1171,7 @@ pub(crate) fn show(app: &mut VectorcraftApp, ui: &mut egui::Ui) { if let Some(panel) = app.dock_rt.raise.take() { dock = dock.raise_panel(panel); } - let mut output = dock.show_content(ui, &layout, &style, &mut PanelContent { app: &mut *app }); + let mut output = dock.show_content(ui, &layout, &style, &mut PanelContent { app: &mut *app, layout: &layout }); app.dock_rt.space = craft_ui::docking::float_space(&ctx, area); if let Some(panel) = output.focused.take() { app.dock_rt.focused = Some(panel); @@ -1343,9 +1345,11 @@ pub(crate) fn floating_size(panel: &str) -> egui::Vec2 { /// The panels' content, labels and icons. struct PanelContent<'a> { app: &'a mut VectorcraftApp, + layout: &'a Layout, } impl craft_ui::docking::DockContent for PanelContent<'_> { + /// Draw a registered panel body, fitting untouched flyouts and pinning sized panels' footers. fn body(&mut self, ui: &mut egui::Ui, id: &String) { if id == CANVAS { crate::toolbar::show_beside_canvas(self.app, ui); @@ -1364,15 +1368,23 @@ impl craft_ui::docking::DockContent for PanelContent<'_> { } let width = ui.available_width(); let height = (ui.available_height() - 20.0).max(0.0); - if DockTab::from_id(id).is_some() { - crate::dock::panel_body(self.app, ui, id, width, height); - } else { - // Shrinking to the content's height lets craft-ui fit a float or flyout to it. - crate::widgets::panel_scroll(ui, egui::ScrollArea::vertical().id_salt(("shared-panel-body", id)).auto_shrink([false, true]), |ui| { - let width = ui.available_width(); - crate::dock::panel_body(self.app, ui, id, width, height) - }); - } + let fitted = in_iconic_column(self.layout, id) + && self.layout.location(id).ok().and_then(|location| location.group).is_none_or(|group| group.height.is_none()); + // Properties embeds other panels as sections, so their local button rows + // must remain beside those sections rather than pinning to the outer panel. + let bottom = (!fitted && id != "properties").then_some(back.bottom() - 10.0); + let info = egui::UiStackInfo::default().with_tag_value(crate::widgets::PANEL_FOOTER, bottom); + ui.scope_builder(egui::UiBuilder::new().ui_stack_info(info), |ui| { + if DockTab::from_id(id).is_some() { + crate::dock::panel_body(self.app, ui, id, width, height); + } else { + // Shrinking to the content's height lets craft-ui fit a float or flyout to it. + crate::widgets::panel_scroll(ui, egui::ScrollArea::vertical().id_salt(("shared-panel-body", id)).auto_shrink([false, true]), |ui| { + let width = ui.available_width(); + crate::dock::panel_body(self.app, ui, id, width, height) + }); + } + }); if field_had_keyboard { keep_field_ending_keys(ui); } diff --git a/crates/ui-egui/src/panel_docking/tests.rs b/crates/ui-egui/src/panel_docking/tests.rs index 1b8b1a168..2d33420b9 100644 --- a/crates/ui-egui/src/panel_docking/tests.rs +++ b/crates/ui-egui/src/panel_docking/tests.rs @@ -12,13 +12,16 @@ struct Harness { app: VectorcraftApp, ctx: egui::Context, time: f64, + cursor: egui::CursorIcon, + nodes: Vec<(egui::accesskit::NodeId, egui::accesskit::Node)>, } impl Harness { + /// Create a blank document and settle the initial docking layout over three frames. fn new() -> Self { let mut app = VectorcraftApp::new(Session::new(), Default::default()); app.run("file.new", json!({"width": 400, "height": 300})).unwrap(); - let mut h = Self { app, ctx: egui::Context::default(), time: 0.0 }; + let mut h = Self { app, ctx: egui::Context::default(), time: 0.0, cursor: egui::CursorIcon::Default, nodes: Vec::new() }; for _ in 0..3 { h.frame(vec![]); } @@ -40,6 +43,7 @@ impl Harness { self.run(input); } + /// Draw one application frame and retain its cursor and accessibility output for assertions. fn run(&mut self, input: egui::RawInput) { let app = &mut self.app; let mut out = self.ctx.run_ui(input, |ui| { @@ -47,6 +51,8 @@ impl Harness { app.ui(ui); }); out.textures_delta.clear(); + self.cursor = out.platform_output.cursor_icon; + self.nodes = out.platform_output.accesskit_update.map_or_else(Vec::new, |update| update.nodes); } fn hit(&self, hit: &Hit) -> Option { @@ -138,6 +144,120 @@ fn an_icon_opens_its_flyout_and_open_panel_follows() { assert_eq!(craft_ui::docking::flyout::(&h.ctx, area_id()), None); } +/// #5713: real Glyphs and Artboards bodies must retain a flyout resize through +/// content fitting, closing/reopening and serialization of the workspace layout. +#[test] +fn glyphs_and_artboards_flyouts_keep_user_sizes() { + for panel in ["glyphs", "artboards"] { + let mut h = Harness::new(); + let icon = h.hit(&Hit::Icon(panel.into())).unwrap().center(); + h.click(icon); + // Glyphs reserves space from the available height, so its untouched auto-fit + // converges over several frames before the first user resize. + for _ in 0..30 { + h.frame(vec![]); + } + // Area memory can include a panel body's minimum allocation beyond its clip. + // The frame's actual edge widgets describe the visible, resizable rectangle. + let rect = |h: &Harness| { + let id = area_id().with("flyout"); + let left = h.ctx.read_response(id.with("resize-left")).unwrap().rect; + let bottom = h.ctx.read_response(id.with("resize-bottom")).unwrap().rect; + Rect::from_min_max(pos2(left.left(), left.top() - 12.0), pos2(bottom.right() + 12.0, bottom.bottom())) + }; + let before = rect(&h); + for (at, cursor) in [ + (before.left_center() + vec2(2.0, 0.0), egui::CursorIcon::ResizeHorizontal), + (before.center_bottom() - vec2(0.0, 2.0), egui::CursorIcon::ResizeVertical), + ] { + h.frame(vec![Event::PointerMoved(at)]); + h.frame(vec![]); + assert_eq!(h.cursor, cursor, "{panel} at {at:?}, frame {before:?}"); + } + let from = before.left_bottom() + vec2(2.0, -2.0); + h.drag(from, from + vec2(-100.0, 150.0)); + for _ in 0..3 { + h.frame(vec![]); + } + let grown = rect(&h); + assert!((grown.width() - before.width() - 100.0).abs() < 1.0, "{panel}: {before:?} -> {grown:?}"); + assert!((grown.height() - before.height() - 150.0).abs() < 1.0, "{panel}: {before:?} -> {grown:?}"); + assert_eq!(grown.right(), before.right(), "the icon-column edge stays anchored"); + h.click(icon); + assert_eq!(craft_ui::docking::flyout::(&h.ctx, area_id()), None); + h.click(icon); + for _ in 0..3 { + h.frame(vec![]); + } + assert_eq!(rect(&h).size(), grown.size()); + let saved = serde_json::to_string(&h.layout()).unwrap(); + let mut restored = Harness::new(); + restored.app.ui.docking = Some(serde_json::from_str(&saved).unwrap()); + for _ in 0..3 { + restored.frame(vec![]); + } + restored.click(restored.hit(&Hit::Icon(panel.into())).unwrap().center()); + for _ in 0..3 { + restored.frame(vec![]); + } + assert_eq!(rect(&restored).size(), grown.size(), "{panel} saved layout"); + } +} + +/// Footer controls stay at the resized panel's bottom, including empty or short lists. +/// The same shared footer is used by the other icon panels and library windows. +#[test] +fn expanded_panel_footers_stay_at_the_bottom_with_short_and_long_lists() { + for (panel, label, artboards) in [ + ("artboards", "New Artboard", 1), + ("artboards", "New Artboard", 30), + ("brushes", "New Brush", 1), + ("swatches", "New Swatch", 1), + ("symbols", "New Symbol", 1), + ("graphicStyles", "New Graphic Style (Alt-click to name it)", 1), + ("appearance", "Add New Stroke", 1), + ("charStyles", "Create New Character Style", 1), + ("paraStyles", "Create New Paragraph Style", 1), + ("colorGuide", "Save color group to Swatch panel", 1), + ("links", "Show Link Info", 1), + ("actions", "Create New Action", 1), + ("variables", "Capture Data Set", 1), + ("assetExport", "Export for Screens…", 1), + ("cssProperties", "Generate CSS: the whole document's CSS while nothing is selected", 1), + ("svgInteractivity", "Link JavaScript Files…", 1), + ("colorThemes", "Add to Swatches", 1), + ("glyphs", "Zoom In", 1), + ] { + let mut h = Harness::new(); + h.ctx.enable_accesskit(); + for _ in 1..artboards { + h.app.run("artboard.new", json!({})).unwrap(); + } + h.app.run("window.panel.dock", json!({"panel":panel,"icon":true})).unwrap(); + let mut layout = h.layout(); + layout.apply(Action::ResizeGroup { panel: panel.into(), height: Some(540.0) }).unwrap(); + layout.apply(Action::ResizeFlyoutWidth { panel: panel.into(), width: Some(420.0) }).unwrap(); + h.app.ui.docking = Some(layout); + h.app.ui.open_panel = Some(panel.into()); + for _ in 0..5 { + h.frame(vec![]); + } + let bottom = h.ctx.read_response(area_id().with("flyout").with("resize-bottom")).unwrap().rect.bottom(); + let control = h + .nodes + .iter() + .find(|(_, node)| node.label() == Some(label)) + .and_then(|(_, node)| node.bounds()) + .unwrap_or_else(|| panic!("missing {label} in {panel}")); + assert!((control.y1 as f32 - (bottom - 10.0)).abs() < 2.0, "{panel}/{artboards}: footer {control:?}, bottom {bottom}"); + if panel == "artboards" && artboards == 1 { + let at = pos2(((control.x0 + control.x1) * 0.5) as f32, ((control.y0 + control.y1) * 0.5) as f32); + h.click(at); + assert_eq!(h.app.session.active().unwrap().doc.artboards.len(), 2, "the pinned New button works"); + } + } +} + #[test] fn a_tab_dragged_out_floats_and_comes_back_where_it_was_after_its_window_closes() { let mut h = Harness::new(); diff --git a/crates/ui-egui/src/panels/actions.rs b/crates/ui-egui/src/panels/actions.rs index 93d781e5a..2533ef029 100644 --- a/crates/ui-egui/src/panels/actions.rs +++ b/crates/ui-egui/src/panels/actions.rs @@ -1377,6 +1377,7 @@ enum InclusionChange { Step(usize, usize, usize, bool), } +/// Draw action sets, steps and playback controls, keeping the list above a pinned footer. pub fn show(app: &mut VectorcraftApp, ui: &mut egui::Ui) { let t = Tokens::get(ui.ctx()); // The selected action as (set, action). Read as the type it is stored as: egui keeps a @@ -1398,7 +1399,8 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut egui::Ui) { let mut expanded: BTreeSet = pstate(ui.ctx(), "actions-expanded"); let mut inclusion_changes = Vec::new(); let mut play_req = None; - egui::ScrollArea::vertical().max_height(260.0).show(ui, |ui| { + let list_height = widgets::panel_list_height(ui, 260.0, 0.0).min(260.0); + egui::ScrollArea::vertical().max_height(list_height).min_scrolled_height(list_height.min(64.0)).show(ui, |ui| { for (si, set) in app.ui.action_sets.iter().enumerate() { let (r, resp) = ui.allocate_exact_size(egui::vec2(ui.available_width(), 20.0), egui::Sense::click()); if sets_selected.contains(&si) && selected.is_none() { @@ -1551,9 +1553,11 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut egui::Ui) { set_pstate(ui.ctx(), SELECTED_STEP, step_selected); app.ui.action_set = current; app.ui.action_sets_selected = sets_selected; - widgets::divider(ui); + if app.ui.action_sets.is_empty() { + dim_label(ui, tl!("No actions. Press ● to record.")); + } // Bottom bar: stop, record, play, new set, new action, delete. - ui.horizontal(|ui| { + widgets::bottom_bar(ui, |ui| { let recording = app.ui.recording.is_some(); if widgets::icon_button(ui, "square", tl!("Stop Playing/Recording"), false, 24.0).clicked() && let Err(e) = app.run(STOP_RECORDING, json!({})) @@ -1598,9 +1602,6 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut egui::Ui) { app.status(e); } let _ = icons::exists; - if app.ui.action_sets.is_empty() { - dim_label(ui, tl!("No actions. Press ● to record.")); - } } #[cfg(test)] @@ -1608,6 +1609,57 @@ mod tests { use super::*; use vectorcraft_engine::Session; + /// A short sized panel clips the list above its footer and can scroll to select its final row. + #[test] + fn a_short_actions_panel_keeps_its_last_row_reachable_above_the_footer() { + use egui::{Event, PointerButton, Rect, UiBuilder, UiStackInfo, pos2, vec2}; + for height in [60.0, 120.0] { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + let ctx = egui::Context::default(); + let mut time = 0.0; + let mut frame = |app: &mut VectorcraftApp, events| { + time += 0.1; + let input = egui::RawInput { + time: Some(time), + screen_rect: Some(Rect::from_min_size(pos2(0.0, 0.0), vec2(300.0, height))), + events, + ..Default::default() + }; + let mut output = ctx.run_ui(input, |ui| { + let info = UiStackInfo::default().with_tag_value(widgets::PANEL_FOOTER, Some(height)); + ui.scope_builder(UiBuilder::new().ui_stack_info(info), |ui| show(app, ui)); + }); + output.textures_delta.clear(); + output.shapes + }; + let last_row = |shapes: &[egui::epaint::ClippedShape]| { + shapes + .iter() + .find_map(|shape| match &shape.shape { + egui::Shape::Text(text) if text.galley.text() == "Reflect Horizontal" => Some((text.visual_bounding_rect(), shape.clip_rect)), + _ => None, + }) + .expect("the final action is drawn") + }; + for _ in 0..3 { + let shapes = frame(&mut app, vec![]); + assert!(last_row(&shapes).1.bottom() <= height - 31.0, "height={height}: the list overlaps its footer"); + } + for _ in 0..12 { + frame(&mut app, crate::toolbar::tests::wheel(pos2(100.0, 12.0), 1000.0)); + } + let shapes = frame(&mut app, vec![]); + let (row, clip) = last_row(&shapes); + assert!(clip.contains_rect(row), "height={height}: the final row {row:?} is outside {clip:?}"); + let at = row.center(); + let button = |pressed| Event::PointerButton { pos: at, button: PointerButton::Primary, pressed, modifiers: Default::default() }; + for events in [vec![Event::PointerMoved(at)], vec![button(true)], vec![button(false)]] { + frame(&mut app, events); + } + assert_eq!(pstate::>(&ctx, "actions-selected"), Some((0, 5))); + } + } + #[test] fn defaults_are_valid_commands() { for set in default_sets() { diff --git a/crates/ui-egui/src/panels/artboards.rs b/crates/ui-egui/src/panels/artboards.rs index d46e5c4ac..04f2719e7 100644 --- a/crates/ui-egui/src/panels/artboards.rs +++ b/crates/ui-egui/src/panels/artboards.rs @@ -503,6 +503,7 @@ fn select_copies(app: &mut VectorcraftApp, all: &[usize], r: &Value) { } } +/// Draw the artboard list and its commands, filling the space above a sized panel's footer. pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); let abs: Vec = app.session.active().map(|d| d.doc.artboards.iter().map(|a| a.name.clone()).collect()).unwrap_or_default(); @@ -517,10 +518,17 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let mut row_clicked = false; // A row dropped on another row: (from, to), the index it lands at. let mut dropped = None; + let fixed = widgets::panel_footer_bottom(ui).is_some(); + let list_height = widgets::panel_list_height(ui, 220.0, 2.0 + ui.spacing().item_spacing.y); let list_rect = widgets::list_box(ui, |ui| { - ui.set_min_height(110.0); + ui.set_min_height(if fixed { list_height.min(110.0) } else { 110.0 }); ui.spacing_mut().item_spacing.y = 0.0; - let scrolled = egui::ScrollArea::vertical().id_salt("ab-scroll").max_height(220.0).show(ui, |ui| { + let scroll = egui::ScrollArea::vertical() + .id_salt("ab-scroll") + .max_height(list_height) + .min_scrolled_height(list_height.min(64.0)) + .auto_shrink([false, !fixed]); + let scrolled = scroll.show(ui, |ui| { for (i, name) in abs.iter().enumerate() { let (r, resp) = ui.allocate_exact_size(vec2(ui.available_width(), 24.0), Sense::click_and_drag()); if resp.drag_started() { diff --git a/crates/ui-egui/src/panels/glyphs.rs b/crates/ui-egui/src/panels/glyphs.rs index 5c07441be..9192aa5f0 100644 --- a/crates/ui-egui/src/panels/glyphs.rs +++ b/crates/ui-egui/src/panels/glyphs.rs @@ -212,6 +212,7 @@ fn insert(app: &mut VectorcraftApp, ctx: &egui::Context, ch: char) { } } +/// Browse and insert glyphs from the chosen font, with font controls below the glyph grid. pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); let db = FontDb::global(); @@ -244,14 +245,14 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let cols = ((w / cell).floor() as usize).max(1); let rows = chars.len().div_ceil(cols); let px = (cell * ui.ctx().pixels_per_point()).round().clamp(8.0, 256.0) as u32; - let list_h = (ui.available_height() - 64.0).max(cell * 3.0); + let list_h = widgets::panel_list_height(ui, (ui.available_height() - 64.0).max(cell * 3.0), 33.0); let hover_id = egui::Id::unique("gl-hover"); let selected = selected_char(app); // The selected character and the rows shown last frame: the grid scrolls to the selected glyph // only when the selection moves to another character, so it still scrolls freely meanwhile. let follow_id = egui::Id::unique("gl-follow"); let (followed, shown): (Option, std::ops::Range) = ui.ctx().data(|d| d.get_temp(follow_id)).unwrap_or_default(); - let mut area = egui::ScrollArea::vertical().max_height(list_h).auto_shrink([false, false]); + let mut area = egui::ScrollArea::vertical().max_height(list_h).min_scrolled_height(list_h.min(64.0)).auto_shrink([false, false]); if selected != followed && let Some(c) = selected && let Ok(i) = chars.binary_search_by_key(&c, |x| x.0) @@ -308,6 +309,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { /// The family and style menus of the font the panel browses, and Zoom Out and Zoom In. fn font_row(app: &mut VectorcraftApp, ui: &mut Ui, family: &str, style: &str, w: f32) { let db = FontDb::global(); + widgets::panel_footer_space(ui, 24.0); ui.horizontal(|ui| { ui.spacing_mut().item_spacing.x = 4.0; // Picks the font the panel browses only: nothing is previewed on the document. @@ -371,10 +373,14 @@ fn alternates(app: &mut VectorcraftApp, ui: &mut Ui, w: f32) { let cols = ((w / cell).floor() as usize).max(1); let rows = list.len().div_ceil(cols); let px = (cell * ui.ctx().pixels_per_point()).round().clamp(8.0, 256.0) as u32; - let list_h = (ui.available_height() - 64.0).max(cell * 3.0); + let list_h = widgets::panel_list_height(ui, (ui.available_height() - 64.0).max(cell * 3.0), 33.0); let mut pick = None; // A scroll position of its own for each character, so the grid starts at the top for another. - let area = egui::ScrollArea::vertical().id_salt(("gl-alternates", id.0, byte)).max_height(list_h).auto_shrink([false, false]); + let area = egui::ScrollArea::vertical() + .id_salt(("gl-alternates", id.0, byte)) + .max_height(list_h) + .min_scrolled_height(list_h.min(64.0)) + .auto_shrink([false, false]); area.show_rows(ui, cell, rows, |ui, range| { for r in range { ui.horizontal(|ui| { diff --git a/crates/ui-egui/src/panels/variables.rs b/crates/ui-egui/src/panels/variables.rs index 87aa5cd13..a8987855e 100644 --- a/crates/ui-egui/src/panels/variables.rs +++ b/crates/ui-egui/src/panels/variables.rs @@ -97,6 +97,7 @@ pub(crate) fn delete_active_dataset(app: &mut VectorcraftApp) { app.run("dataset.delete", json!({})).ok(); } +/// Draw document variables, data sets and their two-row footer of capture and binding controls. pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let Some(st) = app.session.active() else { widgets::dim_label(ui, tl!("No document")); @@ -209,46 +210,49 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { // The bottom bar: make the selection dynamic, define and delete what the lists show, and // lock the variables. - ui.separator(); - ui.horizontal(|ui| { - if widgets::icon_button_enabled(ui, "type", tl!("Make Text Dynamic"), false, single_type && !locked, 24.0).clicked() { - app.run("variable.makeTextDynamic", json!({})).ok(); - } - if widgets::icon_button_enabled(ui, "eye", tl!("Make Visibility Dynamic"), false, has_selection && !locked, 24.0).clicked() { - app.run("variable.makeVisibilityDynamic", json!({})).ok(); - } - if widgets::icon_button_enabled(ui, "image", tl!("Make Image Dynamic"), false, single_image && !locked, 24.0).clicked() { - app.run("variable.makeImageDynamic", json!({})).ok(); - } - if widgets::icon_button_enabled(ui, "chart-column", tl!("Make Graph Dynamic"), false, selection_graph && !locked, 24.0).clicked() { - app.run("variable.makeGraphDynamic", json!({})).ok(); - } - if widgets::icon_button_enabled(ui, "plus", tl!("New Variable…"), false, !locked, 24.0).clicked() { - crate::menus::invoke(app, "variable.define", json!({})); - } - if widgets::icon_button_enabled(ui, "link-2-off", tl!("Unbind Variable"), false, selection_bound, 24.0).clicked() { - app.run("variable.unbind", json!({"ids": selected})).ok(); - } - // The bin deletes the highlighted variable only, as the reference panel's does. - if widgets::icon_button_enabled(ui, "trash-2", tl!("Delete Variable"), false, highlighted.is_some() && !locked, 24.0).clicked() { - delete_highlighted(app); - } - // The lock button is drawn pressed while the variables are locked. - let tip = if locked { tl!("Unlock Variables") } else { tl!("Lock Variables") }; - if widgets::icon_button(ui, "lock", tip, locked, 24.0).clicked() { - app.run("variable.lock", json!({})).ok(); - } - }); - ui.horizontal(|ui| { - // Capture records what the art holds now as a new row (paste: take it in); Update - // writes the art back into the row that is active (save: write it out). - if widgets::icon_button(ui, "clipboard-paste", tl!("Capture Data Set"), false, 24.0).clicked() { - app.run("dataset.capture", json!({})).ok(); - } - let can_update = active.is_some(); - if widgets::icon_button_enabled(ui, "save", tl!("Update Data Set"), false, can_update, 24.0).clicked() { - app.run("dataset.update", json!({})).ok(); - } + let footer_height = 48.0 + 2.0 * ui.spacing().item_spacing.y + 1.0; + widgets::panel_footer(ui, footer_height, |ui| { + ui.add(egui::Separator::default().spacing(1.0)); + ui.horizontal(|ui| { + if widgets::icon_button_enabled(ui, "type", tl!("Make Text Dynamic"), false, single_type && !locked, 24.0).clicked() { + app.run("variable.makeTextDynamic", json!({})).ok(); + } + if widgets::icon_button_enabled(ui, "eye", tl!("Make Visibility Dynamic"), false, has_selection && !locked, 24.0).clicked() { + app.run("variable.makeVisibilityDynamic", json!({})).ok(); + } + if widgets::icon_button_enabled(ui, "image", tl!("Make Image Dynamic"), false, single_image && !locked, 24.0).clicked() { + app.run("variable.makeImageDynamic", json!({})).ok(); + } + if widgets::icon_button_enabled(ui, "chart-column", tl!("Make Graph Dynamic"), false, selection_graph && !locked, 24.0).clicked() { + app.run("variable.makeGraphDynamic", json!({})).ok(); + } + if widgets::icon_button_enabled(ui, "plus", tl!("New Variable…"), false, !locked, 24.0).clicked() { + crate::menus::invoke(app, "variable.define", json!({})); + } + if widgets::icon_button_enabled(ui, "link-2-off", tl!("Unbind Variable"), false, selection_bound, 24.0).clicked() { + app.run("variable.unbind", json!({"ids": selected})).ok(); + } + // The bin deletes the highlighted variable only, as the reference panel's does. + if widgets::icon_button_enabled(ui, "trash-2", tl!("Delete Variable"), false, highlighted.is_some() && !locked, 24.0).clicked() { + delete_highlighted(app); + } + // The lock button is drawn pressed while the variables are locked. + let tip = if locked { tl!("Unlock Variables") } else { tl!("Lock Variables") }; + if widgets::icon_button(ui, "lock", tip, locked, 24.0).clicked() { + app.run("variable.lock", json!({})).ok(); + } + }); + ui.horizontal(|ui| { + // Capture records what the art holds now as a new row (paste: take it in); Update + // writes the art back into the row that is active (save: write it out). + if widgets::icon_button(ui, "clipboard-paste", tl!("Capture Data Set"), false, 24.0).clicked() { + app.run("dataset.capture", json!({})).ok(); + } + let can_update = active.is_some(); + if widgets::icon_button_enabled(ui, "save", tl!("Update Data Set"), false, can_update, 24.0).clicked() { + app.run("dataset.update", json!({})).ok(); + } + }); }); } diff --git a/crates/ui-egui/src/widgets.rs b/crates/ui-egui/src/widgets.rs index c70cca173..56bac9808 100644 --- a/crates/ui-egui/src/widgets.rs +++ b/crates/ui-egui/src/widgets.rs @@ -63,6 +63,7 @@ pub use numeric::{ }; #[cfg(test)] use numeric::{decimal_point, field_number, share_of, spinner_step, step_through, step_to_multiple, within_a_millionth}; +pub(crate) use panel_chrome::{PANEL_FOOTER, panel_footer, panel_footer_bottom, panel_footer_space, panel_list_height}; pub use panel_chrome::{bottom_bar, layout_button, list_box, orientation_button, tab_bar}; pub(crate) use text::{ arm_field_focus, dialog_focus_flag, dialog_reject_commit_flag, disarm_field_focus, enter_modifiers, focus_field, own_field_shift_enter, diff --git a/crates/ui-egui/src/widgets/panel_chrome.rs b/crates/ui-egui/src/widgets/panel_chrome.rs index e07367dab..2c8471628 100644 --- a/crates/ui-egui/src/widgets/panel_chrome.rs +++ b/crates/ui-egui/src/widgets/panel_chrome.rs @@ -1,5 +1,41 @@ use super::{Color32, CornerRadius, Pos2, Rect, Response, Sense, Stroke, StrokeKind, Tokens, Ui, Vec2, icons, pos2, theme, vec2}; +pub(crate) const PANEL_FOOTER: &str = "vectorcraft-panel-footer"; +const FOOTER_HEIGHT: f32 = 31.0; + +/// The finite body edge supplied by a sized dock, float or user-resized flyout. +/// Untouched flyouts omit it so their content can still determine their height. +pub(crate) fn panel_footer_bottom(ui: &Ui) -> Option { + ui.stack().iter().find_map(|stack| stack.tags().get_downcast::>(PANEL_FOOTER)).copied().flatten() +} + +/// Space left for a list above the footer and any intervening controls, or its unfitted fallback. +pub(crate) fn panel_list_height(ui: &Ui, fallback: f32, after: f32) -> f32 { + panel_footer_bottom(ui).map_or(fallback, |bottom| (bottom - ui.next_widget_position().y - FOOTER_HEIGHT - after).max(0.0)) +} + +/// Pad short content so a footer of `height` ends at the panel's saved bottom edge. +pub(crate) fn panel_footer_space(ui: &mut Ui, height: f32) { + if let Some(bottom) = panel_footer_bottom(ui) { + ui.add_space((bottom - ui.next_widget_position().y - height).max(0.0)); + } +} + +/// Pin a custom footer (for example the Variables panel's two rows). +pub(crate) fn panel_footer(ui: &mut Ui, height: f32, add: impl FnOnce(&mut Ui)) { + let Some(bottom) = panel_footer_bottom(ui) else { return add(ui) }; + let top = bottom - height; + if ui.next_widget_position().y > top { + ui.add_space(height); + } + let rect = Rect::from_min_size(pos2(ui.max_rect().left(), top), vec2(ui.available_width(), height)); + ui.painter().rect_filled(rect, 0.0, Tokens::get(ui.ctx()).panel); + // Own the blank parts too, so a click cannot select a scrolled row behind the footer. + let _background = ui.interact(rect, ui.scope_id().with("panel-footer-background"), Sense::CLICK); + ui.scope_builder(egui::UiBuilder::new().max_rect(rect), add); + ui.advance_cursor_after_rect(rect); +} + /// A bordered list box (Swatches tiles, Appearance rows, Artboards list). pub fn list_box(ui: &mut Ui, add: impl FnOnce(&mut Ui) -> R) -> R { let t = Tokens::get(ui.ctx()); @@ -9,6 +45,25 @@ pub fn list_box(ui: &mut Ui, add: impl FnOnce(&mut Ui) -> R) -> R { /// A bottom button bar separated from the content by a divider (panel footers). pub fn bottom_bar(ui: &mut Ui, add: impl FnOnce(&mut Ui)) { let t = Tokens::get(ui.ctx()); + if let Some(bottom) = panel_footer_bottom(ui) { + // Reserve scrollable room after long content so its final row can move above + // the pinned footer instead of remaining hidden underneath its buttons. + let top = bottom - FOOTER_HEIGHT; + if ui.next_widget_position().y > top { + ui.add_space(FOOTER_HEIGHT); + } + let footer = Rect::from_min_size(pos2(ui.max_rect().left(), top), vec2(ui.available_width(), FOOTER_HEIGHT)); + ui.painter().rect_filled(footer, 0.0, t.panel); + let _background = ui.interact(footer, ui.scope_id().with("panel-footer-background"), Sense::CLICK); + ui.painter().hline(footer.x_range(), top + 4.0, Stroke::new(1.0, t.divider)); + let row = Rect::from_min_max(pos2(footer.left(), top + 7.0), footer.max); + ui.scope_builder(egui::UiBuilder::new().max_rect(row).layout(egui::Layout::left_to_right(egui::Align::Center)), |ui| { + ui.spacing_mut().item_spacing.x = 4.0; + add(ui); + }); + ui.advance_cursor_after_rect(footer); + return; + } ui.add_space(4.0); let (r, _) = ui.allocate_exact_size(vec2(ui.available_width(), 1.0), Sense::hover()); ui.painter().rect_filled(r, 0.0, t.divider); @@ -202,3 +257,40 @@ pub(super) fn chevron_cell(ui: &mut Ui, h: f32) -> Response { dresp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, ui.is_enabled(), tl!("Presets"))); dresp } + +#[cfg(test)] +mod footer_tests { + use super::{PANEL_FOOTER, bottom_bar, panel_footer}; + + /// Blank footer space intercepts clicks instead of selecting list rows underneath it. + #[test] + fn blank_footer_space_does_not_click_a_row_scrolled_behind_it() { + use egui::{Event, PointerButton, Rect, Sense, UiBuilder, UiStackInfo, pos2}; + for custom in [false, true] { + let ctx = egui::Context::default(); + let mut clicked = false; + let at = pos2(150.0, 190.0); + let button = |pressed| Event::PointerButton { pos: at, button: PointerButton::Primary, pressed, modifiers: Default::default() }; + for events in [vec![], vec![], vec![Event::PointerMoved(at), button(true)], vec![button(false)]] { + let input = + egui::RawInput { screen_rect: Some(Rect::from_min_max(pos2(0.0, 0.0), pos2(200.0, 200.0))), events, ..Default::default() }; + let mut output = ctx.run_ui(input, |ui| { + let info = UiStackInfo::default().with_tag_value(PANEL_FOOTER, Some(200.0_f32)); + ui.scope_builder(UiBuilder::new().ui_stack_info(info), |ui| { + clicked |= ui.allocate_rect(Rect::from_min_max(pos2(0.0, 100.0), pos2(200.0, 200.0)), Sense::click()).clicked(); + let contents = |ui: &mut egui::Ui| { + let _button = ui.button("Footer button"); + }; + if custom { + panel_footer(ui, 31.0, contents); + } else { + bottom_bar(ui, contents); + } + }); + }); + output.textures_delta.clear(); + } + assert!(!clicked, "custom={custom}: a hidden row received the footer click"); + } + } +} diff --git a/docs/gaps.md b/docs/gaps.md index 2910585f6..e9e16036d 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -2,12 +2,16 @@ > **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-10 · **Change:** minor (G5: built-in Map Markers symbol library; estimate 11–21 → 10–20 h; previously minor: right-click on type: Spelling › Auto Spell Check, with its check mark, #4936; previously minor: right-click on a word Auto Spell Check underlined: its suggestions, Ignore All and Add to Dictionary, #4936; previously minor: G2: Effect Gallery thumbnails and in-dialog preview; estimate 1–2 h → 0; Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: PDF editable import preserves authored ActualText once, including empty/nested spans and authored ordering (#6165); cross-line/style layout remains approximate; scores and estimates unchanged; PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; G5: built-in UI Frames symbol library with default 9-slice; estimate 11–21 h; previously minor: G5: dynamic symbol part on-canvas takes opacity, blend, gradient and pattern; docs Overlap/auto-corners sync; estimate 12–22 h; previously minor: interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; previously minor: Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; previously minor: G5 docs: Art brush Overlap, Pattern brush auto corners and Global Edit Shift-click leave-out moved from Missing to Done (code already shipped); previously minor: custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: EPS import: embedded fonts (composite, Type 42, CID-keyed, Type 1, CFF FontSet) draw their glyphs; spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; G9: EPS gradients drawn as abutting strips import as one gradient, without seams; Tools stays beside the canvas with left-side panels; new gradients follow document color mode; Selection QoL: marquee modes and visible feedback, #5313; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; Indian-language hyphenation review and license allowlist, #4947; opening a slow file shows a progress window, part of #5930; Hindi catalog and contributor loanword preferences; agent wording pass complete; wider native proofreading welcome; an image zoomed in on is separated, colorized or proofed only where it shows (#4155); scores and estimates unchanged; a document's first frame renders on the render worker, so opening a heavy file no longer freezes the window; scores and estimates unchanged; large images in CMYK documents are shrunk before they are separated into ink planes (#4155); scores and estimates unchanged; `.ai` paths with a Calligraphic brush open brushed, with the 6D Art Pen's channels, and Windows reads the pen's tilt and barrel rotation, #372; scores and estimates unchanged; Compound Shapes add-on, beyond Illustrator; scores and estimates unchanged; Contour add-on, beyond Illustrator; scores and estimates unchanged; a slow first frame is waited for instead of restarting without the graphics adapter, and the menu bar no longer waits for the installed fonts' scan; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x) +> **Flyout update:** 2026-11-06 — resizable icon-strip flyouts and bottom-aligned panel footers (#5713); scores and estimates unchanged. + ## Feature submission evidence **Feature evidence last updated:** 2026-11-02. | 2026-11-06 | minor | Opening a file that takes longer than 0.4 s shows a progress window: the file name, a moving bar (reading reports no steps), how many more files wait, Cancel (Esc; the file doesn't open) and Continue in Background (`file.cancelOpen`, `file.openInBackground`); it never opens over another dialog or progress window (part of #5930); scores and estimates unchanged | Raster rendering fits and preflattens curved stroke outlines more tightly only below four times the raster tolerance, retaining thin-band coverage without changing stroke weight or vector export geometry. Current cached character/object outlines, cropped long strokes, gradient clips and effect rasters share the helper; cropped-region reuse and guarded flattening remain. The unchanged `fractional_curved_strokes_retain_coverage_between_notches` regression covers fractional weights, rotation, phase, opacity and f32-rounded endpoints. This is a quality/cost tradeoff, not a speed claim. Scores and estimates unchanged. +Icon-strip flyouts resize from the canvas-side edge and bottom, keeping the icon-column edge anchored (#5713). Independent group heights and flyout widths persist in workspaces; saved heights override content fitting while untouched heights still fit content. Shared panel button bars and the Glyphs font row stay at the bottom after enlargement; the Artboards list grows into the extra space. Regressions: `panel_docking::tests::glyphs_and_artboards_flyouts_keep_user_sizes` and `expanded_panel_footers_stay_at_the_bottom_with_short_and_long_lists`. Scores and estimates unchanged. + **PR #3273 revision — 2026-11-04:** Thin curved stroke coverage ported to the current raster-only cache, cropped-long-stroke and effect paths with guarded flattening; vector export geometry unchanged. Scores and estimates unchanged. Group Selection keeps an already selected component selected during a drag; a completed click still adds its next enclosing group. The current Direct Selection click/release machinery is shared, preserving compound-member picks, Shift deselection, isolation and masked-art dragging. Outline Stroke continues to preserve all original fills, including open contours; the change does not remove fills. Regressions: `group_selection_distinguishes_selected_leaf_drags_from_clicks` and `outline_fill_and_stroke_move_independently_with_group_selection` cover tool events, independent component movement and Undo/Redo. Scores and estimates unchanged. @@ -51,6 +55,7 @@ Artboard Options forwards the active Artboard tool's Scale Artwork with Artboard | 2026-11-05 | minor | G15: Selection skips building corner and pie widgets when the pointer is well outside a large selection; empty-canvas hover with 20,000 selected paths took 28.72 ms before and 16.41 ms after in local median runs. Other cursor targets remain active; estimate unchanged. | | 2026-11-05 | minor | G15: Direct Selection reuses one document hit result when choosing the cursor over a path; after the path hit-test improvement, a 200,000-anchor path's segment cursor took 3.72 ms before and 2.66 ms after in local median runs. Cursor behavior is unchanged; estimate unchanged. | | 2026-11-05 | minor | G15: large-selection transforms reuse object locations found during command validation; a 20,000-object nudge took 58.07 ms before and 51.26 ms after in paired median runs. Estimate unchanged. | +| 2026-11-06 | minor | Icon-strip flyouts resize on their free edges and retain sizes with workspaces (#5713); saved heights beat auto-fit; panel footers stay at the bottom after enlargement. Scores and estimates unchanged. | | 2026-11-05 | minor | G9, G14: `.ai` paths with a Calligraphic brush open brushed (the brush on the stroke, at its weight) where the object's settings are the brush's own, and the brush's Tilt, Bearing and Rotation codes come in as those channels; Windows reads the pen's tilt and barrel rotation from Windows Ink (#372). Checked against a user's file of 6D Art Pen strokes, used locally; not tried with a pen. Scores and estimates unchanged. | | 2026-11-04 | minor | Contour add-on (beyond Illustrator): drag away from a selected shape's edge to grow a live copy of it, or toward the inside to shrink it; the copy carries a live Offset Path effect, so it stays editable until Bake (Expand Appearance), a second drag changes the same Contour instead of nesting one, and a floating bar sets the distance and the Sharp, Round or Square corners. `addon.contour.set` is the command behind it. It is a built-in add-on: Object › Plug-ins › Contour, and its own heading in All Tools, with no default shortcut or toolbar slot. A VectorCraft design built on Offset Path; no other application was inspected. Scores and estimates unchanged. | | 2026-11-04 | minor | Contour tool (beyond Illustrator): drag away from a selected shape's edge to grow a live copy of it, or toward the inside to shrink it; the copy carries a live Offset Path effect, so it stays editable until Bake (Expand Appearance), a second drag changes the same Contour instead of nesting one, and a floating bar sets the distance and the Sharp, Round or Square corners. `object.contour` is the command behind it. A VectorCraft design built on Offset Path; no other application was inspected. Scores and estimates unchanged. | diff --git a/docs/roadmap.md b/docs/roadmap.md index 7204ed34b..7f2600384 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,6 +1,8 @@ # Roadmap: milestones and what's next -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-10 · **Change:** minor (M10/G5: Map Markers built-in symbol library; previously minor: right-click on type: Spelling › Auto Spell Check, with its check mark, #4936; previously minor: right-click on a word Auto Spell Check underlined: its suggestions, Ignore All and Add to Dictionary, #4936; previously minor: G2: Effect Gallery thumbnails and in-dialog preview are in; Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: PDF editable import preserves authored ActualText once, including empty/nested spans and authored ordering (#6165); cross-line/style layout remains approximate; scores and estimates unchanged; PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: EPS import: embedded fonts (composite, Type 42, CID-keyed, Type 1, CFF FontSet) draw their glyphs; spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; M4/M7: `.ai`/EPS type keeps its tab stops, and right-aligned lines with tabs lay out as the file's page, #6147; M4/M7: `.ai`/EPS type from the text document lays its lines out closer to the file's page, within limits, #6137; numeric fields step on wheel hover without taking keyboard focus, #5949; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen; Selection QoL: marquee modes and visible feedback, #5313; SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; Indian-language hyphenation review and license allowlist, #4947; Hindi catalog and contributor loanword preferences; full wording review in progress; Effect dialogs: a colour swatch beside a hex colour opens the Color Picker, #5365; shape tools move the selection from its centre point and turn it from the zone outside a box corner, #5477; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-10 · **Change:** minor (resizable icon-strip flyouts and bottom-aligned panel footers, #5713; previously minor: M10/G5: Map Markers built-in symbol library; previously minor: right-click on type: Spelling › Auto Spell Check, with its check mark, #4936; previously minor: right-click on a word Auto Spell Check underlined: its suggestions, Ignore All and Add to Dictionary, #4936; previously minor: G2: Effect Gallery thumbnails and in-dialog preview are in; Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: PDF editable import preserves authored ActualText once, including empty/nested spans and authored ordering (#6165); cross-line/style layout remains approximate; scores and estimates unchanged; PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: EPS import: embedded fonts (composite, Type 42, CID-keyed, Type 1, CFF FontSet) draw their glyphs; spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; M4/M7: `.ai`/EPS type keeps its tab stops, and right-aligned lines with tabs lay out as the file's page, #6147; M4/M7: `.ai`/EPS type from the text document lays its lines out closer to the file's page, within limits, #6137; numeric fields step on wheel hover without taking keyboard focus, #5949; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen; Selection QoL: marquee modes and visible feedback, #5313; SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; Indian-language hyphenation review and license allowlist, #4947; Hindi catalog and contributor loanword preferences; full wording review in progress; Effect dialogs: a colour swatch beside a hex colour opens the Color Picker, #5365; shape tools move the selection from its centre point and turn it from the zone outside a box corner, #5477; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x) + +> **Flyout update:** 2026-11-06 — resizable icon-strip flyouts and bottom-aligned panel footers (#5713); scores and estimates unchanged. > Numeric spinner update (2026-11-10): held mouse arrows repeat past the click timeout; scores and estimates unchanged (#6307). @@ -10,6 +12,8 @@ Numeric fields step on wheel hover without taking keyboard focus (#5949). Arrow Raster rendering fits and preflattens curved stroke outlines more tightly only below four times the raster tolerance, retaining thin-band coverage without changing stroke weight or vector export geometry. Current cached character/object outlines, cropped long strokes, gradient clips and effect rasters share the helper; cropped-region reuse and guarded flattening remain. The unchanged `fractional_curved_strokes_retain_coverage_between_notches` regression covers fractional weights, rotation, phase, opacity and f32-rounded endpoints. This is a quality/cost tradeoff, not a speed claim. Scores and estimates unchanged. +Icon-strip flyouts resize from the canvas-side edge and bottom, keeping the icon-column edge anchored (#5713). Independent group heights and flyout widths persist in workspaces; saved heights override content fitting while untouched heights still fit content. Shared panel button bars and the Glyphs font row stay at the bottom after enlargement; the Artboards list grows into the extra space. Regressions: `panel_docking::tests::glyphs_and_artboards_flyouts_keep_user_sizes` and `expanded_panel_footers_stay_at_the_bottom_with_short_and_long_lists`. Scores and estimates unchanged. + **PR #3273 revision — 2026-11-04:** Thin curved stroke coverage ported to the current raster-only cache, cropped-long-stroke and effect paths with guarded flattening; vector export geometry unchanged. Scores and estimates unchanged. Group Selection keeps an already selected component selected during a drag; a completed click still adds its next enclosing group. The current Direct Selection click/release machinery is shared, preserving compound-member picks, Shift deselection, isolation and masked-art dragging. Outline Stroke continues to preserve all original fills, including open contours; the change does not remove fills. Regressions: `group_selection_distinguishes_selected_leaf_drags_from_clicks` and `outline_fill_and_stroke_move_independently_with_group_selection` cover tool events, independent component movement and Undo/Redo. Scores and estimates unchanged. @@ -50,6 +54,7 @@ M7 type: Touch Type moves a right-to-left character along its line and up from i | 2026-11-02 | minor | Selection QoL: saved marquee modes, screen drag direction with a 3 px dead zone, and translated mode/E feedback (#5313); VectorCraft addition, scores and estimates unchanged. | | 2026-11-07 | minor | Indian-language hyphenation (#4947): eleven MIT-licensed pattern sets, with grapheme and shaping-cluster protection in both composers and existing paragraph limits. Assamese and Sanskrit are selectable; Sanskrit automatic patterns remain pending an allowlisted source. Scores and estimates unchanged. | | 2026-11-05 | minor | Effect dialogs: a colour swatch beside a hex colour field follows typed values, and a double-click opens the Color Picker over the dialog, whose OK fills the field (#5365); scores and estimates unchanged. | +| 2026-11-06 | minor | Icon-strip flyouts resize on their free edges and retain sizes with workspaces (#5713); saved heights beat auto-fit; panel footers stay at the bottom after enlargement. Scores and estimates unchanged. | | 2026-11-05 | minor | Selection Tools add-on: Select Linked, Embedded, Missing and Modified select the images and placed documents with that link status (Object › Plug-ins › Selection Tools and the Links panel menu; Shift adds), and Select by Link Status… lists them by name, status and format to pick from, optionally zooming to them; what is hidden, locked, on a template layer or outside the isolated group is left out and the status bar says how many (#5526, a VectorCraft addition); scores and estimates unchanged (estimated). | | 2026-11-05 | minor | Pencil and Paintbrush: a new stroke with Keep selected off leaves nothing selected, the earlier selection included, while Keep selected on selects only the new path (#5752); scores and estimates unchanged. | | 2026-11-05 | minor | Layers panel: a layer whose Layer Options › Print is off shows its name italic, as the reference application shows non-printing layers, and keeps it while the row is dragged (#5739); scores and estimates unchanged. | @@ -155,7 +160,7 @@ native wording review, layout checks and a web Devanagari font remain. [Notes](l | M12 | Advanced color & art (CMYK/ICC, separations, Gradient Mesh, Live Paint, Image Trace, Graphs) | 🟡 Gradient Mesh, Live Paint, Image Trace (junction-returning pieces retain their positioned boundaries; a simpler contour walk with identical output; an edge's sub-pixel shift is the same from either side; notches read only their own piece; collapsed loops keep few segments; large images trace with a warning; Auto Color keeps rare distinct colours; quick presets trace at once and the Image Trace button opens the adjustment controls; Properties reopens them; tiny fitted contours retain islands and holes; Create Strokes: exact crop EDT, Guo–Hall thinning and maximal-disc graph simplification; 15 presets, the 14 documented ones each with an enhanced version; Ignore Color, Shapes, Auto Grouping and Gradients options; measured in `image-trace-quality.md`; closed outlines retain their interior after straightening; the Flat Logo mode places edges to a fraction of a pixel, restores sharp corners, snaps lines and circles, and refuses gradients; live trace result bounds and selection highlights exclude the retained bitmap except in source-image views), Recolor Artwork, colour management (ICC, soft proofing, separations preview), Graphs (all 9 tools, Graph Data/Type, group-selected series colours persist through regenerate/save/open) done; Graph Design and Marker designs, Transpose and Switch x/y, `\|` label line breaks, the pie options, drop shadows, the legend across the top, overlapping widths, First Row/Column in Front, filled lines, kept label formatting and the Graph Data worksheet done; graph Column designs pending | 6–10 | | M13 | Automation (Actions ✅ record/playback, persisted; variables, scripting, batch) | 🟡 MCP `add_text` exposes existing vertical point, area and path type; `script.run` and File › Scripts › Other Script… run JavaScript files; the script document model and console and batch work remain | 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) (status messages and readouts use stable responsive allocations sized for localized labels and ordinary coordinates; sample widths cached by language/unit/DPI; regression-tested and contributor-confirmed 2026-10-19; estimates unchanged) | 🟡 Interim macOS cursor delivery (#2292) uses existing artwork and host-owned AppKit cursors; headless coverage only, native menu/focus/handback and display validation pending. Dock groups collapse to their tabs by a double-click on a tab (UI.8); Preferences › General › Constrain Angle acts for the arrow keys, Shift drags and new objects (#394, 2026-10-21); numeric-field pointer selection and focus regression-tested (#1326, in part; short movements still select all); RPM runtime capabilities and glibc floor checked (#1275; real-distro install/start-up pending), Eyedropper cursor with precise-setting checks (#1269), Preferences, shortcut editor (chords with Shift and a punctuation key fire as typed; other apps' text shortcut sets imported), workspaces (each keeps its layout changes until Reset , UI.7), Window › Help Bar shown or hidden apart from the status bar and kept in saved workspaces, panels docked by dragging into any dock group (as tabs or a top/bottom split) or into the icon column, and closed from a dock tab's context menu (UI.6), a custom title bar on Windows/Linux with a Home button, content-sized dialogs, menus that scroll when longer than the window and macOS native menu dismissal done (M14.8), and native macOS save sheets with a Format menu (#866); titled PDF exports set `DisplayDocTitle` for viewers and accessibility checkers (#3646); accessibility and broader Windows/Linux packaging/QA pending; floating tab groups collapse into the side icon rail and opened rail panels fold back into it (#5925, in part), with pointer and native-window regression coverage; estimates unchanged; numeric spinner arrows keep repeating while held with the mouse (#6307) | 18–28 | +| M14 | 1.0 polish (preferences, shortcut editor, workspaces, accessibility, packaging for all OSes) (status messages and readouts use stable responsive allocations sized for localized labels and ordinary coordinates; sample widths cached by language/unit/DPI; regression-tested and contributor-confirmed 2026-10-19; estimates unchanged) | 🟡 Icon-strip flyouts resize on their free edges and retain sizes (#5713); interim macOS cursor delivery (#2292) uses existing artwork and host-owned AppKit cursors; headless coverage only, native menu/focus/handback and display validation pending. Dock groups collapse to their tabs by a double-click on a tab (UI.8); Preferences › General › Constrain Angle acts for the arrow keys, Shift drags and new objects (#394, 2026-10-21); numeric-field pointer selection and focus regression-tested (#1326, in part; short movements still select all); RPM runtime capabilities and glibc floor checked (#1275; real-distro install/start-up pending), Eyedropper cursor with precise-setting checks (#1269), Preferences, shortcut editor (chords with Shift and a punctuation key fire as typed; other apps' text shortcut sets imported), workspaces (each keeps its layout changes until Reset , UI.7), Window › Help Bar shown or hidden apart from the status bar and kept in saved workspaces, panels docked by dragging into any dock group (as tabs or a top/bottom split) or into the icon column, and closed from a dock tab's context menu (UI.6), a custom title bar on Windows/Linux with a Home button, content-sized dialogs, menus that scroll when longer than the window and macOS native menu dismissal done (M14.8), and native macOS save sheets with a Format menu (#866); titled PDF exports set `DisplayDocTitle` for viewers and accessibility checkers (#3646); accessibility and broader Windows/Linux packaging/QA pending; floating tab groups collapse into the side icon rail and opened rail panels fold back into it (#5925, in part), with pointer and native-window regression coverage; estimates unchanged; numeric spinner arrows keep repeating while held with the mouse (#6307) | 18–28 | | — | Interaction fidelity pass (every tool's modifiers, Properties panel per context, isolation, nuance; width-point creation and adjoining adjustment regression-tested; dialog layout reviewed) | ⬜ (new ruler-guide Alt orientation implemented; existing-guide Escape cancellation regression-tested; broader pass remains) | 60–90 | | — | Hardening at scale (big-file corpus, fuzzing, cross-platform + browser QA) | 🟡 PngSuite integrity and bounded fetching verified (#1729); real-world corpus/manual QA remain | 50–80 | | M14 follow-up | Color Picker layout (#2008) | ✅ Compact controls, centered header, stacked actions, aligned fields and screen sampling; narrow windows scroll vertically | Included above | diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index dd37113d7..73fad19c2 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -2,6 +2,8 @@ > **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-10 · **Change:** minor (G5: Map Markers built-in symbol library; bundled content ~32% → ~34%; previously minor: right-click on type: Spelling › Auto Spell Check, with its check mark, #4936; previously minor: right-click on a word Auto Spell Check underlined: its suggestions, Ignore All and Add to Dictionary, #4936; previously minor: Canvas: a 3D effect changed on type or a group redraws where its solid was; Fit to Artwork Bounds and per-artboard SVG export build each solid once for all artboards; previously minor: Effect Gallery: a thumbnail for each filter and a preview area inside the dialog, on an original drawing; Raster effects 52% → 55%, hours 1–2 → none; headline numbers unchanged; Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: PDF editable import preserves authored ActualText once, including empty/nested spans and authored ordering (#6165); cross-line/style layout remains approximate; scores and estimates unchanged; PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; G5: UI Frames built-in symbol library with default 9-slice; bundled content ~32%; previously minor: G5: dynamic symbol part on-canvas opacity/blend/gradient/pattern; strokes remaining no longer lists Overlap/auto corners; symbols ~88%; previously minor: interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; previously minor: harakat paint fallback clarified; previously minor: current Guides feature-area rows consolidated using existing ruler evidence; scores and estimates unchanged; previously minor: Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; previously minor: G5 docs: Art brush Overlap and Pattern brush auto corners removed from strokes remaining; symbols remaining lists dynamic on-canvas beyond solid and guides outside the art; previously minor: custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: EPS import: embedded fonts (composite, Type 42, CID-keyed, Type 1, CFF FontSet) draw their glyphs; spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; `.ai`/EPS type keeps its tab stops, and right-aligned lines with tabs lay out as the file's page, #6147; `.ai`/EPS type from the text document lays its lines out closer to the file's page, within limits, #6137; `.ai` and EPS import: older Art brush objects read; numeric fields step on wheel hover without taking keyboard focus, and a panel wheel turn cannot seed a canvas zoom glide, #5949; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen; Tools stays beside the canvas with left-side panels; new gradients follow document color mode; Selection QoL: marquee modes and visible feedback, #5313; SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; the canvas keeps selection outlines and anchors between frames; Indian-language hyphenation review and license allowlist, #4947; Stroke gradients: an arrowhead aligned to the tip is painted to the corners of its base, #5964; Drop Shadow and glow dialogs: spinner arrows, #5367, #5368; Stroke Weight Up and Down shortcuts, #5797; Hindi catalog and contributor loanword preferences; full wording review in progress; Properties in Edit Artboards: a Transform section with the reference point above the Artboards section, #5219; the Control bar's Stroke: and Opacity: links scrub their fields, #4088; an image zoomed in on is separated, colorized or proofed only where it shows (#4155); a document's first frame renders on the render worker, so opening a heavy file no longer freezes the window, #4155; Control bar Paragraph section with type selected, #5372; translating unchanged type reuses its glyph and character-stroke outlines; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026; Units › East Asian Type acts, with Q and Ha units; scores unchanged) +> **Flyout update:** 2026-11-06 — resizable icon-strip flyouts and bottom-aligned panel footers (#5713); scores and estimates unchanged. + ## Feature submission evidence EPS embedded fonts, 2026-11-09 (#6023–#6067): the fonts an EPS carries draw their glyphs instead of being set as type in an installed font: composite (Type 0) fonts with the 8/8 mapping, plain Type 42 fonts (characters from their glyph names or character map, widths from the font), CID-keyed fonts over CMaps (Type 0 and Type 2 CIDFonts, CIDFonts in a FontSet's Compact Font Format data, base-font CMaps, named CIDFonts over predefined Unicode CMaps set as type) and embedded Type 1 fonts; the show operators' spacing along the baseline is kept as tracking, `kshow` included. Each step is charged to the interpreter's budgets. Hand-written test files only (no corpus file uses these fonts); scores and estimates unchanged. Evidence: `crates/eps/src/import/tests_composite.rs`, `tests_type42.rs`, `tests_cid.rs`, `tests_type1.rs`; details in [file-format-parity.md](file-format-parity.md). @@ -10,6 +12,8 @@ Numeric fields step on wheel hover without taking keyboard focus (#5949). Arrow Raster rendering fits and preflattens curved stroke outlines more tightly only below four times the raster tolerance, retaining thin-band coverage without changing stroke weight or vector export geometry. Current cached character/object outlines, cropped long strokes, gradient clips and effect rasters share the helper; cropped-region reuse and guarded flattening remain. The unchanged `fractional_curved_strokes_retain_coverage_between_notches` regression covers fractional weights, rotation, phase, opacity and f32-rounded endpoints. This is a quality/cost tradeoff, not a speed claim. Scores and estimates unchanged. +Icon-strip flyouts resize from the canvas-side edge and bottom, keeping the icon-column edge anchored (#5713). Independent group heights and flyout widths persist in workspaces; saved heights override content fitting while untouched heights still fit content. Shared panel button bars stay at the bottom after enlargement. Regressions: `panel_docking::tests::glyphs_and_artboards_flyouts_keep_user_sizes` and `expanded_panel_footers_stay_at_the_bottom_with_short_and_long_lists`. Scores and estimates unchanged. + **PR #3273 revision — 2026-11-04:** Thin curved stroke coverage ported to the current raster-only cache, cropped-long-stroke and effect paths with guarded flattening; vector export geometry unchanged. Scores and estimates unchanged. Group Selection keeps an already selected component selected during a drag; a completed click still adds its next enclosing group. The current Direct Selection click/release machinery is shared, preserving compound-member picks, Shift deselection, isolation and masked-art dragging. Outline Stroke continues to preserve all original fills, including open contours; the change does not remove fills. Regressions: `group_selection_distinguishes_selected_leaf_drags_from_clicks` and `outline_fill_and_stroke_move_independently_with_group_selection` cover tool events, independent component movement and Undo/Redo. Scores and estimates unchanged. @@ -58,6 +62,7 @@ Artboard Options forwards the active Artboard tool's Scale Artwork with Artboard | 2026-11-06 | minor | Stroke gradients: an arrowhead whose tip sits on the end of a long straight leg is painted by the gradient to the corners of its base. The slices that reach across a head now run to the end of its reach instead of narrowing towards the next corner (#5964). Test: `stroke::tests_gradient::slices_cover_an_arrowhead_on_a_long_straight_leg`. Scores and estimates unchanged. | | 2026-11-06 | minor | Width profiles: the width between two width points follows a smooth curve instead of a straight line, so a stroke no longer kinks at each width point (#5714). Each side is a monotone cubic through its points: flat at a widest or narrowest point, never past either point of a span; stacked points still step. Straight runs are sampled between width points to the drawing tolerance. Scores and estimates unchanged. | | 2026-11-05 | minor | A nearby hit on a plain long stroke or outline avoids assembling its whole path. A hit near the middle of a 200,000-anchor path took about 11–13 ms before and 1.0–1.4 ms after in paired runs on a busy Windows machine; a miss inside the path's box fell from 18.7 ms to 1.6 ms, while a distant miss was unchanged. Other stroke styles and fill hits keep their existing checks. Scores and estimates unchanged; idle performance budgets remain unverified. | +| 2026-11-06 | minor | Icon-strip flyouts resize on their free edges and retain sizes with workspaces (#5713); saved heights beat auto-fit; panel footers stay at the bottom after enlargement. Scores and estimates unchanged. | | 2026-11-05 | minor | Translating a type object without changing its text or formatting reuses glyph geometry and expanded character-stroke outlines between frames. The text appears at its new position with unchanged pixels. Scaling the type, editing its content or formatting, or changing installed fonts rebuilds the geometry; scores and estimates unchanged. | | 2026-11-05 | minor | Moving a large selection reuses the object locations found during command validation. A 20,000-object nudge took 58.07 ms before and 51.26 ms after in paired median runs; object positions, selection, and undo behavior stay the same. Scores and estimates unchanged. | | 2026-11-05 | minor | Canvas: an image drawn larger than 1,024 pixels at the size it shows, with its colours made again (separated into ink planes, colorized or shown through Separations Preview), has them made only for the 1,024-pixel tiles in view, each kept for later frames (#4155). Zooming in to 50%, 100% and 200% on the file of #4155 went from 8.6–9.0 s to 0.78 s, 0.05 s and 0.05 s (measured, M3 Pro, one run each), with the same pixels. Scores and estimates unchanged. | @@ -236,7 +241,7 @@ dominate; 3D, graphs and automation are specialist). They sum to 105. Feature de | Export for Screens, Asset Export, slices, Save for Web | 3 | 85% | anti-aliasing ghost borders (#983); TIFF choices and settings are implemented | 2–4 | estimated | | Print, colour management, separations, flattener | 3 | 80% | print preview fidelity | 2–4 | estimated | | Automation (Actions, Variables, scripting, batch) | 3 | 60% | a scripting surface (Illustrator: JavaScript/ExtendScript), Batch, image and graph variables, dataset import/export | 8–14 | estimated; MCP `add_text` exposes existing vertical type; score and hours unchanged | -| UI chrome (panels, Properties, workspaces, preferences) | 6 | 80% | Properties per context, 3 Preferences options stored but unused (#394, audited 2026-10-30: Open Documents As Tabs, Scratch Disks, legacy vector clipboard), SVG Interactivity, context menus beyond the canvas, accessibility | 13–20 | estimated; floating panels in OS windows of their own on the desktop (#4135), score and hours unchanged; macOS save Format menu implemented (#866), score and hours unchanged; Eyedropper cursor and precise setting checked (#1269), OS visual QA unverified; numeric-field focus and normal text drags regression-tested (#1326, in part); movements below the click threshold still select all; artboard navigation synchronizes the tool selection before destructive actions; status messages and readouts use stable responsive allocations sized for localized labels and ordinary coordinates (sample widths cached by language/unit/DPI; regression and contributor original-file evidence below; scores/hours unchanged); effect dialogs' colour fields show a swatch that opens the Color Picker (#5365), score and hours unchanged; floating tab groups collapse into the side icon rail and opened rail panels fold back into it (#5925, in part), scores and hours unchanged ; interim macOS cursor delivery (#2292), headless coverage only; native validation pending, score and hours unchanged | +| UI chrome (panels, Properties, workspaces, preferences) | 6 | 80% | Properties per context, 3 Preferences options stored but unused (#394, audited 2026-10-30: Open Documents As Tabs, Scratch Disks, legacy vector clipboard), SVG Interactivity, context menus beyond the canvas, accessibility | 13–20 | estimated; floating panels in OS windows of their own on the desktop (#4135), score and hours unchanged; macOS save Format menu implemented (#866), score and hours unchanged; Eyedropper cursor and precise setting checked (#1269), OS visual QA unverified; numeric-field focus and normal text drags regression-tested (#1326, in part); movements below the click threshold still select all; artboard navigation synchronizes the tool selection before destructive actions; status messages and readouts use stable responsive allocations sized for localized labels and ordinary coordinates (sample widths cached by language/unit/DPI; regression and contributor original-file evidence below; scores/hours unchanged); effect dialogs' colour fields show a swatch that opens the Color Picker (#5365), score and hours unchanged; icon-strip flyouts retain user sizes (#5713); floating tab groups collapse into the side icon rail and opened rail panels fold back into it (#5925, in part), scores and hours unchanged ; interim macOS cursor delivery (#2292), headless coverage only; native validation pending, score and hours unchanged | | Libraries, Links, Package | 2 | 82% | shared libraries, linked graphics, gradients and patterns as library items (no cloud by design) | 2–3 | estimated | | Generative AI (Text to Vector, Generative Recolor, Shape Fill, Concept to Vector, Prompt to Edit, Rewrite Text) | 2 | 0% | a pluggable provider and the panels; needs an owner decision on models | 30–60 | measured (absent) | | **Feature depth** | **105** | **~75%** | | **~250–420** | | diff --git a/docs/ui-parity.md b/docs/ui-parity.md index 7629084b8..0a3ae90c9 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -4,6 +4,8 @@ > Numeric spinner update (2026-11-10): held mouse arrows repeat past the click timeout; scores and estimates unchanged (#6307). +> **Flyout update:** 2026-11-06 — resizable icon-strip flyouts and bottom-aligned panel footers (#5713); scores and estimates unchanged. + ## Feature submission evidence Numeric fields step on wheel hover without taking keyboard focus (#5949). Arrow keys still require focus; modifiers, units, limits, mixed values and existing rounding stay in the shared stepping path. Headless regressions in `tests/numfields.rs` cover hovered unfocused fields, focused fields, trackpad accumulation and panel scroll exclusion. Scores and estimates unchanged. @@ -11,6 +13,8 @@ Numeric fields step on wheel hover without taking keyboard focus (#5949). Arrow | 2026-11-06 | minor | Opening a file that takes longer than 0.4 s shows a progress window: the file name, a moving bar (reading reports no steps), how many more files wait, Cancel (Esc; the file doesn't open) and Continue in Background (`file.cancelOpen`, `file.openInBackground`); it never opens over another dialog or progress window (part of #5930); scores and estimates unchanged | Raster rendering fits and preflattens curved stroke outlines more tightly only below four times the raster tolerance, retaining thin-band coverage without changing stroke weight or vector export geometry. Current cached character/object outlines, cropped long strokes, gradient clips and effect rasters share the helper; cropped-region reuse and guarded flattening remain. The unchanged `fractional_curved_strokes_retain_coverage_between_notches` regression covers fractional weights, rotation, phase, opacity and f32-rounded endpoints. This is a quality/cost tradeoff, not a speed claim. Scores and estimates unchanged. +Icon-strip flyouts resize from the canvas-side edge and bottom, keeping the icon-column edge anchored (#5713). Independent group heights and flyout widths persist in workspaces; saved heights override content fitting while untouched heights still fit content. Shared panel button bars and the Glyphs font row stay at the bottom after enlargement; the Artboards list grows into the extra space. Regressions: `panel_docking::tests::glyphs_and_artboards_flyouts_keep_user_sizes` and `expanded_panel_footers_stay_at_the_bottom_with_short_and_long_lists`. Scores and estimates unchanged. + **PR #3273 revision — 2026-11-04:** Thin curved stroke coverage ported to the current raster-only cache, cropped-long-stroke and effect paths with guarded flattening; vector export geometry unchanged. Scores and estimates unchanged. Group Selection keeps an already selected component selected during a drag; a completed click still adds its next enclosing group. The current Direct Selection click/release machinery is shared, preserving compound-member picks, Shift deselection, isolation and masked-art dragging. Outline Stroke continues to preserve all original fills, including open contours; the change does not remove fills. Regressions: `group_selection_distinguishes_selected_leaf_drags_from_clicks` and `outline_fill_and_stroke_move_independently_with_group_selection` cover tool events, independent component movement and Undo/Redo. Scores and estimates unchanged. @@ -58,6 +62,7 @@ Artboard Options forwards the active Artboard tool's Scale Artwork with Artboard | 2026-11-06 | minor | Shape tools: a press on the selection's centre point (Show Center) moves the selection, also where no art is painted, and a press in the rotate zone just outside a bounding-box corner turns it about the centre, both as the Selection tool's do (#5477); the Selection tool takes its centre point too, so an unfilled shape moves instead of starting a marquee; scores and estimates unchanged | | 2026-11-06 | minor | Transform fields: one Tab moves from one numeric field to the next in the Transform panel, the Properties panel's Transform section and the Control bar — a field's scrub label was a second, invisible tab stop, and the W/H link between W and H is now made after the fields, so it stays a tab stop after them; scores and estimates unchanged | | 2026-11-06 | minor | Scrubby labels: the Control bar's Stroke: and Opacity: and the Properties panel's Stroke, links to their panels, scrub the field after them as other labels do; a click still opens the panel (#4088); scores and estimates unchanged | +| 2026-11-06 | minor | Icon-strip flyouts resize on their free edges and retain sizes with workspaces (#5713); saved heights beat auto-fit; panel footers stay at the bottom after enlargement. Scores and estimates unchanged. | | 2026-11-05 | minor | Direct Selection: Shift-click picks several points of a blend's spine and dragging one moves them all, with the key objects on them, in one undo step (#5285); scores and estimates unchanged | | 2026-11-05 | minor | Direct Selection: Cmd/Ctrl held lends the Selection tool, whose bounding box shows while the key is down over objects selected whole; its handles scale and rotate, and a Cmd drag moves the whole object with its direct-selected anchors kept (#3901); scores and estimates unchanged | | 2026-11-05 | minor | Join tool: scrubbing over the ends of two crossing open paths that run past the crossing trims those ends and joins the paths there with a corner, instead of drawing a segment between the ends (#5844); scores and estimates unchanged | @@ -154,6 +159,7 @@ tools, never checked side by side) isn't captured row by row. | Busy cursor during long operations | done | Background open, save and export show the system progress cursor (gap G1) | | Each anchor snaps to Smart Guides and construction angles | done | `DrawSnap` | | Curvature tool: click/drag points, double-click or Alt-click for a corner, temporary Direct Selection, point conversion | partial | circular point markers without direction handles or duplicate markers, Anchor Size and Hide Edges regression-tested (#1236, in part); curve geometry: each stretch of three points follows the circle through them (handles along its tangent, chord/(3·cos²(α/2)) long, shorter in a turn tighter than 30°), open ends curving in, the same rule for the rubber band, dragged, inserted and converted points (#1236, in part); neighbours of a point dragged on an existing path keep their tangents, and modifiers requested in #975 remain | +| Icon-strip flyout resizing (#5713) | done | Canvas-side edge and bottom; user size persists after reopening and workspace restore; Glyphs and Artboards pointer/cursor regression | | Layers panel lists a blend's spine (#4820) | done | an open blend shows a `` row below its key objects, where Release leaves the spine; a click selects the blend (Shift/Cmd toggles it) so Direct Selection edits the spine; a locked blend's row selects nothing; no row while searching or filtering (`a_blends_spine_is_a_row_below_its_keys_that_selects_the_blend`) | | Layers panel: a layer whose Layer Options › Print is off shows its name italic (#5739) | done | the panel row and its drag ghost; a printing layer stays upright, and Print on again restores it; `panels::layers` test `a_layer_that_does_not_print_has_its_name_italic` | | Blend spine Anchor Point conversion and handle editing (#2544) | implemented | symmetric handles, independent drag/retraction, visible-handle policy and selection/stacking covered by tool and engine regressions; Windows native canvas checks and screenshots reviewed in PR #2643 | From 74ae6fdd05ada2e8763916eb9180134836ff7c48 Mon Sep 17 00:00:00 2001 From: ruchirguitar <113586074+ruchirguitar@users.noreply.github.com> Date: Tue, 10 Nov 2026 19:35:29 +0530 Subject: [PATCH 24/38] #3565: Direct Selection shows and drags type on a path's direction handles Direct Selection showed and dragged the anchors of type on a path's path (#3980) but not their direction handles: handle_anchors read only a path's own data, and the canvas draws only a path's handles. - text.setPathHandle moves a handle of type on a path's path, in document space, as path.setHandle moves a path's; the brackets keep their fractions and the text reflows. One undo step. - Direct Selection draws the handles of selected type on a path and drags them with it (Alt: one side, Shift: 45 degree steps). --- crates/doc/src/text/object.rs | 18 ++++++++++ crates/engine/src/cmd/typecmd.rs | 38 +++++++++++++++++++++ crates/engine/src/cmd/typecmd/area_tests.rs | 32 +++++++++++++++++ crates/tools/src/direct.rs | 33 ++++++++++++++++-- crates/tools/src/direct/tests.rs | 35 +++++++++++++++++++ crates/tools/src/guides/snap.rs | 8 +++-- docs/mcp.md | 3 ++ docs/type-parity.md | 5 +-- 8 files changed, 166 insertions(+), 6 deletions(-) diff --git a/crates/doc/src/text/object.rs b/crates/doc/src/text/object.rs index 12bc7ec89..bb1fa0eb2 100644 --- a/crates/doc/src/text/object.rs +++ b/crates/doc/src/text/object.rs @@ -367,6 +367,24 @@ impl TextObject { pub fn move_path_anchors(&mut self, refs: &[(usize, usize)], d: Vec2) -> bool { matches!(self.kind, TextKind::OnPath { .. }) && self.reshape_local(|path, to_text| move_anchors(path, to_text, refs, d)) } + /// Move the out (`out`) or in direction handle of anchor `ai` of subpath `si` of type on a + /// path's path to `pos` in document space, as a path's anchor takes it + /// ([`vectorcraft_geom::Anchor::set_handle`]; `independent`: that side only, the anchor a + /// corner): a Direct Selection drag of a handle (#3565). The handle is set in document space, + /// so a smooth anchor keeps its other handle's length as drawn; the brackets keep their + /// fractions of the path. False, changing nothing, for other type, when the anchor doesn't + /// exist, a degenerate `xf` or a path that would not be finite. + pub fn set_path_handle(&mut self, (si, ai): (usize, usize), out: bool, pos: Point, independent: bool) -> bool { + let xf = self.xf; + matches!(self.kind, TextKind::OnPath { .. }) + && self.reshape_local(|path, to_text| { + let Some(a) = path.anchor_mut(si, ai) else { return false }; + let mut drawn = a.transform(xf); + drawn.set_handle(out, pos, independent); + *a = drawn.transform(to_text); + true + }) + } /// Edit a copy of the area frame with `f` (given the document → text space map) and keep it /// when `f` succeeds and the result is finite. fn reshape_area(&mut self, f: impl FnOnce(&mut PathData, Affine) -> bool) -> bool { diff --git a/crates/engine/src/cmd/typecmd.rs b/crates/engine/src/cmd/typecmd.rs index a4fe33762..2257b1a8a 100644 --- a/crates/engine/src/cmd/typecmd.rs +++ b/crates/engine/src/cmd/typecmd.rs @@ -115,6 +115,15 @@ pub fn specs() -> Vec { has_doc, reshape_path ), + cmd!( + "text.setPathHandle", + "Move Type Path Handle", + [], + None, + "{id, subpath?, anchor, which: \"in\"|\"out\", x, y, independent?: bool} move a direction handle of an anchor of type on a path's path to (x, y), as path.setHandle moves a path's (independent: that side only, the anchor becomes a corner); the start and end brackets keep their fractions of the path and the text reflows along it (what Direct Selection does dragging a handle of the path)", + has_doc, + set_path_handle + ), cmd!( "text.fitHeadline", "Fit Headline", @@ -1268,6 +1277,35 @@ fn reshape_path(s: &mut Session, p: &Value) -> Result { ok() } +fn set_path_handle(s: &mut Session, p: &Value) -> Result { + const C: &str = "text.setPathHandle"; + let id = id_param(p, "id").ok_or_else(|| bad(C, "missing id"))?; + let si = super::draw2::usize_opt(p, "subpath", C)?.unwrap_or(0); + let ai = p.get("anchor").and_then(Value::as_u64).map(index).ok_or_else(|| bad(C, "missing anchor"))?; + let out = str_param(p, "which") != Some("in"); + let pos = vectorcraft_geom::Point::new(f64_req(p, "x", C)?, f64_req(p, "y", C)?); + let independent = switch_or(p, "independent", false, C)?; + let node = s.doc()?.doc.node(id); + let on_path = matches!(node.map(|n| &n.kind), Some(NodeKind::Text(t)) if matches!(t.kind, vectorcraft_doc::TextKind::OnPath { .. })); + if !on_path || node.is_some_and(|n| n.extras().perspective.is_some()) { + return Err(bad(C, "not type on a path outside perspective")); + } + s.edit("Reshape Type Path", |doc, _| { + let Some(NodeKind::Text(t)) = doc.node_mut(id).map(|n| &mut n.kind) else { return Err(EngineError::NoNode(id)) }; + let before = t.kind.clone(); + let within = |t: &TextObject| { + t.type_path().and_then(|p| p.bounds()).is_some_and(|b| [b.x0, b.y0, b.x1, b.y1].iter().all(|v| v.abs() <= crate::MAX_COORD)) + }; + if !(t.set_path_handle((si, ai), out, pos, independent) && within(t)) { + t.kind = before; + return Err(bad(C, "no such path anchor, or the path would reach past the canvas")); + } + refresh_bounds(t); + Ok(()) + })?; + ok() +} + #[cfg(test)] mod area_tests; diff --git a/crates/engine/src/cmd/typecmd/area_tests.rs b/crates/engine/src/cmd/typecmd/area_tests.rs index 88ccb36da..571c9c346 100644 --- a/crates/engine/src/cmd/typecmd/area_tests.rs +++ b/crates/engine/src/cmd/typecmd/area_tests.rs @@ -2081,3 +2081,35 @@ fn fit_min_percent_for_several_frames_keeps_each_frames_fit() { assert_eq!(fit(&s, plain), vectorcraft_doc::AreaFit::None, "the plain frame doesn't start shrinking"); assert_eq!(fit(&s, shrink), vectorcraft_doc::AreaFit::ShrinkText { min_percent: 30.0 }); } + +/// #3565: `text.setPathHandle` moves a direction handle of type on a path's path, in document +/// space, as `path.setHandle` moves a path's: a smooth anchor's other handle turns with it at +/// its length, `independent` leaves it and makes the anchor a corner; one undo step. +#[test] +fn set_path_handle_reshapes_type_on_a_path() { + let mut s = Session::new(); + s.execute("file.new", &json!({"width": 600, "height": 400})).unwrap(); + let path = + s.execute("path.create", &json!({"anchors": [{"x": 100, "y": 300, "in": [50, 300], "out": [150, 300]}, {"x": 400, "y": 300}]})).unwrap(); + let text = s.execute("text.createInPath", &json!({"path": path["id"], "mode": "onPath", "text": "Along the curve"})).unwrap(); + let id = NodeId(text["id"].as_u64().unwrap()); + let first = |s: &Session| match &s.doc().unwrap().doc.node(id).unwrap().kind { + NodeKind::Text(t) => *t.type_path().unwrap().subpaths[0].anchors.first().unwrap(), + _ => panic!("not text"), + }; + let smooth = first(&s).kind == vectorcraft_geom::AnchorKind::Smooth; + s.execute("text.setPathHandle", &json!({"id": id.0, "anchor": 0, "which": "out", "x": 100, "y": 250})).unwrap(); + let a = first(&s); + assert!((a.h_out.x - 100.0).abs() < 1e-6 && (a.h_out.y - 250.0).abs() < 1e-6, "{a:?}"); + if smooth { + assert!((a.h_in.x - 100.0).abs() < 1e-6 && (a.h_in.y - 350.0).abs() < 1e-6, "smooth: the in handle turns with it at its length: {a:?}"); + } + s.execute("text.setPathHandle", &json!({"id": id.0, "anchor": 0, "which": "in", "x": 60, "y": 300, "independent": true})).unwrap(); + let b = first(&s); + assert_eq!(b.h_out, a.h_out, "independent: the other handle stays"); + assert_eq!(b.kind, vectorcraft_geom::AnchorKind::Corner); + s.execute("edit.undo", &json!({})).unwrap(); + assert_eq!(first(&s), a, "one undo step each"); + assert!(s.execute("text.setPathHandle", &json!({"id": id.0, "anchor": 9, "which": "out", "x": 0, "y": 0})).is_err(), "no such anchor"); + assert!(s.execute("text.setPathHandle", &json!({"id": path["id"], "anchor": 0, "which": "out", "x": 0, "y": 0})).is_err(), "not type"); +} diff --git a/crates/tools/src/direct.rs b/crates/tools/src/direct.rs index 833f48225..1477cd5bb 100644 --- a/crates/tools/src/direct.rs +++ b/crates/tools/src/direct.rs @@ -557,7 +557,7 @@ fn hit_frame_edge(cx: &ToolContext, p: Point, tol: f64) -> Option<(NodeId, Vec Vec<(NodeId, usize, usize, Anchor)> { let mut v = vec![]; for &id in &cx.selection.objects { - let Some(pd) = cx.doc.node(id).and_then(|n| n.path_data()) else { continue }; + let Some(pd) = cx.doc.node(id).and_then(handle_path) else { continue }; let shown = cx.selection.partial(id); v.extend(pd.anchors().filter(|(si, ai, _)| shown.is_none_or(|set| set.contains(&(*si, *ai)))).map(|(si, ai, a)| { let mut a = *a; @@ -579,6 +579,33 @@ fn handle_anchors(cx: &ToolContext) -> Vec<(NodeId, usize, usize, Anchor)> { v } +/// The path whose direction handles Direct Selection shows and drags on `n` once it is selected: +/// a path's, or type on a path's path (#3565), in document space. +fn handle_path(n: &Node) -> Option> { + match &n.kind { + NodeKind::Path { path, .. } => Some(Cow::Borrowed(path)), + NodeKind::Text(t) if is_path_type(n) => t.type_path().map(Cow::Owned), + _ => None, + } +} + +/// The direction handles of selected type on a path ([`handle_anchors`]), drawn by the tool: +/// the canvas draws only a path's own (#3565). +fn type_path_handles(cx: &ToolContext) -> Vec { + let mut out = vec![]; + for (id, _, _, a) in handle_anchors(cx) { + if !cx.doc.node(id).is_some_and(is_path_type) { + continue; + } + let color = cx.doc.layer_color(id); + for h in [a.h_in, a.h_out].into_iter().filter(|h| h.distance(a.p) > 1e-6) { + out.push(Overlay::Line { a: a.p, b: h, color, dashed: false }); + out.push(Overlay::Handle { p: h, color }); + } + } + out +} + /// The end of a shown direction handle ([`handle_anchors`]) nearest to `p` within `tol`, and /// nearer to it than the handle's anchor (a short handle leaves its anchor to be picked): (path, /// subpath, anchor, is_out). @@ -897,8 +924,9 @@ impl Tool for DirectSelectionTool { let (q, guides) = self.handle_snap.snap(cx, (id, si, ai), p, ev.mods.shift); self.snapped = snapped(q.to_vec2(), p.to_vec2(), ev.mods.shift, &guides); self.guides = guides; + let cmd = if cx.doc.node(id).is_some_and(is_path_type) { "text.setPathHandle" } else { "path.setHandle" }; vec![Action::Preview( - "path.setHandle".into(), + cmd.into(), json!({"id": id.0, "subpath": si, "anchor": ai, "which": if out {"out"} else {"in"}, "x": q.x, "y": q.y, "independent": ev.mods.alt}), )] } @@ -1001,6 +1029,7 @@ impl Tool for DirectSelectionTool { out.push(Overlay::Path { path, color: cx.doc.layer_color(sp.instance), width: 1.0, dashed: false }); } out.extend(frame_overlays(cx, &self.frame_anchors(cx))); + out.extend(type_path_handles(cx)); let drag = if let State::Bracket(b) = &self.state { Some(b) } else { None }; out.extend(pathtype::overlays(cx, self.bracket_hover, drag)); } diff --git a/crates/tools/src/direct/tests.rs b/crates/tools/src/direct/tests.rs index 7948b4707..d7d4bb85b 100644 --- a/crates/tools/src/direct/tests.rs +++ b/crates/tools/src/direct/tests.rs @@ -2334,3 +2334,38 @@ fn a_press_on_a_clipping_path_in_a_dynamic_instance_picks_it() { outline.iter().find_map(|o| if let Overlay::Path { path, .. } = o { Some(vectorcraft_geom::kurbo::Shape::bounding_box(path)) } else { None }); assert_eq!(drawn, Some(Rect::new(310.0, 310.0, 320.0, 320.0))); } + +/// #3565: selected type on a path shows its path's direction handles, as a path selected whole +/// shows its own, and a press on one drags it with `text.setPathHandle` (Shift: 45° steps round +/// its anchor; Alt: that side only); none with Show handles when multiple anchors are selected off. +#[test] +fn type_on_a_path_shows_and_drags_its_paths_handles() { + let (mut d, text) = doc_with_path_type(); + let Some(vectorcraft_doc::NodeKind::Text(t)) = d.node_mut(text).map(|n| &mut n.kind) else { panic!("type") }; + let vectorcraft_doc::TextKind::OnPath { path, .. } = &mut t.kind else { panic!("on a path") }; + path.subpaths[0].anchors[0].h_out = Point::new(100.0, 200.0); + path.subpaths[0].anchors[1].h_in = Point::new(400.0, 200.0); + let p = paint(); + let mut s = Selection::default(); + s.add(text); + let c = cx(&d, &s, &p); + let handles = |c: &ToolContext| -> Vec { + DirectSelectionTool::new(false).overlays(c).iter().filter_map(|o| if let Overlay::Handle { p, .. } = o { Some(*p) } else { None }).collect() + }; + assert_eq!(handles(&c), vec![Point::new(100.0, 200.0), Point::new(400.0, 200.0)]); + let mut t = DirectSelectionTool::new(false); + assert_eq!(t.pointer(&c, &PointerEvent::new(PointerKind::Down, 101.0, 201.0)), vec![Action::Begin("Reshape".into())]); + let a = t.pointer(&c, &PointerEvent::new(PointerKind::Drag, 130.0, 210.0).with_mods(Mods { alt: true, ..Mods::default() })); + assert!( + matches!(&a[..], [Action::Preview(n, v)] if n == "text.setPathHandle" && v["id"] == text.0 && v["subpath"] == 0 && v["anchor"] == 0 && v["which"] == "out" && v["independent"] == true), + "{a:?}" + ); + assert_eq!(t.pointer(&c, &PointerEvent::new(PointerKind::Up, 130.0, 210.0)), vec![Action::Commit]); + let mut t = DirectSelectionTool::new(false); + t.pointer(&c, &PointerEvent::new(PointerKind::Down, 399.0, 201.0)); + let a = t.pointer(&c, &PointerEvent::new(PointerKind::Drag, 390.0, 120.0).with_mods(Mods { shift: true, ..Mods::default() })); + let Some(Action::Preview(n, v)) = a.first() else { panic!("{a:?}") }; + assert_eq!((n.as_str(), &v["anchor"], &v["which"]), ("text.setPathHandle", &json!(1), &json!("in"))); + assert!((v["x"].as_f64().unwrap() - 400.0).abs() < 1e-9, "Shift keeps it straight above its anchor: {v}"); + assert!(handles(&ToolContext { handles_multiple: false, ..cx(&d, &s, &p) }).is_empty()); +} diff --git a/crates/tools/src/guides/snap.rs b/crates/tools/src/guides/snap.rs index 9d530f4db..32941c5f7 100644 --- a/crates/tools/src/guides/snap.rs +++ b/crates/tools/src/guides/snap.rs @@ -428,8 +428,12 @@ impl HandleSnap { /// Where the handle of anchor `ai` of subpath `si` of path `id` goes with the pointer at `p`, /// and the guides showing why. pub fn snap(&mut self, cx: &ToolContext, (id, si, ai): (NodeId, usize, usize), p: Point, shift: bool) -> (Point, Vec) { - let path = cx.doc.node(id).and_then(|n| n.path_data()); - if shift && let Some(a) = path.and_then(|pd| pd.subpaths.get(si)?.anchors.get(ai)) { + // A path's anchor, or one of type on a path's path (#3565), in document space. + let anchor = cx.doc.node(id).and_then(|n| match &n.kind { + vectorcraft_doc::NodeKind::Text(t) => t.type_path()?.subpaths.get(si)?.anchors.get(ai).copied(), + _ => n.path_data()?.subpaths.get(si)?.anchors.get(ai).copied(), + }); + if shift && let Some(a) = anchor { return (a.p + constrain_angle_from(p - a.p, 45.0, cx.constrain_angle), vec![]); } if self.0.targets.is_none() { diff --git a/docs/mcp.md b/docs/mcp.md index a9b1f1cd8..77041cb28 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -5902,6 +5902,9 @@ undo step. Object › Transform › Scale, the Scale tool and the Transform pane `text.reshapePath {id, anchors: [[subpath, anchor]…], dx, dy}` does the same for the path of type on a path (Direct Selection dragging one of its anchors): the start and end brackets keep their fractions of the path and the text reflows along it. +`text.setPathHandle {id, subpath?, anchor, which: "in"|"out", x, y, independent?}` moves a direction handle of one +of its anchors, as `path.setHandle` moves a path's (Direct Selection dragging a handle of the path, #3565), in one +undo step. ```json {"name":"run_command","arguments":{"command":"text.reshapeArea","params":{"id":42,"anchors":[[0,2]],"dx":40,"dy":60}}} diff --git a/docs/type-parity.md b/docs/type-parity.md index 8f3c8e7c2..aac66f77c 100644 --- a/docs/type-parity.md +++ b/docs/type-parity.md @@ -1,6 +1,6 @@ # Type parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-10 · **Change:** minor (right-click on type: Spelling › Auto Spell Check, with its check mark, #4936; previously minor: right-click on a word Auto Spell Check underlined: its suggestions, Ignore All and Add to Dictionary, #4936; previously minor: Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; paint-only Arabic harakat keep joining with GDEF-mark ownership and Insert Kashidas base-letter paint; scores and estimates unchanged; custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: right-aligned area type sets the text after a line's last tab against the right edge; a soft return in justified type no longer keeps its line from being justified; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-10 · **Change:** minor (Direct Selection shows and drags the direction handles of type on a path's path, #3565; previously minor: right-click on type: Spelling › Auto Spell Check, with its check mark, #4936; previously minor: right-click on a word Auto Spell Check underlined: its suggestions, Ignore All and Add to Dictionary, #4936; previously minor: Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; paint-only Arabic harakat keep joining with GDEF-mark ownership and Insert Kashidas base-letter paint; scores and estimates unchanged; custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: right-aligned area type sets the text after a line's last tab against the right edge; a soft return in justified type no longer keeps its line from being justified; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen) · **Target:** Adobe Illustrator 2026 (30.x) Type is Illustrator's largest single area by use (weight 13 of 105 across "Type core" and "Type advanced" in [target-app-parity.md](target-app-parity.md)). This checklist lists the features one by one. Status is from the @@ -25,7 +25,7 @@ against Illustrator's public documentation; **estimated** unless noted. | Area Type Options (rows, columns, inset, first baseline, Text Flow by rows or by columns, Auto Size, Shrink Text to Fit) | done | Text Flow fills the cells down each column first (By Columns, the default, as files from before it open) or across each row first (By Rows), in the dialog and `text.areaOptions {flow}` (#4978); the commands that make or restyle type say when it does not fit its frame; MCP `add_text` takes Auto Size and Shrink Text to Fit (#1471) | | Threaded text, Fit Headline, Text Wrap | done | Fit Headline sees wrapping in a frame one line tall (#1444); a thread holds area type and type on a path in any mix, as Illustrator links type in an area or along a path ([help](https://helpx.adobe.com/illustrator/using/manage-text-area.html)) | | Threading from the ports: selected area type and type on a path show an in port and an out port, empty, with an arrow on a side where the frame is threaded, or with a red "+" in the out port when the text doesn't fit; a click on either port loads the text; a click on a closed path, area type or type on a path threads into it, a click on empty canvas makes a frame of the same kind, shape and size there, a drag draws a rectangular frame; the frame placed comes after the clicked one from its out port and before it from its in port (the story then starts there); Escape or another tool cancels; a double-click on a threaded port breaks the thread there | partial | #2572: the Selection tool, `text.thread.add {from, to?, rect?, at?, before?}`, as the reference app's help describes ports and threading ([archived help, Threading text between objects](https://web.archive.org/web/20210119051048/https://helpx.adobe.com/illustrator/using/creating-text.html), [create text threads](https://helpx.adobe.com/illustrator/desktop/design-with-text/add-manage-text/create-text-threads-between-objects.html)); area type's in port sits on the left side below the top-left corner, type on a path's just before its start bracket; Show Text Threads draws its lines between the same ports; with more than 1,000 such objects selected no ports are drawn. a double-click on a port where the frame is threaded breaks the thread there, the story flowing into the frames before the break and the frames after it left empty, still threaded with each other when two or more (`text.thread.break`, [archived help](https://web.archive.org/web/20220127090642/https://helpx.adobe.com/illustrator/using/creating-text.html)). Remaining: the ports only take clicks with the Selection tool, and the loaded cursor borrows the I-beam until it's drawn | -| Type on a Path effects and options; brackets | done | #429; Type on a Path Options has a Preview checkbox (#5360, as public tutorials of the dialog show) | +| Type on a Path effects and options; brackets | done | #429; Type on a Path Options has a Preview checkbox (#5360, as public tutorials of the dialog show); Direct Selection shows the anchors and direction handles of selected type on a path's path and drags both, the brackets keeping their fractions of the path and the text reflowing along it (#3980, #3565; `text.reshapePath`, `text.setPathHandle`), as the [Direct Selection page](https://helpx.adobe.com/illustrator/using/tool-techniques/direct-selection-tool.html) describes for a path | | Character and Paragraph panels, styles with overrides | done | Character-style Auto-leading resets, partial definitions, local leading overrides, override markers, Undo/Redo and native serialization have regression coverage; differing paragraph alignments leave the shared alignment buttons unselected (headless regressions, full integration CI and visual review pass); mixed-value display for other paragraph fields remains missing. Double-clicking a user character style's row opens its Character Style Options, and the dialog keeps a fixed width while its sections scroll when the window is shorter, so its buttons stay on screen; a paragraph style's row opens its Paragraph Style Options, the rename dialog the panel menu opens. Evidence: `panels::text_styles::tests::double_clicking_a_character_style_row_opens_its_options`, `dialogs::char_style_options::tests::the_sections_scroll_in_a_window_shorter_than_the_general_list` | | Copy or Cut selected characters, then paste as separate text art | done | A Type-range copy keeps the selected characters' font family, size and color when pasted on the canvas after leaving text editing, in the same document or another. On desktop the clipboard offers plain text to text consumers and rich art for canvas Paste; Paste Without Formatting while editing uses the destination text's style. `tests::clipboard::type_range_copy_stays_styled_when_pasted_as_art` (#5636) | | Character size and horizontal scale after object/group scaling | done | #1034: fields and setters compensate the stored affine; point size follows the transformed em-height axis, not the glyph ink bounds. Range edits, keyboard increments, leading and baseline shift use document points; rotation, reflection and shear remain editable. | @@ -81,6 +81,7 @@ Character Styles menu New/Redefine and footer New capture the first selected cha | Date | Change | Summary | |---|---|---| +| 2026-11-10 | minor | Type on a path (#3565): Direct Selection shows the direction handles of selected type on a path's path, as of a path selected whole, and drags them (`text.setPathHandle`; Alt: one side, Shift: 45° steps); the text reflows along the new curve. Scores and estimates unchanged. | | 2026-11-10 | minor | Spelling in the canvas context menu (#4936): a right-click on type has Spelling › Auto Spell Check, checked while it is on; the Check Spelling and Edit Custom Dictionary dialogs remain. Scores and estimates unchanged. | | 2026-11-10 | minor | Spelling in the canvas context menu (#4936): a right-click on a word Auto Spell Check underlined lists its suggestions, Ignore All and Add to Dictionary; the Check Spelling and Edit Custom Dictionary dialogs remain. Scores and estimates unchanged. | | 2026-11-09 | minor | Auto Spell Check (#4936): Edit › Spelling › Auto Spell Check (`edit.autoSpellCheck`) underlines the unknown words of the visible type, without type with effects; the Check Spelling and Edit Custom Dictionary dialogs and the right-click Spelling menu remain. Scores and estimates unchanged. | From 5fe12c205cc9cece666a18c20350acc61fc9995f Mon Sep 17 00:00:00 2001 From: ruchirguitar <113586074+ruchirguitar@users.noreply.github.com> Date: Tue, 10 Nov 2026 19:39:11 +0530 Subject: [PATCH 25/38] Test #3566: test entity references in element content against the expansion limit The existing entity tests are unit tests that place the references in attributes. This adds an import-level test with the references inside , which the import refuses, and checks that the namespace entities drawing apps write (&ns_svg;) still import. Co-Authored-By: Claude Opus 5.5 --- crates/svg/tests/it/import_details.rs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/crates/svg/tests/it/import_details.rs b/crates/svg/tests/it/import_details.rs index 57a7251bd..10418f344 100644 --- a/crates/svg/tests/it/import_details.rs +++ b/crates/svg/tests/it/import_details.rs @@ -645,3 +645,26 @@ fn nested_svg_images_past_count_cap_are_left_out() { let (_d, warnings) = import_with(&svg, &ImportOptions::default()).unwrap(); assert!(warnings.iter().any(|w| w.contains(&format!("more than {MAX_NESTED_SVG_IMAGES} nested SVG images")))); } + +/// #3566: entity references in element content count toward the expansion limit, as in +/// attributes. A 10 KB entity, a second entity that refers to it 250 times, and eight references +/// to the second inside `` would expand to about 20 MB from a file of a few KB: the import +/// refuses it before the XML reader expands anything. The namespace entities drawing apps write +/// (`&ns_svg;`) still import. +#[test] +fn entities_repeated_in_element_content_are_refused_and_namespace_entities_import() { + let bomb = format!( + r#"]>{}"#, + "x".repeat(10_000), + "&a;".repeat(250), + "&b;".repeat(8) + ); + assert!(bomb.len() < 20_000, "{} bytes", bomb.len()); + match import_with(&bomb, &ImportOptions::default()) { + Err(SvgError::Parse(e)) => assert!(e.contains("entities"), "{e}"), + other => panic!("refused as a parse error, not {:?}", other.map(|(_, w)| w)), + } + let ns = r#"]>"#; + let (d, _) = import_with(ns, &ImportOptions::default()).unwrap(); + assert_eq!(art(&d).len(), 1, "the rectangle"); +} From b29f02a42e5787d32a1dbb99d4edaa498033d06e Mon Sep 17 00:00:00 2001 From: ruchirguitar <113586074+ruchirguitar@users.noreply.github.com> Date: Tue, 10 Nov 2026 19:39:11 +0530 Subject: [PATCH 26/38] Test #3084: test that Export for Screens JPGs open in every colour model Exports RGB, CMYK and grey JPGs at qualities 100, 80 and 50, at 1x and 2x, and decodes each one. It also checks that only the CMYK files leave out the JFIF segment (#3140). Covers #2096 too. Co-Authored-By: Claude Opus 5.5 --- crates/engine/src/cmd/fileio/tests.rs | 31 +++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/crates/engine/src/cmd/fileio/tests.rs b/crates/engine/src/cmd/fileio/tests.rs index c523284ba..6236d281e 100644 --- a/crates/engine/src/cmd/fileio/tests.rs +++ b/crates/engine/src/cmd/fileio/tests.rs @@ -914,3 +914,34 @@ fn read_path_for_a_named_importer_stops_at_the_limit() { assert!(e.to_string().contains("larger than") && e.to_string().contains("drawing.tuv"), "{e}"); let _ = std::fs::remove_dir_all(&dir); } + +/// #3084, #2096: Export for Screens writes JPGs a strict reader opens, in every colour model and at +/// the qualities and scales the reports used. A four-component (CMYK) file has no JFIF segment, +/// which describes only one- and three-component images (#3140); RGB and gray files keep it. +#[test] +fn export_for_screens_jpgs_open_in_every_colour_model() { + let mut s = session(100.0, 80.0, 1); + s.execute("paint.setFill", &json!({"color": "#cc3300"})).unwrap(); + s.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 60, "height": 40})).unwrap(); + for (model, components) in [("rgb", 3), ("cmyk", 4), ("gray", 1)] { + for quality in [100, 80, 50] { + let r = s + .execute( + "document.exportForScreens", + &json!({"formats": [{"format": "jpg"}, {"format": "jpg", "scale": 2}], "settings": {"jpg": {"colorModel": model, "quality": quality}}}), + ) + .unwrap(); + let files = r["files"].as_array().unwrap(); + assert_eq!(files.len(), 2, "{model} {quality}: {r}"); + for (f, width) in files.iter().zip([100, 200]) { + let bytes = b64(f); + let what = format!("{model} q{quality} {}", f["name"]); + assert_eq!(bytes.get(..2), Some(&[0xFF, 0xD8][..]), "{what}: starts with SOI"); + let jfif = bytes.get(2..4) == Some(&[0xFF, 0xE0][..]); + assert_eq!(jfif, components != 4, "{what}: JFIF only on one- and three-component files"); + let image = image::load_from_memory(&bytes).unwrap_or_else(|e| panic!("{what}: {e}")); + assert_eq!(image.width(), width, "{what}"); + } + } + } +} From 710e14a33aece7378053e81a91a4001e49bba3ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabio=20William=20Concei=C3=A7=C3=A3o?= Date: Tue, 10 Nov 2026 11:44:07 +0000 Subject: [PATCH 27/38] G5: built-in Game HUD graphic style library --- crates/doc/src/style_libs.rs | 33 +++++++++++++++++++++++++++++ crates/engine/src/tests/stylelib.rs | 11 ++++++++-- crates/ui-egui/src/i18n/bg.tsv | 7 ++++++ crates/ui-egui/src/i18n/bs.tsv | 7 ++++++ crates/ui-egui/src/i18n/ca.tsv | 7 ++++++ crates/ui-egui/src/i18n/cs.tsv | 7 ++++++ crates/ui-egui/src/i18n/da.tsv | 7 ++++++ crates/ui-egui/src/i18n/de.tsv | 7 ++++++ crates/ui-egui/src/i18n/el.tsv | 7 ++++++ crates/ui-egui/src/i18n/es.tsv | 7 ++++++ crates/ui-egui/src/i18n/fi.tsv | 7 ++++++ crates/ui-egui/src/i18n/fr.tsv | 7 ++++++ crates/ui-egui/src/i18n/hi.tsv | 7 ++++++ crates/ui-egui/src/i18n/hu.tsv | 7 ++++++ crates/ui-egui/src/i18n/id.tsv | 7 ++++++ crates/ui-egui/src/i18n/it.tsv | 7 ++++++ crates/ui-egui/src/i18n/ja.tsv | 7 ++++++ crates/ui-egui/src/i18n/ka.tsv | 7 ++++++ crates/ui-egui/src/i18n/ko.tsv | 7 ++++++ crates/ui-egui/src/i18n/nl.tsv | 7 ++++++ crates/ui-egui/src/i18n/pl.tsv | 7 ++++++ crates/ui-egui/src/i18n/pt-br.tsv | 7 ++++++ crates/ui-egui/src/i18n/ru.tsv | 7 ++++++ crates/ui-egui/src/i18n/sv.tsv | 7 ++++++ crates/ui-egui/src/i18n/tr.tsv | 7 ++++++ crates/ui-egui/src/i18n/uk.tsv | 7 ++++++ crates/ui-egui/src/i18n/vi.tsv | 7 ++++++ crates/ui-egui/src/i18n/zh-hans.tsv | 7 ++++++ crates/ui-egui/src/i18n/zh-hant.tsv | 7 ++++++ docs/gaps.md | 14 +++++++----- docs/mcp.md | 2 +- docs/roadmap.md | 5 ++--- docs/target-app-parity.md | 7 +++--- 33 files changed, 247 insertions(+), 14 deletions(-) diff --git a/crates/doc/src/style_libs.rs b/crates/doc/src/style_libs.rs index 6eda3acef..90424d5bc 100644 --- a/crates/doc/src/style_libs.rs +++ b/crates/doc/src/style_libs.rs @@ -158,6 +158,7 @@ pub const STYLE_LIBRARIES: &[BuiltinStyleLibrary] = &[ BuiltinStyleLibrary { id: "gradient-finishes", name: "Gradient Finishes", make: gradient_finishes }, BuiltinStyleLibrary { id: "shape-effects", name: "Shape Effects", make: shape_effects }, BuiltinStyleLibrary { id: "blends-transparency", name: "Blends and Transparency", make: blends_transparency }, + BuiltinStyleLibrary { id: "game-hud", name: "Game HUD", make: game_hud }, ]; /// Built-in library `id`, made once. @@ -453,6 +454,34 @@ fn blends_transparency() -> Vec { ] } +/// Game HUD: fills and glows for health, mana, damage and quest chrome (original arcade/HUD look). +fn game_hud() -> Vec { + vec![ + style("Health Fill", vec![F(fill("#e74c3c")), S(stroke("#5c1a1a", 1.25))], vec![]), + style( + "Mana Glow", + vec![F(fill("#3498db"))], + vec![fx("stylize.outerGlow", json!({"mode": "normal", "opacity": 85.0, "blur": 7.0, "color": "#5dade2"}))], + ), + style( + "Damage Flash", + vec![F(fill("#ff6b35"))], + vec![fx("stylize.innerGlow", json!({"mode": "screen", "opacity": 80.0, "blur": 8.0, "color": "#ffe66d", "source": "edge"}))], + ), + style("Quest Outline", vec![F(fill("#f7f3e8")), S(round("#2c3e50", 3.0))], vec![]), + style( + "Coin Shine", + vec![F(fill("#f1c40f")), S(stroke("#b7950b", 1.5))], + vec![fx("stylize.dropShadow", json!({"mode": "multiply", "opacity": 40.0, "x": 2.0, "y": 3.0, "blur": 4.0, "color": "#1d1b2a"}))], + ), + style( + "Boss Alert", + vec![F(fill("#1a1a2e")), S(round("#e94560", 2.5))], + vec![fx("stylize.outerGlow", json!({"mode": "normal", "opacity": 75.0, "blur": 6.0, "color": "#e94560"}))], + ), + ] +} + #[cfg(test)] mod tests { use super::*; @@ -475,6 +504,10 @@ mod tests { } assert!(Arc::ptr_eq(&builtin_style_library("hand-drawn").unwrap(), &builtin_style_library("hand-drawn").unwrap()), "made once"); assert!(builtin_style_library("nope").is_none()); + let game = builtin_style_library("game-hud").unwrap(); + assert_eq!(game.name, "Game HUD"); + let names: Vec<&str> = game.styles.iter().map(|g| g.name.as_str()).collect(); + assert_eq!(names, ["Health Fill", "Mana Glow", "Damage Flash", "Quest Outline", "Coin Shine", "Boss Alert"]); } #[test] diff --git a/crates/engine/src/tests/stylelib.rs b/crates/engine/src/tests/stylelib.rs index 31ceb2434..3b396f663 100644 --- a/crates/engine/src/tests/stylelib.rs +++ b/crates/engine/src/tests/stylelib.rs @@ -38,10 +38,11 @@ fn rect(s: &mut Session, x: f64, y: f64, w: f64, h: f64) -> NodeId { #[test] fn builtin_libraries_are_listed_with_original_styles_using_catalogued_effects() { - let mut s = Session::new(); + let mut s = session(); let list = run(&mut s, "graphicStyle.libraries", json!({})); let libs = list["libraries"].as_array().unwrap(); - assert!(libs.len() >= 6 && libs.iter().all(|l| l["category"] == "builtIn"), "{libs:?}"); + assert!(libs.len() >= 7 && libs.iter().all(|l| l["category"] == "builtIn"), "{libs:?}"); + assert!(libs.iter().any(|l| l["id"] == "game-hud" && l["name"] == "Game HUD"), "{libs:?}"); for l in libs { let id = l["id"].as_str().unwrap(); let g = run(&mut s, "graphicStyle.library", json!({"library": id})); @@ -56,6 +57,12 @@ fn builtin_libraries_are_listed_with_original_styles_using_catalogued_effects() } } } + let game = run(&mut s, "graphicStyle.library", json!({"library": "game-hud"})); + let game_names: Vec<&str> = game["styles"].as_array().unwrap().iter().map(|x| x["name"].as_str().unwrap()).collect(); + assert_eq!(game_names, ["Health Fill", "Mana Glow", "Damage Flash", "Quest Outline", "Coin Shine", "Boss Alert"]); + let id = rect(&mut s, 10.0, 20.0, 40.0, 30.0); + let r = run(&mut s, "graphicStyle.addFromLibrary", json!({"library": "game-hud", "name": "Mana Glow", "apply": true, "ids": [id.0]})); + assert_eq!((names(&r["added"]), r["applied"].as_str()), (vec!["Mana Glow"], Some("Mana Glow"))); // By name in any case. assert_eq!(run(&mut s, "graphicStyle.library", json!({"library": "shadows and glows"}))["id"], "shadows-glows"); assert!(s.execute("graphicStyle.library", &json!({"library": "nope"})).is_err()); diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index 92c0eb459..90cad663d 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -3052,6 +3052,13 @@ digits Malayalam Малаялам Settings… Настройки… Setup… Настройка… Shadows and Glows Сенки и блясъци + Game HUD Игрови HUD + Health Fill Здраве + Mana Glow Мана + Damage Flash Щета + Quest Outline Квест + Coin Shine Монета + Boss Alert Бос Shape Фигура Shape Builder Създател на фигури Shape Builder Regions Области на създателя на фигури diff --git a/crates/ui-egui/src/i18n/bs.tsv b/crates/ui-egui/src/i18n/bs.tsv index 35a50dd07..0c22c035b 100644 --- a/crates/ui-egui/src/i18n/bs.tsv +++ b/crates/ui-egui/src/i18n/bs.tsv @@ -766,6 +766,13 @@ Other Library… Druga biblioteka… Graphic Style Libraries Biblioteke grafičkih stilova Shadows and Glows Sjene i sjajevi + Game HUD Igraći HUD + Health Fill Zdravlje + Mana Glow Mana + Damage Flash Šteta + Quest Outline Zadatak + Coin Shine Novčić + Boss Alert Bos Outlines and Rules Obrisi i linije Hand-Drawn Ručno crtano Gradient Finishes Gradijentne završnice diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index 0a05f93bc..9dccc586c 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -3926,6 +3926,13 @@ digits Malayalam Malaiàlam Shading Sombra Shadow Intensity Intensitat de l’ombra Shadows and Glows Ombres i resplendors + Game HUD HUD de joc + Health Fill Salut + Mana Glow Mana + Damage Flash Dany + Quest Outline Missió + Coin Shine Moneda + Boss Alert Cap Shape Forma Shape Builder Constructor de formes Shape Builder Delete Suprimeix del constructor de formes diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 4db1434f7..cb2072dfa 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -775,6 +775,13 @@ Other Library… Jiná knihovna… Graphic Style Libraries Knihovny grafických stylů Shadows and Glows Stíny a záře + Game HUD Herní HUD + Health Fill Život + Mana Glow Mana + Damage Flash Poškození + Quest Outline Úkol + Coin Shine Mince + Boss Alert Boss Outlines and Rules Obrysy a linky Hand-Drawn Ručně kreslené Gradient Finishes Přechodové povrchy diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index e0143dd66..e928b67e6 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -712,6 +712,13 @@ Other Library… Andet bibliotek… Graphic Style Libraries Biblioteker med grafiske stile Shadows and Glows Skygger og gløder + Game HUD Spil-HUD + Health Fill Helbred + Mana Glow Mana + Damage Flash Skade + Quest Outline Quest + Coin Shine Mønt + Boss Alert Boss Outlines and Rules Konturer og linjer Hand-Drawn Håndtegnet Gradient Finishes Gradientfinish diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index fda23a01f..6179d6596 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -3099,6 +3099,13 @@ digits Malayalam Malayalam Settings… Einstellungen… Setup… Einrichten… Shadows and Glows Schatten und Leuchteffekte + Game HUD Spiel-HUD + Health Fill Leben + Mana Glow Mana + Damage Flash Schaden + Quest Outline Quest + Coin Shine Münze + Boss Alert Boss Shape Form Shape Builder Formerstellung Shape Builder Regions Formerstellungsbereiche diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 387de896c..87009ba69 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -3601,6 +3601,13 @@ digits Malayalam Μαλαγιάλαμ Set Zoom Ορισμός ζουμ Setup… Ρύθμιση… Shadows and Glows Σκιές και λάμψεις + Game HUD HUD παιχνιδιού + Health Fill Υγεία + Mana Glow Μάνα + Damage Flash Ζημιά + Quest Outline Αποστολή + Coin Shine Νόμισμα + Boss Alert Αρχηγός Shape Builder Regions Περιοχές δημιουργίας σχημάτων Shape Effects Εφέ σχημάτων Shape Mode Λειτουργία σχήματος diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 7850df8cb..60ed53981 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -3139,6 +3139,13 @@ digits Malayalam Malayálam Settings… Ajustes… Setup… Configuración… Shadows and Glows Sombras y resplandores + Game HUD HUD de juego + Health Fill Salud + Mana Glow Maná + Damage Flash Daño + Quest Outline Misión + Coin Shine Moneda + Boss Alert Jefe Shape Forma Shape Builder Generador de formas Shape Builder Regions Regiones del generador de formas diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index b2567df75..ead7f435e 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -766,6 +766,13 @@ Other Library… Muu kirjasto… Graphic Style Libraries Grafiikkatyylikirjastot Shadows and Glows Varjot ja hehkut + Game HUD Peli-HUD + Health Fill Terveys + Mana Glow Mana + Damage Flash Vahinko + Quest Outline Tehtävä + Coin Shine Kolikko + Boss Alert Bossi Outlines and Rules Ääriviivat ja viivat Hand-Drawn Käsin piirretyt Gradient Finishes Liukuväriviimeistelyt diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index d4ed847db..18877562e 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -3131,6 +3131,13 @@ digits Malayalam Malayalam Settings… Réglages… Setup… Configuration… Shadows and Glows Ombres et lueurs + Game HUD HUD de jeu + Health Fill Santé + Mana Glow Mana + Damage Flash Dégâts + Quest Outline Quête + Coin Shine Pièce + Boss Alert Boss Shape Forme Shape Builder Concepteur de forme Shape Builder Regions Zones du Concepteur de forme diff --git a/crates/ui-egui/src/i18n/hi.tsv b/crates/ui-egui/src/i18n/hi.tsv index f1922221f..4f2c397e1 100644 --- a/crates/ui-egui/src/i18n/hi.tsv +++ b/crates/ui-egui/src/i18n/hi.tsv @@ -4238,6 +4238,13 @@ antiAlias Sharp शार्प Shading शेडिंग Shadow Intensity शैडो तीव्रता Shadows and Glows छाया और चमक + Game HUD गेम HUD + Health Fill स्वास्थ्य + Mana Glow माना + Damage Flash क्षति + Quest Outline क्वेस्ट + Coin Shine सिक्का + Boss Alert बॉस Shaft: दस्ता: Shape शेप Shape Builder शेप बिल्डर diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 463bac7cd..d8ef403ad 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -3753,6 +3753,13 @@ mark-style Japanese Japán Shades of Gray Szürkeárnyalatok Shading Árnyékolás Shadows and Glows Árnyékok és ragyogások + Game HUD Játék HUD + Health Fill Életerő + Mana Glow Mana + Damage Flash Sebzés + Quest Outline Küldetés + Coin Shine Érme + Boss Alert Főellenség Shape Alakzat Shape Builder Alakzatépítő Shape Builder Regions Alakzatépítő területei diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index 4b1502dd9..695800822 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -712,6 +712,13 @@ Other Library… Pustaka lain… Graphic Style Libraries Pustaka gaya grafis Shadows and Glows Bayangan dan pendar + Game HUD HUD Game + Health Fill Nyawa + Mana Glow Mana + Damage Flash Damage + Quest Outline Quest + Coin Shine Koin + Boss Alert Boss Outlines and Rules Garis luar dan garis Hand-Drawn Gambar tangan Gradient Finishes Finishing gradien diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 854d92b40..9ba6b85f0 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -3175,6 +3175,13 @@ digits Malayalam Malayalam Settings… Impostazioni… Setup… Configurazione… Shadows and Glows Ombre e bagliori + Game HUD HUD di gioco + Health Fill Salute + Mana Glow Mana + Damage Flash Danno + Quest Outline Missione + Coin Shine Moneta + Boss Alert Boss Shape Forma Shape Builder Generatore forme Shape Builder Regions Aree del Generatore forme diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index c35ad35cc..975f9ad0c 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -799,6 +799,13 @@ digits Malayalam マラヤーラム数字 Other Library… その他のライブラリ… Graphic Style Libraries グラフィックスタイルライブラリ Shadows and Glows シャドウと光彩 + Game HUD ゲーム HUD + Health Fill 体力 + Mana Glow マナ + Damage Flash ダメージ + Quest Outline クエスト + Coin Shine コイン + Boss Alert ボス Outlines and Rules アウトラインと罫線 Hand-Drawn 手描き風 Gradient Finishes グラデーション仕上げ diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index 4c3759852..8a92257e1 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -662,6 +662,13 @@ Other Library… სხვა ბიბლიოთეკა… Graphic Style Libraries გრაფიკული სტილების ბიბლიოთეკები Shadows and Glows ჩრდილები და ნათებები + Game HUD თამაშის HUD + Health Fill სიცოცხლე + Mana Glow მანა + Damage Flash ზიანი + Quest Outline ქვესტი + Coin Shine მონეტა + Boss Alert ბოსი Outlines and Rules კონტურები და ხაზები Hand-Drawn ხელით დახატული Gradient Finishes გრადიენტული ზედაპირები diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index 885b90381..638c11f36 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -792,6 +792,13 @@ digits Malayalam 말라얄람어 Other Library… 다른 라이브러리… Graphic Style Libraries 그래픽 스타일 라이브러리 Shadows and Glows 그림자와 광선 + Game HUD 게임 HUD + Health Fill 체력 + Mana Glow 마나 + Damage Flash 피해 + Quest Outline 퀘스트 + Coin Shine 코인 + Boss Alert 보스 Outlines and Rules 윤곽선과 괘선 Hand-Drawn 손그림 Gradient Finishes 그레이디언트 마감 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index f37225fef..e7a4afa9a 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -726,6 +726,13 @@ Other Library… Andere bibliotheek… Graphic Style Libraries Bibliotheken met grafische stijlen Shadows and Glows Schaduwen en gloed + Game HUD Game-HUD + Health Fill Leven + Mana Glow Mana + Damage Flash Schade + Quest Outline Quest + Coin Shine Munt + Boss Alert Baas Outlines and Rules Omtrekken en lijnen Hand-Drawn Handgetekend Gradient Finishes Verloopafwerkingen diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 8840b28eb..78e76f67f 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -722,6 +722,13 @@ Other Library… Inna biblioteka… Graphic Style Libraries Biblioteki stylów graficznych Shadows and Glows Cienie i blaski + Game HUD HUD gry + Health Fill Zdrowie + Mana Glow Mana + Damage Flash Obrażenia + Quest Outline Zadanie + Coin Shine Moneta + Boss Alert Boss Outlines and Rules Kontury i linie Hand-Drawn Rysowane odręcznie Gradient Finishes Wykończenia gradientowe diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index d0a1f5a81..c6db4b21d 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -2631,6 +2631,13 @@ digits Malayalam Malaiala Set the selected color to the current color Definir a cor selecionada para a cor atual Setup… Configuração… Shadows and Glows Sombras e Brilhos + Game HUD HUD de jogo + Health Fill Vida + Mana Glow Mana + Damage Flash Dano + Quest Outline Missão + Coin Shine Moeda + Boss Alert Chefe Shape Forma Shape Builder Construtor de Formas Shape Builder Regions Regiões do Construtor de Formas diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 5b6f91220..e90ef32eb 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -3170,6 +3170,13 @@ digits Malayalam Малаялам Settings… Параметры… Setup… Настройка… Shadows and Glows Тени и свечения + Game HUD Игровой HUD + Health Fill Здоровье + Mana Glow Мана + Damage Flash Урон + Quest Outline Квест + Coin Shine Монета + Boss Alert Босс Shape Фигура Shape Builder Создание фигур Shape Builder Regions Области создания фигур diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index d637a33e0..ff230e72e 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -3063,6 +3063,13 @@ digits Malayalam Malayalam Settings… Inställningar… Setup… Konfigurera… Shadows and Glows Skuggor och glöd + Game HUD Spel-HUD + Health Fill Hälsa + Mana Glow Mana + Damage Flash Skada + Quest Outline Uppdrag + Coin Shine Mynt + Boss Alert Boss Shape Form Shape Builder Formbyggare Shape Builder Regions Formbyggarområden diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index e6f96e7a8..b86673eac 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -2967,6 +2967,13 @@ digits Malayalam Malayalam Settings… Ayarlar… Setup… Kurulum… Shadows and Glows Gölgeler ve Parıltılar + Game HUD Oyun HUD + Health Fill Can + Mana Glow Mana + Damage Flash Hasar + Quest Outline Görev + Coin Shine Jeton + Boss Alert Boss Shape Şekil Shape Builder Şekil Oluşturucu Shape Builder Regions Şekil Oluşturucu Bölgeleri diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 2d0fe9859..9cf94ef7c 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -3167,6 +3167,13 @@ digits Malayalam Малаялам Settings… Параметри… Setup… Налаштування… Shadows and Glows Тіні та свічення + Game HUD Ігровий HUD + Health Fill Здоров'я + Mana Glow Мана + Damage Flash Шкода + Quest Outline Квест + Coin Shine Монета + Boss Alert Бос Shape Фігура Shape Builder Створення фігур Shape Builder Regions Області створення фігур diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index 762b7a9f7..c279eb426 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -3499,6 +3499,13 @@ mark-style Japanese Kiểu Nhật Setup… Thiết lập… Shading Tô bóng Shadows and Glows Bóng và ánh sáng + Game HUD HUD trò chơi + Health Fill Máu + Mana Glow Mana + Damage Flash Sát thương + Quest Outline Nhiệm vụ + Coin Shine Xu + Boss Alert Trùm Shape Hình dạng Shape Builder Tạo hình dạng Shape Builder Regions Vùng Tạo hình dạng diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 749efbdbe..e05089b43 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -3005,6 +3005,13 @@ digits Malayalam 马拉雅拉姆数字 Settings… 设置… Setup… 设置… Shadows and Glows 阴影与光晕 + Game HUD 游戏 HUD + Health Fill 生命 + Mana Glow 法力 + Damage Flash 伤害 + Quest Outline 任务 + Coin Shine 金币 + Boss Alert 首领 Shape 形状 Shape Builder 形状生成器 Shape Builder Regions 形状生成器区域 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 182dd95f3..b61643a42 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -4090,6 +4090,13 @@ digits Malayalam 馬來亞拉姆數字 Settings… 設定… Setup… 設定… Shadows and Glows 陰影與光暈 + Game HUD 遊戲 HUD + Health Fill 生命 + Mana Glow 魔力 + Damage Flash 傷害 + Quest Outline 任務 + Coin Shine 金幣 + Boss Alert 首領 Shape 形狀 Shape Builder 形狀建立 Shape Builder Regions 形狀建立區域 diff --git a/docs/gaps.md b/docs/gaps.md index e9e16036d..f0a231a34 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -1,6 +1,6 @@ # Gaps -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-10 · **Change:** minor (G5: built-in Map Markers symbol library; estimate 11–21 → 10–20 h; previously minor: right-click on type: Spelling › Auto Spell Check, with its check mark, #4936; previously minor: right-click on a word Auto Spell Check underlined: its suggestions, Ignore All and Add to Dictionary, #4936; previously minor: G2: Effect Gallery thumbnails and in-dialog preview; estimate 1–2 h → 0; Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: PDF editable import preserves authored ActualText once, including empty/nested spans and authored ordering (#6165); cross-line/style layout remains approximate; scores and estimates unchanged; PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; G5: built-in UI Frames symbol library with default 9-slice; estimate 11–21 h; previously minor: G5: dynamic symbol part on-canvas takes opacity, blend, gradient and pattern; docs Overlap/auto-corners sync; estimate 12–22 h; previously minor: interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; previously minor: Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; previously minor: G5 docs: Art brush Overlap, Pattern brush auto corners and Global Edit Shift-click leave-out moved from Missing to Done (code already shipped); previously minor: custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: EPS import: embedded fonts (composite, Type 42, CID-keyed, Type 1, CFF FontSet) draw their glyphs; spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; G9: EPS gradients drawn as abutting strips import as one gradient, without seams; Tools stays beside the canvas with left-side panels; new gradients follow document color mode; Selection QoL: marquee modes and visible feedback, #5313; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; Indian-language hyphenation review and license allowlist, #4947; opening a slow file shows a progress window, part of #5930; Hindi catalog and contributor loanword preferences; agent wording pass complete; wider native proofreading welcome; an image zoomed in on is separated, colorized or proofed only where it shows (#4155); scores and estimates unchanged; a document's first frame renders on the render worker, so opening a heavy file no longer freezes the window; scores and estimates unchanged; large images in CMYK documents are shrunk before they are separated into ink planes (#4155); scores and estimates unchanged; `.ai` paths with a Calligraphic brush open brushed, with the 6D Art Pen's channels, and Windows reads the pen's tilt and barrel rotation, #372; scores and estimates unchanged; Compound Shapes add-on, beyond Illustrator; scores and estimates unchanged; Contour add-on, beyond Illustrator; scores and estimates unchanged; a slow first frame is waited for instead of restarting without the graphics adapter, and the menu bar no longer waits for the installed fonts' scan; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-10 · **Change:** minor (G5: built-in Game HUD graphic style library; estimate 10–20 → 9–19 h; previously minor: G5: built-in Map Markers symbol library; estimate 11–21 → 10–20 h; previously minor: right-click on type: Spelling › Auto Spell Check, with its check mark, #4936; previously minor: right-click on a word Auto Spell Check underlined: its suggestions, Ignore All and Add to Dictionary, #4936; previously minor: G2: Effect Gallery thumbnails and in-dialog preview; estimate 1–2 h → 0; Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: PDF editable import preserves authored ActualText once, including empty/nested spans and authored ordering (#6165); cross-line/style layout remains approximate; scores and estimates unchanged; PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; G5: built-in UI Frames symbol library with default 9-slice; estimate 11–21 h; previously minor: G5: dynamic symbol part on-canvas takes opacity, blend, gradient and pattern; docs Overlap/auto-corners sync; estimate 12–22 h; previously minor: interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; previously minor: Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; previously minor: G5 docs: Art brush Overlap, Pattern brush auto corners and Global Edit Shift-click leave-out moved from Missing to Done (code already shipped); previously minor: custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: EPS import: embedded fonts (composite, Type 42, CID-keyed, Type 1, CFF FontSet) draw their glyphs; spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; G9: EPS gradients drawn as abutting strips import as one gradient, without seams; Tools stays beside the canvas with left-side panels; new gradients follow document color mode; Selection QoL: marquee modes and visible feedback, #5313; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; Indian-language hyphenation review and license allowlist, #4947; opening a slow file shows a progress window, part of #5930; Hindi catalog and contributor loanword preferences; agent wording pass complete; wider native proofreading welcome; an image zoomed in on is separated, colorized or proofed only where it shows (#4155); scores and estimates unchanged; a document's first frame renders on the render worker, so opening a heavy file no longer freezes the window; scores and estimates unchanged; large images in CMYK documents are shrunk before they are separated into ink planes (#4155); scores and estimates unchanged; `.ai` paths with a Calligraphic brush open brushed, with the 6D Art Pen's channels, and Windows reads the pen's tilt and barrel rotation, #372; scores and estimates unchanged; Compound Shapes add-on, beyond Illustrator; scores and estimates unchanged; Contour add-on, beyond Illustrator; scores and estimates unchanged; a slow first frame is waited for instead of restarting without the graphics adapter, and the menu bar no longer waits for the installed fonts' scan; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x) > **Flyout update:** 2026-11-06 — resizable icon-strip flyouts and bottom-aligned panel footers (#5713); scores and estimates unchanged. @@ -102,7 +102,7 @@ stage ([ROADMAP.md](../ROADMAP.md)). | 8 | [G2 Photoshop-style raster effects and the Effect Gallery](#g2-photoshop-style-raster-effects-and-the-effect-gallery) | 0 | no | [effects-parity.md](effects-parity.md) | | 9 | [G13 Illustrator 2026 (30.x) additions](#g13-illustrator-2026-30x-additions) | 13–23 | no | [target-app-parity.md](target-app-parity.md) | | 10 | [G14 Pen hardware beyond Windows pressure](#g14-pen-hardware-beyond-windows-pressure) | 4–10 | Wayland pressure | [hardware-parity.md](hardware-parity.md) | -| 11 | [G5 Brushes, symbols and libraries](#g5-brushes-symbols-and-libraries) | 10–20 | no | [target-app-parity.md](target-app-parity.md) | +| 11 | [G5 Brushes, symbols and libraries](#g5-brushes-symbols-and-libraries) | 9–19 | no | [target-app-parity.md](target-app-parity.md) | | 12 | [G3 3D and Materials](#g3-3d-and-materials) | 25–45 | no | [effects-parity.md](effects-parity.md) | | 13 | [G7 Views and windows](#g7-views-and-windows) | 15–25 | no | [ui-parity.md](ui-parity.md) | | 14 | [G6 Automation and scripting](#g6-automation-and-scripting) | 4–8 | no | [target-app-parity.md](target-app-parity.md) | @@ -657,12 +657,15 @@ macOS cursor delivery (#2292) is narrowed to a native host adapter using the exi is on, a Shift-click on a highlighted match leaves it out (`select.globalEditExclude`, #3521 evidence); Direct Selection of a path, compound path or type inside a dynamic instance also takes opacity and blend (`transparency.set`) and a gradient or pattern fill or stroke (`paint.setFill` / `paint.setStroke`, - Eyedropper), for that instance only. -- **Missing:** more built-in brush and graphic-style libraries (and further symbol libraries beyond Basic + Eyedropper), for that instance only; + graphic style libraries (Window › Graphic Style Libraries; seven original libraries generated in code, including + Game HUD with Health Fill, Mana Glow, Damage Flash, Quest Outline, Coin Shine and Boss Alert; User Defined; Other + Library…; `graphicStyle.libraries`, `graphicStyle.addFromLibrary`). +- **Missing:** more built-in brush libraries (and further graphic-style libraries beyond the seven shipped, and further symbol libraries beyond Basic Shapes, UI Frames and Map Markers; original, generated in code, never Adobe's); dynamic symbols' `.ai` read and write; Global Edit of groups holding type or images, rotated copies as matches and highlighting matches before the start; 9-slice guides outside the symbol's art (a dragged guide stops at the art's edge). -- **Estimate:** 10–20 h. +- **Estimate:** 9–19 h. ## G3 3D and Materials @@ -860,6 +863,7 @@ trap, and the defaults (0.25 pt, 100%, 40%) are VectorCraft's own, the pages giv | Date | Change | Summary | |---|---|---| +| 2026-11-10 | minor | G5: built-in Game HUD graphic style library (Health Fill, Mana Glow, Damage Flash, Quest Outline, Coin Shine, Boss Alert); Missing trimmed; estimate 10–20 → 9–19 h. | | 2026-11-10 | minor | G5: built-in Map Markers symbol library (Pin, Flag, Crosshair, Checkpoint; no default 9-slice); Missing trimmed; estimate 11–21 → 10–20 h. | | 2026-11-10 | minor | G4 type: a right-click on type has Spelling › Auto Spell Check, checked while it is on, #4936; the Check Spelling and Edit Custom Dictionary dialogs remain; scores and estimates unchanged. | | 2026-11-10 | minor | G4 type: a right-click on a word Auto Spell Check underlined lists its suggestions, Ignore All and Add to Dictionary, #4936; the Check Spelling and Edit Custom Dictionary dialogs remain; scores and estimates unchanged. | diff --git a/docs/mcp.md b/docs/mcp.md index 76033453e..ff3031ebb 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -2791,7 +2791,7 @@ as a value left out. ## Graphic style libraries Graphic style libraries are read-only sets of graphic styles generated in code (Shadows and Glows, Outlines and -Rules, Hand-Drawn, Gradient Finishes, Shape Effects, Blends and Transparency). `graphicStyle.libraries` lists them +Rules, Hand-Drawn, Gradient Finishes, Shape Effects, Blends and Transparency, Game HUD). `graphicStyle.libraries` lists them (`id`, `name`, `category`: `builtIn`, `user` or `loaded`, `count`) and `graphicStyle.library {library}` returns one's styles in the `graphicStyle.list` shape (`library` is an id or a name). `graphicStyle.addFromLibrary {library, name? | names?}` copies styles into the document's Graphic Styles as one undo step (no names: the whole library); styles the diff --git a/docs/roadmap.md b/docs/roadmap.md index 7f2600384..cc5c2a13e 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,8 +1,6 @@ # Roadmap: milestones and what's next -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-10 · **Change:** minor (resizable icon-strip flyouts and bottom-aligned panel footers, #5713; previously minor: M10/G5: Map Markers built-in symbol library; previously minor: right-click on type: Spelling › Auto Spell Check, with its check mark, #4936; previously minor: right-click on a word Auto Spell Check underlined: its suggestions, Ignore All and Add to Dictionary, #4936; previously minor: G2: Effect Gallery thumbnails and in-dialog preview are in; Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: PDF editable import preserves authored ActualText once, including empty/nested spans and authored ordering (#6165); cross-line/style layout remains approximate; scores and estimates unchanged; PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: EPS import: embedded fonts (composite, Type 42, CID-keyed, Type 1, CFF FontSet) draw their glyphs; spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; M4/M7: `.ai`/EPS type keeps its tab stops, and right-aligned lines with tabs lay out as the file's page, #6147; M4/M7: `.ai`/EPS type from the text document lays its lines out closer to the file's page, within limits, #6137; numeric fields step on wheel hover without taking keyboard focus, #5949; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen; Selection QoL: marquee modes and visible feedback, #5313; SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; Indian-language hyphenation review and license allowlist, #4947; Hindi catalog and contributor loanword preferences; full wording review in progress; Effect dialogs: a colour swatch beside a hex colour opens the Color Picker, #5365; shape tools move the selection from its centre point and turn it from the zone outside a box corner, #5477; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x) - -> **Flyout update:** 2026-11-06 — resizable icon-strip flyouts and bottom-aligned panel footers (#5713); scores and estimates unchanged. +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-10 · **Change:** minor (M10/G5: Game HUD graphic style library; previously minor: resizable icon-strip flyouts and bottom-aligned panel footers, #5713; previously minor: M10/G5: Map Markers built-in symbol library; previously minor: right-click on type: Spelling › Auto Spell Check, with its check mark, #4936; previously minor: right-click on a word Auto Spell Check underlined: its suggestions, Ignore All and Add to Dictionary, #4936; previously minor: G2: Effect Gallery thumbnails and in-dialog preview are in; Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: PDF editable import preserves authored ActualText once, including empty/nested spans and authored ordering (#6165); cross-line/style layout remains approximate; scores and estimates unchanged; PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: EPS import: embedded fonts (composite, Type 42, CID-keyed, Type 1, CFF FontSet) draw their glyphs; spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; M4/M7: `.ai`/EPS type keeps its tab stops, and right-aligned lines with tabs lay out as the file's page, #6147; M4/M7: `.ai`/EPS type from the text document lays its lines out closer to the file's page, within limits, #6137; numeric fields step on wheel hover without taking keyboard focus, #5949; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen; Selection QoL: marquee modes and visible feedback, #5313; SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; Indian-language hyphenation review and license allowlist, #4947; Hindi catalog and contributor loanword preferences; full wording review in progress; Effect dialogs: a colour swatch beside a hex colour opens the Color Picker, #5365; shape tools move the selection from its centre point and turn it from the zone outside a box corner, #5477; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x) > Numeric spinner update (2026-11-10): held mouse arrows repeat past the click timeout; scores and estimates unchanged (#6307). @@ -197,6 +195,7 @@ separate. Scores and hours unchanged. [Behavior and command details](development | Date | Change | Summary | |---|---|---| +| 2026-11-10 | minor | M10/G5: Game HUD built-in graphic style library; estimates unchanged | | 2026-11-10 | minor | M10/G5: Map Markers built-in symbol library (Pin, Flag, Crosshair, Checkpoint); estimates unchanged | | 2026-11-10 | minor | M7: Spelling › Auto Spell Check in the context menu of type (#4936); the dialogs remain; scores and estimates unchanged. | | 2026-11-10 | minor | M7: right-click suggestions, Ignore All and Add to Dictionary on a word Auto Spell Check underlined (#4936); the dialogs remain; scores and estimates unchanged. | diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 75cfb79ef..dfa2a2957 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1,6 +1,6 @@ # Parity with Adobe Illustrator -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-10 · **Change:** minor (Line Arrow arrowhead (#5421; scores and estimates unchanged); G5: Map Markers built-in symbol library; bundled content ~32% → ~34%; previously minor: right-click on type: Spelling › Auto Spell Check, with its check mark, #4936; previously minor: right-click on a word Auto Spell Check underlined: its suggestions, Ignore All and Add to Dictionary, #4936; previously minor: Canvas: a 3D effect changed on type or a group redraws where its solid was; Fit to Artwork Bounds and per-artboard SVG export build each solid once for all artboards; previously minor: Effect Gallery: a thumbnail for each filter and a preview area inside the dialog, on an original drawing; Raster effects 52% → 55%, hours 1–2 → none; headline numbers unchanged; Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: PDF editable import preserves authored ActualText once, including empty/nested spans and authored ordering (#6165); cross-line/style layout remains approximate; scores and estimates unchanged; PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; G5: UI Frames built-in symbol library with default 9-slice; bundled content ~32%; previously minor: G5: dynamic symbol part on-canvas opacity/blend/gradient/pattern; strokes remaining no longer lists Overlap/auto corners; symbols ~88%; previously minor: interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; previously minor: harakat paint fallback clarified; previously minor: current Guides feature-area rows consolidated using existing ruler evidence; scores and estimates unchanged; previously minor: Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; previously minor: G5 docs: Art brush Overlap and Pattern brush auto corners removed from strokes remaining; symbols remaining lists dynamic on-canvas beyond solid and guides outside the art; previously minor: custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: EPS import: embedded fonts (composite, Type 42, CID-keyed, Type 1, CFF FontSet) draw their glyphs; spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; `.ai`/EPS type keeps its tab stops, and right-aligned lines with tabs lay out as the file's page, #6147; `.ai`/EPS type from the text document lays its lines out closer to the file's page, within limits, #6137; `.ai` and EPS import: older Art brush objects read; numeric fields step on wheel hover without taking keyboard focus, and a panel wheel turn cannot seed a canvas zoom glide, #5949; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen; Tools stays beside the canvas with left-side panels; new gradients follow document color mode; Selection QoL: marquee modes and visible feedback, #5313; SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; the canvas keeps selection outlines and anchors between frames; Indian-language hyphenation review and license allowlist, #4947; Stroke gradients: an arrowhead aligned to the tip is painted to the corners of its base, #5964; Drop Shadow and glow dialogs: spinner arrows, #5367, #5368; Stroke Weight Up and Down shortcuts, #5797; Hindi catalog and contributor loanword preferences; full wording review in progress; Properties in Edit Artboards: a Transform section with the reference point above the Artboards section, #5219; the Control bar's Stroke: and Opacity: links scrub their fields, #4088; an image zoomed in on is separated, colorized or proofed only where it shows (#4155); a document's first frame renders on the render worker, so opening a heavy file no longer freezes the window, #4155; Control bar Paragraph section with type selected, #5372; translating unchanged type reuses its glyph and character-stroke outlines; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026; Units › East Asian Type acts, with Q and Ha units; scores unchanged) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-10 · **Change:** minor (G5: Game HUD graphic style library; bundled content ~34% → ~36%; previously minor: Line Arrow arrowhead (#5421; scores and estimates unchanged); G5: Map Markers built-in symbol library; bundled content ~32% → ~34%; previously minor: right-click on type: Spelling › Auto Spell Check, with its check mark, #4936; previously minor: right-click on a word Auto Spell Check underlined: its suggestions, Ignore All and Add to Dictionary, #4936; previously minor: Canvas: a 3D effect changed on type or a group redraws where its solid was; Fit to Artwork Bounds and per-artboard SVG export build each solid once for all artboards; previously minor: Effect Gallery: a thumbnail for each filter and a preview area inside the dialog, on an original drawing; Raster effects 52% → 55%, hours 1–2 → none; headline numbers unchanged; Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: PDF editable import preserves authored ActualText once, including empty/nested spans and authored ordering (#6165); cross-line/style layout remains approximate; scores and estimates unchanged; PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; G5: UI Frames built-in symbol library with default 9-slice; bundled content ~32%; previously minor: G5: dynamic symbol part on-canvas opacity/blend/gradient/pattern; strokes remaining no longer lists Overlap/auto corners; symbols ~88%; previously minor: interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; previously minor: harakat paint fallback clarified; previously minor: current Guides feature-area rows consolidated using existing ruler evidence; scores and estimates unchanged; previously minor: Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; previously minor: G5 docs: Art brush Overlap and Pattern brush auto corners removed from strokes remaining; symbols remaining lists dynamic on-canvas beyond solid and guides outside the art; previously minor: custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: EPS import: embedded fonts (composite, Type 42, CID-keyed, Type 1, CFF FontSet) draw their glyphs; spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; `.ai`/EPS type keeps its tab stops, and right-aligned lines with tabs lay out as the file's page, #6147; `.ai`/EPS type from the text document lays its lines out closer to the file's page, within limits, #6137; `.ai` and EPS import: older Art brush objects read; numeric fields step on wheel hover without taking keyboard focus, and a panel wheel turn cannot seed a canvas zoom glide, #5949; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen; Tools stays beside the canvas with left-side panels; new gradients follow document color mode; Selection QoL: marquee modes and visible feedback, #5313; SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; the canvas keeps selection outlines and anchors between frames; Indian-language hyphenation review and license allowlist, #4947; Stroke gradients: an arrowhead aligned to the tip is painted to the corners of its base, #5964; Drop Shadow and glow dialogs: spinner arrows, #5367, #5368; Stroke Weight Up and Down shortcuts, #5797; Hindi catalog and contributor loanword preferences; full wording review in progress; Properties in Edit Artboards: a Transform section with the reference point above the Artboards section, #5219; the Control bar's Stroke: and Opacity: links scrub their fields, #4088; an image zoomed in on is separated, colorized or proofed only where it shows (#4155); a document's first frame renders on the render worker, so opening a heavy file no longer freezes the window, #4155; Control bar Paragraph section with type selected, #5372; translating unchanged type reuses its glyph and character-stroke outlines; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026; Units › East Asian Type acts, with Q and Ha units; scores unchanged) > **Flyout update:** 2026-11-06 — resizable icon-strip flyouts and bottom-aligned panel footers (#5713); scores and estimates unchanged. @@ -536,7 +536,7 @@ since 2026-10-01. | **Interaction fidelity:** modifiers, cursors, small behaviours | ~30–40% | The dedicated pass hasn't started, and there has been no side-by-side session with Illustrator yet. A power user notices this first | | **Look & feel** | ~75–80% | Measured against Illustrator 2026 screenshots (2026-10-02); the panels added since haven't been re-measured | | **File interop** | ~85% | SVG/SVGZ, PDF and PDF-compatible `.ai` (and legacy version 7 `.ai` out, keeping layers, groups and type as text), EPS, DXF, EMF/WMF, raster formats, PSD export and PSD/PSB placement (merged image), Place and Links, Package, Print, clipboard flavours. Affinity documents (Affinity 1–3) open natively as editable art, checked against 51 pinned public files (33 current `.af`), with selected saved-thumbnail comparisons and every-board native/SVG/PDF/PSD checks for current files; Affinity export is missing and effects, adjustments and brushes don't import. Affinity isn't an Illustrator feature, so this doesn't raise the parity score. The editing copy inside an Illustrator EPS or `.ai` is read for its structure: layers, groups, names, hidden objects and type, colours, gradients, transparency, images and every artboard (see Out of scope for what it doesn't read yet); DWG has no open spec | -| **Bundled content:** brush, symbol, style and swatch libraries | ~34% | Ours are original and generated in code, and far fewer than Illustrator ships; symbol libraries have three (Basic Shapes, UI Frames with default 9-slice, Map Markers) and brush libraries four (Art Ribbons, Calligraphic Nibs, Pattern Borders, Scatter Shapes) beside User Defined and Other Library… | +| **Bundled content:** brush, symbol, style and swatch libraries | ~36% | Ours are original and generated in code, and far fewer than Illustrator ships; symbol libraries have three (Basic Shapes, UI Frames with default 9-slice, Map Markers), graphic style libraries seven (including Game HUD) and brush libraries four (Art Ribbons, Calligraphic Nibs, Pattern Borders, Scatter Shapes) beside User Defined and Other Library… | | **Performance** | unverified | Multithreaded, off-thread rendering and caches are in place. The last budget run (2026-10-01, loaded machine) measured 290 ms for a 50k-path fit against a 16 ms budget. Re-run `vectorcraft-cli perf` on an idle machine | | **Robustness** | good, new | ~2,840 tests, property tests, no panics in shipped code (lints, the `guard` safety net, import fuzzing), Data Recovery. Missing: a corpus of real-world files, Windows/Linux/browser QA | | **Platforms and 1.0 polish** | ~60% | The macOS app and the web build work, and Finder opens every readable format with the macOS app (#295, #354); on macOS the menus are in the system menu bar, laid out as a Mac app's, with the in-window menus as a fallback (`--in-window-menus`; verified on Windows and by cross-compilation, not yet by hand on a Mac); hybrid-graphics laptops render on the power-saving GPU by default on Windows and macOS (#306), and Windows starts on Direct3D 12, falling back to OpenGL, and loads no Vulkan driver unless `WGPU_BACKEND` asks for it (#545: Vulkan flickered black on an Intel GPU, DX12 and OpenGL didn't; #806: a faulty Vulkan driver crashed the app before its window appeared; verified by unit tests and the reporter's runs with `WGPU_BACKEND`), and Linux on the GPU the desktop runs on, since a Wayland compositor may refuse frames from the other one; a window that fails on its GPU while starting up starts again on the next one instead of crashing (#502; the dual-GPU Wayland case is verified by unit tests and cross-compilation, not on such hardware); web drops land in the document they were dropped on (#359); desktop drops carry no pointer position (winit 0.30), so a dropped document opens as a tab of its own and a picture or text is placed in the middle of the view, while the web places anything dropped on the canvas and opens what is dropped off it, as Illustrator does (#472); a web page that loses its WebGPU device or WebGL context restarts its graphics with the same documents (#369); on Linux/Wayland the clipboard reads the compositor's own (data-control) and copied image files paste, but files can't be dropped on the window under native Wayland until eframe moves to winit 0.31 (#398; the Wayland side is verified by compilation only), so Preferences › Performance › Linux Display Server › X11 (XWayland) opens the window through XWayland, where drops work (#1151; verified by unit tests and cross-compilation, not on a Wayland desktop); on Linux the Paste menu checks the system clipboard on a background thread, so a clipboard owner that never answers no longer freezes the window, while Windows and macOS keep their quick in-line check (#510; the X11 hang verified by the contributor, here by headless tests); on KDE Plasma 6.3+ under Wayland, pens and pen displays reach the app only under XWayland (`WAYLAND_DISPLAY=`) or with KWin's `KWIN_WAYLAND_EMULATE_TABLET=1`, since winit 0.30 has no Wayland tablet input (#491, not verified on hardware); Windows/Linux packaging and accessibility are pending | @@ -1293,7 +1293,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - **Transparency:** three-state Knockout Group (on, neutral, off), Opacity & Mask Define Knockout Shape, Page Isolated Blending and Page Knockout Group, on screen and in SVG/PDF. SVG export writes a knockout group of opaque art plainly, without masks, so shapes meeting edge to edge leave no seam; translucent, blended, masked or brushed art knocks out through masks. Blend modes match the standard formulas on screen, in SVG and in PDF, and non-isolated groups blend with the art below. Flatten Transparency with presets, a dialog and a Flattener Preview panel. Flattening for files without transparency (PDF/X-1a, PDF/X-3) in a CMYK document keeps the inks where it rasterizes, as under an image with see-through pixels: the images it makes hold ink amounts, not screen colours the file would separate again, so a C100 M6 Y41 K3 panel stays C100 M6 Y41 K3 there (#2160, `engine/src/cmd/fileio/tests_pdfx.rs`). On the canvas, the renderer no longer draws the picture of the art below back as an image each time it copies the backdrop of a non-isolated group that blends: it keeps that picture and draws the art that follows over it. At 2880 × 1800, 50 translucent groups with Multiply members take 65 ms a frame instead of 299 ms on 8 threads (`vectorcraft-cli bench`), and the frames have the same pixels. Type inside a group, layer, symbol instance, blend or repeat keeps its opacity, blend mode, opacity mask, Opacity & Mask Define Knockout Shape, own effects, perspective and visibility when the effects on the container outline it. Hidden type with a geometry effect of its own is left out of exports. - - **Graphic styles:** styles keep opacity and blend mode, capture groups and type, apply on top with Alt, and link to the objects using them; Redefine, Break Link, Graphic Style Options, Select All Unused, Sort by Name, and Select > Same Graphic Style / Appearance Attribute. The panel has list and thumbnail views with rendered previews, merge, reorder and drag and drop, plus a Control bar Style picker; six generated style libraries save and load as `.vcstyles`. + - **Graphic styles:** styles keep opacity and blend mode, capture groups and type, apply on top with Alt, and link to the objects using them; Redefine, Break Link, Graphic Style Options, Select All Unused, Sort by Name, and Select > Same Graphic Style / Appearance Attribute. The panel has list and thumbnail views with rendered previews, merge, reorder and drag and drop, plus a Control bar Style picker; seven generated style libraries (including Game HUD) save and load as `.vcstyles`. In the Graphic Styles panel, Shift-click selects a range of styles and Ctrl/Cmd-click adds or removes one. - **Several text styles at once:** in the Character Styles and Paragraph Styles panels, Shift-click highlights a range of styles and Ctrl/Cmd-click adds or removes one, without applying them; the panel menu's Select All Unused (`charStyle.unused`, `paraStyle.unused`) highlights the styles no type uses (in the layers, opacity masks, symbols, graph designs, pattern tiles and the art of brushes), never the Normal style; Delete, the button or the menu item, deletes every highlighted style but Normal in one undo step (`charStyle.delete {names}`, `paraStyle.delete {names}`). The menu's Duplicate copies every highlighted style in one undo step and highlights the copies. A highlighted row dragged onto Create New Style or Delete carries every highlighted style: dropped on Create New Style it copies them all and highlights the copies, and dropped on Delete it deletes them all but Normal and clears the highlight; a row from outside the highlight carries only its own style. Public reference: [Character and paragraph styles](https://web.archive.org/web/20261015081534/https://helpx.adobe.com/illustrator/using/character-paragraph-styles.html). - **Load text styles from another document:** the Character Styles and Paragraph Styles panel menus have Load Character Styles… or Load Paragraph Styles… and Load All Styles…, which copy the styles of a document picked in an open dialog, with the swatches and patterns they paint with, in one undo step (`charStyle.load`, `paraStyle.load`, `textStyle.loadAll`); a style the document already has, under its name or a numbered one, is not added again, any other whose name is taken gets a number, and the file's Normal styles are not loaded. Public reference: [Character and paragraph styles](https://web.archive.org/web/20261015081534/https://helpx.adobe.com/illustrator/using/character-paragraph-styles.html). @@ -1936,6 +1936,7 @@ Edit › Copy and Cut put the selected ruler guides on the clipboard, with the s | Date | Change | Summary | |---|---|---| +| 2026-11-10 | minor | Bundled content: built-in Game HUD graphic style library; style libraries evidence updated; ~34% → ~36%; more built-in graphic-style libraries remain. | | 2026-11-10 | minor | Bundled content: built-in Map Markers symbol library (Pin, Flag, Crosshair, Checkpoint); symbol libraries evidence updated; ~32% → ~34%; more built-in symbol libraries remain. | | 2026-11-10 | minor | Type advanced: Spelling › Auto Spell Check in the context menu of type, checked while it is on, #4936; evidence added; the dialogs remain; scores and hours unchanged. | | 2026-11-10 | minor | Type advanced: right-click suggestions, Ignore All and Add to Dictionary on a word Auto Spell Check underlined, #4936; evidence added; the dialogs remain; scores and hours unchanged. | From df6065b5b4ebe6a27f1dcb8a11b748c081cf48c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabio=20William=20Concei=C3=A7=C3=A3o?= Date: Tue, 10 Nov 2026 12:04:40 +0000 Subject: [PATCH 28/38] G5: assert Mana Glow appearance after addFromLibrary --- crates/engine/src/tests/stylelib.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/engine/src/tests/stylelib.rs b/crates/engine/src/tests/stylelib.rs index 3b396f663..43838ff8e 100644 --- a/crates/engine/src/tests/stylelib.rs +++ b/crates/engine/src/tests/stylelib.rs @@ -63,6 +63,10 @@ fn builtin_libraries_are_listed_with_original_styles_using_catalogued_effects() let id = rect(&mut s, 10.0, 20.0, 40.0, 30.0); let r = run(&mut s, "graphicStyle.addFromLibrary", json!({"library": "game-hud", "name": "Mana Glow", "apply": true, "ids": [id.0]})); assert_eq!((names(&r["added"]), r["applied"].as_str()), (vec!["Mana Glow"], Some("Mana Glow"))); + let node = doc(&s).node(id).unwrap(); + assert_eq!(node.appearance.fill_paint(), Paint::solid(vectorcraft_color::Color::from_hex("#3498db").unwrap())); + assert_eq!(node.appearance.stroke_paint(), Paint::None); + assert_eq!(node.appearance.effects.iter().map(|e| e.id.as_str()).collect::>(), ["stylize.outerGlow"]); // By name in any case. assert_eq!(run(&mut s, "graphicStyle.library", json!({"library": "shadows and glows"}))["id"], "shadows-glows"); assert!(s.execute("graphicStyle.library", &json!({"library": "nope"})).is_err()); From 34a3734798b87822dc0da36e448755ee0df3c358 Mon Sep 17 00:00:00 2001 From: ruchirguitar <113586074+ruchirguitar@users.noreply.github.com> Date: Tue, 10 Nov 2026 21:30:52 +0530 Subject: [PATCH 29/38] Docs: tidy stray notes at the top of ROADMAP.md Move the unheaded change bullets above the Stage line and the revision row inside the Headline numbers table into Revision history, each under the date of the commit that added it. No score or estimate changes. Co-Authored-By: Claude Opus 5.5 --- ROADMAP.md | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index e83a01efe..b6b792cb3 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,18 +1,5 @@ # VectorCraft Roadmap -- Contour add-on (beyond Illustrator): drag a selected shape's edge outward or inward for a live, editable offset copy, with a floating bar for distance and Sharp, Round or Square corners; Bake turns it into paths. Scores and estimates unchanged. -- Compound Shapes add-on (beyond Illustrator): a menu for Make, Release and Expand Compound Shape and a setting, off by default, that makes the Pathfinder panel's Shape Mode buttons create compound shapes on a click. Scores and estimates unchanged. - -- Paint: new gradients use the document's color model for default stops, including Gradient tool creation. Scores and estimates unchanged. -- UI: Window → Help Bar toggles the tool-help strip separately from the status bar and saves visibility with workspaces. Scores and estimates unchanged. - -- PDF.R1: editable PDF import uses the shared reader, scans page content in windows, and rejects known incomplete input before returning a document. -- PDF.W1: PDF export and print composition use the shared optional writer for configuration, metadata, font preparation and guarded finalization; drawing and output policy remain app-owned. - -- UI.5: Shared docking adds tab reordering, docking into other groups, dock splits and resizable floating panel groups. Panel positions and return locations persist with workspaces; the native icon rail and Tools panel remain available. - -- UI.4: Dock and floating-panel headers use shared accessible tabs with bounded overflow. Widget-state theme application uses `craft-ui`; all four palettes, font stacks and preferences remain app-owned. - **Stage: alpha** · next: beta, ~13 points (ready for real work ~62% → ~75%, and reliable `.ai` exchange) and ~170–270 h away > **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-10 · **Change:** minor (Raster effects 85% → 88%, gallery thumbnails and in-dialog preview in; headline numbers unchanged; Symbols area ~87% → ~88%: dynamic instance parts take opacity, blend, gradient and pattern on the canvas; G5 docs sync for Overlap and Pattern auto corners; previously minor: Tools stays beside the canvas with left-side panels; Hindi catalog draft complete under the translation tests; contributor loanword terminology reviewed; wider proofreading welcome; Compound Shapes add-on, beyond Illustrator; scores and estimates unchanged; Contour add-on, beyond Illustrator; scores and estimates unchanged; new gradients follow document color mode; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x) @@ -35,7 +22,6 @@ blocking it ([Alpha gate](docs/roadmap.md#alpha-gate)). | | Value | Kind | |---|---|---| -| 2026-11-03 | minor | Docked Tools stays between left-side panels and the canvas; redocking accepts canvas and window edges. Requested layout choice; scores and estimates unchanged. | | **Feature breadth** (Illustrator's menu items, tools, effects, formats exist) | **~88%** | measured in part: menu items 348/375 wired (92.8%), tools 89/92, Illustrator effects 46/50, Photoshop effects 57/57, 3D 4/4 (initial types, no materials) | | **Feature depth** (weighted by use, scored by behaviour) | **~75%** | estimated | | **To beta** | **~170–270 h** one agent · ~50–80 h with 4–6 agents | estimated | @@ -225,7 +211,13 @@ The full earlier record of what landed is kept in [docs/roadmap.md](docs/roadmap | Date | Change | Summary | |---|---|---| +| 2026-11-10 | minor | Tidy-up, no content change: the unheaded change bullets at the top of the file and a revision row inside the headline-numbers table move to this table under their commit dates. Scores and estimates unchanged. | | 2026-11-10 | minor | Raster effects: Effect Gallery thumbnails and an in-dialog preview (an original drawing through each filter). Features-table Raster effects 85% → 88%, hours 1–2 → none. Headline numbers unchanged, so the progress log is unchanged. | +| 2026-11-05 | minor | Compound Shapes add-on (beyond Illustrator): a menu for Make, Release and Expand Compound Shape and a setting, off by default, that makes the Pathfinder panel's Shape Mode buttons create compound shapes on a click. Scores and estimates unchanged. | +| 2026-11-04 | minor | Contour add-on (beyond Illustrator): drag a selected shape's edge outward or inward for a live, editable offset copy, with a floating bar for distance and Sharp, Round or Square corners; Bake turns it into paths. Scores and estimates unchanged. | +| 2026-11-03 | minor | Paint: new gradients use the document's color model for default stops, including Gradient tool creation. Scores and estimates unchanged. | +| 2026-11-03 | minor | UI: Window → Help Bar toggles the tool-help strip separately from the status bar and saves visibility with workspaces. Scores and estimates unchanged. | +| 2026-11-03 | minor | Docked Tools stays between left-side panels and the canvas; redocking accepts canvas and window edges. Requested layout choice; scores and estimates unchanged. | | 2026-11-02 | minor | Selection QoL: saved marquee modes, screen drag direction with a 3 px dead zone, and translated mode/E feedback (#5313); VectorCraft addition, scores and estimates unchanged. | | 2026-11-06 | minor | Hindi (`hi`): 7,877-row machine-assisted catalog, zero/one singular plurals, desktop font fallback and optional Devanagari craft-fonts fallback; all 55 translation tests pass; native review and a web font remain; scores and estimates unchanged | | 2026-11-05 | minor | Progress log row for the Type Tools add-on's Release Type on a Path (#5532); headline numbers unchanged. | @@ -262,10 +254,14 @@ The full earlier record of what landed is kept in [docs/roadmap.md](docs/roadmap | 2026-10-13 | minor | Bounded synthetic control input (#1709): counts and queued events validated before mutation; control-channel bursts yield between frames. Scores and hours unchanged. | | 2026-10-14 | minor | Progress log row for the merges of 2026-10-14 (#1641–#1808 as listed there) and Image Trace's #1647 and #1657; the 2026-10-13 row now says that a guide's position in the Transform panel (#1659) is not established by the documentation found and is kept as a convenience pending the owner's decision; headline numbers unchanged. | | 2026-10-13 | minor | Control transport deadlines (#1726): idle and partial clients and blocked replies release connection slots; parity scores and estimates unchanged. | +| 2026-10-13 | minor | PDF.R1: editable PDF import uses the shared reader, scans page content in windows, and rejects known incomplete input before returning a document. | +| 2026-10-13 | minor | PDF.W1: PDF export and print composition use the shared optional writer for configuration, metadata, font preparation and guarded finalization; drawing and output policy remain app-owned. | | 2026-10-10 | minor | Method changed, no new evidence: full ready for real work is the additive weighted sum, ~62%; hours per audience; beta ~13 points away | | 2026-10-10 | minor | Added the essentials-user readiness score (~75%) | | 2026-10-10 | minor | Ready for real work re-examined against the issue tracker and user feedback: 50% → 55% (full), 65% mainstream added; see target-app-parity.md | | 2026-10-10 | minor | Applied the core-workflow gate (docs/roadmap.md › Alpha gate): passes, stage stays alpha | | 2026-10-10 | major | Re-measured against Illustrator 2026 (30.x) from code counts, 71 open issues and public docs; stage set to alpha; moved the parity estimate, honest assessment and "Shipped so far" to `docs/target-app-parity.md`, the gap list to `docs/gaps.md`, milestones to `docs/roadmap.md`, the `.ai` scope to `docs/file-format-parity.md` | +| 2026-10-10 | minor | UI.5: Shared docking adds tab reordering, docking into other groups, dock splits and resizable floating panel groups. Panel positions and return locations persist with workspaces; the native icon rail and Tools panel remain available. | +| 2026-10-10 | minor | UI.4: Dock and floating-panel headers use shared accessible tabs with bounded overflow. Widget-state theme application uses `craft-ui`; all four palettes, font stacks and preferences remain app-owned. | | 2026-10-05 | major | Honest assessment by dimension and the prioritized gap list | | 2026-10-02 | major | First 22-area weighted parity estimate | From d144014cee5b36e3e611979238ce624f64ce57e7 Mon Sep 17 00:00:00 2001 From: ruchirguitar <113586074+ruchirguitar@users.noreply.github.com> Date: Tue, 10 Nov 2026 21:31:05 +0530 Subject: [PATCH 30/38] Docs: tidy stray notes at the top of docs/gaps.md Move a stray table row and the PR #3273/#3272 revision notes into Feature revisions, keep one "Feature evidence last updated" line, drop the duplicate Flyout update status line and fold eight reconciliation rows into one per date. No score or estimate changes. Co-Authored-By: Claude Opus 5.5 --- docs/gaps.md | 28 ++++++++-------------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/docs/gaps.md b/docs/gaps.md index f0a231a34..2b58ed84f 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -2,22 +2,16 @@ > **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-10 · **Change:** minor (G5: built-in Game HUD graphic style library; estimate 10–20 → 9–19 h; previously minor: G5: built-in Map Markers symbol library; estimate 11–21 → 10–20 h; previously minor: right-click on type: Spelling › Auto Spell Check, with its check mark, #4936; previously minor: right-click on a word Auto Spell Check underlined: its suggestions, Ignore All and Add to Dictionary, #4936; previously minor: G2: Effect Gallery thumbnails and in-dialog preview; estimate 1–2 h → 0; Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: PDF editable import preserves authored ActualText once, including empty/nested spans and authored ordering (#6165); cross-line/style layout remains approximate; scores and estimates unchanged; PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; G5: built-in UI Frames symbol library with default 9-slice; estimate 11–21 h; previously minor: G5: dynamic symbol part on-canvas takes opacity, blend, gradient and pattern; docs Overlap/auto-corners sync; estimate 12–22 h; previously minor: interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; previously minor: Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; previously minor: G5 docs: Art brush Overlap, Pattern brush auto corners and Global Edit Shift-click leave-out moved from Missing to Done (code already shipped); previously minor: custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: EPS import: embedded fonts (composite, Type 42, CID-keyed, Type 1, CFF FontSet) draw their glyphs; spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; G9: EPS gradients drawn as abutting strips import as one gradient, without seams; Tools stays beside the canvas with left-side panels; new gradients follow document color mode; Selection QoL: marquee modes and visible feedback, #5313; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; Indian-language hyphenation review and license allowlist, #4947; opening a slow file shows a progress window, part of #5930; Hindi catalog and contributor loanword preferences; agent wording pass complete; wider native proofreading welcome; an image zoomed in on is separated, colorized or proofed only where it shows (#4155); scores and estimates unchanged; a document's first frame renders on the render worker, so opening a heavy file no longer freezes the window; scores and estimates unchanged; large images in CMYK documents are shrunk before they are separated into ink planes (#4155); scores and estimates unchanged; `.ai` paths with a Calligraphic brush open brushed, with the 6D Art Pen's channels, and Windows reads the pen's tilt and barrel rotation, #372; scores and estimates unchanged; Compound Shapes add-on, beyond Illustrator; scores and estimates unchanged; Contour add-on, beyond Illustrator; scores and estimates unchanged; a slow first frame is waited for instead of restarting without the graphics adapter, and the menu bar no longer waits for the installed fonts' scan; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x) -> **Flyout update:** 2026-11-06 — resizable icon-strip flyouts and bottom-aligned panel footers (#5713); scores and estimates unchanged. - ## Feature submission evidence -**Feature evidence last updated:** 2026-11-02. -| 2026-11-06 | minor | Opening a file that takes longer than 0.4 s shows a progress window: the file name, a moving bar (reading reports no steps), how many more files wait, Cancel (Esc; the file doesn't open) and Continue in Background (`file.cancelOpen`, `file.openInBackground`); it never opens over another dialog or progress window (part of #5930); scores and estimates unchanged | +**Feature evidence last updated:** 2026-11-03. + Raster rendering fits and preflattens curved stroke outlines more tightly only below four times the raster tolerance, retaining thin-band coverage without changing stroke weight or vector export geometry. Current cached character/object outlines, cropped long strokes, gradient clips and effect rasters share the helper; cropped-region reuse and guarded flattening remain. The unchanged `fractional_curved_strokes_retain_coverage_between_notches` regression covers fractional weights, rotation, phase, opacity and f32-rounded endpoints. This is a quality/cost tradeoff, not a speed claim. Scores and estimates unchanged. Icon-strip flyouts resize from the canvas-side edge and bottom, keeping the icon-column edge anchored (#5713). Independent group heights and flyout widths persist in workspaces; saved heights override content fitting while untouched heights still fit content. Shared panel button bars and the Glyphs font row stay at the bottom after enlargement; the Artboards list grows into the extra space. Regressions: `panel_docking::tests::glyphs_and_artboards_flyouts_keep_user_sizes` and `expanded_panel_footers_stay_at_the_bottom_with_short_and_long_lists`. Scores and estimates unchanged. -**PR #3273 revision — 2026-11-04:** Thin curved stroke coverage ported to the current raster-only cache, cropped-long-stroke and effect paths with guarded flattening; vector export geometry unchanged. Scores and estimates unchanged. - Group Selection keeps an already selected component selected during a drag; a completed click still adds its next enclosing group. The current Direct Selection click/release machinery is shared, preserving compound-member picks, Shift deselection, isolation and masked-art dragging. Outline Stroke continues to preserve all original fills, including open contours; the change does not remove fills. Regressions: `group_selection_distinguishes_selected_leaf_drags_from_clicks` and `outline_fill_and_stroke_move_independently_with_group_selection` cover tool events, independent component movement and Undo/Redo. Scores and estimates unchanged. -**PR #3272 revision — 2026-11-04:** Group Selection component drags defer ancestor selection until a completed click, using current Direct Selection release handling; original Outline Stroke fills and newer selection behavior retained. Scores and estimates unchanged. - Creating a gradient from solid or empty paint uses the document's color model for its default stops, including drawing its vector with the Gradient tool. Explicit `keepModel: true` retains RGB defaults, and existing gradients keep their own colors. Scores and estimates are unchanged. Window → Help Bar toggles the bottom tool-help strip independently of the status bar. Its visibility is saved with workspaces and UI preferences; Presentation Mode hides both bars. Scores and estimates are unchanged. @@ -28,12 +22,8 @@ adds one undo step; the list follows saved font filters, ignores search text, an stops at its ends. These interaction details are VectorCraft's choices. Scores and estimates are unchanged. -**Feature evidence last updated:** 2026-11-02. -**Feature evidence last updated:** 2026-11-03. - The shared assessment status line and revision history are current upstream; this dated section records the submission's feature evidence and revisions without rescoring upstream or claiming a new combined build or runtime measurement. -- Current-main conflict reconciliation retains upstream and feature behavior; scores and estimates unchanged - Superseded 2026-10-20 behavior, retained as history: Artboard Options shares existing presets and orientation controls and can fit artwork without moving it; artwork fitting uses the original target artboard and revision-scoped bounds caches - Artboard Options puts Fit to Artwork Bounds and Fit to Selected Art in the Preset menu. Artwork bounds cover all drawn document art, including other artboards and off-board art; selection bounds may span artboards. Fit changes only the active artboard and keeps artwork stationary, including position-only fitting with Move Artwork enabled. Existing Background Color and presets remain available; scores and estimates unchanged. @@ -47,6 +37,9 @@ Artboard Options forwards the active Artboard tool's Scale Artwork with Artboard | 2026-11-08 | minor | #3380, deletion only: preserve gradient placement and exact surviving geometry; consume owned subpaths with a shared original-node snapshot; consolidate status/evidence and document command/helper contracts. Compound behavior and the existing M2 status entry are retained. Endpoint highlighting remains open; scores and estimates unchanged. | | 2026-11-07 | minor | #3380 (deletion only): independent surviving path objects; transformed appearance, stacking, selection, cubic handles and Undo/Redo covered by `delete_path_pieces`; compound semantics retained. Endpoint highlighting remains open; scores and estimates unchanged. | | 2026-11-06 | minor | G9: EPS import reads gradients a page paints as abutting strips as one linear gradient each (no seams); an Illustrator EPS whose page drew them that way keeps its layers. Estimate unchanged. | +| 2026-11-06 | minor | Opening a file that takes longer than 0.4 s shows a progress window: the file name, a moving bar (reading reports no steps), how many more files wait, Cancel (Esc; the file doesn't open) and Continue in Background (`file.cancelOpen`, `file.openInBackground`); it never opens over another dialog or progress window (part of #5930); scores and estimates unchanged | +| 2026-11-04 | minor | PR #3273: Thin curved stroke coverage ported to the current raster-only cache, cropped-long-stroke and effect paths with guarded flattening; vector export geometry unchanged. Scores and estimates unchanged. | +| 2026-11-04 | minor | PR #3272: Group Selection component drags defer ancestor selection until a completed click, using current Direct Selection release handling; original Outline Stroke fills and newer selection behavior retained. Scores and estimates unchanged. | | 2026-11-03 | minor | Docked Tools stays between left-side panels and the canvas; redocking accepts canvas and window edges. Requested layout choice; scores and estimates unchanged. | | 2026-11-02 | minor | Selection QoL: saved marquee modes, screen drag direction with a 3 px dead zone, and translated mode/E feedback (#5313); VectorCraft addition, scores and estimates unchanged. | | 2026-11-07 | minor | Indian-language hyphenation (#4947): eleven MIT-licensed pattern sets, with grapheme and shaping-cluster protection in both composers and existing paragraph limits. Assamese and Sanskrit are selectable; Sanskrit automatic patterns remain pending an allowlisted source. Scores and estimates unchanged. | @@ -65,17 +58,11 @@ Artboard Options forwards the active Artboard tool's Scale Artwork with Artboard | 2026-11-03 | minor | Help Bar has its own Window menu toggle and workspace visibility setting. Scores and estimates unchanged. | | 2026-11-02 | minor | Font-family fields step selected text with the mouse wheel (#5253); no scored gap or estimate reassessed. | | 2026-10-24 | minor | G12: General › Corner Radius is the Rounded Rectangle tool's radius, a change while the tool is active reaches it, and the radius ↑/↓ set while drawing (or a Tool Options value) becomes the preference, within 0–1296 pt (#394, `crates/engine/src/tests/constrain_tools.rs`); no entry closed, estimate unchanged. | -| 2026-10-22 | minor | Reconcile upstream documentation after review F-3; preserve every dated revision and the original scaleArt runtime evidence without repeating completed jobs. | -| 2026-10-22 | minor | Reconcile upstream documentation after review F-2; preserve every dated revision and the original scaleArt runtime evidence without repeating completed jobs. | -| 2026-10-22 | minor | Reconcile subsequent upstream progress updates without changing the runtime or repeating completed evidence; retain every dated upstream and feature revision. | -| 2026-10-22 | minor | Resynchronize newer upstream progress documentation using the same owned source/build cache; preserve every feature revision and the original scaleArt runtime evidence. | -| 2026-10-22 | minor | Retain the latest upstream shared assessment and complete revision history after newer documentation landed; scaleArt runtime and its original evidence remain unchanged. | -| 2026-10-22 | minor | Correct the scaleArt follow-up evidence description to match current upstream shared status and revision history; runtime, evidence, scores and estimates unchanged. | +| 2026-10-22 | minor | Documentation reconciled with newer upstream progress docs in six passes (after reviews F-2 and F-3), including a corrected description of the scaleArt follow-up evidence; every dated revision and the original scaleArt runtime evidence kept; runtime, scores and estimates unchanged. | | 2026-10-22 | minor | Artboard Options honors Scale Artwork with Artboard for preset, orientation and typed-size changes; both Fits keep artwork stationary; scores and estimates unchanged. | | 2026-10-21 | minor | Correct current global-all-art and document/revision cache descriptions; mark target-scoped fitting as superseded 2026-10-20 history. Runtime, tests, scores and estimates unchanged. | | 2026-10-21 | minor | Artboard Options puts Fit to Artwork Bounds and Fit to Selected Art in the Preset menu. Artwork bounds cover all drawn document art, including other artboards and off-board art; selection bounds may span artboards. Fit changes only the active artboard and keeps artwork stationary, including position-only fitting with Move Artwork enabled. Existing Background Color and presets remain available; scores and estimates unchanged. | -| 2026-10-21 | minor | Reconcile feature status and revision evidence in a dated section; retain shared baseline metadata for independent upstream updates. Scores and estimates are not reassessed. | -| 2026-10-21 | minor | Reconcile this feature with current main, preserving both sets of behavior and progress evidence; scores and estimates unchanged. | +| 2026-10-21 | minor | Feature status and revision evidence reconciled with current main in a dated section, keeping both sets of behaviour and progress evidence and the shared baseline metadata for independent upstream updates; scores and estimates unchanged. | | 2026-10-20 | minor | Review follow-up: artwork fitting uses the original target artboard and revision-scoped bounds caches; focused regression evidence and behavior documentation updated; scores and hours unchanged. | | 2026-10-20 | minor | Artboard Options shares existing presets and orientation controls and can fit artwork without moving it; scores and hours unchanged. | @@ -863,6 +850,7 @@ trap, and the defaults (0.25 pt, 100%, 40%) are VectorCraft's own, the pages giv | Date | Change | Summary | |---|---|---| +| 2026-11-10 | minor | Tidy-up, no content change: in Feature submission evidence, a stray table row and two "PR #NNNN revision" notes move into Feature revisions, the three "Feature evidence last updated" lines become one, the duplicate Flyout update note goes (its row stays), and eight reconciliation rows become two. Scores and estimates unchanged. | | 2026-11-10 | minor | G5: built-in Game HUD graphic style library (Health Fill, Mana Glow, Damage Flash, Quest Outline, Coin Shine, Boss Alert); Missing trimmed; estimate 10–20 → 9–19 h. | | 2026-11-10 | minor | G5: built-in Map Markers symbol library (Pin, Flag, Crosshair, Checkpoint; no default 9-slice); Missing trimmed; estimate 11–21 → 10–20 h. | | 2026-11-10 | minor | G4 type: a right-click on type has Spelling › Auto Spell Check, checked while it is on, #4936; the Check Spelling and Edit Custom Dictionary dialogs remain; scores and estimates unchanged. | From 466da0f8c8aeafb91771015f1b0d3a8fef1941bb Mon Sep 17 00:00:00 2001 From: ruchirguitar <113586074+ruchirguitar@users.noreply.github.com> Date: Tue, 10 Nov 2026 21:55:33 +0530 Subject: [PATCH 31/38] Docs: record the ROADMAP.md tidy-up in its status line Co-Authored-By: Claude Opus 5.5 --- ROADMAP.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ROADMAP.md b/ROADMAP.md index b6b792cb3..327036c37 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,7 +2,7 @@ **Stage: alpha** · next: beta, ~13 points (ready for real work ~62% → ~75%, and reliable `.ai` exchange) and ~170–270 h away -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-10 · **Change:** minor (Raster effects 85% → 88%, gallery thumbnails and in-dialog preview in; headline numbers unchanged; Symbols area ~87% → ~88%: dynamic instance parts take opacity, blend, gradient and pattern on the canvas; G5 docs sync for Overlap and Pattern auto corners; previously minor: Tools stays beside the canvas with left-side panels; Hindi catalog draft complete under the translation tests; contributor loanword terminology reviewed; wider proofreading welcome; Compound Shapes add-on, beyond Illustrator; scores and estimates unchanged; Contour add-on, beyond Illustrator; scores and estimates unchanged; new gradients follow document color mode; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-10 · **Change:** minor (docs tidy-up, no content change: stray notes at the top moved into the revision table; scores and estimates unchanged; previously minor: Raster effects 85% → 88%, gallery thumbnails and in-dialog preview in; headline numbers unchanged; Symbols area ~87% → ~88%: dynamic instance parts take opacity, blend, gradient and pattern on the canvas; G5 docs sync for Overlap and Pattern auto corners; previously minor: Tools stays beside the canvas with left-side panels; Hindi catalog draft complete under the translation tests; contributor loanword terminology reviewed; wider proofreading welcome; Compound Shapes add-on, beyond Illustrator; scores and estimates unchanged; Contour add-on, beyond Illustrator; scores and estimates unchanged; new gradients follow document color mode; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x) VectorCraft is a clean-room, open-source, pure-Rust reimplementation of the Adobe Illustrator workflow. It runs on macOS, Windows, Linux, FreeBSD and the web (WASM), and agents can drive all of it over MCP, the CLI and a JSON From 223cb3c81456d6b5a9fae69b19825178911417c5 Mon Sep 17 00:00:00 2001 From: ruchirguitar <113586074+ruchirguitar@users.noreply.github.com> Date: Tue, 10 Nov 2026 21:55:33 +0530 Subject: [PATCH 32/38] Docs: record the docs/gaps.md tidy-up in its status line Co-Authored-By: Claude Opus 5.5 --- docs/gaps.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/gaps.md b/docs/gaps.md index 2b58ed84f..c51db208d 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -1,6 +1,6 @@ # Gaps -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-10 · **Change:** minor (G5: built-in Game HUD graphic style library; estimate 10–20 → 9–19 h; previously minor: G5: built-in Map Markers symbol library; estimate 11–21 → 10–20 h; previously minor: right-click on type: Spelling › Auto Spell Check, with its check mark, #4936; previously minor: right-click on a word Auto Spell Check underlined: its suggestions, Ignore All and Add to Dictionary, #4936; previously minor: G2: Effect Gallery thumbnails and in-dialog preview; estimate 1–2 h → 0; Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: PDF editable import preserves authored ActualText once, including empty/nested spans and authored ordering (#6165); cross-line/style layout remains approximate; scores and estimates unchanged; PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; G5: built-in UI Frames symbol library with default 9-slice; estimate 11–21 h; previously minor: G5: dynamic symbol part on-canvas takes opacity, blend, gradient and pattern; docs Overlap/auto-corners sync; estimate 12–22 h; previously minor: interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; previously minor: Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; previously minor: G5 docs: Art brush Overlap, Pattern brush auto corners and Global Edit Shift-click leave-out moved from Missing to Done (code already shipped); previously minor: custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: EPS import: embedded fonts (composite, Type 42, CID-keyed, Type 1, CFF FontSet) draw their glyphs; spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; G9: EPS gradients drawn as abutting strips import as one gradient, without seams; Tools stays beside the canvas with left-side panels; new gradients follow document color mode; Selection QoL: marquee modes and visible feedback, #5313; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; Indian-language hyphenation review and license allowlist, #4947; opening a slow file shows a progress window, part of #5930; Hindi catalog and contributor loanword preferences; agent wording pass complete; wider native proofreading welcome; an image zoomed in on is separated, colorized or proofed only where it shows (#4155); scores and estimates unchanged; a document's first frame renders on the render worker, so opening a heavy file no longer freezes the window; scores and estimates unchanged; large images in CMYK documents are shrunk before they are separated into ink planes (#4155); scores and estimates unchanged; `.ai` paths with a Calligraphic brush open brushed, with the 6D Art Pen's channels, and Windows reads the pen's tilt and barrel rotation, #372; scores and estimates unchanged; Compound Shapes add-on, beyond Illustrator; scores and estimates unchanged; Contour add-on, beyond Illustrator; scores and estimates unchanged; a slow first frame is waited for instead of restarting without the graphics adapter, and the menu bar no longer waits for the installed fonts' scan; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-10 · **Change:** minor (docs tidy-up, no content change: stray notes at the top moved into the revision table; scores and estimates unchanged; previously minor: G5: built-in Game HUD graphic style library; estimate 10–20 → 9–19 h; previously minor: G5: built-in Map Markers symbol library; estimate 11–21 → 10–20 h; previously minor: right-click on type: Spelling › Auto Spell Check, with its check mark, #4936; previously minor: right-click on a word Auto Spell Check underlined: its suggestions, Ignore All and Add to Dictionary, #4936; previously minor: G2: Effect Gallery thumbnails and in-dialog preview; estimate 1–2 h → 0; Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: PDF editable import preserves authored ActualText once, including empty/nested spans and authored ordering (#6165); cross-line/style layout remains approximate; scores and estimates unchanged; PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; G5: built-in UI Frames symbol library with default 9-slice; estimate 11–21 h; previously minor: G5: dynamic symbol part on-canvas takes opacity, blend, gradient and pattern; docs Overlap/auto-corners sync; estimate 12–22 h; previously minor: interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; previously minor: Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; previously minor: G5 docs: Art brush Overlap, Pattern brush auto corners and Global Edit Shift-click leave-out moved from Missing to Done (code already shipped); previously minor: custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: EPS import: embedded fonts (composite, Type 42, CID-keyed, Type 1, CFF FontSet) draw their glyphs; spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; G9: EPS gradients drawn as abutting strips import as one gradient, without seams; Tools stays beside the canvas with left-side panels; new gradients follow document color mode; Selection QoL: marquee modes and visible feedback, #5313; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; Indian-language hyphenation review and license allowlist, #4947; opening a slow file shows a progress window, part of #5930; Hindi catalog and contributor loanword preferences; agent wording pass complete; wider native proofreading welcome; an image zoomed in on is separated, colorized or proofed only where it shows (#4155); scores and estimates unchanged; a document's first frame renders on the render worker, so opening a heavy file no longer freezes the window; scores and estimates unchanged; large images in CMYK documents are shrunk before they are separated into ink planes (#4155); scores and estimates unchanged; `.ai` paths with a Calligraphic brush open brushed, with the 6D Art Pen's channels, and Windows reads the pen's tilt and barrel rotation, #372; scores and estimates unchanged; Compound Shapes add-on, beyond Illustrator; scores and estimates unchanged; Contour add-on, beyond Illustrator; scores and estimates unchanged; a slow first frame is waited for instead of restarting without the graphics adapter, and the menu bar no longer waits for the installed fonts' scan; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x) ## Feature submission evidence From 47b20b436698fc04536921617a7f189bfd406ac3 Mon Sep 17 00:00:00 2001 From: jmverges Date: Tue, 10 Nov 2026 04:24:12 +0000 Subject: [PATCH 33/38] .ai import: envelopes of type open live where they draw as their drawn art An envelope whose contents hold type kept its drawn art, with the type hidden, because the type is laid out from the text document only after the layers are read. It now waits for that, as warped type does: once its text slots hold their type, the envelope is made of that type and drawn by VectorCraft's own rule, and where it draws as the drawn art the file saves (the same check and budget as other envelopes) it takes the drawn art's place, with the type visible and editable in Edit Contents. The hidden copy of the type goes. As other live envelopes are, it is held to the page: when the layers with it don't draw as the page, its drawn art and hidden type are put back. A .ai whose layers would open alone, without its PDF part, now waits on the PDF part when an envelope of type is in them; where none opens live, the layers open alone as before. Layers built without a page leave envelopes of type their drawn art. Elsewhere it keeps its drawn art and hidden type, with the note: type in a font that isn't available, a slot without made type (threaded, bound to a text variable, a wrap object, type a look draws), an envelope that doesn't read, no check left, or one that draws differently. Envelopes in a symbol's or a pattern's art keep their drawn art. Part of #3179. Co-Authored-By: Claude Opus 5.5 --- crates/eps/src/import/ai.rs | 1 - crates/eps/src/import/ai/envelope.rs | 178 +++++++++++++- crates/eps/src/import/ai/read.rs | 4 + crates/eps/src/import/ai/read/containers.rs | 55 ++++- crates/eps/src/import/ai/read/document.rs | 11 +- crates/eps/src/import/ai/read/types.rs | 3 + crates/eps/src/import/ai/tests.rs | 2 +- crates/eps/src/import/native/build.rs | 66 +++-- crates/eps/src/import/native/layers.rs | 49 +++- crates/eps/src/import/tests_native.rs | 253 ++++++++++++++++++++ docs/file-format-parity.md | 4 +- docs/target-app-parity.md | 2 +- 12 files changed, 579 insertions(+), 49 deletions(-) diff --git a/crates/eps/src/import/ai.rs b/crates/eps/src/import/ai.rs index 5f4a5b631..be2d679a7 100644 --- a/crates/eps/src/import/ai.rs +++ b/crates/eps/src/import/ai.rs @@ -37,7 +37,6 @@ use crate::ps; pub(crate) use envelope::DRAWN as ENVELOPE_DRAWN; #[cfg(test)] pub(crate) use envelope::MAX_CHECKED; -#[cfg(test)] pub(crate) use envelope::TYPE_DRAWN as ENVELOPE_TYPE_DRAWN; pub(crate) use envelope::{WARPED_TYPE_DRAWN, WarpedType}; pub(crate) use frames::text_frames; diff --git a/crates/eps/src/import/ai/envelope.rs b/crates/eps/src/import/ai/envelope.rs index 3368a97be..04b859946 100644 --- a/crates/eps/src/import/ai/envelope.rs +++ b/crates/eps/src/import/ai/envelope.rs @@ -23,7 +23,7 @@ //! one-patch mesh. An envelope whose parts don't read, or that doesn't come out as drawn, keeps its //! drawn art. -use std::collections::{BTreeMap, BTreeSet}; +use std::collections::{BTreeMap, BTreeSet, HashMap}; use std::sync::Arc; use kurbo::{Affine, BezPath, Point, Rect, Vec2}; @@ -46,6 +46,12 @@ pub(crate) const DRAWN: &str = "an envelope VectorCraft couldn't read as a live pub(crate) const TYPE_DRAWN: &str = "type in an envelope comes in as the envelope's drawn art, not as editable type"; /// The note for warped type that keeps its drawn art, before how many. pub(crate) const WARPED_TYPE_DRAWN: &str = "warped text objects that came in as drawn art, not as editable type"; + +/// The note for an envelope that keeps its drawn art: [`TYPE_DRAWN`] for an envelope of type +/// (`typed`), else [`DRAWN`]. +pub(crate) fn drawn_note(typed: bool) -> &'static str { + if typed { TYPE_DRAWN } else { DRAWN } +} /// The most envelopes waiting for their drawn art at once. pub(super) const MAX_PENDING: usize = 64; /// The most envelopes and warped text objects one import draws to check; more keep their drawn art. @@ -371,18 +377,157 @@ pub(super) struct WarpedLook { pub(super) left_out: Option, } -/// The text objects read with a Warp, and what the import has left of the budget for checks -/// ([`drawn_alike`]). Type is laid out after the layers are read, so these wait for that -/// ([`Self::settle`]). +/// An envelope of type read with its drawn art: its dictionary, the group on `%_` lines before it +/// and the art in its `AIDeformArtKey` block, as [`live`] takes them (their text slots still +/// empty), and the id of its drawn art in the document. Type is laid out after the layers are read, +/// so it waits for that ([`WarpedType::settle_envelopes`]). +#[derive(Debug)] +pub(crate) struct TypedEnvelope { + pub(super) obj: Obj, + pub(super) before: Option, + pub(super) inner: Vec, + pub(super) drawn: NodeId, +} + +/// Why an envelope of type kept its drawn art. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum TypeKept { + /// Its type is set in a font that isn't available: the fallback font doesn't draw as the file. + Font, + /// A text slot of it has no type of its own (none made, or type a look draws, threaded type, + /// type bound to a variable or a wrap object). + Type, + /// Its parts don't make a live envelope, or its drawn art is gone. + Read, + /// The import has no check left. + Budget, + /// Its live envelope doesn't draw as its drawn art. + Differs, +} + +/// What [`WarpedType::settle_envelopes`] did. +#[derive(Debug, Default)] +pub(crate) struct TypedSettled { + /// How many envelopes of type opened live. + pub(crate) live: usize, + /// Why each of the others kept its drawn art. + pub(crate) kept: Vec, + /// Each live envelope's id, with what takes its place again when the layers with it don't draw + /// as the page: its hidden type, then its drawn art. + pub(crate) put_back: Vec<(NodeId, Vec)>, +} + +/// The text objects read with a Warp and the envelopes of type, and what the import has left of the +/// budget for checks ([`drawn_alike`]). Type is laid out after the layers are read, so these wait +/// for that ([`Self::settle`], [`Self::settle_envelopes`]). #[derive(Debug, Default)] pub struct WarpedType { pub(super) looks: Vec, + pub(super) envelopes: Vec, pub(super) checks: usize, pub(super) spent: Spent, pub(super) page: Option, } impl WarpedType { + /// Are envelopes of type waiting for their type? + pub(crate) fn has_envelopes(&self) -> bool { + !self.envelopes.is_empty() + } + + /// These, for layers `compared` with a page. Layers that aren't leave the envelopes of type + /// their drawn art and their hidden type, with the note ([`TYPE_DRAWN`]) added to `notes`: a + /// live envelope opens only where the page chooses it. + pub(crate) fn compared(mut self, compared: bool, notes: &mut Vec) -> Self { + if !compared && !std::mem::take(&mut self.envelopes).is_empty() && !notes.iter().any(|n| n == TYPE_DRAWN) { + notes.push(TYPE_DRAWN.into()); + } + self + } + + /// Make each envelope of type live where it draws as its drawn art, its contents' text slots + /// holding their type: `type_of` gives, for a slot's id, the hidden text object it became in + /// `doc` and the type the contents take in its place (the slot's visibility and lock are the + /// type's). The live envelope takes its drawn art's place and the hidden type goes. Each check + /// uses one of the import's checks and, for an envelope that shows, adds to what the page check + /// allows ([`drawn_alike`]). Elsewhere, and where the type is set in a font that isn't + /// available, the drawn art and the hidden type stay. + pub(crate) fn settle_envelopes(&mut self, doc: &mut Document, type_of: impl Fn(&Document, NodeId) -> Option<(NodeId, Node)>) -> TypedSettled { + let mut settled = TypedSettled::default(); + if self.envelopes.is_empty() { + return settled; + } + let mut shown = BTreeSet::new(); + doc.layers.iter().for_each(|l| collect_shown(l, &mut shown)); + for envelope in std::mem::take(&mut self.envelopes) { + match self.typed_live(doc, envelope, &type_of, &shown) { + Ok(put_back) => { + settled.live += 1; + settled.put_back.push(put_back); + } + Err(why) => settled.kept.push(why), + } + } + settled + } + + /// [`Self::settle_envelopes`] for `envelope`, the shown objects of `doc` being `shown`: the live + /// envelope's id and what takes its place again, or why its drawn art stays. + fn typed_live( + &mut self, + doc: &mut Document, + envelope: TypedEnvelope, + type_of: &impl Fn(&Document, NodeId) -> Option<(NodeId, Node)>, + shown: &BTreeSet, + ) -> Result<(NodeId, Vec), TypeKept> { + let TypedEnvelope { obj, mut before, mut inner, drawn } = envelope; + let art = doc.node(drawn).cloned().ok_or(TypeKept::Read)?; + // The text slots in the contents, in the order the reader put their hidden copies in. + let mut slots = vec![]; + before.iter().chain(&inner).for_each(|n| text_slots_in(n, &mut slots)); + if slots.is_empty() { + return Err(TypeKept::Type); + } + let db = vectorcraft_text::FontDb::global(); + let (mut texts, mut hidden) = (HashMap::new(), vec![]); + for (slot, look) in slots { + let (id, mut text) = type_of(doc, slot).filter(|_| !look).ok_or(TypeKept::Type)?; + let NodeKind::Text(t) = &text.kind else { return Err(TypeKept::Type) }; + if t.runs.iter().any(|r| r.style.font_family.is_empty() || !db.has_family(&r.style.font_family)) { + return Err(TypeKept::Font); + } + text.id = doc.alloc_id(); + texts.insert(slot, text); + hidden.push(id); + } + for n in before.iter_mut().chain(inner.iter_mut()) { + put_type(n, &texts); + } + let kind = live(&obj, before, inner).ok_or(TypeKept::Read)?; + if self.checks == 0 { + return Err(TypeKept::Budget); + } + self.checks -= 1; + let mut envelope = art.clone(); + (envelope.id, envelope.kind) = (doc.alloc_id(), kind); + let layer = doc.alloc_id(); + let shared = Shared { patterns: &doc.patterns, images: &doc.images }; + let (mut live_one, mut plain) = (envelope.clone(), art.clone()); + (live_one.visible, live_one.opacity) = (true, 1.0); + (plain.visible, plain.opacity) = (true, 1.0); + let page = self.page.filter(|_| shown.contains(&drawn)); + if !drawn_alike(&live_one, &plain, layer, &shared, page, &mut self.spent) { + return Err(TypeKept::Differs); + } + // The type is the envelope's now: its hidden copy goes, kept for when the drawn art comes back. + let mut put_back: Vec = hidden.into_iter().filter_map(|id| doc.remove(id).ok()).map(Arc::unwrap_or_clone).collect(); + let id = envelope.id; + let Some(slot) = doc.node_mut(drawn) else { return Err(TypeKept::Read) }; + *slot = envelope; + put_back.push(art); + Ok((id, put_back)) + } + /// Replace each warped text object of `doc` that doesn't draw as its saved look (or that the /// budget has no check left for) with that look: `made` gives the text object a slot became. A /// frame in `threaded` stays type, since the frames of a thread share one story. A replaced @@ -395,7 +540,7 @@ impl WarpedType { threaded: &BTreeSet, restyled: &mut Vec, ) -> usize { - let Self { looks, mut checks, mut spent, page } = self; + let Self { looks, mut checks, mut spent, page, .. } = self; let mut shown_ids = BTreeSet::new(); doc.layers.iter().for_each(|l| collect_shown(l, &mut shown_ids)); let mut replaced = 0; @@ -439,6 +584,29 @@ impl WarpedType { } } +/// The ids of the text slots in `n` and its groups, in order, each with whether it is a slot of type +/// that a look draws ([`super::is_look_slot`]). +fn text_slots_in(n: &Node, out: &mut Vec<(NodeId, bool)>) { + let look = super::is_look_slot(n.name.as_deref()); + if look || super::slot_of(n.name.as_deref()).is_some() { + out.push((n.id, look)); + } else if !matches!(n.kind, NodeKind::Compound { .. }) { + n.children().into_iter().flatten().for_each(|c| text_slots_in(c, out)); + } +} + +/// `n` with each text slot in it or its groups replaced by its type in `texts`, by the slot's id, +/// shown and locked as the slot is. +fn put_type(n: &mut Node, texts: &HashMap) { + if let Some(text) = texts.get(&n.id) { + let (visible, locked) = (n.visible, n.locked); + *n = text.clone(); + (n.visible, n.locked) = (visible, locked); + } else if !matches!(n.kind, NodeKind::Compound { .. }) { + n.children_mut().into_iter().flatten().for_each(|c| put_type(Arc::make_mut(c), texts)); + } +} + /// Add to `out` the ids of `n` and what is in it, unless hidden. fn collect_shown(n: &Node, out: &mut BTreeSet) { if n.visible { diff --git a/crates/eps/src/import/ai/read.rs b/crates/eps/src/import/ai/read.rs index cb0bc1909..5a253f21a 100644 --- a/crates/eps/src/import/ai/read.rs +++ b/crates/eps/src/import/ai/read.rs @@ -557,6 +557,9 @@ struct Reader<'a> { envelope_spent: super::envelope::Spent, /// The text objects read with a Warp ([`super::envelope::WarpedType`]). warped_type: Vec, + /// The envelopes of type read with their drawn art, settled once the type is laid out + /// ([`super::envelope::TypedEnvelope`]). + typed_envelopes: Vec, /// The live envelopes made so far and the drawn art each took the place of /// ([`Structure::envelopes_drawn`]). envelopes_drawn: Vec<(NodeId, Node)>, @@ -769,6 +772,7 @@ impl<'a> Reader<'a> { envelope_checks: super::envelope::MAX_CHECKED, envelope_spent: super::envelope::Spent::default(), warped_type: Vec::new(), + typed_envelopes: Vec::new(), envelopes_drawn: Vec::new(), clip_fills: HashSet::new(), looks: HashSet::new(), diff --git a/crates/eps/src/import/ai/read/containers.rs b/crates/eps/src/import/ai/read/containers.rs index 3de4daadf..02f6f8097 100644 --- a/crates/eps/src/import/ai/read/containers.rs +++ b/crates/eps/src/import/ai/read/containers.rs @@ -516,17 +516,17 @@ impl<'a> Reader<'a> { slot.visible = false; self.add(slot, false)?; } - // An envelope of type keeps its drawn art and its type, hidden, as above: the - // type is filled in after reading, which the envelope can't wait for. - if typed { - self.warn(crate::import::ai::envelope::TYPE_DRAWN); - return Ok(()); - } - if self.envelopes.len() >= crate::import::ai::envelope::MAX_PENDING { - self.warn(crate::import::ai::envelope::DRAWN); + // An envelope of type keeps its drawn art and its type, hidden, as above, until + // the type is filled in after reading: then it opens live where it draws as its + // drawn art ([`crate::import::ai::envelope::TypedEnvelope`]). In a symbol's or a + // pattern's art it keeps them. + let waits = !typed || (self.symbol.is_none() && self.pattern.is_none()); + if !waits || self.envelopes.len() >= crate::import::ai::envelope::MAX_PENDING { + self.warn(crate::import::ai::envelope::drawn_note(typed)); } else { let siblings = self.frames.last().map_or(0, |f| f.children.len()); - self.envelopes.push(PendingEnvelope { obj: *o, before, inner: children, depth: self.frames.len(), siblings }); + let depth = self.frames.len(); + self.envelopes.push(PendingEnvelope { obj: *o, before, inner: children, depth, siblings, typed }); } } } @@ -863,9 +863,8 @@ impl<'a> Reader<'a> { /// in containers that closed keep their drawn art. pub(super) fn end_envelope(&mut self) { let depth = self.frames.len(); - while self.envelopes.last().is_some_and(|p| p.depth > depth) { - self.envelopes.pop(); - self.warn(crate::import::ai::envelope::DRAWN); + while let Some(p) = self.envelopes.pop_if(|p| p.depth > depth) { + self.warn(crate::import::ai::envelope::drawn_note(p.typed)); } if self.envelopes.last().is_none_or(|p| p.depth != depth) { return; @@ -873,9 +872,13 @@ impl<'a> Reader<'a> { let Some(p) = self.envelopes.pop() else { return }; let drawn = self.frames.last().filter(|f| f.children.len() == p.siblings + 1).and_then(|f| f.children.last()); let Some(drawn) = drawn else { - self.warn(crate::import::ai::envelope::DRAWN); + self.warn(crate::import::ai::envelope::drawn_note(p.typed)); return; }; + if p.typed { + self.wait_for_type(p); + return; + } let live = (self.envelope_checks > 0).then(|| crate::import::ai::envelope::live(&p.obj, p.before, p.inner)).flatten(); let Some(kind) = live else { self.warn(crate::import::ai::envelope::DRAWN); @@ -932,6 +935,32 @@ impl<'a> Reader<'a> { } } +impl Reader<'_> { + /// An envelope of type `p` whose drawn art was just read (the last object of this container): + /// it waits for its type to be laid out ([`crate::import::ai::envelope::TypedEnvelope`]), unless + /// objects the reader couldn't read are in it, or too many already wait. Then it keeps its + /// drawn art and its type, hidden. + fn wait_for_type(&mut self, p: PendingEnvelope) { + let Some(drawn) = self.frames.last().and_then(|f| f.children.last()) else { return }; + let mut ids = HashSet::new(); + fn collect(n: &Node, ids: &mut HashSet) { + ids.insert(n.id); + for c in n.children().into_iter().flatten() { + collect(c, ids); + } + } + collect(drawn, &mut ids); + p.before.iter().chain(&p.inner).for_each(|n| collect(n, &mut ids)); + let unread = self.unread.iter().any(|(id, _)| ids.contains(id)); + if unread || self.typed_envelopes.len() >= crate::import::ai::envelope::MAX_CHECKED { + self.warn(crate::import::ai::envelope::TYPE_DRAWN); + return; + } + let drawn = drawn.id; + self.typed_envelopes.push(crate::import::ai::envelope::TypedEnvelope { obj: p.obj, before: p.before, inner: p.inner, drawn }); + } +} + /// Does `node` hold a text slot? fn has_text_slot(node: &Node) -> bool { let mut slots = vec![]; diff --git a/crates/eps/src/import/ai/read/document.rs b/crates/eps/src/import/ai/read/document.rs index 1b962064c..63e9f3df7 100644 --- a/crates/eps/src/import/ai/read/document.rs +++ b/crates/eps/src/import/ai/read/document.rs @@ -1165,9 +1165,8 @@ impl<'a> Reader<'a> { self.warn(super::types::GRAPH_UNENDED); } // Envelopes whose plugin never closed them keep their drawn art. - if !self.envelopes.is_empty() { - self.envelopes.clear(); - self.warn(crate::import::ai::envelope::DRAWN); + for p in std::mem::take(&mut self.envelopes) { + self.warn(crate::import::ai::envelope::drawn_note(p.typed)); } if !self.unknown.is_empty() { let ops: Vec = self.unknown.iter().map(|o| format!("`{o}`")).collect(); @@ -1297,6 +1296,11 @@ impl<'a> Reader<'a> { for w in &mut self.warped_type { vectorcraft_doc::swatches::map_node_solid_paints(&mut w.look, &mut follow); } + for e in &mut self.typed_envelopes { + for n in e.before.iter_mut().chain(e.inner.iter_mut()) { + vectorcraft_doc::swatches::map_node_solid_paints(n, &mut follow); + } + } } let mut warnings = self.warnings; if let Some((lib, unread)) = brushes { @@ -1341,6 +1345,7 @@ impl<'a> Reader<'a> { envelopes_drawn: self.envelopes_drawn, warped_type: super::super::envelope::WarpedType { looks: self.warped_type, + envelopes: self.typed_envelopes, checks: self.envelope_checks, spent: self.envelope_spent, page, diff --git a/crates/eps/src/import/ai/read/types.rs b/crates/eps/src/import/ai/read/types.rs index 473a07079..6457c1cde 100644 --- a/crates/eps/src/import/ai/read/types.rs +++ b/crates/eps/src/import/ai/read/types.rs @@ -224,6 +224,9 @@ pub(super) struct PendingEnvelope { /// had: its drawn art is the one child after them. pub(super) depth: usize, pub(super) siblings: usize, + /// Its contents hold type: it is settled once the type is laid out + /// ([`super::super::envelope::TypedEnvelope`]). + pub(super) typed: bool, } /// What a reader of a pattern's definition has of its head, `(name) llx lly urx ury`: the operands diff --git a/crates/eps/src/import/ai/tests.rs b/crates/eps/src/import/ai/tests.rs index 2f0fca73b..2b127e275 100644 --- a/crates/eps/src/import/ai/tests.rs +++ b/crates/eps/src/import/ai/tests.rs @@ -7992,7 +7992,7 @@ fn warped_type_on_the_page_spends_what_is_left_of_the_budget() { let copy = |w: &super::envelope::WarpedLook| super::envelope::WarpedLook { slot: w.slot, look: w.look.clone(), left_out: None }; let page = Some(Rect::new(0.0, 60.0, 60.0, 100.0)); let settle = |w: &super::envelope::WarpedLook, spent| { - let warped = super::envelope::WarpedType { looks: vec![copy(w)], checks: 1, spent, page }; + let warped = super::envelope::WarpedType { looks: vec![copy(w)], envelopes: vec![], checks: 1, spent, page }; warped.settle(&mut doc.clone(), Some, &Default::default(), &mut vec![]) }; let spent = || super::envelope::Spent { any: MAX_DIFFERENCE / 2.0, solid: MAX_SOLID_DIFFERENCE / 2.0 }; diff --git a/crates/eps/src/import/native/build.rs b/crates/eps/src/import/native/build.rs index e0d00ac82..4266b945c 100644 --- a/crates/eps/src/import/native/build.rs +++ b/crates/eps/src/import/native/build.rs @@ -104,8 +104,7 @@ pub(super) type Built = Result<(Document, Vec), String>; /// units ([`page_scale_alone`]). The document is drawn at that scale. Else `Err(structure)`, for /// [`build_from`] with the page. pub(super) fn build_without_page(structure: Structure, data: &[u8], pages: &[(f64, f64)], unit: f64) -> Result> { - let Some(scale) = page_scale_alone(&structure, pages, unit).filter(|_| nothing_waits_on_page(&structure) && type_all_made(&structure, data)) - else { + let Some(scale) = alone_scale(&structure, data, pages, unit) else { return Err(Box::new(structure)); }; let styles = structure.styles_read; @@ -120,6 +119,12 @@ pub(super) fn build_without_page(structure: Structure, data: &[u8], pages: &[(f6 Ok(built) } +/// The scale [`build_without_page`] draws `structure` (the editing copy `data` read) at on `pages` +/// in units of `unit` points, when it builds it without the page. +pub(super) fn alone_scale(structure: &Structure, data: &[u8], pages: &[(f64, f64)], unit: f64) -> Option { + page_scale_alone(structure, pages, unit).filter(|_| nothing_waits_on_page(structure) && type_all_made(structure, data)) +} + /// Is nothing in `structure` waiting on the page: no object unread (the page's art takes its /// place), no placed file with another placement, no live envelope with the drawn art it replaced /// (the comparison with the page chooses between them), no linked image showing a placeholder @@ -425,7 +430,17 @@ pub(super) fn build_from( // Area type flows around the wrap objects above it before it is compared with the page. resolve_text_wrap(&mut doc)?; let bound_text = text_bound_slots(&doc, &bindings, &xml_ids); - settle_warped_type(warped_type, &mut doc, &filled, &threaded_frames, &bound_text, &mut restyled, &mut notes)?; + // Without a page to compare the layers with, envelopes of type keep their drawn art: like the + // other live envelopes, one opens live only where the layers with it draw as the page. + let warped_type = warped_type.compared(frame.is_some(), &mut notes); + let typed = settle_warped_type(warped_type, &mut doc, &filled, &threaded_frames, &bound_text, &mut restyled, &mut notes)?; + // The live envelopes and what takes the place of each again, when the layers with them don't + // draw as the page, with the note that says so. + let envelopes_drawn: Vec<(NodeId, Vec, &str)> = envelopes_drawn + .into_iter() + .map(|(id, art)| (id, vec![art], ai::ENVELOPE_DRAWN)) + .chain(typed.into_iter().map(|(id, nodes)| (id, nodes, ai::ENVELOPE_TYPE_DRAWN))) + .collect(); if empty > 0 { notes.push(format!("{empty} {}", if frame.is_some() { TEXT_LEFT_OUT } else { TYPE_LEFT_OUT })); } @@ -630,26 +645,44 @@ fn text_bound_slots(doc: &Document, bindings: &[ai::BindingRef], ids: &std::coll /// (`bound_text`, by slot). Elsewhere the look takes its place, and one note says how many did. /// `filled` says what each slot became, and the comparisons with the page leave out the boxes in /// `restyled`. Fails as [`resolve_text_wrap`] does when the looks give the wrap shapes too many -/// anchors. +/// anchors. Before that, the envelopes of type open live where they draw as their drawn art, with +/// the type of their text slots (made type, one text object for the slot, not threaded, bound to +/// no text variable and no wrap object); the others keep their drawn art, with a note. Returns +/// each live envelope of type's id with what takes its place again (its hidden type, then its +/// drawn art). fn settle_warped_type( - warped: ai::WarpedType, + mut warped: ai::WarpedType, doc: &mut Document, filled: &std::collections::HashMap, threaded: &BTreeSet, bound_text: &BTreeSet, restyled: &mut Vec, notes: &mut Vec, -) -> Result<(), String> { +) -> Result)>, String> { let made = |slot| match filled.get(&slot).map(|f| f.text.as_slice()).filter(|_| !bound_text.contains(&slot)) { Some(&[one]) => Some(one), _ => None, }; + // An envelope's type: the one text object a slot became (hidden, with nothing else), named as + // the slot (made type is no longer weighed against the page: the envelope draws it). + let type_of = |doc: &Document, slot: NodeId| { + let id = made(slot).filter(|id| !threaded.contains(id) && filled.get(&slot).is_some_and(|f| f.objects == [*id]))?; + let mut text = doc.node(id).filter(|n| matches!(n.kind, NodeKind::Text(_)) && n.extras().wrap.is_none())?.clone(); + if let Some(name) = text.name.as_deref().and_then(|n| n.strip_prefix(MADE)) { + text.name = (!name.is_empty()).then(|| name.to_string()); + } + Some((id, text)) + }; + let typed = warped.settle_envelopes(doc, type_of); + if !typed.kept.is_empty() && !notes.iter().any(|n| n == ai::ENVELOPE_TYPE_DRAWN) { + notes.push(ai::ENVELOPE_TYPE_DRAWN.into()); + } let replaced = warped.settle(doc, made, threaded, restyled); if replaced > 0 { notes.push(format!("{}: {replaced}", ai::WARPED_TYPE_DRAWN)); resolve_text_wrap(doc)?; } - Ok(()) + Ok(typed.put_back) } /// Most anchors copied into the wrap shapes of a file's area type ([`vectorcraft_doc::text::wrap_work`]). @@ -1261,7 +1294,7 @@ fn drawn_or_trusted( frame: &Frame<'_>, view: &mut PageView, doc: &mut Document, - drawn: &[(NodeId, Node)], + drawn: &[(NodeId, Vec, &str)], notes: &[String], unseen: usize, trust: bool, @@ -1270,18 +1303,19 @@ fn drawn_or_trusted( ) -> Result, String> { let unseen_note = |n: usize| (n > 0).then(|| format!("{n} {UNSEEN_TEXT}")); let mut put_back = doc.clone(); - let mut any = false; - for (id, art) in drawn { - if let Some(n) = put_back.node_mut(*id) { - *n = art.clone(); - any = true; - } - } + // Each envelope still there, in place of the art it took the place of. + let there: Vec<&(NodeId, Vec, &str)> = drawn.iter().filter(|(id, ..)| put_back.node(*id).is_some()).collect(); + let any = !there.is_empty() && put_back.insert_beside(there.iter().map(|(id, art, _)| (*id, art.clone())).collect(), true).is_ok(); if any { let more = prune_unseen(frame, view, &mut put_back, reflow); if let Ok(note) = compare(frame, view, &put_back, reflow) { *doc = put_back; - let envelope = (!notes.iter().any(|n| n == ai::ENVELOPE_DRAWN)).then(|| ai::ENVELOPE_DRAWN.to_string()); + let mut envelope: Vec = vec![]; + for (.., say) in there { + if !notes.iter().chain(&envelope).any(|n| n == say) { + envelope.push(say.to_string()); + } + } return Ok(envelope.into_iter().chain(unseen_note(unseen + more)).chain(note).collect()); } } diff --git a/crates/eps/src/import/native/layers.rs b/crates/eps/src/import/native/layers.rs index c0331574c..ee39aec1f 100644 --- a/crates/eps/src/import/native/layers.rs +++ b/crates/eps/src/import/native/layers.rs @@ -1,4 +1,4 @@ -use super::build::{PAGE_MOVE, Page, build, build_from, build_on_page, build_without_page, reid_page_art}; +use super::build::{PAGE_MOVE, Page, alone_scale, build, build_from, build_on_page, build_without_page, reid_page_art}; use super::slots::LAYERS_UNREAD; use super::{Affine, Arc, Cow, Document, Imported, Node, NodeKind, PathData, Rect, Vec2, ai}; @@ -137,7 +137,7 @@ pub fn layered_ai_from( all.push(format!("{LAYERS_UNREAD} from its editing data ({why}): they come from its PDF part")); all }; - let AiLayers { data, structure } = layers; + let AiLayers { data, structure, alone } = layers; let data = match data { Ok(data) => data, Err(why) => return (visible, unread(warnings, &why), false), @@ -180,6 +180,18 @@ pub fn layered_ai_from( let built = structure .unwrap_or_else(|| ai::read_over_page(&data, false, names).map(Box::new)) .and_then(|structure| build_from(*structure, &data, Some(&visible), Page::Artboard, outlined, trust, unit)); + // Layers that waited on the page only for their envelopes of type open alone, as they would + // have without them, each envelope keeping its drawn art, unless one opened live with the page. + let live = matches!(&built, Ok((done, _)) if holds_type_envelope(&done.layers)); + if !live + && let Some((mut done, notes)) = alone.and_then(|(pages, unit)| { + let structure = ai::read_over_page(&data, false, names).ok()?; + build_without_page(structure, &data, &pages, unit).ok()?.ok() + }) + { + done.setup.bleed = visible.setup.bleed; + return (done, notes, true); + } match built { Ok((mut done, notes)) => { done.setup.bleed = visible.setup.bleed; @@ -191,6 +203,17 @@ pub fn layered_ai_from( } } +/// Is there an envelope of type (one whose contents hold type) in `nodes` or what they hold? +fn holds_type_envelope(nodes: &[Arc]) -> bool { + fn holds_type(n: &Node) -> bool { + matches!(n.kind, NodeKind::Text(_)) || n.children().is_some_and(|c| c.iter().any(|c| holds_type(c))) + } + nodes.iter().any(|n| match &n.kind { + NodeKind::Envelope { content, .. } => content.iter().any(|c| holds_type(c)), + _ => n.children().is_some_and(|c| holds_type_envelope(c)), + }) +} + /// The `.ai` whose editing copy is `private` (as [`layered_ai`] takes it) from its layers alone, /// without its PDF part, whose pages (each its trim box in points, width and height) are `pages` /// and are in units of `unit` points (its first page's `UserUnit`): the document and the notes of @@ -201,20 +224,27 @@ pub fn layered_ai_from( pub fn ai_layers_alone(private: &[u8], pages: &[(f64, f64)], unit: f64, names: ai::NameEncoding) -> Result<(Document, Vec), AiLayers> { let data = match ai::decode_private(private) { Ok(data) => data, - Err(why) => return Err(AiLayers { data: Err(why), structure: None }), + Err(why) => return Err(AiLayers { data: Err(why), structure: None, alone: None }), }; if !has_layers(&data) { - return Err(AiLayers { data: Ok(data), structure: None }); + return Err(AiLayers { data: Ok(data), structure: None, alone: None }); } // An image past the hard limits is read as an unread box, which the PDF part's art replaces. let structure = match ai::read_over_page(&data, false, names) { Ok(structure) => structure, - Err(why) => return Err(AiLayers { data: Ok(data), structure: Some(Err(why)) }), + Err(why) => return Err(AiLayers { data: Ok(data), structure: Some(Err(why)), alone: None }), }; + // An envelope of type opens live only where the layers with it draw as the page, as the other + // live envelopes do: the PDF part is imported to compare them with. Should none open live, the + // layers open alone as they would have, the envelopes keeping their drawn art ([`layered_ai_from`]). + if structure.warped_type.has_envelopes() && alone_scale(&structure, &data, pages, unit).is_some() { + let alone = Some((pages.to_vec(), unit)); + return Err(AiLayers { data: Ok(data), structure: Some(Ok(Box::new(structure))), alone }); + } match build_without_page(structure, &data, pages, unit) { Ok(Ok((doc, notes))) => Ok((doc, notes)), - Ok(_) => Err(AiLayers { data: Ok(data), structure: None }), - Err(structure) => Err(AiLayers { data: Ok(data), structure: Some(Ok(structure)) }), + Ok(_) => Err(AiLayers { data: Ok(data), structure: None, alone: None }), + Err(structure) => Err(AiLayers { data: Ok(data), structure: Some(Ok(structure)), alone: None }), } } @@ -226,12 +256,15 @@ pub struct AiLayers { /// Its layers read with [`ai::read`] as [`layered_ai`] reads them, or why they can't be; `None` /// when they are still to be read. structure: Option, String>>, + /// The pages and their unit the layers open on without the PDF part, when they wait on it only + /// for their envelopes of type ([`ai_layers_alone`]). + alone: Option<(Vec<(f64, f64)>, f64)>, } impl AiLayers { /// The editing copy `private` (as [`layered_ai`] takes it), decoded. pub fn decode(private: &[u8]) -> Self { - Self { data: ai::decode_private(private), structure: None } + Self { data: ai::decode_private(private), structure: None, alone: None } } } diff --git a/crates/eps/src/import/tests_native.rs b/crates/eps/src/import/tests_native.rs index 931717655..cbfb7c928 100644 --- a/crates/eps/src/import/tests_native.rs +++ b/crates/eps/src/import/tests_native.rs @@ -5226,6 +5226,259 @@ fn envelope_options_keep_the_source_text_hidden_under_its_outlined_appearance() assert_eq!(super::tests::program_warnings(&r.warnings), [crate::import::ai::ENVELOPE_TYPE_DRAWN], "{:?}", r.warnings); } +/// A family that is installed, for type that must draw as itself; `None` (the test is skipped) +/// when no font is available. +fn installed_family() -> Option { + let db = vectorcraft_text::FontDb::global(); + let plain = |f: &String| !f.is_empty() && f.chars().all(|c| c.is_ascii_alphanumeric() || c == ' '); + ["DejaVu Sans", "Liberation Sans", "Noto Sans", "Source Sans 3", "Arial", "Helvetica"] + .iter() + .map(|f| f.to_string()) + .find(|f| db.has_family(f)) + .or_else(|| db.families().into_iter().find(|f| plain(f) && db.has_family(f))) +} + +/// The art space of the 100 × 100 page of [`native_with_text`] has y up. +fn to_art(p: kurbo::Point) -> (f64, f64) { + (p.x, 100.0 - p.y) +} + +/// What the live envelope `envelope` (on the page axes) draws, as VectorCraft draws it: the +/// format's paths (each subpath filled on its own, in the color its type is drawn in) and the +/// page's PostScript for them. +fn drawn_envelope(envelope: &Node) -> (String, String) { + fn walk(n: &Node, color: [f32; 3], art: &mut String, page: &mut String) { + let [r, g, b] = n.appearance.fill().and_then(|f| f.paint.color()).map_or(color, |c| c.to_rgb()); + let NodeKind::Path { path, .. } = &n.kind else { + n.children().into_iter().flatten().for_each(|c| walk(c, [r, g, b], art, page)); + return; + }; + let pt = |p: kurbo::Point| { + let (x, y) = to_art(p); + format!("{x} {y}") + }; + for el in path.to_bezpath().elements() { + match *el { + kurbo::PathEl::MoveTo(p) => { + *art += &format!("0 0 0 0 {r} {g} {b} Xa\n{} m\n", pt(p)); + *page += &format!("{r} {g} {b} setrgbcolor newpath {} moveto ", pt(p)); + } + kurbo::PathEl::LineTo(p) => { + *art += &format!("{} L\n", pt(p)); + *page += &format!("{} lineto ", pt(p)); + } + kurbo::PathEl::CurveTo(a, c, p) => { + *art += &format!("{} {} {} C\n", pt(a), pt(c), pt(p)); + *page += &format!("{} {} {} curveto ", pt(a), pt(c), pt(p)); + } + kurbo::PathEl::QuadTo(..) => {} + kurbo::PathEl::ClosePath => { + *art += "f\n"; + *page += "closepath fill\n"; + } + } + } + } + let (mut art, mut page) = (String::new(), String::new()); + walk(&vectorcraft_effects::expand_live_deep(None, envelope), [0.0; 3], &mut art, &mut page); + (art, page) +} + +/// An arc warp of `bend` (a fraction) in the editing data's keys, with the Envelope Options new +/// envelopes have and Fidelity 50. +fn arc_keys(bend: f64) -> String { + format!( + "131073 /Int (AIDeformOptionsKey) ,\n50 /Int (AIDeformFidelityKey) ,\n0 /Bool (AIDeformEditModeKey) ,\n1 /Int (DeformStyle) ,\n0 /Real (DeformHoriz) ,\n{bend} /Real (DeformValue) ,\n0 /Real (DeformVert) ,\n0 /Bool (Rotate) ,\n" + ) +} + +/// An envelope made with a warp (`keys`) around the type of story 0, as the editing data writes +/// it: on `%_` lines the group with its mesh, its dictionary with the text object in its contents, +/// then its drawn art `drawn` on plain lines and the plugin's `XP`. +fn type_envelope(keys: &str, drawn: &str) -> String { + let hidden = |s: &str| -> String { s.lines().map(|l| format!("%_{l}\n")).collect() }; + let corner = |x: f64, y: f64, nx: f64, ny: f64, px: f64, py: f64| format!("[0.8 1 {x} {y} {nx} {ny} 1 {px} {py} 1] /N X#\n"); + let mesh = format!( + "/Mesh X!\n2 /Version X#\n/Cartesian /Type X#\n[1 1] /Size X#\n/Data X#\n/DeviceGray /CS X#\n[0 0] /P X#\n{}{}{}{}/E X#\n/End X!\n", + corner(20.0, 40.0, 20.0, 50.0, 30.0, 40.0), + corner(20.0, 70.0, 30.0, 70.0, 20.0, 60.0), + corner(80.0, 70.0, 80.0, 60.0, 70.0, 70.0), + corner(80.0, 40.0, 70.0, 40.0, 80.0, 50.0), + ); + format!( + "{}{}%_X=\n{}%_X+\n%_ (AIDeformArtKey) ,\n%_1 /Int (AIDeformStyle) ,\n%_;\n%_\n%_9 () XW\nu\n{drawn}U\n9 () XW\n(Adobe Deform Plugin) 1 0 0 XP\n", + hidden(&format!("u\n{mesh}U\n")), + hidden(&format!("/ArtDictionary :\n{keys}")), + hidden(&format!("u\n{}U\n9 () XW\n", text_object(0))), + ) +} + +/// The text document of the type in [`type_envelope`]: "FLUX" set in `family`, drawn in `paint` (a +/// text paint, the default's when `None`). +fn type_envelope_text(family: &str, paint: Option<&str>) -> String { + let doc = text_document(&[("FLUX\r", 0, (8170.0, 8180.0), (0.0, 0.0), 0.0)]).replace("(Helvetica)", &format!("({family})")); + let mut doc = match paint { + Some(p) => doc.replace("/0 << /0 2 /1 [ 1.0 0.0 1.0 0.0 0.0 ] >>", p), + None => doc, + }; + // [`native_with_text`] writes 60 characters a line: the end of the data, `~>`, isn't split. + while ascii85(doc.as_bytes()).len() % 60 == 1 { + doc.push('\n'); + } + doc +} + +/// The live envelope an arc warp of `bend` (in percent) makes of `text`, as the reader makes it. +fn arc_envelope(text: &Node, bend: f64) -> Node { + let mut text = text.clone(); + text.visible = true; + let kind = vectorcraft_doc::live::EnvelopeKind::Warp { style: "arc".into(), bend, h: 0.0, v: 0.0, horizontal: true }; + Node::new( + vectorcraft_doc::NodeId(1_000_000), + NodeKind::Envelope { + content: vec![std::sync::Arc::new(text)], + kind: Box::new(kind), + fidelity: 50.0, + editing: false, + options: vectorcraft_doc::live::EnvelopeOptions::NEW, + frame: Box::default(), + }, + ) +} + +/// The type of the file with an envelope of type whose drawn art is `drawn`, read with its text +/// document `doc`: the hidden text object it keeps, as the reader makes it. +fn made_envelope_type(doc: &str) -> Node { + let art = native_with_text(&layer("Art", true, &type_envelope(&arc_keys(0.3), &format!("{}\n", square(10, 10)))), doc); + let r = import(&eps(&page_square(10, 10), &art)).unwrap(); + let mut text = None; + r.document.walk(|n| { + if matches!(n.kind, NodeKind::Text(_)) { + text = Some(n.clone()); + } + }); + text.unwrap_or_else(|| panic!("no type: {:?}", r.warnings)) +} + +/// The envelopes and the type, shown or not, in the layers of `d`: each envelope's contents, and +/// the plain text of the type outside envelopes with whether it shows. +fn envelopes_and_type(d: &Document) -> (Vec>, Vec<(String, bool)>) { + let (mut envelopes, mut loose) = (vec![], vec![]); + fn walk(n: &Node, envelopes: &mut Vec>, loose: &mut Vec<(String, bool)>) { + match &n.kind { + NodeKind::Envelope { content, .. } => envelopes.push( + content + .iter() + .map(|c| { + if let NodeKind::Text(t) = &c.kind { + format!("{}{}", t.plain_text(), if c.visible { "" } else { " (hidden)" }) + } else { + "art".into() + } + }) + .collect(), + ), + NodeKind::Text(t) => loose.push((t.plain_text(), n.visible)), + _ => n.children().into_iter().flatten().for_each(|c| walk(c, envelopes, loose)), + } + } + d.layers.iter().for_each(|l| walk(l, &mut envelopes, &mut loose)); + (envelopes, loose) +} + +/// An envelope of type whose live envelope, its type laid out from the text document, draws as its +/// drawn art opens live: the type is its contents, shown and editable, and the hidden copy of the +/// type goes (#3179). +#[test] +fn an_envelope_of_type_that_draws_as_its_drawn_art_opens_live() { + let Some(family) = installed_family() else { return }; + let doc = type_envelope_text(&family, None); + let (drawn, page) = drawn_envelope(&arc_envelope(&made_envelope_type(&doc), 30.0)); + let art = native_with_text(&layer("Art", true, &format!("{}\n{}", square(10, 10), type_envelope(&arc_keys(0.3), &drawn))), &doc); + let r = import(&eps(&format!("{}\n{page}", page_square(10, 10)), &art)).unwrap(); + assert_eq!(names(&r.document), ["Art"], "{:?}", r.warnings); + let (envelopes, loose) = envelopes_and_type(&r.document); + assert_eq!(envelopes, [vec!["FLUX".to_string()]], "{:?}", r.warnings); + assert!(loose.is_empty(), "{loose:?}"); + assert!(super::tests::program_warnings(&r.warnings).is_empty(), "{:?}", r.warnings); +} + +/// An envelope of type whose live envelope draws apart from its drawn art (the file's art bends the +/// other way) keeps its drawn art and its type, hidden, with a note. +#[test] +fn an_envelope_of_type_that_draws_apart_from_its_drawn_art_keeps_it() { + let Some(family) = installed_family() else { return }; + let doc = type_envelope_text(&family, None); + let (drawn, page) = drawn_envelope(&arc_envelope(&made_envelope_type(&doc), -30.0)); + let art = native_with_text(&layer("Art", true, &format!("{}\n{}", square(10, 10), type_envelope(&arc_keys(0.3), &drawn))), &doc); + let r = import(&eps(&format!("{}\n{page}", page_square(10, 10)), &art)).unwrap(); + assert_eq!(names(&r.document), ["Art"], "{:?}", r.warnings); + let (envelopes, loose) = envelopes_and_type(&r.document); + assert!(envelopes.is_empty(), "{envelopes:?}"); + assert_eq!(loose, [("FLUX".to_string(), false)], "{:?}", r.warnings); + assert_eq!(super::tests::program_warnings(&r.warnings), [crate::import::ai::ENVELOPE_TYPE_DRAWN], "{:?}", r.warnings); +} + +/// White type in an envelope that draws apart from its drawn art keeps the drawn art too: the +/// envelope and its drawn art are each drawn on their own, where white shows only on black. +#[test] +fn an_envelope_of_white_type_that_draws_apart_from_its_drawn_art_keeps_it() { + let Some(family) = installed_family() else { return }; + let doc = type_envelope_text(&family, Some("/0 << /0 1 /1 [ 1.0 1.0 1.0 1.0 ] >>")); + let text = made_envelope_type(&doc); + assert_eq!(text_fill(&text), Some(vectorcraft_color::Color::rgb(1.0, 1.0, 1.0))); + let (drawn, page) = drawn_envelope(&arc_envelope(&text, -30.0)); + // A dark square under it, so the page shows the white type. + let ground = "0 0 0 1 k\n0 0 m\n100 0 L\n100 100 L\n0 100 L\nf\n"; + let art = native_with_text(&layer("Art", true, &format!("{ground}{}", type_envelope(&arc_keys(0.3), &drawn))), &doc); + let r = import(&eps(&format!("0 0 0 1 setcmykcolor 0 0 moveto 100 0 lineto 100 100 lineto 0 100 lineto closepath fill\n{page}"), &art)).unwrap(); + assert_eq!(names(&r.document), ["Art"], "{:?}", r.warnings); + let (envelopes, loose) = envelopes_and_type(&r.document); + assert!(envelopes.is_empty(), "{envelopes:?}"); + assert_eq!(loose, [("FLUX".to_string(), false)], "{:?}", r.warnings); +} + +/// A `.ai` whose layers would open alone, without its PDF part, waits on the PDF part when an +/// envelope of type is in them, as for the other live envelopes: one opens live only where the +/// layers with it draw as the page. Where they don't, they open alone as before, the envelope +/// keeping its drawn art and its type, hidden, with the note, even though it would draw as that +/// drawn art; where they do, it opens live (#3179). +#[test] +fn an_envelope_of_type_waits_on_the_page_of_layers_that_would_open_alone() { + let Some(family) = installed_family() else { return }; + let stories = [("FLUX\r", 0, (8170.0, 8180.0), (0.0, 0.0), 0.0), ("Note\r", 0, (8170.0, 8220.0), (0.0, 0.0), 0.0)]; + let mut doc = text_document(&stories).replace("(Helvetica)", &format!("({family})")); + // [`native_with_text`] writes 60 characters a line: the end of the data, `~>`, isn't split. + while ascii85(doc.as_bytes()).len() % 60 == 1 { + doc.push('\n'); + } + let (drawn, _) = drawn_envelope(&arc_envelope(&made_envelope_type(&doc), 30.0)); + let art = format!("u\n{}U\n{}", text_object(1), type_envelope(&arc_keys(0.3), &drawn)); + let file = private(&native_with_text(&layer("Art", true, &art), &doc)); + let names = crate::NameEncoding::default(); + let Err(layers) = crate::ai_layers_alone(&file, &[(100.0, 100.0)], 1.0, names) else { panic!("opened without the page") }; + // A PDF part the layers don't draw as. + let (d, notes, read) = crate::layered_ai_from(layers, pdf_part(80.0, 80.0), vec![], false, false, 1.0, names); + assert!(read, "{notes:?}"); + let (envelopes, loose) = envelopes_and_type(&d); + assert!(envelopes.is_empty(), "{envelopes:?}"); + assert_eq!(loose, [("Note".to_string(), true), ("FLUX".to_string(), false)], "{notes:?}"); + assert!(notes.iter().any(|n| n == crate::import::ai::ENVELOPE_TYPE_DRAWN), "{notes:?}"); + // A PDF part that draws as those layers, with the drawn art: the envelope opens live. + let Err(layers) = crate::ai_layers_alone(&file, &[(100.0, 100.0)], 1.0, names) else { panic!("opened without the page") }; + let (d, notes, read) = crate::layered_ai_from(layers, d, vec![], false, false, 1.0, names); + assert!(read, "{notes:?}"); + let (envelopes, loose) = envelopes_and_type(&d); + assert_eq!(envelopes, [vec!["FLUX".to_string()]], "{notes:?}"); + assert_eq!(loose, [("Note".to_string(), true)], "{notes:?}"); +} + +/// The fill of the first run of type `n`. +fn text_fill(n: &Node) -> Option { + let NodeKind::Text(t) = &n.kind else { return None }; + t.runs.first().and_then(|r| r.style.fill.color()) +} + /// A live envelope that draws a little apart from its drawn art, in layers that already differ /// from the page almost as much as the page check takes, would make them differ too much: its /// drawn art is put back, so the file still opens from its layers. diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index f4652f539..60bec1191 100644 --- a/docs/file-format-parity.md +++ b/docs/file-format-parity.md @@ -1,6 +1,6 @@ # File format parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-10 · **Change:** minor (`.ai` and EPS import: a note for type in an envelope, #3179; `.ai` and EPS import: the live envelope and warped type check sees white art; PDF import: DeviceN images of one ink or of process plates keep their inks, #6348; `.ai` import: images in a spot ink's Separation space keep their ink, #6350; PDF import: Separation images keep their ink, #6314; PDF editable import preserves authored ActualText once, including empty/nested spans and authored ordering (#6165); cross-line/style layout remains approximate; scores and estimates unchanged; PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; PDF import: image masks open colorized by the color they are painted in, #6313; EPS and `.ai` editing data: a ligature the page names by a private-use glyph matches its text object; EPS import: CIDFonts only named, over a Unicode CMap, set as live type; EPS import: CMap mappings to base-font codes and glyph names; EPS import: CID-keyed fonts in FontSets (CFF) draw their glyphs; EPS import: `kshow` procedures kern live type; EPS import: embedded Type 1 fonts' glyphs and widths; EPS import: CID-keyed fonts draw their glyphs; EPS import: Type 42 fonts' characters and widths from the font they carry, show operators' spacing kept as tracking; EPS import: an image decoder's own buffers stop counting against the memory budget once it is decoded; EPS import: sampled functions interpolate between their samples; `.ai` and EPS type from the text document keeps its tab stops; it keeps auto leading and hyphenation options, and may show more of the file's last lines in an area frame; `.ai` and EPS import: Art brushes in the older form of their objects come into the Brushes panel; EPS import: gradients drawn as abutting strips come in as one gradient, without seams; EPS import: composite fonts draw their Type 42 glyphs; SVG import: filter and mask regions clip, #5876, #5875; SVG import: Gaussian blurs keep `color-interpolation-filters`, #5877; PDF and `.ai` export: objects' URLs as links, #5785; SVG import: `color-interpolation="linearRGB"` gradients, #5878; `.ai` import: paths with a Calligraphic brush open brushed, #372) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-10 · **Change:** minor (`.ai` import: envelopes of type open live where they draw as their drawn art, #3179; `.ai` and EPS import: a note for type in an envelope, #3179; `.ai` and EPS import: the live envelope and warped type check sees white art; PDF import: DeviceN images of one ink or of process plates keep their inks, #6348; `.ai` import: images in a spot ink's Separation space keep their ink, #6350; PDF import: Separation images keep their ink, #6314; PDF editable import preserves authored ActualText once, including empty/nested spans and authored ordering (#6165); cross-line/style layout remains approximate; scores and estimates unchanged; PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; PDF import: image masks open colorized by the color they are painted in, #6313; EPS and `.ai` editing data: a ligature the page names by a private-use glyph matches its text object; EPS import: CIDFonts only named, over a Unicode CMap, set as live type; EPS import: CMap mappings to base-font codes and glyph names; EPS import: CID-keyed fonts in FontSets (CFF) draw their glyphs; EPS import: `kshow` procedures kern live type; EPS import: embedded Type 1 fonts' glyphs and widths; EPS import: CID-keyed fonts draw their glyphs; EPS import: Type 42 fonts' characters and widths from the font they carry, show operators' spacing kept as tracking; EPS import: an image decoder's own buffers stop counting against the memory budget once it is decoded; EPS import: sampled functions interpolate between their samples; `.ai` and EPS type from the text document keeps its tab stops; it keeps auto leading and hyphenation options, and may show more of the file's last lines in an area frame; `.ai` and EPS import: Art brushes in the older form of their objects come into the Brushes panel; EPS import: gradients drawn as abutting strips come in as one gradient, without seams; EPS import: composite fonts draw their Type 42 glyphs; SVG import: filter and mask regions clip, #5876, #5875; SVG import: Gaussian blurs keep `color-interpolation-filters`, #5877; PDF and `.ai` export: objects' URLs as links, #5785; SVG import: `color-interpolation="linearRGB"` gradients, #5878; `.ai` import: paths with a Calligraphic brush open brushed, #372) · **Target:** Adobe Illustrator 2026 (30.x) Every format Illustrator opens, places, saves or exports, against VectorCraft's format table (`FORMATS`, `OPEN_EXTS`, `PLACE_EXTS` in `crates/engine/src/cmd/fileio/formats.rs`). Illustrator's list comes from its public @@ -336,6 +336,8 @@ Stop opacity on translated artboards remains outside this regression coverage. | Date | Change | Summary | |---|---|---| +| 2026-11-10 | minor | `.ai` and EPS import: an envelope of type (a warp, a mesh or a top object around type) kept its drawn art, its type hidden, because the type is laid out from the text document only after the layers are read. It now waits for that: its text slots get their type, and where VectorCraft's own envelope of that type draws as the drawn art the file saves (the same check and budget as other envelopes, seen on white and on black), it opens live in the drawn art's place, its type editable in Edit Contents, and the hidden copy goes. As with other live envelopes, the layers with one are then compared with the page, and when they don't draw as it the drawn art and the hidden type are put back: a `.ai` whose layers would open alone, without its PDF part, imports the PDF part to compare them with, and should they still not draw as it, or should no envelope of type open live, they open alone as before, with the envelopes' drawn art. Type in a font that isn't available, slots without type of their own (threaded, bound to a text variable, a wrap object, type a look draws), envelopes in a symbol's or a pattern's art, and envelopes that draw differently keep their drawn art, with the note (#3179). Tests `tests_native::an_envelope_of_type_that_draws_as_its_drawn_art_opens_live`, `an_envelope_of_type_that_draws_apart_from_its_drawn_art_keeps_it`, `an_envelope_of_white_type_that_draws_apart_from_its_drawn_art_keeps_it`, `an_envelope_of_type_waits_on_the_page_of_layers_that_would_open_alone` | +| 2026-11-10 | minor | `.ai` and EPS import: an envelope of type, which keeps its drawn art with its type hidden, now says so in the import notes ("type in an envelope comes in as the envelope's drawn art, not as editable type: the type is kept, hidden"); nothing said it before, as the other envelopes that keep their drawn art do. The type is still not editable in the envelope (part of #3179). Test `tests_native::envelope_options_keep_the_source_text_hidden_under_its_outlined_appearance` | | 2026-11-10 | minor | `.ai` and EPS import: an envelope of type, which keeps its drawn art with its type hidden, now says so in the import notes ("type in an envelope comes in as the envelope's drawn art, not as editable type"); nothing said it before, while the other envelopes that keep their drawn art have a note. The type is still not editable in the envelope (part of #3179). Test `tests_native::envelope_options_keep_the_source_text_hidden_under_its_outlined_appearance` | | 2026-11-10 | minor | `.ai` and EPS import: the check that keeps a live envelope or warped type only where it draws as the art saved with it renders each on its own, over nothing, and compared the pixels seen on white, where white art looks like nothing. White contents that drew somewhere else passed it and opened live in the wrong place (white warped type off its banner, in users' files). Pixels now also differ when they look different seen on black, so such art keeps its saved art, as colored art does. Tests `import::ai::tests::a_white_envelope_that_doesnt_come_out_as_drawn_keeps_its_drawn_art`, `white_warped_type_that_draws_elsewhere_comes_in_as_its_look` | | 2026-11-10 | minor | PDF import: an image in a DeviceN space of one colorant opens as a Separation image does (#6314): a grayscale image of its tints colorized by its ink. One of two or more colorants that are all process plates (Cyan, Magenta, Yellow, Black, and the ink `None`) opens as a CMYK image, each colorant's tint in its plate and its Decode array applied (8 or 16 bits a sample; one that can't be read, such as a JPEG, opens in RGB with the CMYK images' warning). Both used to open as RGB images, losing their inks. A DeviceN image that mixes a spot ink with other colorants, such as a duotone, still opens as RGB: an image holds one ink or the process plates, not both (#6348). Tests `colorize::a_devicen_image_of_one_ink_opens_colorized_by_it`, `colorize::a_devicen_image_of_process_plates_opens_in_cmyk`, `colorize::a_duotone_devicen_image_still_opens_as_rgb` | diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index a8bd43ad5..76d190f08 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1403,7 +1403,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur A `.ai`, or a PDF saved with Illustrator's editing data, whose editing data can't be decompressed opens from its PDF part with a warning that its layers weren't read and why, and an export or Save As to its own path asks first (`acknowledgeLoss`), as for a file whose layers the reader can't read (`crates/eps/src/import/tests_native.rs`, `crates/engine/src/cmd/fileio/tests_aiimport.rs`). A `.ai` or EPS opened from its editing data with one object on a shown layer that the reader can't read (an unknown operator inside a group, a pattern fill whose pattern it doesn't have, a symbol instance, a placed file without a usable preview) keeps its layers and groups: in that object's place, its layer gets a group of the page's objects inside the object's box, named after it, with a warning that lists what came in as the page's art and an export or Save As to its own path asking first (`acknowledgeLoss`). The file still opens from its page or PDF part when no box can be found, when live art or another unread object shares the page art inside it, when the object spans two artboards or lies on an artboard that has no page, when no page art lies inside its box, when that page art is inside a page group with a clip, mask, transparency or appearance, when the object holds a frame of threaded type, when the page art uses a pattern, image or swatch that the page lacks or that differs from the editing data's, or when the page art doesn't carry the object's transparency (#4806; `crates/eps/src/import/tests_native.rs`, `crates/engine/src/cmd/fileio/tests_aiimport.rs`). A `.ai` or EPS opened from its editing data with one object on a shown layer that the reader can't read (an unknown operator inside a group, a pattern fill whose pattern it doesn't have, a symbol instance, a placed file without a usable preview) keeps its layers and groups: in that object's place, its layer gets a group of the page's objects inside the object's box, named after it, with a warning that lists what came in as the page's art and an export or Save As to its own path asking first (`acknowledgeLoss`). The file still opens from its page or PDF part when no box can be found, when live art or another unread object shares the page art inside it, when the object spans two artboards, or when the page art doesn't carry the object's transparency (#4806; `crates/eps/src/import/tests_native.rs`, `crates/engine/src/cmd/fileio/tests_aiimport.rs`). - In a `.ai` opened from its editing data, an envelope made with a warp, a mesh or a top object opens as a live envelope of its contents, with its warp style, bend and distortions, its mesh points and handles or its top object's outline, its Envelope Options, its Fidelity and whether its contents are being edited. When Distort Appearance is on and the size the file saves for the contents matches their bounds with strokes, the contents map from that stroked box, until an edit in Edit Contents changes how far the strokes reach; VectorCraft's new envelopes map from the geometric box, as do contents with no width or no height, such as a single straight line. An envelope that can't be read, or that VectorCraft draws differently from the file's drawn art, keeps that drawn art, with a note. An envelope of type keeps its drawn art, with its type hidden and a note that says the type isn't editable (#3179; `crates/eps/src/import/ai/tests.rs`, `crates/eps/src/import/tests_native.rs`). + In a `.ai` opened from its editing data, an envelope made with a warp, a mesh or a top object opens as a live envelope of its contents, with its warp style, bend and distortions, its mesh points and handles or its top object's outline, its Envelope Options, its Fidelity and whether its contents are being edited. When Distort Appearance is on and the size the file saves for the contents matches their bounds with strokes, the contents map from that stroked box, until an edit in Edit Contents changes how far the strokes reach; VectorCraft's new envelopes map from the geometric box, as do contents with no width or no height, such as a single straight line. An envelope that can't be read, or that VectorCraft draws differently from the file's drawn art, keeps that drawn art, with a note. An envelope of type opens live, its type laid out from the text document as its contents, where VectorCraft's own envelope of that type draws as the drawn art the file saves (compared as for the other envelopes, seen on white and on black, so white type counts, and, as with them, only where the layers with it are compared with the page and draw as it); elsewhere, and where the type's font isn't available or the envelope is in a symbol's or a pattern's art, it keeps its drawn art, with its type hidden and a note that says the type isn't editable (#3179; `crates/eps/src/import/ai/tests.rs`, `crates/eps/src/import/tests_native.rs`). In a `.ai` opened from its editing data, Convert to Shape on a whole path or type object opens as a live Rectangle, Rounded Rectangle or Ellipse effect with its Absolute or Relative size, its corner radius, its place among the object's effects and whether it is hidden. A relative size in the file is added to each side. A relative Ellipse, or a setting VectorCraft doesn't read, keeps the drawn art (`crates/eps/src/import/ai/style/tests.rs`, `crates/eps/src/import/ai/tests.rs`, `crates/eps/src/import/tests_native.rs`). In a `.ai` opened from its editing data, Bas Relief in an art style takes its Detail, Smoothness and Light direction from the style's settings block, and an object other than type with it opens as itself with the live effect. A settings block that VectorCraft can't read (malformed, another effect's, a value out of range or a key it doesn't know) keeps the object's drawn look (part of #637; [raster effects reference](https://helpx.adobe.com/illustrator/desktop/special-effects-styles/apply-filter-effects/raster-effects-reference.html), [Photoshop File Formats Specification](https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/), [scripting the filter gallery](https://community.adobe.com/questions-712/scripting-support-for-the-filter-gallery-1064646); `crates/eps/src/import/ai/style/tests.rs`, `crates/eps/src/import/ai/tests.rs`). PDF export and `.ai` saves write a page with a side longer than 14,400 pt with a `UserUnit` (PDF 1.6), its longer side divided by 14,400 and rounded up to the next thousandth, so that no side is longer than 14,400 units (ISO 32000-1, Annex C.2); in PDF 1.3 to 1.5 files and PDF/X files such a page is written in points. From 0916b406549294db24d37b70004083353f79764e Mon Sep 17 00:00:00 2001 From: jmverges Date: Tue, 10 Nov 2026 15:46:26 +0000 Subject: [PATCH 34/38] .ai import: the page check weighs a live envelope of type as the art it draws The comparison with the page counts the area of each text object that shows as type, which it holds to less (type laid out again from the file's text can sit a little apart from the page's). It took the text object in a live envelope's contents for such type, at its own unbent box, so most of what an envelope of type draws went unweighed: one opened live where the page draws other art there, with only a note that the type differs. The envelope draws its type bent, as art, and is now weighed as such, as the drawn art it takes the place of is. Part of #3179. Co-Authored-By: Claude Opus 5.5 --- crates/eps/src/import/native/compare.rs | 7 +++++-- crates/eps/src/import/tests_native.rs | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/crates/eps/src/import/native/compare.rs b/crates/eps/src/import/native/compare.rs index 99cafb771..5b87a03c7 100644 --- a/crates/eps/src/import/native/compare.rs +++ b/crates/eps/src/import/native/compare.rs @@ -714,7 +714,8 @@ fn compared_with_solid(x: &vectorcraft_render::Rendered, y: &vectorcraft_render: } /// Where the type that shows in `nodes` is, in pixels of `region` drawn at `scale` (with a margin -/// for the strokes round it). +/// for the strokes round it). The type in a live envelope isn't: the envelope draws it bent, away +/// from its text object's box, and is weighed as the art it draws, as its drawn art would be. fn type_areas(nodes: &[Arc], region: Rect, scale: f64, out: &mut Vec) { for n in nodes.iter().filter(|n| n.visible) { if let NodeKind::Text(_) = n.kind @@ -723,7 +724,9 @@ fn type_areas(nodes: &[Arc], region: Rect, scale: f64, out: &mut Vec let px = |r: Rect| Rect::new((r.x0 - region.x0) * scale, (r.y0 - region.y0) * scale, (r.x1 - region.x0) * scale, (r.y1 - region.y0) * scale); out.push(px(b.inflate(OUTLINE_REACH, OUTLINE_REACH))); - } else if let Some(c) = n.children() { + } else if !matches!(n.kind, NodeKind::Envelope { .. }) + && let Some(c) = n.children() + { type_areas(c, region, scale, out); } } diff --git a/crates/eps/src/import/tests_native.rs b/crates/eps/src/import/tests_native.rs index cbfb7c928..ffa209a84 100644 --- a/crates/eps/src/import/tests_native.rs +++ b/crates/eps/src/import/tests_native.rs @@ -5473,6 +5473,24 @@ fn an_envelope_of_type_waits_on_the_page_of_layers_that_would_open_alone() { assert_eq!(loose, [("Note".to_string(), true)], "{notes:?}"); } +/// An envelope of type that draws as its drawn art doesn't open live where the page draws other art +/// there (the warp bent the other way): the page check weighs the live envelope as the art it draws, +/// not as type laid out again, so it sees the two differ. The file comes in as its page, as it does +/// with the drawn art (#3179). +#[test] +fn an_envelope_of_type_that_draws_apart_from_the_page_does_not_open_live() { + let Some(family) = installed_family() else { return }; + let doc = type_envelope_text(&family, None); + let text = made_envelope_type(&doc); + let (drawn, _) = drawn_envelope(&arc_envelope(&text, 30.0)); + let (_, page) = drawn_envelope(&arc_envelope(&text, -30.0)); + let art = native_with_text(&layer("Art", true, &format!("{}\n{}", square(10, 10), type_envelope(&arc_keys(0.3), &drawn))), &doc); + let r = import(&eps(&format!("{}\n{page}", page_square(10, 10)), &art)).unwrap(); + let (envelopes, _) = envelopes_and_type(&r.document); + assert!(envelopes.is_empty(), "{envelopes:?} {:?}", r.warnings); + assert!(r.warnings.iter().any(|n| n.contains("layers weren't read") && n.contains("differs from the page's")), "{:?}", r.warnings); +} + /// The fill of the first run of type `n`. fn text_fill(n: &Node) -> Option { let NodeKind::Text(t) = &n.kind else { return None }; From bf716bfeccc87b141cf69741cf93507fd88aeec6 Mon Sep 17 00:00:00 2001 From: jmverges Date: Tue, 10 Nov 2026 15:49:23 +0000 Subject: [PATCH 35/38] .ai import: envelopes of type keep their drawn art in layers used though they differ from the page An envelope of type opens live only where the layers with it draw as the page. Two ways of using layers that don't left it live all the same: the editing data trusted (they come from it with a note), and the objects that draw differently coming in as the page's art. In both the envelope now keeps its drawn art and its type, hidden, with the note, as where its drawn art is put back. A .ai whose layers would open alone, without its PDF part, then opens alone again, as it did before envelopes of type could open live, rather than compared with the page. The type put back no longer carries the mark of type made from the text document in its name. Part of #3179. Co-Authored-By: Claude Opus 5.5 --- crates/eps/src/import/native/build.rs | 41 ++++++++++++-- crates/eps/src/import/tests_native.rs | 79 +++++++++++++++++++++++++++ 2 files changed, 114 insertions(+), 6 deletions(-) diff --git a/crates/eps/src/import/native/build.rs b/crates/eps/src/import/native/build.rs index 4266b945c..e2480d6c5 100644 --- a/crates/eps/src/import/native/build.rs +++ b/crates/eps/src/import/native/build.rs @@ -493,9 +493,14 @@ pub(super) fn build_from( other => match drawn_or_trusted(&frame, &mut view, &mut doc, &envelopes_drawn, ¬es, unseen, trust, why, reflow) { Ok(more) => notes.extend(more), // The objects that draw differently from the page come in as its art, from - // the placement that draws closer to it. + // the placement that draws closer to it. The layers don't draw as the page: + // envelopes of type keep their drawn art and their type, hidden. Err(why) => { - let other = other.map(|(_, other, unseen)| (other, unseen)); + let typed = type_drawn(&mut doc, &envelopes_drawn); + let other = other.map(|(_, mut other, unseen)| { + type_drawn(&mut other, &envelopes_drawn); + (other, unseen) + }); let placements = closer_first(&frame, &mut view, (doc, unseen), other, reflow); let Some((kept, unseen, kept_notes)) = placements.into_iter().find_map(|(base, unseen)| { keep_differing(&base, &frame, &mut view, &threaded_frames, &shown, reflow).map(|(kept, n)| (kept, unseen, n)) @@ -503,7 +508,8 @@ pub(super) fn build_from( return Err(why); }; doc = kept; - notes.extend(unseen_note(unseen).into_iter().chain(kept_notes)); + let typed = typed.then(|| ai::ENVELOPE_TYPE_DRAWN.to_string()).filter(|n| !notes.contains(n)); + notes.extend(typed.into_iter().chain(unseen_note(unseen)).chain(kept_notes)); } }, }, @@ -1287,8 +1293,9 @@ fn slot_content( /// The layers `doc` don't draw as the page (`why`), `unseen` text objects of them taken out: the /// notes to add when `doc` with each live envelope `drawn` lists replaced by the drawn art it took /// the place of (see [`ai::Structure::envelopes_drawn`]) draws as the page, `doc` then being that; -/// else, with `trust`, the notes for `doc` as it is. An error, `why`, otherwise. The envelope note -/// is added unless `notes` has it. +/// else, with `trust`, the notes for `doc` as it is but for its envelopes of type, which keep their +/// drawn art ([`type_drawn`]). An error, `why`, otherwise. The envelope note is added unless +/// `notes` has it. #[allow(clippy::too_many_arguments)] fn drawn_or_trusted( frame: &Frame<'_>, @@ -1320,11 +1327,33 @@ fn drawn_or_trusted( } } if trust { - return Ok(unseen_note(unseen).into_iter().chain([format!("{why}: they come from its editing data all the same")]).collect()); + let typed = type_drawn(doc, drawn).then(|| ai::ENVELOPE_TYPE_DRAWN.to_string()).filter(|n| !notes.contains(n)); + let trusted = format!("{why}: they come from its editing data all the same"); + return Ok(typed.into_iter().chain(unseen_note(unseen)).chain([trusted]).collect()); } Err(why) } +/// Put each live envelope of type in `drawn` (as [`drawn_or_trusted`] takes them) that `doc` still +/// has back as its hidden type and its drawn art: whether one was. An envelope of type opens live +/// only where the layers with it draw as the page, so where they don't, though they are used all +/// the same (the editing data trusted, or the objects that differ come in as the page's art), it +/// keeps its drawn art. +fn type_drawn(doc: &mut Document, drawn: &[(NodeId, Vec, &str)]) -> bool { + let typed: Vec<(NodeId, Vec)> = drawn + .iter() + .filter(|(id, _, say)| *say == ai::ENVELOPE_TYPE_DRAWN && doc.node(*id).is_some()) + .map(|(id, art, _)| (*id, art.clone())) + .collect(); + if typed.is_empty() || doc.insert_beside(typed, true).is_err() { + return false; + } + // The type put back is still marked as made from the text document, which the rest no longer + // is once the page has weighed it ([`prune_unseen`]). + unmark(&mut doc.layers, true, true, &mut vec![]); + true +} + /// `doc` with the placed files `placed` lists at their other placement (see /// [`ai::Structure::placed_elsewhere`]): each image's transform followed by its move. `None` when /// no listed image survives replacement. diff --git a/crates/eps/src/import/tests_native.rs b/crates/eps/src/import/tests_native.rs index ffa209a84..6a2723dc9 100644 --- a/crates/eps/src/import/tests_native.rs +++ b/crates/eps/src/import/tests_native.rs @@ -5473,6 +5473,20 @@ fn an_envelope_of_type_waits_on_the_page_of_layers_that_would_open_alone() { assert_eq!(loose, [("Note".to_string(), true)], "{notes:?}"); } +/// The hidden type outside envelopes in the layers of `d`: each text object's name. +fn loose_type_names(d: &Document) -> Vec> { + let mut out = vec![]; + fn walk(n: &Node, out: &mut Vec>) { + match &n.kind { + NodeKind::Envelope { .. } => {} + NodeKind::Text(_) => out.push(n.name.clone()), + _ => n.children().into_iter().flatten().for_each(|c| walk(c, out)), + } + } + d.layers.iter().for_each(|l| walk(l, &mut out)); + out +} + /// An envelope of type that draws as its drawn art doesn't open live where the page draws other art /// there (the warp bent the other way): the page check weighs the live envelope as the art it draws, /// not as type laid out again, so it sees the two differ. The file comes in as its page, as it does @@ -5491,6 +5505,71 @@ fn an_envelope_of_type_that_draws_apart_from_the_page_does_not_open_live() { assert!(r.warnings.iter().any(|n| n.contains("layers weren't read") && n.contains("differs from the page's")), "{:?}", r.warnings); } +/// Layers used though they draw differently from the page (the editing data trusted) leave an +/// envelope of type its drawn art and its type, hidden, with the note, even where it draws as that +/// drawn art: one opens live only where the layers with it draw as the page. A `.ai` whose layers +/// would open alone then opens alone, as it would without the envelope (#3179). +#[test] +fn trusted_layers_that_draw_apart_from_the_page_leave_an_envelope_of_type_its_drawn_art() { + let Some(family) = installed_family() else { return }; + let doc = type_envelope_text(&family, None); + let (drawn, _) = drawn_envelope(&arc_envelope(&made_envelope_type(&doc), 30.0)); + let art = native_with_text(&layer("Art", true, &format!("{}\n{}", square(10, 10), type_envelope(&arc_keys(0.3), &drawn))), &doc); + // The page has the square only. + let r = crate::import_trusting(&eps(&page_square(10, 10), &art), true, true).unwrap(); + assert_eq!(names(&r.document), ["Art"], "{:?}", r.warnings); + let (envelopes, loose) = envelopes_and_type(&r.document); + assert!(envelopes.is_empty(), "{envelopes:?}"); + assert_eq!(loose, [("FLUX".to_string(), false)], "{:?}", r.warnings); + // The type put back is named as the file names it. + assert_eq!(loose_type_names(&r.document), [None]); + assert!(r.warnings.iter().any(|n| n == crate::import::ai::ENVELOPE_TYPE_DRAWN), "{:?}", r.warnings); + assert!(r.warnings.iter().any(|n| n.ends_with("they come from its editing data all the same")), "{:?}", r.warnings); + // A `.ai` whose layers would open alone, its PDF part drawn apart from them. + let stories = [("FLUX\r", 0, (8170.0, 8180.0), (0.0, 0.0), 0.0), ("Note\r", 0, (8170.0, 8220.0), (0.0, 0.0), 0.0)]; + let mut doc = text_document(&stories).replace("(Helvetica)", &format!("({family})")); + while ascii85(doc.as_bytes()).len() % 60 == 1 { + doc.push('\n'); + } + let art = format!("u\n{}U\n{}", text_object(1), type_envelope(&arc_keys(0.3), &drawn)); + let file = private(&native_with_text(&layer("Art", true, &art), &doc)); + let names = crate::NameEncoding::default(); + let Err(layers) = crate::ai_layers_alone(&file, &[(100.0, 100.0)], 1.0, names) else { panic!("opened without the page") }; + let (d, notes, read) = crate::layered_ai_from(layers, pdf_part(80.0, 80.0), vec![], false, true, 1.0, names); + assert!(read, "{notes:?}"); + let (envelopes, loose) = envelopes_and_type(&d); + assert!(envelopes.is_empty(), "{envelopes:?}"); + assert_eq!(loose, [("Note".to_string(), true), ("FLUX".to_string(), false)], "{notes:?}"); + assert!(notes.iter().any(|n| n == crate::import::ai::ENVELOPE_TYPE_DRAWN), "{notes:?}"); + assert!(!notes.iter().any(|n| n.ends_with("they come from its editing data all the same")), "{notes:?}"); +} + +/// Layers whose objects that draw differently from the page come in as its art leave an envelope +/// of type its drawn art and its type, hidden, with the note, even where the envelope draws as the +/// page: the layers with it don't (#3179). +#[test] +fn layers_with_objects_that_come_in_as_the_page_art_leave_an_envelope_of_type_its_drawn_art() { + let Some(family) = installed_family() else { return }; + let doc = type_envelope_text(&family, None); + let (drawn, page) = drawn_envelope(&arc_envelope(&made_envelope_type(&doc), 30.0)); + // A square black in the layers and cyan on the page, away from the envelope. + let body = format!("{}\n{}\n{}", square(10, 10), square_16(76, BLACK), type_envelope(&arc_keys(0.3), &drawn)); + let art = native_with_text(&layer("Art", true, &body), &doc); + let cyan = "1 0 0 0 setcmykcolor 76 10 moveto 92 10 lineto 92 26 lineto 76 26 lineto closepath fill"; + let r = import(&eps(&format!("{}\n{cyan}\n{page}", page_square(10, 10)), &art)).unwrap(); + assert_eq!(names(&r.document), ["Art"], "{:?}", r.warnings); + assert!( + r.warnings.iter().any(|n| n.starts_with("objects that drew differently from the file's own page came in as its art")), + "{:?}", + r.warnings + ); + let (envelopes, loose) = envelopes_and_type(&r.document); + assert!(envelopes.is_empty(), "{envelopes:?}"); + assert_eq!(loose, [("FLUX".to_string(), false)], "{:?}", r.warnings); + assert_eq!(loose_type_names(&r.document), [None]); + assert!(r.warnings.iter().any(|n| n == crate::import::ai::ENVELOPE_TYPE_DRAWN), "{:?}", r.warnings); +} + /// The fill of the first run of type `n`. fn text_fill(n: &Node) -> Option { let NodeKind::Text(t) = &n.kind else { return None }; From 06d0651ebf3a8eaf2219852394c7f56bffd6c473 Mon Sep 17 00:00:00 2001 From: jmverges Date: Tue, 10 Nov 2026 15:51:01 +0000 Subject: [PATCH 36/38] .ai import: an envelope of type bound to a visibility variable keeps its drawn art A variable library binds the text object a slot of the envelope's contents became, which stays hidden beside the drawn art. Once the envelope opened live that hidden text object went, and its type in the envelope has an id of its own, so a visibility binding was dropped and counted as one that wasn't made. A slot bound to a text variable already kept the drawn art; one bound to a visibility variable does too now, with the note, so the binding is kept. Carrying the binding over to the type in the live envelope would bind an object inside an envelope's contents, which nothing in the import, the data sets or the .ai writer is known to handle, so it is left for later. Part of #3179. Co-Authored-By: Claude Opus 5.5 --- crates/eps/src/import/native/build.rs | 39 ++++++++++++++++--------- crates/eps/src/import/tests_native.rs | 42 ++++++++++++++++++++++++++- 2 files changed, 67 insertions(+), 14 deletions(-) diff --git a/crates/eps/src/import/native/build.rs b/crates/eps/src/import/native/build.rs index e2480d6c5..b037b33fe 100644 --- a/crates/eps/src/import/native/build.rs +++ b/crates/eps/src/import/native/build.rs @@ -429,11 +429,13 @@ pub(super) fn build_from( } // Area type flows around the wrap objects above it before it is compared with the page. resolve_text_wrap(&mut doc)?; - let bound_text = text_bound_slots(&doc, &bindings, &xml_ids); + let bound_text = bound_slots(&doc, &bindings, &xml_ids, VariableKind::Text); + let bound_visible = bound_slots(&doc, &bindings, &xml_ids, VariableKind::Visibility); // Without a page to compare the layers with, envelopes of type keep their drawn art: like the // other live envelopes, one opens live only where the layers with it draw as the page. let warped_type = warped_type.compared(frame.is_some(), &mut notes); - let typed = settle_warped_type(warped_type, &mut doc, &filled, &threaded_frames, &bound_text, &mut restyled, &mut notes)?; + let bound = [&bound_text, &bound_visible]; + let typed = settle_warped_type(warped_type, &mut doc, &filled, &threaded_frames, bound, &mut restyled, &mut notes)?; // The live envelopes and what takes the place of each again, when the layers with them don't // draw as the page, with the note that says so. let envelopes_drawn: Vec<(NodeId, Vec, &str)> = envelopes_drawn @@ -637,41 +639,52 @@ fn bind_variables( notes.extend(ai::bindings_note(skipped)); } -/// The text object slots of `doc` that `bindings` binds to a text variable, each named by its XML id -/// (`ids`). -fn text_bound_slots(doc: &Document, bindings: &[ai::BindingRef], ids: &std::collections::HashMap>) -> BTreeSet { +/// The objects of `doc`, text object slots among them, that `bindings` binds to a variable of +/// `kind`, each named by its XML id (`ids`). +fn bound_slots( + doc: &Document, + bindings: &[ai::BindingRef], + ids: &std::collections::HashMap>, + kind: VariableKind, +) -> BTreeSet { bindings .iter() - .filter(|b| doc.variables.variable(&b.variable).is_some_and(|v| v.kind == VariableKind::Text)) + .filter(|b| doc.variables.variable(&b.variable).is_some_and(|v| v.kind == kind)) .filter_map(|b| b.target.as_ref().and_then(|t| ids.get(t).copied().flatten())) .collect() } /// Warped type stays type where its live warp draws as its look, and where a text variable binds it -/// (`bound_text`, by slot). Elsewhere the look takes its place, and one note says how many did. +/// (`bound`'s first set, by slot). Elsewhere the look takes its place, and one note says how many did. /// `filled` says what each slot became, and the comparisons with the page leave out the boxes in /// `restyled`. Fails as [`resolve_text_wrap`] does when the looks give the wrap shapes too many /// anchors. Before that, the envelopes of type open live where they draw as their drawn art, with /// the type of their text slots (made type, one text object for the slot, not threaded, bound to -/// no text variable and no wrap object); the others keep their drawn art, with a note. Returns -/// each live envelope of type's id with what takes its place again (its hidden type, then its -/// drawn art). +/// no text variable and no visibility variable, `bound`'s second set, and no wrap object); the +/// others keep their drawn art, with a note. Returns each live envelope of type's id with what +/// takes its place again (its hidden type, then its drawn art). fn settle_warped_type( mut warped: ai::WarpedType, doc: &mut Document, filled: &std::collections::HashMap, threaded: &BTreeSet, - bound_text: &BTreeSet, + bound: [&BTreeSet; 2], restyled: &mut Vec, notes: &mut Vec, ) -> Result)>, String> { + let [bound_text, bound_visible] = bound; let made = |slot| match filled.get(&slot).map(|f| f.text.as_slice()).filter(|_| !bound_text.contains(&slot)) { Some(&[one]) => Some(one), _ => None, }; // An envelope's type: the one text object a slot became (hidden, with nothing else), named as - // the slot (made type is no longer weighed against the page: the envelope draws it). + // the slot (made type is no longer weighed against the page: the envelope draws it). A variable + // binds the hidden text object a slot became, which the live envelope's type takes the place of + // with an id of its own: a slot bound to one keeps its type hidden, and the drawn art stays. let type_of = |doc: &Document, slot: NodeId| { + if bound_visible.contains(&slot) { + return None; + } let id = made(slot).filter(|id| !threaded.contains(id) && filled.get(&slot).is_some_and(|f| f.objects == [*id]))?; let mut text = doc.node(id).filter(|n| matches!(n.kind, NodeKind::Text(_)) && n.extras().wrap.is_none())?.clone(); if let Some(name) = text.name.as_deref().and_then(|n| n.strip_prefix(MADE)) { @@ -2063,7 +2076,7 @@ mod candidate_tests { let warped = ai::WarpedType::unchecked(vec![(id, (*look).clone())]); let filled = std::collections::HashMap::from([(id, Filled { text: vec![id], objects: vec![id] })]); let mut notes = vec![]; - let settled = settle_warped_type(warped, &mut doc, &filled, &BTreeSet::new(), &BTreeSet::new(), &mut vec![], &mut notes); + let settled = settle_warped_type(warped, &mut doc, &filled, &BTreeSet::new(), [&BTreeSet::new(); 2], &mut vec![], &mut notes); assert_eq!(settled.is_ok(), ok, "{texts} text objects"); assert_eq!(notes, ["warped text objects that came in as drawn art, not as editable type: 1"]); } diff --git a/crates/eps/src/import/tests_native.rs b/crates/eps/src/import/tests_native.rs index 6a2723dc9..f71c037c9 100644 --- a/crates/eps/src/import/tests_native.rs +++ b/crates/eps/src/import/tests_native.rs @@ -5296,6 +5296,12 @@ fn arc_keys(bend: f64) -> String { /// it: on `%_` lines the group with its mesh, its dictionary with the text object in its contents, /// then its drawn art `drawn` on plain lines and the plugin's `XP`. fn type_envelope(keys: &str, drawn: &str) -> String { + type_envelope_with(keys, drawn, "") +} + +/// [`type_envelope`], with `after` (on plain lines, written on `%_` ones) after the text object in +/// its contents, such as the text object's XML id. +fn type_envelope_with(keys: &str, drawn: &str, after: &str) -> String { let hidden = |s: &str| -> String { s.lines().map(|l| format!("%_{l}\n")).collect() }; let corner = |x: f64, y: f64, nx: f64, ny: f64, px: f64, py: f64| format!("[0.8 1 {x} {y} {nx} {ny} 1 {px} {py} 1] /N X#\n"); let mesh = format!( @@ -5309,7 +5315,7 @@ fn type_envelope(keys: &str, drawn: &str) -> String { "{}{}%_X=\n{}%_X+\n%_ (AIDeformArtKey) ,\n%_1 /Int (AIDeformStyle) ,\n%_;\n%_\n%_9 () XW\nu\n{drawn}U\n9 () XW\n(Adobe Deform Plugin) 1 0 0 XP\n", hidden(&format!("u\n{mesh}U\n")), hidden(&format!("/ArtDictionary :\n{keys}")), - hidden(&format!("u\n{}U\n9 () XW\n", text_object(0))), + hidden(&format!("u\n{}{after}U\n9 () XW\n", text_object(0))), ) } @@ -5570,6 +5576,40 @@ fn layers_with_objects_that_come_in_as_the_page_art_leave_an_envelope_of_type_it assert!(r.warnings.iter().any(|n| n == crate::import::ai::ENVELOPE_TYPE_DRAWN), "{:?}", r.warnings); } +/// An envelope of type whose type a visibility variable binds keeps its drawn art, as one whose type +/// a text variable binds does: the binding stays the hidden type's, which the live envelope would +/// take the place of (#3179). +#[test] +fn an_envelope_of_type_bound_to_a_visibility_variable_keeps_its_drawn_art() { + use vectorcraft_testkit::ai::{XmlValue, named, variable_library_data, xml_element, xml_node}; + let Some(family) = installed_family() else { return }; + let doc = type_envelope_text(&family, None); + let (drawn, page) = drawn_envelope(&arc_envelope(&made_envelope_type(&doc), 30.0)); + let reference = "/XMLUIDREF : (type_x5F_1) ;"; + let attributes = [("trait", XmlValue::Str("visibility")), ("varName", XmlValue::Unicode("v_shown")), ("docRef", XmlValue::Raw(reference))]; + let declared = [xml_node(1, "variable", XmlValue::Str(""), &attributes, &[])]; + let set = xml_element("variableSet", &[("locked", "none")], &[xml_element("variables", &[], &declared)]); + let library = variable_library_data(&xml_element("variableSets", &[], &[set])); + // The text object's XML id, on plain lines as `type_envelope_with` takes it. + let id = named("type_x5F_1").replace("%_", ""); + let envelope = type_envelope_with(&arc_keys(0.3), &drawn, &id); + let art = native_with_text(&(library + &layer("Art", true, &format!("{}\n{envelope}", square(10, 10)))), &doc); + let r = import(&eps(&format!("{}\n{page}", page_square(10, 10)), &art)).unwrap(); + assert_eq!(names(&r.document), ["Art"], "{:?}", r.warnings); + let (envelopes, loose) = envelopes_and_type(&r.document); + assert!(envelopes.is_empty(), "{envelopes:?}"); + assert_eq!(loose, [("FLUX".to_string(), false)], "{:?}", r.warnings); + let mut text = vec![]; + r.document.walk(|n| { + if matches!(n.kind, NodeKind::Text(_)) { + text.push(n.id); + } + }); + assert_eq!(r.document.variables.objects_of("v_shown"), text, "{:?}", r.warnings); + assert!(r.warnings.iter().any(|n| n == crate::import::ai::ENVELOPE_TYPE_DRAWN), "{:?}", r.warnings); + assert!(!r.warnings.iter().any(|n| n.contains("variable bindings")), "{:?}", r.warnings); +} + /// The fill of the first run of type `n`. fn text_fill(n: &Node) -> Option { let NodeKind::Text(t) = &n.kind else { return None }; From 3995dc79e4e7719091df3ef35398011485d572e2 Mon Sep 17 00:00:00 2001 From: jmverges Date: Tue, 10 Nov 2026 15:51:18 +0000 Subject: [PATCH 37/38] docs: envelopes of type held to the page, and visibility bindings The file-format revision row for #3179 says that the page check weighs a live envelope of type as the art it draws, that layers used though they don't draw as the page leave envelopes of type their drawn art, and that a slot bound to a visibility variable keeps it too, and lists the new tests. The parity assessment's evidence says the same, and the change gets its row and status entry there. Part of #3179. Co-Authored-By: Claude Opus 5.5 --- docs/file-format-parity.md | 4 ++-- docs/target-app-parity.md | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index 60bec1191..7f36fd1c3 100644 --- a/docs/file-format-parity.md +++ b/docs/file-format-parity.md @@ -1,6 +1,6 @@ # File format parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-10 · **Change:** minor (`.ai` import: envelopes of type open live where they draw as their drawn art, #3179; `.ai` and EPS import: a note for type in an envelope, #3179; `.ai` and EPS import: the live envelope and warped type check sees white art; PDF import: DeviceN images of one ink or of process plates keep their inks, #6348; `.ai` import: images in a spot ink's Separation space keep their ink, #6350; PDF import: Separation images keep their ink, #6314; PDF editable import preserves authored ActualText once, including empty/nested spans and authored ordering (#6165); cross-line/style layout remains approximate; scores and estimates unchanged; PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; PDF import: image masks open colorized by the color they are painted in, #6313; EPS and `.ai` editing data: a ligature the page names by a private-use glyph matches its text object; EPS import: CIDFonts only named, over a Unicode CMap, set as live type; EPS import: CMap mappings to base-font codes and glyph names; EPS import: CID-keyed fonts in FontSets (CFF) draw their glyphs; EPS import: `kshow` procedures kern live type; EPS import: embedded Type 1 fonts' glyphs and widths; EPS import: CID-keyed fonts draw their glyphs; EPS import: Type 42 fonts' characters and widths from the font they carry, show operators' spacing kept as tracking; EPS import: an image decoder's own buffers stop counting against the memory budget once it is decoded; EPS import: sampled functions interpolate between their samples; `.ai` and EPS type from the text document keeps its tab stops; it keeps auto leading and hyphenation options, and may show more of the file's last lines in an area frame; `.ai` and EPS import: Art brushes in the older form of their objects come into the Brushes panel; EPS import: gradients drawn as abutting strips come in as one gradient, without seams; EPS import: composite fonts draw their Type 42 glyphs; SVG import: filter and mask regions clip, #5876, #5875; SVG import: Gaussian blurs keep `color-interpolation-filters`, #5877; PDF and `.ai` export: objects' URLs as links, #5785; SVG import: `color-interpolation="linearRGB"` gradients, #5878; `.ai` import: paths with a Calligraphic brush open brushed, #372) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-10 · **Change:** minor (`.ai` and EPS import: envelopes of type open live where they draw as their drawn art and the layers with them draw as the page, #3179; `.ai` and EPS import: a note for type in an envelope, #3179; `.ai` and EPS import: the live envelope and warped type check sees white art; PDF import: DeviceN images of one ink or of process plates keep their inks, #6348; `.ai` import: images in a spot ink's Separation space keep their ink, #6350; PDF import: Separation images keep their ink, #6314; PDF editable import preserves authored ActualText once, including empty/nested spans and authored ordering (#6165); cross-line/style layout remains approximate; scores and estimates unchanged; PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; PDF import: image masks open colorized by the color they are painted in, #6313; EPS and `.ai` editing data: a ligature the page names by a private-use glyph matches its text object; EPS import: CIDFonts only named, over a Unicode CMap, set as live type; EPS import: CMap mappings to base-font codes and glyph names; EPS import: CID-keyed fonts in FontSets (CFF) draw their glyphs; EPS import: `kshow` procedures kern live type; EPS import: embedded Type 1 fonts' glyphs and widths; EPS import: CID-keyed fonts draw their glyphs; EPS import: Type 42 fonts' characters and widths from the font they carry, show operators' spacing kept as tracking; EPS import: an image decoder's own buffers stop counting against the memory budget once it is decoded; EPS import: sampled functions interpolate between their samples; `.ai` and EPS type from the text document keeps its tab stops; it keeps auto leading and hyphenation options, and may show more of the file's last lines in an area frame; `.ai` and EPS import: Art brushes in the older form of their objects come into the Brushes panel; EPS import: gradients drawn as abutting strips come in as one gradient, without seams; EPS import: composite fonts draw their Type 42 glyphs; SVG import: filter and mask regions clip, #5876, #5875; SVG import: Gaussian blurs keep `color-interpolation-filters`, #5877; PDF and `.ai` export: objects' URLs as links, #5785; SVG import: `color-interpolation="linearRGB"` gradients, #5878; `.ai` import: paths with a Calligraphic brush open brushed, #372) · **Target:** Adobe Illustrator 2026 (30.x) Every format Illustrator opens, places, saves or exports, against VectorCraft's format table (`FORMATS`, `OPEN_EXTS`, `PLACE_EXTS` in `crates/engine/src/cmd/fileio/formats.rs`). Illustrator's list comes from its public @@ -336,7 +336,7 @@ Stop opacity on translated artboards remains outside this regression coverage. | Date | Change | Summary | |---|---|---| -| 2026-11-10 | minor | `.ai` and EPS import: an envelope of type (a warp, a mesh or a top object around type) kept its drawn art, its type hidden, because the type is laid out from the text document only after the layers are read. It now waits for that: its text slots get their type, and where VectorCraft's own envelope of that type draws as the drawn art the file saves (the same check and budget as other envelopes, seen on white and on black), it opens live in the drawn art's place, its type editable in Edit Contents, and the hidden copy goes. As with other live envelopes, the layers with one are then compared with the page, and when they don't draw as it the drawn art and the hidden type are put back: a `.ai` whose layers would open alone, without its PDF part, imports the PDF part to compare them with, and should they still not draw as it, or should no envelope of type open live, they open alone as before, with the envelopes' drawn art. Type in a font that isn't available, slots without type of their own (threaded, bound to a text variable, a wrap object, type a look draws), envelopes in a symbol's or a pattern's art, and envelopes that draw differently keep their drawn art, with the note (#3179). Tests `tests_native::an_envelope_of_type_that_draws_as_its_drawn_art_opens_live`, `an_envelope_of_type_that_draws_apart_from_its_drawn_art_keeps_it`, `an_envelope_of_white_type_that_draws_apart_from_its_drawn_art_keeps_it`, `an_envelope_of_type_waits_on_the_page_of_layers_that_would_open_alone` | +| 2026-11-10 | minor | `.ai` and EPS import: an envelope of type (a warp, a mesh or a top object around type) kept its drawn art, its type hidden, because the type is laid out from the text document only after the layers are read. It now waits for that: its text slots get their type, and where VectorCraft's own envelope of that type draws as the drawn art the file saves (the same check and budget as other envelopes, seen on white and on black), it opens live in the drawn art's place, its type editable in Edit Contents, and the hidden copy goes. As with other live envelopes, the layers with one are then compared with the page, which weighs the envelope as the art it draws (not as type laid out again, which the comparison allows more for), and when they don't draw as it the drawn art and the hidden type are put back. Layers used though they don't draw as the page (the editing data trusted, or the objects that differ coming in as the page's art) leave envelopes of type their drawn art and hidden type too. A `.ai` whose layers would open alone, without its PDF part, imports the PDF part to compare them with, and should they still not draw as it, or should no envelope of type open live, they open alone as before, with the envelopes' drawn art. Type in a font that isn't available, slots without type of their own (threaded, bound to a text or a visibility variable, a wrap object, type a look draws), envelopes in a symbol's or a pattern's art, and envelopes that draw differently keep their drawn art, with the note (#3179). Tests `tests_native::an_envelope_of_type_that_draws_as_its_drawn_art_opens_live`, `an_envelope_of_type_that_draws_apart_from_its_drawn_art_keeps_it`, `an_envelope_of_white_type_that_draws_apart_from_its_drawn_art_keeps_it`, `an_envelope_of_type_waits_on_the_page_of_layers_that_would_open_alone`, `an_envelope_of_type_that_draws_apart_from_the_page_does_not_open_live`, `trusted_layers_that_draw_apart_from_the_page_leave_an_envelope_of_type_its_drawn_art`, `layers_with_objects_that_come_in_as_the_page_art_leave_an_envelope_of_type_its_drawn_art`, `an_envelope_of_type_bound_to_a_visibility_variable_keeps_its_drawn_art` | | 2026-11-10 | minor | `.ai` and EPS import: an envelope of type, which keeps its drawn art with its type hidden, now says so in the import notes ("type in an envelope comes in as the envelope's drawn art, not as editable type: the type is kept, hidden"); nothing said it before, as the other envelopes that keep their drawn art do. The type is still not editable in the envelope (part of #3179). Test `tests_native::envelope_options_keep_the_source_text_hidden_under_its_outlined_appearance` | | 2026-11-10 | minor | `.ai` and EPS import: an envelope of type, which keeps its drawn art with its type hidden, now says so in the import notes ("type in an envelope comes in as the envelope's drawn art, not as editable type"); nothing said it before, while the other envelopes that keep their drawn art have a note. The type is still not editable in the envelope (part of #3179). Test `tests_native::envelope_options_keep_the_source_text_hidden_under_its_outlined_appearance` | | 2026-11-10 | minor | `.ai` and EPS import: the check that keeps a live envelope or warped type only where it draws as the art saved with it renders each on its own, over nothing, and compared the pixels seen on white, where white art looks like nothing. White contents that drew somewhere else passed it and opened live in the wrong place (white warped type off its banner, in users' files). Pixels now also differ when they look different seen on black, so such art keeps its saved art, as colored art does. Tests `import::ai::tests::a_white_envelope_that_doesnt_come_out_as_drawn_keeps_its_drawn_art`, `white_warped_type_that_draws_elsewhere_comes_in_as_its_look` | diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 76d190f08..833579abc 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1,6 +1,6 @@ # Parity with Adobe Illustrator -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-10 · **Change:** minor (G5: Game HUD graphic style library; bundled content ~34% → ~36%; previously minor: Line Arrow arrowhead (#5421; scores and estimates unchanged); G5: Map Markers built-in symbol library; bundled content ~32% → ~34%; previously minor: right-click on type: Spelling › Auto Spell Check, with its check mark, #4936; previously minor: right-click on a word Auto Spell Check underlined: its suggestions, Ignore All and Add to Dictionary, #4936; previously minor: Canvas: a 3D effect changed on type or a group redraws where its solid was; Fit to Artwork Bounds and per-artboard SVG export build each solid once for all artboards; previously minor: Effect Gallery: a thumbnail for each filter and a preview area inside the dialog, on an original drawing; Raster effects 52% → 55%, hours 1–2 → none; headline numbers unchanged; Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: PDF editable import preserves authored ActualText once, including empty/nested spans and authored ordering (#6165); cross-line/style layout remains approximate; scores and estimates unchanged; PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; G5: UI Frames built-in symbol library with default 9-slice; bundled content ~32%; previously minor: G5: dynamic symbol part on-canvas opacity/blend/gradient/pattern; strokes remaining no longer lists Overlap/auto corners; symbols ~88%; previously minor: interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; previously minor: harakat paint fallback clarified; previously minor: current Guides feature-area rows consolidated using existing ruler evidence; scores and estimates unchanged; previously minor: Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; previously minor: G5 docs: Art brush Overlap and Pattern brush auto corners removed from strokes remaining; symbols remaining lists dynamic on-canvas beyond solid and guides outside the art; previously minor: custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: EPS import: embedded fonts (composite, Type 42, CID-keyed, Type 1, CFF FontSet) draw their glyphs; spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; `.ai`/EPS type keeps its tab stops, and right-aligned lines with tabs lay out as the file's page, #6147; `.ai`/EPS type from the text document lays its lines out closer to the file's page, within limits, #6137; `.ai` and EPS import: older Art brush objects read; numeric fields step on wheel hover without taking keyboard focus, and a panel wheel turn cannot seed a canvas zoom glide, #5949; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen; Tools stays beside the canvas with left-side panels; new gradients follow document color mode; Selection QoL: marquee modes and visible feedback, #5313; SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; the canvas keeps selection outlines and anchors between frames; Indian-language hyphenation review and license allowlist, #4947; Stroke gradients: an arrowhead aligned to the tip is painted to the corners of its base, #5964; Drop Shadow and glow dialogs: spinner arrows, #5367, #5368; Stroke Weight Up and Down shortcuts, #5797; Hindi catalog and contributor loanword preferences; full wording review in progress; Properties in Edit Artboards: a Transform section with the reference point above the Artboards section, #5219; the Control bar's Stroke: and Opacity: links scrub their fields, #4088; an image zoomed in on is separated, colorized or proofed only where it shows (#4155); a document's first frame renders on the render worker, so opening a heavy file no longer freezes the window, #4155; Control bar Paragraph section with type selected, #5372; translating unchanged type reuses its glyph and character-stroke outlines; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026; Units › East Asian Type acts, with Q and Ha units; scores unchanged) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-10 · **Change:** minor (`.ai` and EPS import: envelopes of type open live where they draw as their drawn art and the layers with them draw as the page (#3179; scores and estimates unchanged); G5: Game HUD graphic style library; bundled content ~34% → ~36%; previously minor: Line Arrow arrowhead (#5421; scores and estimates unchanged); G5: Map Markers built-in symbol library; bundled content ~32% → ~34%; previously minor: right-click on type: Spelling › Auto Spell Check, with its check mark, #4936; previously minor: right-click on a word Auto Spell Check underlined: its suggestions, Ignore All and Add to Dictionary, #4936; previously minor: Canvas: a 3D effect changed on type or a group redraws where its solid was; Fit to Artwork Bounds and per-artboard SVG export build each solid once for all artboards; previously minor: Effect Gallery: a thumbnail for each filter and a preview area inside the dialog, on an original drawing; Raster effects 52% → 55%, hours 1–2 → none; headline numbers unchanged; Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: PDF editable import preserves authored ActualText once, including empty/nested spans and authored ordering (#6165); cross-line/style layout remains approximate; scores and estimates unchanged; PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; G5: UI Frames built-in symbol library with default 9-slice; bundled content ~32%; previously minor: G5: dynamic symbol part on-canvas opacity/blend/gradient/pattern; strokes remaining no longer lists Overlap/auto corners; symbols ~88%; previously minor: interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; previously minor: harakat paint fallback clarified; previously minor: current Guides feature-area rows consolidated using existing ruler evidence; scores and estimates unchanged; previously minor: Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; previously minor: G5 docs: Art brush Overlap and Pattern brush auto corners removed from strokes remaining; symbols remaining lists dynamic on-canvas beyond solid and guides outside the art; previously minor: custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: EPS import: embedded fonts (composite, Type 42, CID-keyed, Type 1, CFF FontSet) draw their glyphs; spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; `.ai`/EPS type keeps its tab stops, and right-aligned lines with tabs lay out as the file's page, #6147; `.ai`/EPS type from the text document lays its lines out closer to the file's page, within limits, #6137; `.ai` and EPS import: older Art brush objects read; numeric fields step on wheel hover without taking keyboard focus, and a panel wheel turn cannot seed a canvas zoom glide, #5949; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen; Tools stays beside the canvas with left-side panels; new gradients follow document color mode; Selection QoL: marquee modes and visible feedback, #5313; SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; the canvas keeps selection outlines and anchors between frames; Indian-language hyphenation review and license allowlist, #4947; Stroke gradients: an arrowhead aligned to the tip is painted to the corners of its base, #5964; Drop Shadow and glow dialogs: spinner arrows, #5367, #5368; Stroke Weight Up and Down shortcuts, #5797; Hindi catalog and contributor loanword preferences; full wording review in progress; Properties in Edit Artboards: a Transform section with the reference point above the Artboards section, #5219; the Control bar's Stroke: and Opacity: links scrub their fields, #4088; an image zoomed in on is separated, colorized or proofed only where it shows (#4155); a document's first frame renders on the render worker, so opening a heavy file no longer freezes the window, #4155; Control bar Paragraph section with type selected, #5372; translating unchanged type reuses its glyph and character-stroke outlines; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026; Units › East Asian Type acts, with Q and Ha units; scores unchanged) > > Vector Halftone preview update (2026-11-10): temporary dot selection marks are hidden, and the bounding box stays available (#6369). > @@ -1403,7 +1403,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur A `.ai`, or a PDF saved with Illustrator's editing data, whose editing data can't be decompressed opens from its PDF part with a warning that its layers weren't read and why, and an export or Save As to its own path asks first (`acknowledgeLoss`), as for a file whose layers the reader can't read (`crates/eps/src/import/tests_native.rs`, `crates/engine/src/cmd/fileio/tests_aiimport.rs`). A `.ai` or EPS opened from its editing data with one object on a shown layer that the reader can't read (an unknown operator inside a group, a pattern fill whose pattern it doesn't have, a symbol instance, a placed file without a usable preview) keeps its layers and groups: in that object's place, its layer gets a group of the page's objects inside the object's box, named after it, with a warning that lists what came in as the page's art and an export or Save As to its own path asking first (`acknowledgeLoss`). The file still opens from its page or PDF part when no box can be found, when live art or another unread object shares the page art inside it, when the object spans two artboards or lies on an artboard that has no page, when no page art lies inside its box, when that page art is inside a page group with a clip, mask, transparency or appearance, when the object holds a frame of threaded type, when the page art uses a pattern, image or swatch that the page lacks or that differs from the editing data's, or when the page art doesn't carry the object's transparency (#4806; `crates/eps/src/import/tests_native.rs`, `crates/engine/src/cmd/fileio/tests_aiimport.rs`). A `.ai` or EPS opened from its editing data with one object on a shown layer that the reader can't read (an unknown operator inside a group, a pattern fill whose pattern it doesn't have, a symbol instance, a placed file without a usable preview) keeps its layers and groups: in that object's place, its layer gets a group of the page's objects inside the object's box, named after it, with a warning that lists what came in as the page's art and an export or Save As to its own path asking first (`acknowledgeLoss`). The file still opens from its page or PDF part when no box can be found, when live art or another unread object shares the page art inside it, when the object spans two artboards, or when the page art doesn't carry the object's transparency (#4806; `crates/eps/src/import/tests_native.rs`, `crates/engine/src/cmd/fileio/tests_aiimport.rs`). - In a `.ai` opened from its editing data, an envelope made with a warp, a mesh or a top object opens as a live envelope of its contents, with its warp style, bend and distortions, its mesh points and handles or its top object's outline, its Envelope Options, its Fidelity and whether its contents are being edited. When Distort Appearance is on and the size the file saves for the contents matches their bounds with strokes, the contents map from that stroked box, until an edit in Edit Contents changes how far the strokes reach; VectorCraft's new envelopes map from the geometric box, as do contents with no width or no height, such as a single straight line. An envelope that can't be read, or that VectorCraft draws differently from the file's drawn art, keeps that drawn art, with a note. An envelope of type opens live, its type laid out from the text document as its contents, where VectorCraft's own envelope of that type draws as the drawn art the file saves (compared as for the other envelopes, seen on white and on black, so white type counts, and, as with them, only where the layers with it are compared with the page and draw as it); elsewhere, and where the type's font isn't available or the envelope is in a symbol's or a pattern's art, it keeps its drawn art, with its type hidden and a note that says the type isn't editable (#3179; `crates/eps/src/import/ai/tests.rs`, `crates/eps/src/import/tests_native.rs`). + In a `.ai` opened from its editing data, an envelope made with a warp, a mesh or a top object opens as a live envelope of its contents, with its warp style, bend and distortions, its mesh points and handles or its top object's outline, its Envelope Options, its Fidelity and whether its contents are being edited. When Distort Appearance is on and the size the file saves for the contents matches their bounds with strokes, the contents map from that stroked box, until an edit in Edit Contents changes how far the strokes reach; VectorCraft's new envelopes map from the geometric box, as do contents with no width or no height, such as a single straight line. An envelope that can't be read, or that VectorCraft draws differently from the file's drawn art, keeps that drawn art, with a note. An envelope of type opens live, its type laid out from the text document as its contents, where VectorCraft's own envelope of that type draws as the drawn art the file saves (compared as for the other envelopes, seen on white and on black, so white type counts, and only where the layers with it are compared with the page and draw as it, the envelope weighed as the art it draws); elsewhere, and where the type's font isn't available, a text or visibility variable binds the type, or the envelope is in a symbol's or a pattern's art, it keeps its drawn art, with its type hidden and a note that says the type isn't editable (#3179; `crates/eps/src/import/ai/tests.rs`, `crates/eps/src/import/tests_native.rs`). In a `.ai` opened from its editing data, Convert to Shape on a whole path or type object opens as a live Rectangle, Rounded Rectangle or Ellipse effect with its Absolute or Relative size, its corner radius, its place among the object's effects and whether it is hidden. A relative size in the file is added to each side. A relative Ellipse, or a setting VectorCraft doesn't read, keeps the drawn art (`crates/eps/src/import/ai/style/tests.rs`, `crates/eps/src/import/ai/tests.rs`, `crates/eps/src/import/tests_native.rs`). In a `.ai` opened from its editing data, Bas Relief in an art style takes its Detail, Smoothness and Light direction from the style's settings block, and an object other than type with it opens as itself with the live effect. A settings block that VectorCraft can't read (malformed, another effect's, a value out of range or a key it doesn't know) keeps the object's drawn look (part of #637; [raster effects reference](https://helpx.adobe.com/illustrator/desktop/special-effects-styles/apply-filter-effects/raster-effects-reference.html), [Photoshop File Formats Specification](https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/), [scripting the filter gallery](https://community.adobe.com/questions-712/scripting-support-for-the-filter-gallery-1064646); `crates/eps/src/import/ai/style/tests.rs`, `crates/eps/src/import/ai/tests.rs`). PDF export and `.ai` saves write a page with a side longer than 14,400 pt with a `UserUnit` (PDF 1.6), its longer side divided by 14,400 and rounded up to the next thousandth, so that no side is longer than 14,400 units (ISO 32000-1, Annex C.2); in PDF 1.3 to 1.5 files and PDF/X files such a page is written in points. @@ -1939,6 +1939,7 @@ Edit › Copy and Cut put the selected ruler guides on the clipboard, with the s | Date | Change | Summary | |---|---|---| +| 2026-11-10 | minor | File formats: in a `.ai` or EPS opened from its editing data, an envelope of type opens live where it draws as its drawn art and the layers with it draw as the page (the envelope weighed as the art it draws); elsewhere, and where a text or visibility variable binds its type, it keeps its drawn art, with a note (#3179); evidence updated; scores and hours unchanged. | | 2026-11-10 | minor | Bundled content: built-in Game HUD graphic style library; style libraries evidence updated; ~34% → ~36%; more built-in graphic-style libraries remain. | | 2026-11-10 | minor | Bundled content: built-in Map Markers symbol library (Pin, Flag, Crosshair, Checkpoint); symbol libraries evidence updated; ~32% → ~34%; more built-in symbol libraries remain. | | 2026-11-10 | minor | Type advanced: Spelling › Auto Spell Check in the context menu of type, checked while it is on, #4936; evidence added; the dialogs remain; scores and hours unchanged. | From 309d7e53638e67e070789b1dc02d533cad546e37 Mon Sep 17 00:00:00 2001 From: jmverges Date: Tue, 10 Nov 2026 16:55:33 +0000 Subject: [PATCH 38/38] Envelopes: put back only the outermost drawn art, and keep hidden type until the drawn art is found The put-back after a failed page check replaces each live envelope with its drawn art in one go, which fails as a whole when one envelope lies in another's art: only the outermost are put back now. A live envelope of type removes its hidden type only once it has found the drawn art it replaces. The file format docs lose a revision row left twice by a rebase. Co-Authored-By: Claude Opus 5.5 --- crates/eps/src/import/ai/envelope.rs | 3 +++ crates/eps/src/import/native/build.rs | 13 ++++++++++++- docs/file-format-parity.md | 1 - 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/crates/eps/src/import/ai/envelope.rs b/crates/eps/src/import/ai/envelope.rs index 04b859946..c0da27f2a 100644 --- a/crates/eps/src/import/ai/envelope.rs +++ b/crates/eps/src/import/ai/envelope.rs @@ -519,6 +519,9 @@ impl WarpedType { if !drawn_alike(&live_one, &plain, layer, &shared, page, &mut self.spent) { return Err(TypeKept::Differs); } + if doc.node(drawn).is_none() { + return Err(TypeKept::Read); + } // The type is the envelope's now: its hidden copy goes, kept for when the drawn art comes back. let mut put_back: Vec = hidden.into_iter().filter_map(|id| doc.remove(id).ok()).map(Arc::unwrap_or_clone).collect(); let id = envelope.id; diff --git a/crates/eps/src/import/native/build.rs b/crates/eps/src/import/native/build.rs index b037b33fe..31cd90d79 100644 --- a/crates/eps/src/import/native/build.rs +++ b/crates/eps/src/import/native/build.rs @@ -1325,7 +1325,18 @@ fn drawn_or_trusted( let mut put_back = doc.clone(); // Each envelope still there, in place of the art it took the place of. let there: Vec<&(NodeId, Vec, &str)> = drawn.iter().filter(|(id, ..)| put_back.node(*id).is_some()).collect(); - let any = !there.is_empty() && put_back.insert_beside(there.iter().map(|(id, art, _)| (*id, art.clone())).collect(), true).is_ok(); + // One inside another's art goes with it: only the outermost are put back (a place inside + // another place fails the whole put-back). + let mut inside = std::collections::HashSet::new(); + for n in there.iter().filter_map(|(id, ..)| put_back.node(*id)) { + n.walk(&mut |c| { + if c.id != n.id { + inside.insert(c.id); + } + }); + } + let outermost = there.iter().filter(|(id, ..)| !inside.contains(id)).map(|(id, art, _)| (*id, art.clone())).collect(); + let any = !there.is_empty() && put_back.insert_beside(outermost, true).is_ok(); if any { let more = prune_unseen(frame, view, &mut put_back, reflow); if let Ok(note) = compare(frame, view, &put_back, reflow) { diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index 7f36fd1c3..fe06c7d2d 100644 --- a/docs/file-format-parity.md +++ b/docs/file-format-parity.md @@ -337,7 +337,6 @@ Stop opacity on translated artboards remains outside this regression coverage. | Date | Change | Summary | |---|---|---| | 2026-11-10 | minor | `.ai` and EPS import: an envelope of type (a warp, a mesh or a top object around type) kept its drawn art, its type hidden, because the type is laid out from the text document only after the layers are read. It now waits for that: its text slots get their type, and where VectorCraft's own envelope of that type draws as the drawn art the file saves (the same check and budget as other envelopes, seen on white and on black), it opens live in the drawn art's place, its type editable in Edit Contents, and the hidden copy goes. As with other live envelopes, the layers with one are then compared with the page, which weighs the envelope as the art it draws (not as type laid out again, which the comparison allows more for), and when they don't draw as it the drawn art and the hidden type are put back. Layers used though they don't draw as the page (the editing data trusted, or the objects that differ coming in as the page's art) leave envelopes of type their drawn art and hidden type too. A `.ai` whose layers would open alone, without its PDF part, imports the PDF part to compare them with, and should they still not draw as it, or should no envelope of type open live, they open alone as before, with the envelopes' drawn art. Type in a font that isn't available, slots without type of their own (threaded, bound to a text or a visibility variable, a wrap object, type a look draws), envelopes in a symbol's or a pattern's art, and envelopes that draw differently keep their drawn art, with the note (#3179). Tests `tests_native::an_envelope_of_type_that_draws_as_its_drawn_art_opens_live`, `an_envelope_of_type_that_draws_apart_from_its_drawn_art_keeps_it`, `an_envelope_of_white_type_that_draws_apart_from_its_drawn_art_keeps_it`, `an_envelope_of_type_waits_on_the_page_of_layers_that_would_open_alone`, `an_envelope_of_type_that_draws_apart_from_the_page_does_not_open_live`, `trusted_layers_that_draw_apart_from_the_page_leave_an_envelope_of_type_its_drawn_art`, `layers_with_objects_that_come_in_as_the_page_art_leave_an_envelope_of_type_its_drawn_art`, `an_envelope_of_type_bound_to_a_visibility_variable_keeps_its_drawn_art` | -| 2026-11-10 | minor | `.ai` and EPS import: an envelope of type, which keeps its drawn art with its type hidden, now says so in the import notes ("type in an envelope comes in as the envelope's drawn art, not as editable type: the type is kept, hidden"); nothing said it before, as the other envelopes that keep their drawn art do. The type is still not editable in the envelope (part of #3179). Test `tests_native::envelope_options_keep_the_source_text_hidden_under_its_outlined_appearance` | | 2026-11-10 | minor | `.ai` and EPS import: an envelope of type, which keeps its drawn art with its type hidden, now says so in the import notes ("type in an envelope comes in as the envelope's drawn art, not as editable type"); nothing said it before, while the other envelopes that keep their drawn art have a note. The type is still not editable in the envelope (part of #3179). Test `tests_native::envelope_options_keep_the_source_text_hidden_under_its_outlined_appearance` | | 2026-11-10 | minor | `.ai` and EPS import: the check that keeps a live envelope or warped type only where it draws as the art saved with it renders each on its own, over nothing, and compared the pixels seen on white, where white art looks like nothing. White contents that drew somewhere else passed it and opened live in the wrong place (white warped type off its banner, in users' files). Pixels now also differ when they look different seen on black, so such art keeps its saved art, as colored art does. Tests `import::ai::tests::a_white_envelope_that_doesnt_come_out_as_drawn_keeps_its_drawn_art`, `white_warped_type_that_draws_elsewhere_comes_in_as_its_look` | | 2026-11-10 | minor | PDF import: an image in a DeviceN space of one colorant opens as a Separation image does (#6314): a grayscale image of its tints colorized by its ink. One of two or more colorants that are all process plates (Cyan, Magenta, Yellow, Black, and the ink `None`) opens as a CMYK image, each colorant's tint in its plate and its Decode array applied (8 or 16 bits a sample; one that can't be read, such as a JPEG, opens in RGB with the CMYK images' warning). Both used to open as RGB images, losing their inks. A DeviceN image that mixes a spot ink with other colorants, such as a duotone, still opens as RGB: an image holds one ink or the process plates, not both (#6348). Tests `colorize::a_devicen_image_of_one_ink_opens_colorized_by_it`, `colorize::a_devicen_image_of_process_plates_opens_in_cmyk`, `colorize::a_duotone_devicen_image_still_opens_as_rgb` |