diff --git a/ROADMAP.md b/ROADMAP.md index 87aaed1109..b5a2f43c7a 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -128,6 +128,7 @@ Ranked; detail in [docs/roadmap.md](docs/roadmap.md) and [docs/gaps.md](docs/gap - 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. - 2026-10-31: Pattern libraries: Window › Swatch Libraries › Patterns and the Swatches panel's library menu open Geometric Patterns, a built-in library of eleven pattern swatches (dots, stripes, checks, a grid and shapes); clicking one adds its pattern and swatch to the document, once, and applies it to the active fill or stroke (`swatch.library.add`). - 2026-11-11: Pattern libraries: Patterns › Grit Textures, a second built-in pattern library, holds eight seamless black textures (Fine Grain, Coarse Grit, Sand, Speckle, Stipple, Scratches, Sparse Flecks and Dense Flecks) on tiles of 18 to 36 pt, each drawn the same way every time; marks that reach past a tile edge continue on the next tile, so the tiles join without a seam (`swatch.library.add`). Scores and estimates unchanged. +- 2026-11-11: Pattern libraries: Patterns › Optical Effects, a third built-in pattern library, of VectorCraft's own designs, holds eight black wave, moiré and op-art tiles (Sine Bands, Ripple Lines, Interference Rings, Offset Rings, Warped Checks, Pinched Stripes, Chevron Pulse and Radiating Lenses) of 32 to 48 pt, each drawn the same way every time; each tile is one period of its design, and art that reaches past a tile edge continues on the next tile, so the tiles join without a seam (`swatch.library.add`). Scores and estimates unchanged. - 2026-11-01: Find Nearest Swatch joins the Color Effects add-on (Object › Plug-ins › Color Effects): for each colour of the selection, the closest swatches of the document, a built-in library or a library or colour book the user loaded, with their CIEDE2000 difference; Apply gives each colour its nearest swatch as one undo step, linking global and spot swatches and adding a library's to the document first, so a process colour becomes the closest spot colour (`addon.color.nearestSwatch`, `addon.color.applyNearestSwatch`, #4923). - 2026-11-01: Expand Dashes into Paths joins the Path Precision Tools add-on (Object › Plug-ins › Path Precision Tools): each selected path's dashed stroke becomes separate open paths, one per dash, laid out exactly as the stroke draws them (pattern, offset, Align Dashes to Corners and Path Ends), each keeping the stroke with the dashes off, so the gaps are real gaps for cutters and plotters, as one undo step (`addon.pathtools.expandDashes`, #4922). - 2026-11-01: Randomize Opacity joins the Arrange Tools add-on (Object › Plug-ins › Arrange Tools): each selected object gets its own opacity at random between a minimum and a maximum percent, with a seed that repeats the result, as one undo step (`addon.arrange.randomizeOpacity`, #4925). diff --git a/apps/vectorcraft/src/host_services.rs b/apps/vectorcraft/src/host_services.rs index 72d13db08f..85bcc2ddfe 100644 --- a/apps/vectorcraft/src/host_services.rs +++ b/apps/vectorcraft/src/host_services.rs @@ -381,6 +381,8 @@ pub(crate) fn services(parent: Parent, ui: egui::Context) -> Services { exists: Some(std::sync::Arc::new(|p: &str| std::path::Path::new(p).exists())), // Background Save and Export write from a worker thread. write_shared: Some(std::sync::Arc::new(write_file)), + save_write: Some(std::sync::Arc::new(fileio::write_save_file)), + save_stream: Some(std::sync::Arc::new(fileio::write_save_file_with)), // Native saves stream into the file as they are encoded. stream_shared: Some(std::sync::Arc::new(|p: &str, fill: &mut dyn FnMut(&mut dyn std::io::Write) -> std::io::Result<()>| { fileio::write_file_with(p, fill).map_err(|e| e.to_string()) diff --git a/crates/effects/src/revolve.rs b/crates/effects/src/revolve.rs index c73d76e2c8..ec9563f05c 100644 --- a/crates/effects/src/revolve.rs +++ b/crates/effects/src/revolve.rs @@ -29,7 +29,7 @@ const MAX_PAINT_COPIES: usize = 256 << 20; /// The most face and box tests a group's transparent members' faces take together to rejoin their /// members' last runs; past them each starts a run of its own. -const MAX_OVERLAP_TESTS: usize = 1 << 22; +pub(crate) const MAX_OVERLAP_TESTS: usize = 1 << 22; /// The width of the stroke around each face that covers the seams between faces. const SEAM_WIDTH: f64 = 0.75; @@ -628,14 +628,14 @@ pub(crate) fn scoped<'a>(id: NodeId, surfaces: Vec<(&'a [Scope], Arc)>, le /// What a face can paint, to tell whether two faces overlap: the convex hull of the points and /// control points of its outline, which holds the outline, and the hull's box grown by half the /// seam stroke. -struct Coverage { - bounds: Rect, +pub(crate) struct Coverage { + pub(crate) bounds: Rect, hull: Vec, } impl Coverage { /// `None` for a face with no outline and no box. - fn of(face: &Node) -> Option { + pub(crate) fn of(face: &Node) -> Option { let points: Vec = match face.path_data() { Some(path) => path .to_bezpath() @@ -663,7 +663,7 @@ impl Coverage { /// Do the two faces' hulls, each grown by half the seam stroke, overlap? A test of the /// separating axes of their edges and of the page's axes: `true` whenever they overlap, and /// sometimes near a corner when they don't. - fn meets(&self, other: &Coverage) -> bool { + pub(crate) fn meets(&self, other: &Coverage) -> bool { fn normals(hull: &[Point]) -> impl Iterator + '_ { (0..hull.len()).filter_map(|i| { let (a, b) = (hull.get(i)?, hull.get((i + 1) % hull.len())?); @@ -710,7 +710,7 @@ fn convex_hull(mut points: Vec) -> Vec { } /// Do boxes `a` and `b` overlap with some area? -fn overlaps(a: Rect, b: Rect) -> bool { +pub(crate) fn overlaps(a: Rect, b: Rect) -> bool { a.x0 < b.x1 && b.x0 < a.x1 && a.y0 < b.y1 && b.y0 < a.y1 } diff --git a/crates/effects/src/solids.rs b/crates/effects/src/solids.rs index 8d9ff9eea8..538d2a5028 100644 --- a/crates/effects/src/solids.rs +++ b/crates/effects/src/solids.rs @@ -700,10 +700,11 @@ type ShadowAndSurfaces = (Vec>, Vec>); /// Rotate turns every fill and stroke, each in its own paint. Extrude & Bevel and Inflate build a /// solid from the path for the top visible fill and one from the outline of each visible stroke, /// each in its own paint. When every one is opaque with normal blending and no colored light makes -/// a surface one composite or masks it, their faces are drawn in one back-to-front order (where -/// depths tie, as for the caps turned toward the viewer, in the order of the appearance, and with -/// caps, a shape's fill walls before the faces of its covering strokes that overlap them: -/// [`draw_order`]); +/// a surface one composite, their faces are drawn in one back-to-front order (where depths tie, as +/// for the caps turned toward the viewer, in the order of the appearance, and with caps, a shape's +/// fill walls before the faces of its covering strokes that overlap them: [`draw_order`]), the +/// faces of a solid in a gradient with transparent stops in runs that carry its opacity mask +/// ([`in_runs`]); /// otherwise each solid is a surface of its own, in that order. A member's opacity mask and /// blending mode, and those of a group it is in, apply to the surfaces of the members it holds /// ([`masked`]). The art comes in two parts: the effect's shadow ([`cast_shadow`], none with it @@ -856,19 +857,21 @@ fn surfaces_for(n: &Node, fx: &Effect, shapes: &[Profile]) -> Option>> = surfaces.iter().map(|(_, s, _)| s.extras().mask.clone()).collect(); let (mut placed, mut faces) = (vec![], vec![]); - for (depths, s, _) in surfaces { + for (surface, (depths, s, _)) in surfaces.into_iter().enumerate() { if let NodeKind::Group { children, .. } = s.kind { placed.extend(depths); - faces.extend(children.into_iter().map(Some)); + faces.extend(children.into_iter().map(|face| Some((surface, face)))); } } let sealed = match fx.id.as_str() { @@ -877,7 +880,69 @@ fn surfaces_for(n: &Node, fx: &Effect, shapes: &[Profile]) -> Option extrude_options(&p).cap, }; let order = draw_order(&placed, sealed); - Some(vec![Arc::new(Node::group(n.id, order.into_iter().filter_map(|k| faces.get_mut(k).and_then(Option::take)).collect()))]) + let faces: Vec<(usize, Arc)> = order.into_iter().filter_map(|k| faces.get_mut(k).and_then(Option::take)).collect(); + let children = if masks.iter().all(Option::is_none) { faces.into_iter().map(|(_, face)| face).collect() } else { in_runs(n.id, faces, &masks) }; + Some(vec![Arc::new(Node::group(n.id, children))]) +} + +/// `faces` in their back-to-front order, each with the index of its surface, where some surfaces +/// carry an opacity mask (`masks`, by surface). A face of a surface without one goes in the order +/// on its own. The faces of a masked surface share a run, one group with the surface's mask. Such a +/// face joins its surface's last run unless a face drawn after that run overlaps it: only then does +/// the surface split, where one group can't be both behind and in front of another face. +fn in_runs(id: NodeId, faces: Vec<(usize, Arc)>, masks: &[Option>]) -> Vec> { + use crate::revolve::{Coverage, overlaps}; + struct Run { + surface: Option, + bounds: Rect, + faces: Vec>, + cover: Vec, + } + let mut runs: Vec = vec![]; + let mut last_run = vec![None; masks.len()]; + // Past this many tests a face starts a run of its own, which never draws out of order. + let mut budget = crate::revolve::MAX_OVERLAP_TESTS; + for (surface, face) in faces { + let Some(cover) = Coverage::of(&face) else { + // A face with no outline and no box overlaps nothing. + runs.push(Run { surface: None, bounds: Rect::ZERO, faces: vec![face], cover: vec![] }); + continue; + }; + if masks.get(surface).is_none_or(Option::is_none) { + runs.push(Run { surface: None, bounds: cover.bounds, faces: vec![face], cover: vec![cover] }); + continue; + } + let mut clear = |r: &Run| { + budget = budget.saturating_sub(1); + if !overlaps(r.bounds, cover.bounds) { + return budget > 0; + } + budget = budget.saturating_sub(r.cover.len()); + budget > 0 && r.cover.iter().all(|c| !overlaps(c.bounds, cover.bounds) || !c.meets(&cover)) + }; + let joins = last_run.get(surface).copied().flatten().filter(|&i: &usize| runs.get(i + 1..).is_some_and(|later| later.iter().all(&mut clear))); + match joins.and_then(|i| runs.get_mut(i)) { + Some(run) => { + run.bounds = run.bounds.union(cover.bounds); + run.faces.push(face); + run.cover.push(cover); + } + None => { + if let Some(last) = last_run.get_mut(surface) { + *last = Some(runs.len()); + } + runs.push(Run { surface: Some(surface), bounds: cover.bounds, faces: vec![face], cover: vec![cover] }); + } + } + } + runs.into_iter() + .filter_map(|mut r| { + let Some(mask) = r.surface.and_then(|s| masks.get(s)).cloned().flatten() else { return r.faces.pop() }; + let mut group = Node::group(id, r.faces); + group.extras_mut().mask = Some(mask); + Some(Arc::new(group)) + }) + .collect() } fn geometry(path: &PathData, fx: &[Effect], ctx: &GeomContext) -> PathData { diff --git a/crates/effects/src/solids/tests.rs b/crates/effects/src/solids/tests.rs index c960bbe671..1efe826c75 100644 --- a/crates/effects/src/solids/tests.rs +++ b/crates/effects/src/solids/tests.rs @@ -415,15 +415,16 @@ fn a_translucent_stroke_is_a_surface_of_its_own() { } /// With a light that is not white, a fill in a gradient with transparent stops is drawn with an -/// opacity mask over its whole surface: the fill's solid and the stroke's stay surfaces of their -/// own, and the mask stays on the fill's. Under a white light the same object is one surface. +/// opacity mask: its faces still take their places in the one order of the fill's and the +/// stroke's faces, in runs that each carry the mask. Under a white light the faces carry the +/// gradient itself and no mask. #[test] -fn a_masked_surface_under_a_colored_light_stays_whole() { +fn a_masked_surface_under_a_colored_light_keeps_the_shared_face_order() { use vectorcraft_doc::color::{Gradient, GradientKind, GradientPaint, GradientStop}; let mut half = GradientStop::new(1.0, RED); half.opacity = 0.5; let gradient = GradientPaint::new(Gradient::new(GradientKind::Linear, vec![GradientStop::new(0.0, RED), half])); - for (lights, surfaces) in [(json!({"lights": [{"color": "#ff8080"}]}), 2), (json!({}), 1)] { + for (lights, masked) in [(json!({"lights": [{"color": "#ff8080"}]}), true), (json!({}), false)] { let mut n = stroked_rect(100.0); if let Some(AppearanceItem::Fill(f)) = n.appearance.items.iter_mut().find(|i| matches!(i, AppearanceItem::Fill(_))) { f.paint = Paint::Gradient(Box::new(gradient.clone())); @@ -431,8 +432,10 @@ fn a_masked_surface_under_a_colored_light_stays_whole() { n.appearance.effects.push(fx(EXTRUDE, lights.clone())); let art = solid_art(&n).unwrap(); let drawn = art.children().unwrap(); - assert_eq!(drawn.len(), surfaces, "{lights}"); - assert_eq!(drawn[0].extras().mask.is_some(), surfaces == 2, "{lights}"); + assert_eq!(drawn.len(), 1, "{lights}: one order of faces"); + let runs: Vec<&Arc> = drawn[0].children().unwrap().iter().filter(|c| c.extras().mask.is_some()).collect(); + assert_eq!(!runs.is_empty(), masked, "{lights}"); + assert!(runs.iter().all(|r| r.children().unwrap().iter().all(|f| !blue(f))), "{lights}: the mask stays on the fill's faces"); } } @@ -847,7 +850,9 @@ fn classic_shading_keeps_a_gradients_transparency() { } let art = solid_art(&n).unwrap(); let surface = &art.children().unwrap()[0]; - assert_eq!(surface.extras().mask.is_some(), masked, "{end}"); + let mut has_mask = false; + surface.walk(&mut |c| has_mask |= c.extras().mask.is_some()); + assert_eq!(has_mask, masked, "{end}"); let mut opaque = true; surface.walk(&mut |c| { for item in &c.appearance.items { @@ -862,6 +867,67 @@ fn classic_shading_keeps_a_gradients_transparency() { } } +/// A group member in a gradient with transparent stops keeps the shared back-to-front order of +/// every member's faces: its faces go in runs that carry its opacity mask, and the other member's +/// faces are drawn between them where they are in front of some and behind others. Here a square +/// frame in the gradient holds a square in its hole: the frame's inner walls are behind the square +/// and its front cap is in front of the square's walls. +#[test] +fn a_member_in_a_transparent_gradient_keeps_the_shared_face_order() { + use vectorcraft_doc::color::{Gradient, GradientKind, GradientPaint, GradientStop}; + let square = |b: &mut BezPath, x0: f64, x1: f64, clockwise: bool| { + let corners = [(x0, x0), (x1, x0), (x1, x1), (x0, x1)]; + let mut points = corners.to_vec(); + if !clockwise { + points.reverse(); + } + b.move_to(points[0]); + for p in &points[1..] { + b.line_to(*p); + } + b.close_path(); + }; + let art = |id: &str, params: &Value, end_opacity: f32| { + let mut end = GradientStop::new(1.0, Color::BLACK); + end.opacity = end_opacity; + let gradient = Paint::Gradient(Box::new(GradientPaint::new(Gradient::new(GradientKind::Linear, vec![GradientStop::new(0.0, RED), end])))); + let mut b = BezPath::new(); + square(&mut b, 100.0, 300.0, true); + square(&mut b, 150.0, 250.0, false); + let frame = Node::path(NodeId(1), PathData::from_bezpath(&b), Appearance::basic(gradient, Paint::None, 0.0)); + let mut b = BezPath::new(); + square(&mut b, 185.0, 215.0, true); + let inner = Node::path(NodeId(2), PathData::from_bezpath(&b), Appearance::basic(Paint::solid(BLUE), Paint::None, 0.0)); + let mut g = Node::group(NodeId(3), vec![Arc::new(frame), Arc::new(inner)]); + g.appearance.effects.push(fx(id, params.clone())); + solid_art(&g).unwrap_or_else(|| panic!("{id}")) + }; + // The drawing order: `M` for a group with an opacity mask, `F` for a face outside one. + fn visit(n: &Node, order: &mut String) { + if n.extras().mask.is_some() { + order.push('M'); + return; + } + if n.path_data().is_some() { + order.push('F'); + } + for c in n.children().into_iter().flatten() { + visit(c, order); + } + } + let order = |art: &Node| { + let mut order = String::new(); + visit(art, &mut order); + order + }; + for (id, params) in [(CLASSIC_EXTRUDE, json!({})), (EXTRUDE, json!({"lights": [{"color": "#ff8080"}]}))] { + assert!(!order(&art(id, ¶ms, 1.0)).contains('M'), "{id}: an opaque gradient needs no mask"); + let masked = order(&art(id, ¶ms, 0.5)); + let (first, last) = (masked.find('M').unwrap(), masked.rfind('M').unwrap()); + assert!(masked.get(first..last).is_some_and(|between| between.contains('F')), "{id}: faces between the member's runs: {masked}"); + } +} + #[test] fn classic_parameters_are_checked() { let n = classic(json!({})); diff --git a/crates/engine/src/cmd/fileio.rs b/crates/engine/src/cmd/fileio.rs index 329988cb34..61eac58f37 100644 --- a/crates/engine/src/cmd/fileio.rs +++ b/crates/engine/src/cmd/fileio.rs @@ -92,9 +92,9 @@ pub use pdfimport::{LoadOptions, page_document}; pub(crate) use save::job_for; pub(crate) use save::save as save_document; pub use save::{ - SAVE_FORMATS, SaveJob, SaveMode, SavePlan, StreamWrite, TEMPLATE_FORMATS, dialog_folder, export_folder, leave_out_unreadable_save_paths, - reads_option as save_reads_option, remembered_save_options, save_filters, save_format, save_job, save_plan, save_with, stamp_save_dates, - template_filters, templates_dialog_folder, templates_folder, + SAVE_FORMATS, SaveJob, SaveMode, SavePlan, StreamWrite, TEMPLATE_FORMATS, dialog_folder, documents_dialog_folder, export_folder, + leave_out_unreadable_save_paths, reads_option as save_reads_option, remembered_save_options, save_filters, save_format, save_job, save_plan, + save_with, stamp_save_dates, template_filters, templates_dialog_folder, templates_folder, }; pub(crate) use screens::{ PARAM_KEYS as SCREEN_PARAM_KEYS, ROW_KEYS as SCREEN_ROW_KEYS, UNWRITTEN_FORMATS as SCREEN_UNWRITTEN_FORMATS, @@ -123,7 +123,10 @@ mod formats; #[cfg(not(target_arch = "wasm32"))] pub(crate) use access::list_dir; -pub use access::write_file_with; +pub use access::{write_file_with, write_save_file, write_save_file_with}; + +#[cfg(all(test, not(target_arch = "wasm32")))] +mod tests_readonly; #[cfg(not(target_arch = "wasm32"))] pub(crate) use access::{Placed, copy_new, file_metadata}; pub(crate) use access::{ diff --git a/crates/engine/src/cmd/fileio/access.rs b/crates/engine/src/cmd/fileio/access.rs index 7b20326106..c3668645e1 100644 --- a/crates/engine/src/cmd/fileio/access.rs +++ b/crates/engine/src/cmd/fileio/access.rs @@ -112,6 +112,31 @@ pub fn write_file_with(path: &str, fill: &mut dyn FnMut(&mut dyn std::io::Write) vectorcraft_format::write_atomic_with(std::path::Path::new(path), |f| fill(f)).map_err(|e| EngineError::Other(format!("{path}: {e}"))) } +/// The save commands' writer: preserves a filesystem location refusal as a typed error. Other +/// writers keep their existing errors; an automation-root refusal is never reclassified. +pub fn write_save_file(path: &str, bytes: &[u8]) -> Result<()> { + write_save_file_with(path, &mut |w| w.write_all(bytes)) +} + +/// [`write_save_file`] for a streamed save. +#[cfg(not(target_arch = "wasm32"))] +pub fn write_save_file_with(path: &str, fill: &mut dyn FnMut(&mut dyn std::io::Write) -> std::io::Result<()>) -> Result<()> { + vectorcraft_doc::cancel::check()?; + may_write(path)?; + vectorcraft_format::write_atomic_with(std::path::Path::new(path), |f| fill(f)).map_err(|e| save_write_error(path, e)) +} + +#[cfg(not(target_arch = "wasm32"))] +pub(super) fn save_write_error(path: &str, error: std::io::Error) -> EngineError { + let reason = format!("{path}: {error}"); + if vectorcraft_format::read_only_location(&error) { EngineError::ReadOnlyLocation(reason) } else { EngineError::Other(reason) } +} + +#[cfg(target_arch = "wasm32")] +pub fn write_save_file_with(path: &str, fill: &mut dyn FnMut(&mut dyn std::io::Write) -> std::io::Result<()>) -> Result<()> { + write_file_with(path, fill) +} + #[cfg(not(target_arch = "wasm32"))] pub(crate) fn create_dir(path: &str) -> Result<()> { may_write(path)?; diff --git a/crates/engine/src/cmd/fileio/save.rs b/crates/engine/src/cmd/fileio/save.rs index 5d7783665b..58677cd5d4 100644 --- a/crates/engine/src/cmd/fileio/save.rs +++ b/crates/engine/src/cmd/fileio/save.rs @@ -157,6 +157,11 @@ fn documents_folder() -> Option { Some(home.join("Documents")) } +/// A Save As after a refused Save starts in Documents, else home, never the refused folder. +pub fn documents_dialog_folder() -> Option { + documents_folder().filter(|p| p.is_dir()).map(|p| p.to_string_lossy().into_owned()).or_else(home_folder) +} + /// `XDG_DOCUMENTS_DIR` in a `user-dirs.dirs` file's `text`, `$HOME` being `home`. None when it isn't /// set, is relative, or is the home folder itself (the spec's way of turning it off). fn xdg_documents(text: &str, home: &std::path::Path) -> Option { @@ -1043,7 +1048,7 @@ pub(crate) fn save(s: &mut Session, mode: SaveMode, p: &Value) -> Result let plan = save_plan(s, mode, p)?; let job = save_job(s, plan)?; let uid = s.doc()?.uid; - let mut out = job.write_streamed(&mut super::write_file_with)?; + let mut out = job.write_streamed(&mut super::write_save_file_with)?; job.complete(s, uid); leave_out_unreadable_save_paths(&mut out, str_param(p, "path")); Ok(out) @@ -1170,7 +1175,7 @@ pub(super) fn specs() -> Vec { "Save Document", [], None, - "{path?, format?: vectorcraft|template|pdf|svg|svgz|ai|eps (default: the path's extension, else the document's own format), options?: {…the format's options, see file.formatOptions; default: as last saved}, svg?: {…SVG options} (an SVG save keeps hidden layers, display:none, unless hiddenLayers is false), …the SVG and PDF options given flat instead (an option given flat or in svg and in options with different values is an error), compress?: bool (native, also in options: gzip; default: the useCompression preference; warnings say when the format doesn't read compress, version, preview or precision given flat, or a key given in options), version?: 4 (3, 2 or 1: for older VectorCraft versions; 1 and 2 are never compressed), preview?: false (embed a PNG of the first artboard, at most 256 px), precision?: 0..9 (native: round path anchors and handles to this many decimal places, a smaller file; colors, transforms and type stay exact; default: exact, and a later save without it writes exactly again), separateArtboards?: false (native and .ai: also save each artboard of `range` to -. beside the file, holding that artboard and the art touching it; the result's files lists every file written), range?: \"1-3, 5\"|\"all\" (default; with separateArtboards), includeLinked?: false (keep linked files' own pixels, not just their previews; they stay linked), embedProfiles?: true (carry the ICC profiles loaded from files that the document is tagged with; opening the file installs them where missing), pdfCompatible?: true (native: also carry a PDF of every artboard; false omits it) | true for .ai (false: blank pages, only the native document; a .ai file's compress, default true, is its compression.compressText), modified?: Unix seconds|null (the File Info modified date, and created date when there is none, a save to a file stamps; default now, recorded in the journal so a replay matches; null: leave the dates)} → {path, format, bytes, warnings, files?: [path…] (the file and its artboards' files), linked?: [path…] (images an SVG links to)}. Save writes one artboard, except a .ai file and EPS (below): a .ai file is a PDF-compatible file of every artboard carrying the native document (preserveEditing always on; PDF options flat or in options), which document.open restores exactly, on the active artboard (the app's, else the one the document opened on; with editingData: false, the first); separateArtboards writes more files, one per artboard. EPS takes the options of document.exportEps: by default one file whose bounding box fits the visible art (the first artboard when no art shows), carrying the whole editable document; useArtboards: true (also when artboard, artboards or range names artboards; file.formatOptions then reports it true) makes the chosen artboard the page, drawn with its background color (without Use Artboards, only the first artboard's page when no art shows has its color), and with two or more chosen the file at the path is the central file of the visible art (again the first artboard when none shows) and each artboard is also written to _.eps beside it, holding that artboard; selectedOnly is refused; its PostScript appearance flattens transparency, as EPS requires. Without a path it writes the document's own file in its own format: a document opened from or saved as SVG/PDF/EPS saves as that again (warnings name what the format loses). No path known (never saved, converted from an older version, restored by Data Recovery, or another format) → {dataBase64, format, name, folder?, warnings} and the document stays modified. Under automation read roots, a reply leaves out the document's own file outside them when no path is given (files and linked then name the files by their file names), and a suggested folder outside them. A key that no save format takes, beside the path or in options, and an unknown key inside a PDF option's object or svg, are refused", + "{path?, format?: vectorcraft|template|pdf|svg|svgz|ai|eps (default: the path's extension, else the document's own format), options?: {…the format's options, see file.formatOptions; default: as last saved}, svg?: {…SVG options} (an SVG save keeps hidden layers, display:none, unless hiddenLayers is false), …the SVG and PDF options given flat instead (an option given flat or in svg and in options with different values is an error), compress?: bool (native, also in options: gzip; default: the useCompression preference; warnings say when the format doesn't read compress, version, preview or precision given flat, or a key given in options), version?: 4 (3, 2 or 1: for older VectorCraft versions; 1 and 2 are never compressed), preview?: false (embed a PNG of the first artboard, at most 256 px), precision?: 0..9 (native: round path anchors and handles to this many decimal places, a smaller file; colors, transforms and type stay exact; default: exact, and a later save without it writes exactly again), separateArtboards?: false (native and .ai: also save each artboard of `range` to -. beside the file, holding that artboard and the art touching it; the result's files lists every file written), range?: \"1-3, 5\"|\"all\" (default; with separateArtboards), includeLinked?: false (keep linked files' own pixels, not just their previews; they stay linked), embedProfiles?: true (carry the ICC profiles loaded from files that the document is tagged with; opening the file installs them where missing), pdfCompatible?: true (native: also carry a PDF of every artboard; false omits it) | true for .ai (false: blank pages, only the native document; a .ai file's compress, default true, is its compression.compressText), modified?: Unix seconds|null (the File Info modified date, and created date when there is none, a save to a file stamps; default now, recorded in the journal so a replay matches; null: leave the dates)} → {path, format, bytes, warnings, files?: [path…] (the file and its artboards' files), linked?: [path…] (images an SVG links to)}. Save writes one artboard, except a .ai file and EPS (below): a .ai file is a PDF-compatible file of every artboard carrying the native document (preserveEditing always on; PDF options flat or in options), which document.open restores exactly, on the active artboard (the app's, else the one the document opened on; with editingData: false, the first); separateArtboards writes more files, one per artboard. EPS takes the options of document.exportEps: by default one file whose bounding box fits the visible art (the first artboard when no art shows), carrying the whole editable document; useArtboards: true (also when artboard, artboards or range names artboards; file.formatOptions then reports it true) makes the chosen artboard the page, drawn with its background color (without Use Artboards, only the first artboard's page when no art shows has its color), and with two or more chosen the file at the path is the central file of the visible art (again the first artboard when none shows) and each artboard is also written to _.eps beside it, holding that artboard; selectedOnly is refused; its PostScript appearance flattens transparency, as EPS requires. Without a path it writes the document's own file in its own format: a document opened from or saved as SVG/PDF/EPS saves as that again (warnings name what the format loses). No path known (never saved, converted from an older version, restored by Data Recovery, or another format) → {dataBase64, format, name, folder?, warnings} and the document stays modified. Under automation read roots, a reply leaves out the document's own file outside them when no path is given (files and linked then name the files by their file names), and a suggested folder outside them. A key that no save format takes, beside the path or in options, and an unknown key inside a PDF option's object or svg, are refused. A terminal filesystem permission/read-only refusal returns READ_ONLY_LOCATION: followed by the reason; recovery-copy failures keep their original error. Non-UI callers never open a save panel", has_doc, |s, p| save(s, SaveMode::Save, p) ), diff --git a/crates/engine/src/cmd/fileio/tests_readonly.rs b/crates/engine/src/cmd/fileio/tests_readonly.rs new file mode 100644 index 0000000000..badf82a8cf --- /dev/null +++ b/crates/engine/src/cmd/fileio/tests_readonly.rs @@ -0,0 +1,52 @@ +//! Save errors retain their type without changing the other filesystem writers (#6146). + +use std::io::{self, ErrorKind}; + +use serde_json::json; + +use super::{SaveMode, access, save_plan, save_with, write_file_with, write_save_file_with}; +use crate::{EngineError, READ_ONLY_LOCATION, Session}; + +#[test] +fn readonly_error_is_typed_and_other_writers_keep_their_error() { + let dir = vectorcraft_testkit::temp_dir("save-refusal"); + let path = dir.join("Poster.vectorcraft").to_string_lossy().into_owned(); + for kind in [ErrorKind::PermissionDenied, ErrorKind::ReadOnlyFilesystem, ErrorKind::StorageFull, ErrorKind::NotFound, ErrorKind::Other] { + let typed = write_save_file_with(&path, &mut |_| Err(io::Error::new(kind, "injected"))).unwrap_err(); + let ordinary = write_file_with(&path, &mut |_| Err(io::Error::new(kind, "injected"))).unwrap_err(); + let reason = format!("{path}: injected"); + assert!(matches!(&ordinary, EngineError::Other(_))); + assert_eq!(ordinary.to_string(), reason); + if matches!(kind, ErrorKind::PermissionDenied | ErrorKind::ReadOnlyFilesystem) { + assert!(matches!(&typed, EngineError::ReadOnlyLocation(_))); + assert_eq!(typed.to_string(), format!("{READ_ONLY_LOCATION}: {reason}")); + } else { + assert!(matches!(&typed, EngineError::Other(_))); + assert_eq!(typed.to_string(), reason); + } + assert!(!std::path::Path::new(&path).exists()); + } + std::fs::remove_dir_all(dir).unwrap(); +} + +#[test] +fn readonly_failed_save_keeps_document_and_options() { + let mut session = Session::new(); + session.execute("file.new", &json!({"width": 40, "height": 40})).unwrap(); + session.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 10, "height": 10})).unwrap(); + let st = session.doc_mut().unwrap(); + st.path = Some("Poster.svg".into()); + st.format = "svg"; + st.save_options = json!({"decimals": 3}).as_object().unwrap().clone(); + let before = serde_json::to_value(&*st.doc).unwrap(); + let options = st.save_options.clone(); + let plan = save_plan(&session, SaveMode::Save, &json!({"modified": null})).unwrap(); + let e = save_with(&mut session, plan, |path, _| Err(access::save_write_error(path, io::Error::from(ErrorKind::PermissionDenied)))).unwrap_err(); + assert!(matches!(e, EngineError::ReadOnlyLocation(_))); + let st = session.doc().unwrap(); + assert_eq!(st.path.as_deref(), Some("Poster.svg")); + assert_eq!(st.format, "svg"); + assert_eq!(st.save_options, options); + assert!(st.is_dirty()); + assert_eq!(serde_json::to_value(&*st.doc).unwrap(), before); +} diff --git a/crates/engine/src/cmd/fileio/tests_screens.rs b/crates/engine/src/cmd/fileio/tests_screens.rs index 86ee164998..0b529c87c6 100644 --- a/crates/engine/src/cmd/fileio/tests_screens.rs +++ b/crates/engine/src/cmd/fileio/tests_screens.rs @@ -730,7 +730,7 @@ fn a_refused_export_into_a_presets_folder_outside_the_read_root_names_it_by_its_ let settings = json!({"formats": [{"format": "png"}], "folder": parent}); s.execute("document.exportPresets.save", &json!({"name": "Parent", "settings": settings})).unwrap(); let e = confine(confined.as_ref(), || s.execute("document.exportForScreens", &json!({"exportPreset": "Parent"}))).unwrap_err().to_string(); - let root = inside.display().to_string(); + let root = crate::cmd::findfiles::plain(inside.canonicalize().unwrap()).display().to_string(); let name = tmp.0.file_name().unwrap().to_string_lossy().into_owned(); assert!(e.contains(&format!("root {root}: {name}")), "{e}"); } diff --git a/crates/engine/src/cmd/patternlib.rs b/crates/engine/src/cmd/patternlib.rs index 8fd2a0ea5e..f69e4b889e 100644 --- a/crates/engine/src/cmd/patternlib.rs +++ b/crates/engine/src/cmd/patternlib.rs @@ -26,6 +26,7 @@ pub struct BuiltinPatternLibrary { pub const PATTERN_LIBRARIES: &[BuiltinPatternLibrary] = &[ BuiltinPatternLibrary { id: "geometric-patterns", name: "Geometric Patterns", make: geometric_patterns }, BuiltinPatternLibrary { id: "grit-textures", name: "Grit Textures", make: grit_textures }, + BuiltinPatternLibrary { id: "optical-effects", name: "Optical Effects", make: optical_effects }, ]; /// The library category of the built-in pattern libraries. @@ -256,6 +257,162 @@ fn grit_textures(d: &mut Document) { }); } +/// `n` + 1 points of the curve `(t, f(t))` for `t` from `t0` to `t1`, evenly spaced. +fn curve(t0: f64, t1: f64, n: u32, f: impl Fn(f64) -> f64) -> Vec<(f64, f64)> { + (0..=n) + .map(|i| { + let t = t0 + (t1 - t0) * f64::from(i) / f64::from(n); + (t, f(t)) + }) + .collect() +} + +/// The closed band between two curves drawn from the same side to the same side: `upper` forward, +/// then `lower` back. +fn band(upper: &[(f64, f64)], lower: &[(f64, f64)]) -> PathData { + let points: Vec<(f64, f64)> = upper.iter().chain(lower.iter().rev()).copied().collect(); + polygon(&points) +} + +/// 72 corners of the circle of radius `r` about (`cx`, `cy`), turning the other way with `reverse`. +fn circle(cx: f64, cy: f64, r: f64, reverse: bool) -> SubPath { + const CORNERS: u32 = 72; + let turn = if reverse { -std::f64::consts::TAU } else { std::f64::consts::TAU }; + let points: Vec = (0..CORNERS) + .map(|i| { + let a = turn * f64::from(i) / f64::from(CORNERS); + Point::new(cx + r * a.cos(), cy + r * a.sin()) + }) + .collect(); + SubPath::polyline(&points, true) +} + +/// The ring from radius `inner` to `outer` about (`cx`, `cy`): the outer circle and the inner one +/// turning the other way, so the middle stays clear under either fill rule. An `inner` of 0 gives +/// a disk. +fn ring(cx: f64, cy: f64, inner: f64, outer: f64) -> PathData { + let mut subpaths = vec![circle(cx, cy, outer, false)]; + if inner > 0.0 { + subpaths.push(circle(cx, cy, inner, true)); + } + PathData::new(subpaths) +} + +/// A lens `2 × l` long and `2 × h` high about (`cx`, `cy`), lying along x: two half sine waves that +/// meet at its tips, turning the other way with `reverse`. +fn lens(cx: f64, cy: f64, l: f64, h: f64, reverse: bool) -> SubPath { + const STEPS: u32 = 32; + let at = |i: u32, side: f64| { + let t = f64::from(i) / f64::from(STEPS); + Point::new(cx - l + 2.0 * l * t, cy + side * h * (std::f64::consts::PI * t).sin()) + }; + let mut points: Vec = (0..=STEPS).map(|i| at(i, -1.0)).chain((1..STEPS).rev().map(|i| at(i, 1.0))).collect(); + if reverse { + points.reverse(); + } + SubPath::polyline(&points, true) +} + +/// Optical Effects: black waves, rings, warped checks, pinched stripes, chevrons and lenses on a +/// clear background. Each tile is one period of its design, so a band that leaves one edge comes +/// back at the same place on the opposite edge. Art that reaches past an edge is kept whole: the +/// tiling draws the part outside the tile on the next tile. +fn optical_effects(d: &mut Document) { + use std::f64::consts::TAU; + // Three bands 5 pt wide, 12 pt apart, each one sine wave across the tile. + let bands = (0..3u32) + .map(|k| { + let mid = move |x: f64| 6.0 + 12.0 * f64::from(k) + 3.0 * (TAU * x / 36.0).sin(); + band(&curve(0.0, 36.0, 48, |x| mid(x) - 2.5), &curve(0.0, 36.0, 48, |x| mid(x) + 2.5)) + }) + .collect(); + add(d, "Sine Bands", 36.0, 36.0, TileType::Grid, bands); + // Nine thin waves 4 pt apart, two waves across the tile, each a ninth of a wave on from the one + // above it, so the ninth leads into the first wave of the next tile down. + let lines = (0..9u32) + .map(|k| { + let (y, phase) = (2.0 + 4.0 * f64::from(k), TAU * f64::from(k) / 9.0); + let mid = move |x: f64| y + 2.5 * (TAU * x / 18.0 + phase).sin(); + band(&curve(0.0, 36.0, 72, |x| mid(x) - 0.6), &curve(0.0, 36.0, 72, |x| mid(x) + 0.6)) + }) + .collect(); + add(d, "Ripple Lines", 36.0, 36.0, TileType::Grid, lines); + // Thin rings 3 pt apart about the center and rings 3.3 pt apart about a point 4 pt to its + // right. The two sets cross, and the outer rings reach into the next tile across. + let rings = (1..=7u32) + .map(|k| (24.0, 3.0 * f64::from(k))) + .chain((1..=7u32).map(|k| (28.0, 3.3 * f64::from(k)))) + .map(|(cx, r)| ring(cx, 24.0, r - 0.6, r + 0.6)) + .collect(); + add(d, "Interference Rings", 48.0, 48.0, TileType::Grid, rings); + // Rings 2 pt and 1 pt wide by turns about the tile's corner and about its center, the center's + // starting with the narrower one. The corner's rings are the same rings at all four corners. + let mut rings = vec![]; + for (c, first) in [(0.0, 0), (20.0, 1)] { + let mut r = 0.0; + for k in 0..5 { + let width = if (k + first) % 2 == 0 { 2.0 } else { 1.0 }; + rings.push(ring(c, c, r, r + width)); + r += width + 1.2; + } + } + add(d, "Offset Rings", 40.0, 40.0, TileType::Grid, rings); + // An 8 × 8 checkerboard of 4 pt cells whose columns are bent by a sine wave down the tile and + // whose rows by one across it, each edge drawn through five points. + let warp = |x: f64, y: f64| (x + 2.0 * (TAU * y / 32.0).sin(), y + 2.0 * (TAU * x / 32.0).sin()); + let cells = (0..8u32) + .flat_map(|row| (0..8u32).map(move |col| (col, row))) + .filter(|(col, row)| (col + row) % 2 == 0) + .map(|(col, row)| { + let (x, y) = (4.0 * f64::from(col), 4.0 * f64::from(row)); + let corners = [(x, y), (x + 4.0, y), (x + 4.0, y + 4.0), (x, y + 4.0), (x, y)]; + let points: Vec<(f64, f64)> = corners + .windows(2) + .flat_map(|e| { + let (a, b) = (e[0], e[1]); + (0..4u32).map(move |i| { + let t = f64::from(i) / 4.0; + warp(a.0 + (b.0 - a.0) * t, a.1 + (b.1 - a.1) * t) + }) + }) + .collect(); + polygon(&points) + }) + .collect(); + add(d, "Warped Checks", 32.0, 32.0, TileType::Grid, cells); + // Six upright stripes 6 pt apart that widen from 0.8 pt to 5.6 pt and narrow again down the tile, + // each a sixth of a wave on from the one to its left. + let stripes = (0..6u32) + .map(|k| { + let (x, phase) = (3.0 + 6.0 * f64::from(k), TAU * f64::from(k) / 6.0); + let half = move |y: f64| 1.6 + 1.2 * (TAU * y / 36.0 + phase).cos(); + let side = |s: f64| -> Vec<(f64, f64)> { curve(0.0, 36.0, 48, |y| x + s * half(y)).into_iter().map(|(y, at)| (at, y)).collect() }; + band(&side(-1.0), &side(1.0)) + }) + .collect(); + add(d, "Pinched Stripes", 36.0, 36.0, TileType::Grid, stripes); + // Four zigzag bands 8 pt apart, two zigzags across the tile; the bend and the width of the bands + // rise and fall from band to band. + let chevrons = [(0.0, 3.0, 4.0), (8.0, 4.5, 2.5), (16.0, 3.0, 1.0), (24.0, 1.5, 2.5)] + .into_iter() + .map(|(y, bend, width)| { + let upper: Vec<(f64, f64)> = (0..=4u32).map(|i| (8.0 * f64::from(i), if i % 2 == 1 { y + bend } else { y })).collect(); + let lower: Vec<(f64, f64)> = upper.iter().map(|&(x, y)| (x, y + width)).collect(); + band(&upper, &lower) + }) + .collect(); + add(d, "Chevron Pulse", 32.0, 32.0, TileType::Grid, chevrons); + // Five nested lens outlines 1.5 pt wide about the center, 14 pt to 78 pt long. The longer ones + // reach across the side edges and cross the next tiles' lenses. + let lenses = (0..5u32) + .map(|k| { + let (l, h) = (7.0 + 8.0 * f64::from(k), 4.0 + 4.0 * f64::from(k)); + PathData::new(vec![lens(24.0, 24.0, l, h, false), lens(24.0, 24.0, l - 1.5, h - 1.5, true)]) + }) + .collect(); + add(d, "Radiating Lenses", 48.0, 48.0, TileType::Grid, lenses); +} + #[cfg(test)] mod tests { use super::*; @@ -326,4 +483,101 @@ mod tests { } assert!(crossing > 0, "no mark reaches past an edge"); } + + const OPTICAL: [&str; 8] = + ["Sine Bands", "Ripple Lines", "Interference Rings", "Offset Rings", "Warped Checks", "Pinched Stripes", "Chevron Pulse", "Radiating Lenses"]; + + fn optical_doc() -> Document { + let mut d = Document::new(100.0, 100.0); + optical_effects(&mut d); + d + } + + /// The paths of pattern `p`'s art, each with its fill rule. + fn paths(p: &PatternDef) -> Vec<(&PathData, vectorcraft_geom::FillRule)> { + p.art + .iter() + .map(|n| match &n.kind { + vectorcraft_doc::NodeKind::Path { path, rule, .. } => (path, *rule), + _ => panic!("{}: art that is not a path", p.name), + }) + .collect() + } + + #[test] + fn optical_effects_is_a_pattern_library_of_eight_bounded_tiles() { + let b = PATTERN_LIBRARIES.iter().find(|b| b.id == "optical-effects").expect("Optical Effects is listed"); + assert_eq!(b.name, "Optical Effects"); + let names: Vec = library_doc(b.id).unwrap().patterns.iter().map(|p| p.name.clone()).collect(); + assert_eq!(names, OPTICAL); + // Made twice, the art is the same. + let (a, again) = (optical_doc(), optical_doc()); + assert_eq!(a.patterns, again.patterns); + for p in &a.patterns { + let (w, h) = (p.width(), p.height()); + assert!((32.0..=48.0).contains(&w) && w == h && p.tile.x0 == 0.0 && p.tile.y0 == 0.0, "{}", p.name); + assert_eq!(p.tile_type, TileType::Grid, "{}", p.name); + let paths = paths(p); + let segments: usize = paths.iter().flat_map(|(path, _)| &path.subpaths).map(SubPath::segment_count).sum(); + assert!(!paths.is_empty() && paths.len() <= 600 && segments <= 20_000, "{}: {} paths, {segments} segments", p.name, paths.len()); + for (path, _) in &paths { + let b = path.bounds().unwrap_or_else(|| panic!("{}: an empty path", p.name)); + assert!([b.x0, b.y0, b.x1, b.y1].iter().all(|v| v.is_finite()) && b.width() > 0.0 && b.height() > 0.0, "{}: {b:?}", p.name); + assert!(b.x1 > 0.0 && b.y1 > 0.0 && b.x0 < w && b.y0 < h, "{}: {b:?} does not touch the tile", p.name); + } + } + } + + /// Just either side of a tile's right and bottom edges, the tiling paints the same points: a band + /// cut off at an edge, or one that comes back at another height or width, paints one side only. + /// No path is another moved by a whole tile, which the tiling would draw twice in one place. + #[test] + fn optical_tiles_join_without_a_seam() { + const SAMPLES: u32 = 400; + const STEP: f64 = 1e-4; + for p in &optical_doc().patterns { + let (w, h) = (p.width(), p.height()); + let shapes: Vec<(BezPathFill, Rect)> = + paths(p).into_iter().map(|(path, rule)| ((path.to_bezpath(), rule), path.bounds().unwrap_or(Rect::ZERO))).collect(); + let paints = |q: Point| { + let near = Rect::new(q.x - STEP, q.y - STEP, q.x + STEP, q.y + STEP); + p.offsets_covering(near).into_iter().any(|o| { + let at = q - o; + shapes.iter().any(|((bp, rule), b)| { + let inside = b.x0 <= at.x && at.x <= b.x1 && b.y0 <= at.y && at.y <= b.y1; + let turns = if inside { kurbo::Shape::winding(bp, at) } else { 0 }; + match rule { + vectorcraft_geom::FillRule::EvenOdd => turns % 2 != 0, + vectorcraft_geom::FillRule::NonZero => turns != 0, + } + }) + }) + }; + let (mut differ, mut painted) = (0, 0); + for i in 0..SAMPLES { + let t = (f64::from(i) + 0.37) / f64::from(SAMPLES); + let (right, bottom) = (Point::new(w, t * h), Point::new(t * w, h)); + let (dx, dy) = (kurbo::Vec2::new(STEP, 0.0), kurbo::Vec2::new(0.0, STEP)); + for (a, b) in [(right - dx, right + dx), (bottom - dy, bottom + dy)] { + let (pa, pb) = (paints(a), paints(b)); + differ += usize::from(pa != pb); + painted += usize::from(pa); + } + } + assert!(differ <= 2, "{}: {differ} of {} points across the edges differ", p.name, 2 * SAMPLES); + assert!(painted > 0, "{}: nothing crosses an edge", p.name); + let bounds: Vec = shapes.iter().map(|(_, b)| *b).collect(); + let same = |a: &Rect, b: Rect| { + (a.x0 - b.x0).abs() < 1e-9 && (a.y0 - b.y0).abs() < 1e-9 && (a.x1 - b.x1).abs() < 1e-9 && (a.y1 - b.y1).abs() < 1e-9 + }; + for b in &bounds { + for (dx, dy) in [(w, 0.0), (-w, 0.0), (0.0, h), (0.0, -h), (w, h), (w, -h), (-w, h), (-w, -h)] { + let moved = *b + kurbo::Vec2::new(dx, dy); + assert!(!bounds.iter().any(|o| same(o, moved)), "{}: {b:?} is drawn twice by the tiling", p.name); + } + } + } + } + + type BezPathFill = (vectorcraft_geom::BezPath, vectorcraft_geom::FillRule); } diff --git a/crates/engine/src/error.rs b/crates/engine/src/error.rs index d32fda613d..6ab1608bd7 100644 --- a/crates/engine/src/error.rs +++ b/crates/engine/src/error.rs @@ -3,6 +3,9 @@ use super::{NodeId, cancel}; /// The text of [`EngineError::Busy`], which ends every error a frontend reports for it. pub const SCRIPT_BUSY: &str = "a script is running: stop it or wait for it to finish"; +/// Stable prefix for a save refused by the filesystem: choose another destination. +pub const READ_ONLY_LOCATION: &str = "READ_ONLY_LOCATION"; + #[derive(Debug, thiserror::Error)] pub enum EngineError { #[error("unknown command `{0}`")] @@ -20,6 +23,10 @@ pub enum EngineError { NoNode(NodeId), #[error("{0}")] Other(String), + /// The save's terminal filesystem error refuses writes here. The reason keeps the path and + /// system message. This never means an automation-root refusal or a recovery-copy failure. + #[error("{code}: {0}", code = READ_ONLY_LOCATION)] + ReadOnlyLocation(String), /// The caller asked for the command to stop ([`cancel`]). It changed nothing. #[error("cancelled; nothing was changed")] Cancelled, diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index cd3a103a86..9890c9b08c 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -382,7 +382,7 @@ pub(crate) use command_sets::SELECTS_GIVEN_IDS; use command_sets::given_ids; pub use command_sets::{FIT_ACTIVE_ARTBOARD, ON_ACTIVE_ARTBOARD, waits_for_tool_edit}; pub use doc_state::{DocState, OpenRows, RecalledSelection}; -pub use error::{EngineError, Result, SCRIPT_BUSY}; +pub use error::{EngineError, READ_ONLY_LOCATION, Result, SCRIPT_BUSY}; #[cfg(test)] use history::history_budget_for; pub use history::{History, HistoryEntry, NamedStep, default_history_budget}; diff --git a/crates/engine/src/tests/script_io.rs b/crates/engine/src/tests/script_io.rs index d8c5d1d2e4..e056ea2f7c 100644 --- a/crates/engine/src/tests/script_io.rs +++ b/crates/engine/src/tests/script_io.rs @@ -230,7 +230,7 @@ fn script_file_link_escape_refused() { /// not matter, and outside it where case matters, whether or not the file is there. #[test] fn script_file_case_variant_folder() { - let base = folder("case"); + let base = crate::cmd::findfiles::plain(folder("case").canonicalize().unwrap()); write(&base.join("main/own.txt"), b"own"); let upper = base.join("MAIN"); let source = |name: &str| { diff --git a/crates/engine/src/tests/swatchlib.rs b/crates/engine/src/tests/swatchlib.rs index 695a2962a5..b380a4e06b 100644 --- a/crates/engine/src/tests/swatchlib.rs +++ b/crates/engine/src/tests/swatchlib.rs @@ -844,7 +844,7 @@ fn a_grit_texture_adds_once_undoes_and_saves() { let list = run(&mut s, "swatch.library.list", json!({})); let ids: Vec<&str> = list["libraries"].as_array().unwrap().iter().filter(|l| l["category"] == "patterns").map(|l| l["id"].as_str().unwrap()).collect(); - assert_eq!(ids, ["geometric-patterns", "grit-textures"]); + assert_eq!(ids, ["geometric-patterns", "grit-textures", "optical-effects"]); let got = run(&mut s, "swatch.library.get", json!({"library": "Grit Textures"})); let names: Vec<&str> = got["swatches"].as_array().unwrap().iter().map(|w| w["name"].as_str().unwrap()).collect(); assert_eq!(names, ["Fine Grain", "Coarse Grit", "Sand", "Speckle", "Stipple", "Scratches", "Sparse Flecks", "Dense Flecks"]); @@ -869,6 +869,49 @@ fn a_grit_texture_adds_once_undoes_and_saves() { assert_eq!(back.swatch("Stipple").map(|w| &w.paint), doc(&s).swatch("Stipple").map(|w| &w.paint)); } +/// Optical Effects is listed after Grit Textures, and reading it adds nothing. Clicking one of +/// its patterns brings it with its swatch and fills the selection with it, once, as one undo step +/// that also takes both away, and both stay after a save and reopen. +#[test] +fn an_optical_effect_adds_once_fills_undoes_and_saves() { + let mut s = session(); + let counts = |d: &vectorcraft_doc::Document| (d.patterns.len(), d.swatches_iter().count()); + let empty = counts(doc(&s)); + let list = run(&mut s, "swatch.library.list", json!({})); + let ids: Vec<&str> = + list["libraries"].as_array().unwrap().iter().filter(|l| l["category"] == "patterns").map(|l| l["id"].as_str().unwrap()).collect(); + assert_eq!(ids, ["geometric-patterns", "grit-textures", "optical-effects"]); + let got = run(&mut s, "swatch.library.get", json!({"library": "Optical Effects"})); + let names: Vec<&str> = got["swatches"].as_array().unwrap().iter().map(|w| w["name"].as_str().unwrap()).collect(); + assert_eq!( + names, + ["Sine Bands", "Ripple Lines", "Interference Rings", "Offset Rings", "Warped Checks", "Pinched Stripes", "Chevron Pulse", "Radiating Lenses"] + ); + assert_eq!(counts(doc(&s)), empty, "reading a library adds no pattern or swatch"); + let id = vectorcraft_doc::NodeId(run(&mut s, "shape.rectangle", json!({"x": 0, "y": 0, "width": 50, "height": 50}))["id"].as_u64().unwrap()); + let undo = undo_len(&s); + let r = run(&mut s, "swatch.library.add", json!({"library": "optical-effects", "names": ["Warped Checks"], "apply": "fill"})); + assert_eq!((r["added"].clone(), r["applied"].clone()), (json!(["Warped Checks"]), json!("Warped Checks"))); + assert_eq!((counts(doc(&s)), undo_len(&s)), ((empty.0 + 1, empty.1 + 1), undo + 1)); + let def = doc(&s).pattern("Warped Checks").expect("the pattern comes with its swatch").clone(); + assert_eq!(def.art.len(), 32, "the black cells of an 8 × 8 checkerboard"); + assert!(matches!(doc(&s).swatch("Warped Checks").map(|w| &w.paint), Some(Paint::Pattern { pattern, .. }) if pattern == "Warped Checks")); + let filled = |d: &vectorcraft_doc::Document| matches!(d.node(id).unwrap().appearance.fill_paint(), Paint::Pattern { pattern, .. } if pattern == "Warped Checks"); + assert!(filled(doc(&s))); + let r = run(&mut s, "swatch.library.add", json!({"library": "optical-effects", "names": ["Warped Checks"]})); + assert_eq!((r["added"].clone(), r["existing"].clone()), (json!([]), json!(["Warped Checks"]))); + assert_eq!((counts(doc(&s)), undo_len(&s)), ((empty.0 + 1, empty.1 + 1), undo + 1), "added once"); + run(&mut s, "edit.undo", json!({})); + assert_eq!(counts(doc(&s)), empty); + assert!(doc(&s).pattern("Warped Checks").is_none() && doc(&s).swatch("Warped Checks").is_none() && !filled(doc(&s))); + run(&mut s, "edit.redo", json!({})); + assert!(filled(doc(&s))); + let back = vectorcraft_format::load(&vectorcraft_format::save(doc(&s), false)).unwrap(); + assert_eq!(back.pattern("Warped Checks"), Some(&def)); + assert_eq!(back.swatch("Warped Checks").map(|w| &w.paint), doc(&s).swatch("Warped Checks").map(|w| &w.paint)); + assert!(filled(&back)); +} + /// A device name is saved with a `-` after it, which can be another library's own name: neither /// library replaces the other, and saving either again replaces only itself. #[test] diff --git a/crates/format/src/atomic.rs b/crates/format/src/atomic.rs index 863c6fdd34..8e7149cad2 100644 --- a/crates/format/src/atomic.rs +++ b/crates/format/src/atomic.rs @@ -18,6 +18,25 @@ use std::io::{self, Write as _}; use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicU64, Ordering}; +/// Whether the returned write error refuses this location. Recovery errors must still be shown: +/// their message names copies the user may need, even when their kind is PermissionDenied. +pub fn read_only_location(error: &io::Error) -> bool { + matches!(error.kind(), io::ErrorKind::ReadOnlyFilesystem | io::ErrorKind::PermissionDenied) + && !error.get_ref().is_some_and(|e| e.is::()) +} + +/// Marks the existing recovery message without changing its text or the returned error kind. +#[derive(Debug)] +struct RecoveryError(String); + +impl std::fmt::Display for RecoveryError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.0) + } +} + +impl std::error::Error for RecoveryError {} + /// Write `bytes` to `path` atomically (see the module docs). pub fn write_atomic(path: &Path, bytes: &[u8]) -> io::Result<()> { write_atomic_with(path, |f| f.write_all(bytes)) @@ -102,7 +121,7 @@ fn write_over(tmp: &Path, target: &Path, refused: io::Error) -> io::Result<()> { let _ = std::fs::remove_file(old); let why = format!("{refused}, and the file can't be copied aside before it is overwritten ({e}); the new content is kept in {}", tmp.display()); - return Err(io::Error::new(refused.kind(), why)); + return Err(io::Error::new(refused.kind(), RecoveryError(why))); } let copied = to.set_len(0).and_then(|()| io::copy(&mut from, &mut to)).and_then(|_| sync_file(&to)); drop((from, to)); @@ -115,10 +134,11 @@ fn write_over(tmp: &Path, target: &Path, refused: io::Error) -> io::Result<()> { } Ok(()) } - (Err(e), Some(old)) => { - Err(io::Error::new(e.kind(), format!("{e} (the new content is kept in {}, the file as it was in {})", tmp.display(), old.display()))) - } - (Err(e), None) => Err(io::Error::new(e.kind(), format!("{e} (the new content is kept in {})", tmp.display()))), + (Err(e), Some(old)) => Err(io::Error::new( + e.kind(), + RecoveryError(format!("{e} (the new content is kept in {}, the file as it was in {})", tmp.display(), old.display())), + )), + (Err(e), None) => Err(io::Error::new(e.kind(), RecoveryError(format!("{e} (the new content is kept in {})", tmp.display())))), } } @@ -318,6 +338,24 @@ fn keep_permissions(_: &Path, _: &Path) -> io::Result<()> { mod tests { use super::*; + #[test] + fn readonly_classification_uses_the_terminal_kind_and_excludes_recovery() { + for (kind, expected) in [ + (io::ErrorKind::PermissionDenied, true), + (io::ErrorKind::ReadOnlyFilesystem, true), + (io::ErrorKind::StorageFull, false), + (io::ErrorKind::QuotaExceeded, false), + (io::ErrorKind::NotFound, false), + (io::ErrorKind::Other, false), + ] { + assert_eq!(read_only_location(&io::Error::new(kind, "injected")), expected, "{kind:?}"); + let recovery = io::Error::new(kind, RecoveryError("the new content is kept in a.tmp".into())); + assert!(!read_only_location(&recovery), "recovery: {kind:?}"); + assert_eq!(recovery.kind(), kind); + assert_eq!(recovery.to_string(), "the new content is kept in a.tmp"); + } + } + fn dir(name: &str) -> PathBuf { let d = std::env::temp_dir().join(format!("vc-atomic-{name}-{}", std::process::id())); let _ = std::fs::remove_dir_all(&d); diff --git a/crates/format/src/lib.rs b/crates/format/src/lib.rs index cd64fcbb50..4e23ec0842 100644 --- a/crates/format/src/lib.rs +++ b/crates/format/src/lib.rs @@ -55,7 +55,7 @@ use serde::{Deserializer, Serialize}; use vectorcraft_doc::{Document, ImageBlob}; #[cfg(not(target_arch = "wasm32"))] -pub use atomic::{sync_file, write_atomic, write_atomic_with, write_new_with}; +pub use atomic::{read_only_location, sync_file, write_atomic, write_atomic_with, write_new_with}; pub use craft_vectorcraft_format::base64::{decode as base64_decode, decoded_len as base64_decoded_len, encode as base64_encode}; pub use craft_vectorcraft_format::{ BLOB_MAGIC, COMPRESSED_SINCE, EXTENSION, ICC_MIME, INLINE_MAX, LEGACY_EXTENSION, MIN_VERSION, PREVIEW_MAX, VERSION, is_compressed, json_part, diff --git a/crates/mcp/src/tests/fileio.rs b/crates/mcp/src/tests/fileio.rs index 54baeeb0c3..39bcde8d2e 100644 --- a/crates/mcp/src/tests/fileio.rs +++ b/crates/mcp/src/tests/fileio.rs @@ -358,6 +358,29 @@ fn save_file_waits_for_the_file_in_the_app() { } } +#[test] +fn readonly_save_file_keeps_the_stable_error_prefix() { + struct Refused(bool); + impl Backend for Refused { + fn call(&mut self, method: &str, params: Value) -> Result { + assert_eq!(method, "app.save"); + assert_eq!(params.get("wait").and_then(Value::as_bool), self.0.then_some(true)); + Err(vectorcraft_engine::EngineError::ReadOnlyLocation("Poster.svg: injected refusal".into()).to_string()) + } + fn has_ui(&self) -> bool { + self.0 + } + fn describe(&self) -> String { + "injected save refusal".into() + } + } + for desktop in [false, true] { + let r = call_tool(&mut Refused(desktop), "save_file", &json!({})); + assert!(r.is_error); + assert_eq!(r.content[0]["text"], "save_file: READ_ONLY_LOCATION: Poster.svg: injected refusal"); + } +} + fn pdf_pages(pdf: &[u8]) -> usize { let mut h = Headless::new(); h.call("engine.execute", json!({"command": "document.open", "params": {"name": "p.pdf", "dataBase64": vectorcraft_format::base64_encode(pdf)}})) diff --git a/crates/mcp/src/ui_catalog.rs b/crates/mcp/src/ui_catalog.rs index b07964b225..4f3ddeb86d 100644 --- a/crates/mcp/src/ui_catalog.rs +++ b/crates/mcp/src/ui_catalog.rs @@ -39,7 +39,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[ "file.save", "Save", "Cmd+S", - "{path?, format?, options?, svg?: {…SVG options}} = document.save, written through the app (a document saved as SVG, PDF or .ai, or opened from one Save can write back, saves as that again, with the same options); with no path known (never saved, converted) the Save As panel asks first", + "{path?, format?, options?, svg?: {…SVG options}} = document.save, written through the app (a document saved as SVG, PDF or .ai, or opened from one Save can write back, saves as that again, with the same options); with no path known (never saved, converted) the Save As panel asks first; ordinary Save to the document's existing file refused by filesystem permissions or a read-only volume opens Save As in Documents with the same name, format and options; cancelling keeps the original path and modified state. Explicit destinations and Close/Quit keep their existing error dialog. Automation gets READ_ONLY_LOCATION: followed by the reason and no panel", ), ( "file.place", diff --git a/crates/svg/src/import/text.rs b/crates/svg/src/import/text.rs index 52b6663906..51bcffa41e 100644 --- a/crates/svg/src/import/text.rs +++ b/crates/svg/src/import/text.rs @@ -247,8 +247,9 @@ pub(super) fn prepare<'s>( && n.tag_name().namespace() == Some(SVG_NS) && !n.ancestors().any(|a| a.has_tag_name("style")) }; - // The slot of each text, by element, for the `` elements in clip paths below. + // The slot of each text, by element, for the `` elements below. let mut slot_of = HashMap::new(); + let mut inst = Instances::default(); for (t, style_t) in xml.descendants().filter(is_text).zip(style_xml.descendants().filter(is_text)) { let label = t.attribute("id").map_or_else(|| "a text".to_string(), |id| format!("text '{id}'")); // CSS Masking Module Level 1 §6.1 (https://www.w3.org/TR/css-masking-1/#ClipPathElement): @@ -275,20 +276,22 @@ pub(super) fn prepare<'s>( // A `` elsewhere can still show the text by its id: the clip path leaves out this // group (a group isn't clip path content), but the `` finds it. if t.attribute("id").is_some() { - placeholder(&mut out, q, &cx.css, t, style_t, &slots.prefix, index, text.marker, &paints); + placeholder(&mut out, q, &cx.css, t, style_t, &slots.prefix, index, text.marker, &paints, true); } } else { - placeholder(&mut out, q, &cx.css, t, style_t, &slots.prefix, index, text.marker, &paints); + placeholder(&mut out, q, &cx.css, t, style_t, &slots.prefix, index, text.marker, &paints, true); } copied = r.end; edits.push((r, out)); slot_of.insert(t.id(), index); slots.texts.push(text); + inst.paints.push(paints); } if slots.texts.is_empty() { return (Cow::Borrowed(svg), slots); } - clip_uses(svg, xml, style_xml, &cx, &slot_of, &mut slots, &mut edits, warnings); + clip_uses(svg, xml, style_xml, &cx, &slot_of, &mut slots, &mut inst, &mut edits, warnings); + text_uses(svg, xml, style_xml, &cx, &slot_of, &mut slots, &mut inst, &mut edits, warnings); edits.sort_by_key(|(r, _)| r.start); let mut out = String::with_capacity(svg.len()); let mut copied = 0; @@ -332,6 +335,7 @@ fn clip_uses( cx: &Ctx, slot_of: &HashMap, slots: &mut TextSlots, + inst: &mut Instances, edits: &mut Vec<(Range, String)>, warnings: &mut Vec, ) { @@ -343,18 +347,16 @@ fn clip_uses( for (k, (u, style_u)) in xml.descendants().filter(is_use).zip(style_xml.descendants().filter(is_use)).enumerate() { let Some(id) = href(u).and_then(|h| h.strip_prefix('#')) else { continue }; let Some(t) = ids.get(id) else { continue }; - let Some(&i) = slot_of.get(&t.id()) else { continue }; - let Some(marker) = slots.texts.get(i).map(|text| text.marker) else { continue }; + let Some(&shared) = slot_of.get(&t.id()) else { continue }; let Some(q) = qualifier(svg, u) else { continue }; // An undisplayed `` adds nothing to the clip (usvg leaves it out as it is). if cx.css.own(style_u, "display").is_some_and(|d| d.trim() == "none") { continue; } let style_t = style_ids.get(id).copied().filter(|s| s.tag_name() == t.tag_name()); - let em = cx.font_size(style_u); - let length = |a: &str, pct: f64| u.attribute(a).and_then(|v| cx.length(v, em, pct)).unwrap_or(0.0); - let (x, y) = (length("x", cx.viewport.0), length("y", cx.viewport.1)); - let placed = format!("{} translate({x} {y})", own_transform(&cx.css, style_u)); + let i = style_t.map_or(shared, |s| use_slot(cx, style_xml, slots, inst, shared, s, style_u, warnings)); + let Some(marker) = slots.texts.get(i).map(|text| text.marker) else { continue }; + let placed = use_placement(cx, u, style_u); let text_xf = match style_t { Some(s) => own_transform(&cx.css, s), None => t.attribute("transform").unwrap_or("").to_string(), @@ -386,6 +388,186 @@ fn clip_uses( } } +/// Replace each `` outside clip paths that shows a text it passes other properties on to by a +/// group holding that text's placeholder as the `` shows it ([`use_slot`]). Every other +/// `` of a text shows the text's own placeholder. +#[allow(clippy::too_many_arguments)] +fn text_uses( + svg: &str, + xml: &roxmltree::Document, + style_xml: &roxmltree::Document, + cx: &Ctx, + slot_of: &HashMap, + slots: &mut TextSlots, + inst: &mut Instances, + edits: &mut Vec<(Range, String)>, + warnings: &mut Vec, +) { + let is_use = |n: &XNode| { + n.is_element() + && n.tag_name().name() == "use" + && n.tag_name().namespace() == Some(SVG_NS) + && !n.ancestors().any(|a| a.is_element() && a.tag_name().name() == "clipPath") + }; + if !xml.descendants().any(|n| is_use(&n)) { + return; + } + let (ids, style_ids) = (first_ids(xml), first_ids(style_xml)); + for (u, style_u) in xml.descendants().filter(is_use).zip(style_xml.descendants().filter(is_use)) { + let Some(id) = href(u).and_then(|h| h.strip_prefix('#')) else { continue }; + let Some(t) = ids.get(id) else { continue }; + let Some(&shared) = slot_of.get(&t.id()) else { continue }; + let Some(style_t) = style_ids.get(id).copied().filter(|s| s.tag_name() == t.tag_name()) else { continue }; + let Some(q) = qualifier(svg, u) else { continue }; + let i = use_slot(cx, style_xml, slots, inst, shared, style_t, style_u, warnings); + let (Some(text), Some(paints)) = (slots.texts.get(i), inst.paints.get(i)) else { continue }; + if i == shared { + continue; + } + let mut out = String::new(); + use_group(&mut out, q, cx, u, style_u); + placeholder(&mut out, q, &cx.css, *t, style_t, &slots.prefix, i, text.marker, paints, false); + let _ = write!(out, ""); + edits.push((u.range(), out)); + } +} + +/// A text's slot, the parent of a `` showing it and the properties that `` sets itself. +type UseKey = (usize, Option, Vec<(String, String)>); + +/// The slots made for texts as `` elements show them ([`use_slot`]). +#[derive(Default)] +struct Instances { + /// The `url(#…)` paints of each slot, by index. + paints: Vec>, + /// The slot made for each [`UseKey`]; `None` where the text's own slot serves. + memo: HashMap>, + /// The nodes, text bytes and text path elements of the texts built so far, up to + /// [`MAX_INSTANCE_WORK`]. + work: usize, +} + +/// The most nodes, text bytes and elements of the paths ``s follow that [`use_slot`] +/// builds texts from, all `` elements together (the file is untrusted). Past it, a `` +/// shows its text with the text's own properties. +const MAX_INSTANCE_WORK: usize = 1 << 20; + +/// The slot of text `style_t` (slot `shared`) as `` `style_u` shows it. The text inherits +/// from the `` there ([`Ctx::instance`]): when that changes it, it gets a new slot; otherwise +/// the result is `shared`. `` elements with one parent that set the same properties share one. +#[allow(clippy::too_many_arguments)] +fn use_slot( + cx: &Ctx, + style_xml: &roxmltree::Document, + slots: &mut TextSlots, + inst: &mut Instances, + shared: usize, + style_t: XNode, + style_u: XNode, + warnings: &mut Vec, +) -> usize { + // A `` inside the text it shows would make the text inherit from itself. + if style_u.ancestors().any(|a| a == style_t) { + return shared; + } + let placing = |name: &str| matches!(name, "id" | "x" | "y" | "width" | "height" | "href" | "transform" | "style"); + let mut own: Vec<(String, String)> = style_u + .attributes() + .filter(|a| a.namespace().is_none() && !placing(a.name())) + .map(|a| (a.name().to_string(), a.value().to_string())) + .collect(); + own.extend(cx.css.declared(style_u).iter().filter(|(k, _)| k.as_str() != "transform").map(|(k, v)| (format!("style {k}"), v.clone()))); + own.sort(); + let key = (shared, style_u.parent_element().map(|p| p.id()), own); + if let Some(slot) = inst.memo.get(&key) { + return slot.unwrap_or(shared); + } + let label = style_t.attribute("id").map_or_else(|| "a text".to_string(), |id| format!("text '{id}'")); + let work = if inst.work < MAX_INSTANCE_WORK { + // Each build lays the text out along the path its `` follows, so the path counts too. + style_t + .descendants() + .map(|n| if n.is_text() { n.text().map_or(0, str::len) } else { cx.path_elements(n).saturating_add(1) }) + .fold(0, usize::saturating_add) + } else { + 0 + }; + inst.work = inst.work.saturating_add(work); + if work == 0 || inst.work > MAX_INSTANCE_WORK { + inst.work = MAX_INSTANCE_WORK; + let w = format!( + "more than {} KiB of text and text paths shown through elements: the rest show their text without the properties the passes on", + MAX_INSTANCE_WORK >> 10 + ); + if !warnings.contains(&w) { + warnings.push(w); + } + return shared; + } + let shown = cx.instance(style_t, style_u); + let mut found = vec![]; + let built = if style_t.tag_name().name() == "flowRoot" { + shown.build_flow(style_t, &label, &mut found) + } else { + shown.build(style_xml, style_t, &label, &mut found) + }; + let differs = |(text, paints): &(PendingText, Vec)| { + slots.texts.get(shared).is_none_or(|old| old.obj != text.obj || old.servers != text.servers || old.under != text.under) + || inst.paints.get(shared) != Some(paints) + }; + let slot = built.filter(differs).map(|(text, paints)| { + for w in found { + if !warnings.contains(&w) { + warnings.push(w); + } + } + slots.texts.push(text); + inst.paints.push(paints); + slots.texts.len() - 1 + }); + inst.memo.insert(key, slot); + slot.unwrap_or(shared) +} + +/// The transform `` `u` places what it shows with: its own, then its `x`/`y` offset (in its +/// font size). `style_u` supplies its styles. +fn use_placement(cx: &Ctx, u: XNode, style_u: XNode) -> String { + let em = cx.font_size(style_u); + let length = |a: &str, pct: f64| u.attribute(a).and_then(|v| cx.length(v, em, pct)).unwrap_or(0.0); + let (x, y) = (length("x", cx.viewport.0), length("y", cx.viewport.1)); + format!("{} translate({x} {y})", own_transform(&cx.css, style_u)) +} + +/// Write the start tag of the group that takes `` `u`'s place: its attributes in no namespace +/// other than its link, place and size, and as its `style` the properties rules and its `style` set +/// on it (from `style_u`), then its placement ([`use_placement`]) and its non-inherited properties +/// and `visibility`, so no rule that matches the group changes it. +fn use_group(out: &mut String, q: &str, cx: &Ctx, u: XNode, style_u: XNode) { + let _ = write!(out, "<{q}g"); + let placing = |name: &str| matches!(name, "style" | "href" | "x" | "y" | "width" | "height" | "transform"); + for a in u.attributes().filter(|a| a.namespace().is_none() && !placing(a.name())) { + let _ = write!(out, " {}=\"{}\"", a.name(), xml_escape(a.value())); + } + let declared = cx.css.declared(style_u); + let mut props: Vec<_> = declared.iter().filter(|(k, _)| k.as_str() != "transform").collect(); + props.sort(); + let mut style: String = props.into_iter().map(|(k, v)| format!("{k}:{v};")).collect(); + let _ = write!(style, "transform:{};", use_placement(cx, u, style_u)); + for (p, initial) in [ + ("opacity", "1"), + ("clip-path", "none"), + ("mask", "none"), + ("filter", "none"), + ("mix-blend-mode", "normal"), + ("isolation", "auto"), + ("display", "inline"), + ] { + let _ = write!(style, "{p}:{};", cx.css.own(style_u, p).as_deref().unwrap_or(initial)); + } + let _ = write!(style, "visibility:{}", cx.css.prop(style_u, "visibility").as_deref().unwrap_or("visible")); + let _ = write!(out, " style=\"{}\">", xml_escape(&style)); +} + /// Write a copy of `` `u` in a ``: its attributes in no namespace, its link as `href`, /// and the properties set on it by rules and its `style` (from `style_u`) as its `style`. fn use_copy(out: &mut String, q: &str, css: &Styles, u: XNode, style_u: XNode) { @@ -455,11 +637,23 @@ fn clip_placeholder(out: &mut String, q: &str, id: &str, transform: &str, props: let _ = write!(out, " d=\"M{} {}H{}V{}H{}Z\"/>", b.x0, b.y0, b.x1, b.y1, b.x0); } -/// Write the placeholder group for text `t` (namespace prefix `q`), slot `index`. +/// Write the placeholder group for text `t` (namespace prefix `q`), slot `index`, with the text's +/// id when `with_id`. #[allow(clippy::too_many_arguments)] -fn placeholder(out: &mut String, q: &str, css: &Styles, t: XNode, style_t: XNode, prefix: &str, index: usize, b: Rect, paints: &[String]) { +fn placeholder( + out: &mut String, + q: &str, + css: &Styles, + t: XNode, + style_t: XNode, + prefix: &str, + index: usize, + b: Rect, + paints: &[String], + with_id: bool, +) { let _ = write!(out, "<{q}g"); - for a in ["id", "systemLanguage", "requiredFeatures", "requiredExtensions"] { + for a in ["id", "systemLanguage", "requiredFeatures", "requiredExtensions"].into_iter().skip(usize::from(!with_id)) { if let Some(v) = t.attribute(a) { let _ = write!(out, " {a}=\"{}\"", xml_escape(v)); } @@ -488,25 +682,41 @@ fn placeholder(out: &mut String, q: &str, css: &Styles, t: XNode, style_t: XNode /// Computed text properties of the document's elements. struct Ctx { - css: Styles, + css: Rc, /// Pixels per inch absolute lengths convert at ([`super::RootUnits::dpi`]). dpi: f64, /// Viewport size in user units (the base of `x`/`y` percentages). viewport: (f64, f64), + /// For the styles of one `` instance: the element the `` shows and the ``. The + /// shown element inherits from the `` rather than from its parent (SVG 2 §5.6, + /// https://www.w3.org/TR/SVG2/struct.html#UseElement). + host: Option<(roxmltree::NodeId, roxmltree::NodeId)>, sizes: RefCell>, weights: RefCell>, elems: RefCell>>, + /// The path each `` read so far follows ([`Self::text_path`]); `None` where it has + /// no usable one. The styles of a `` instance share it, so each path is found and parsed + /// once however many `` elements show its text. + paths: Rc>>>>, +} + +#[cfg(test)] +thread_local! { + /// How many times [`Ctx::read_text_path`] has read a ``'s path on this thread. + static TEXT_PATH_READS: std::cell::Cell = const { std::cell::Cell::new(0) }; } impl Ctx { fn new(xml: &roxmltree::Document, dpi: f64) -> Self { let mut cx = Self { - css: Styles::new(xml), + css: Rc::new(Styles::new(xml)), dpi, viewport: (100.0, 100.0), + host: None, sizes: RefCell::default(), weights: RefCell::default(), elems: RefCell::default(), + paths: Rc::default(), }; let root = xml.root_element(); let dim = |a: &str| root.attribute(a).and_then(|v| cx.length(v, DEFAULT_FONT_SIZE, 0.0)).filter(|v| *v > 0.0); @@ -517,6 +727,38 @@ impl Ctx { cx } + /// The styles of element `shown` as `` `u` shows it. + fn instance(&self, shown: XNode, u: XNode) -> Self { + Self { + css: self.css.clone(), + dpi: self.dpi, + viewport: self.viewport, + host: Some((shown.id(), u.id())), + sizes: RefCell::default(), + weights: RefCell::default(), + elems: RefCell::default(), + paths: self.paths.clone(), + } + } + + /// The element `n` inherits from: its parent, or the `` for the element it shows. + fn parent<'a, 'i>(&self, n: XNode<'a, 'i>) -> Option> { + match self.host { + Some((shown, u)) if n.id() == shown => n.document().get_node(u), + _ => n.parent_element(), + } + } + + /// Element `n` and the elements it inherits from, nearest first. + fn lineage<'a, 'i>(&self, n: XNode<'a, 'i>) -> impl Iterator> { + std::iter::successors(Some(n), |a| self.parent(*a)) + } + + /// An inherited property: the nearest specified value other than `inherit`. + fn prop(&self, n: XNode, name: &str) -> Option { + self.lineage(n).find_map(|a| self.css.own(a, name).filter(|v| v != "inherit")) + } + /// A length in user units: absolute units convert at [`Self::dpi`], `em` and `ex` are relative /// to `em` (the font size) and percentages to `pct`. fn length(&self, v: &str, em: f64, pct: f64) -> Option { @@ -559,7 +801,7 @@ impl Ctx { if let Some(s) = self.sizes.borrow().get(&n.id()) { return *s; } - let parent = n.parent_element().map_or(DEFAULT_FONT_SIZE, |p| self.font_size(p)); + let parent = self.parent(n).map_or(DEFAULT_FONT_SIZE, |p| self.font_size(p)); let s = self.css.own(n, "font-size").and_then(|v| self.parse_font_size(&v, parent)).unwrap_or(parent); self.sizes.borrow_mut().insert(n.id(), s); s @@ -570,7 +812,7 @@ impl Ctx { if let Some(w) = self.weights.borrow().get(&n.id()) { return *w; } - let parent = n.parent_element().map_or(400, |p| self.weight(p)); + let parent = self.parent(n).map_or(400, |p| self.weight(p)); let w = match self.css.own(n, "font-weight").as_deref() { Some("normal") => 400, Some("bold") => 700, @@ -594,12 +836,12 @@ impl Ctx { /// A fill or stroke: a ready paint, or a `url(#…)` paint for usvg to resolve. fn paint(&self, n: XNode, prop: &str) -> (Paint, Option) { let initial = if prop == "fill" { Paint::solid(Color::BLACK) } else { Paint::None }; - let Some(v) = self.css.prop(n, prop) else { return (initial, None) }; + let Some(v) = self.prop(n, prop) else { return (initial, None) }; match svgtypes::Paint::from_str(&v) { Ok(svgtypes::Paint::None) => (Paint::None, None), Ok(svgtypes::Paint::Color(c)) => (solid(c), None), Ok(svgtypes::Paint::CurrentColor) => { - (self.css.prop(n, "color").and_then(|c| svgtypes::Color::from_str(&c).ok()).map_or(Paint::solid(Color::BLACK), solid), None) + (self.prop(n, "color").and_then(|c| svgtypes::Color::from_str(&c).ok()).map_or(Paint::solid(Color::BLACK), solid), None) } Ok(svgtypes::Paint::FuncIRI(..)) => (Paint::None, Some(v)), _ => (initial, None), @@ -613,7 +855,7 @@ impl Ctx { } let css = &self.css; let mut st = CharStyle::default(); - if let Some(f) = css.prop(n, "font-family") { + if let Some(f) = self.prop(n, "font-family") { let fam = f.split(',').next().unwrap_or("").trim().trim_matches(|c| c == '\'' || c == '"').to_string(); if !fam.is_empty() { st.font_family = fam; @@ -621,45 +863,46 @@ impl Ctx { } let size = self.font_size(n); st.size = size; - let italic = css.prop(n, "font-style").is_some_and(|s| s == "italic" || s == "oblique"); + let italic = self.prop(n, "font-style").is_some_and(|s| s == "italic" || s == "oblique"); st.font_style = style_name(self.weight(n), italic); // Inherited: `inherit`, `unset`, `revert` and a value CSS ignores leave the one inherited. let inherits = |v: &String| ["inherit", "unset", "revert", "revert-layer"].iter().any(|k| v.trim().eq_ignore_ascii_case(k)); let own = |a: XNode| css.own(a, "font-variation-settings").filter(|v| !inherits(v)).and_then(|v| variation_settings(&v)); - st.font_axes = n.ancestors().filter(|a| a.is_element()).find_map(own).unwrap_or_default(); + st.font_axes = self.lineage(n).find_map(own).unwrap_or_default(); let (fill, mut fill_url) = self.paint(n, "fill"); st.fill = fill; let (stroke, mut stroke_url) = self.paint(n, "stroke"); if !stroke.is_none() || stroke_url.is_some() { st.stroke = stroke; - st.stroke_width = css.prop(n, "stroke-width").and_then(|w| self.length(&w, size, size)).unwrap_or(1.0); - st.stroke_cap = match css.prop(n, "stroke-linecap").as_deref() { + st.stroke_width = self.prop(n, "stroke-width").and_then(|w| self.length(&w, size, size)).unwrap_or(1.0); + st.stroke_cap = match self.prop(n, "stroke-linecap").as_deref() { Some("round") => LineCap::Round, Some("square") => LineCap::Square, _ => LineCap::Butt, }; - st.stroke_join = match css.prop(n, "stroke-linejoin").as_deref() { + st.stroke_join = match self.prop(n, "stroke-linejoin").as_deref() { Some("round") => LineJoin::Round, Some("bevel") => LineJoin::Bevel, _ => LineJoin::Miter, }; st.stroke_miter_limit = - css.prop(n, "stroke-miterlimit").and_then(|m| m.trim().parse::().ok()).filter(|m| m.is_finite()).map_or(4.0, |m| m.max(1.0)); - let pattern: Vec = css.prop(n, "stroke-dasharray").map_or(vec![], |d| list(&d).filter_map(|v| self.length(v, size, size)).collect()); - let offset = css.prop(n, "stroke-dashoffset").and_then(|o| self.length(&o, size, size)).unwrap_or(0.0); + self.prop(n, "stroke-miterlimit").and_then(|m| m.trim().parse::().ok()).filter(|m| m.is_finite()).map_or(4.0, |m| m.max(1.0)); + let pattern: Vec = + self.prop(n, "stroke-dasharray").map_or(vec![], |d| list(&d).filter_map(|v| self.length(v, size, size)).collect()); + let offset = self.prop(n, "stroke-dashoffset").and_then(|o| self.length(&o, size, size)).unwrap_or(0.0); st.stroke_dash = Some(Dash { pattern, offset, align_corners: false }).filter(Dash::is_dashed); } - if let Some(v) = css.prop(n, "letter-spacing").and_then(|v| self.length(&v, size, size)) + if let Some(v) = self.prop(n, "letter-spacing").and_then(|v| self.length(&v, size, size)) && size > 0.0 { st.tracking = v / size * 1000.0; } - if let Some(v) = css.prop(n, "kerning").and_then(|v| self.length(&v, size, size)) + if let Some(v) = self.prop(n, "kerning").and_then(|v| self.length(&v, size, size)) && size > 0.0 { st.kerning = Some(v / size * 1000.0); } - if let Some(d) = css.prop(n, "text-decoration") { + if let Some(d) = self.prop(n, "text-decoration") { st.underline = d.contains("underline"); st.strikethrough = d.contains("line-through"); } @@ -672,11 +915,11 @@ impl Ctx { }; let parent = if n.tag_name().name() == "text" { 0.0 } else { n.parent_element().map_or(0.0, |p| self.elem(p).style.baseline_shift) }; st.baseline_shift = parent + own; - if css.prop(n, "visibility").is_some_and(|v| v == "hidden" || v == "collapse") { + if self.prop(n, "visibility").is_some_and(|v| v == "hidden" || v == "collapse") { (st.fill, st.stroke, fill_url, stroke_url) = (Paint::None, Paint::None, None, None); } - let word_spacing = css.prop(n, "word-spacing").and_then(|v| self.length(&v, size, size)).unwrap_or(0.0); - let order = css.prop(n, "paint-order").and_then(|v| svgtypes::PaintOrder::from_str(&v).ok()).unwrap_or_default().order; + let word_spacing = self.prop(n, "word-spacing").and_then(|v| self.length(&v, size, size)).unwrap_or(0.0); + let order = self.prop(n, "paint-order").and_then(|v| svgtypes::PaintOrder::from_str(&v).ok()).unwrap_or_default().order; let at = |k| order.iter().position(|o| *o == k); let stroke_first = at(svgtypes::PaintOrderKind::Stroke) < at(svgtypes::PaintOrderKind::Fill); let e = Rc::new(ElemStyle { style: st, fill: fill_url, stroke: stroke_url, word_spacing, stroke_first }); @@ -761,8 +1004,27 @@ impl Ctx { } } - /// The path a `` follows, in the text's user space. - fn text_path(&self, xml: &roxmltree::Document, tp: XNode) -> Option { + /// The path a `` follows, in the text's user space. It is read from the file once + /// ([`Self::paths`]). + fn text_path(&self, xml: &roxmltree::Document, tp: XNode) -> Option> { + if let Some(known) = self.paths.borrow().get(&tp.id()) { + return known.clone(); + } + let path = self.read_text_path(xml, tp).map(Rc::new); + self.paths.borrow_mut().insert(tp.id(), path.clone()); + path + } + + /// How many elements the path of `` `tp` has, when a text built so far follows it + /// ([`Self::text_path`]); 0 for any other node. + fn path_elements(&self, tp: XNode) -> usize { + self.paths.borrow().get(&tp.id()).and_then(|p| p.as_deref()).map_or(0, |p| p.elements().len()) + } + + /// Find and parse the path `` `tp` follows. + fn read_text_path(&self, xml: &roxmltree::Document, tp: XNode) -> Option { + #[cfg(test)] + TEXT_PATH_READS.set(TEXT_PATH_READS.get() + 1); let mut bp = match tp.attribute("path") { Some(d) => BezPath::from_svg(d).ok()?, None => { @@ -974,7 +1236,7 @@ impl Ctx { .collect(); let under = stroke_under(&mut cells, label, warnings); - let anchor_of = |i: usize| match cs.chars.get(i).and_then(|c| self.css.prop(c.1, "text-anchor")).as_deref() { + let anchor_of = |i: usize| match cs.chars.get(i).and_then(|c| self.prop(c.1, "text-anchor")).as_deref() { Some("middle") => 0.5, Some("end") => 1.0, _ => 0.0, @@ -987,7 +1249,7 @@ impl Ctx { None } }); - let vertical = self.css.prop(t, "writing-mode").is_some_and(|m| m.starts_with("tb") || m.starts_with("vertical")); + let vertical = self.prop(t, "writing-mode").is_some_and(|m| m.starts_with("tb") || m.starts_with("vertical")); self.fit_lengths(&cs.spans, &mut cells, vertical, label, warnings); let mut obj = TextObject::point(Point::ZERO, "", CharStyle::default()); let mut breaks: Vec = vec![]; @@ -1059,7 +1321,7 @@ impl Ctx { } // SVG text runs left to right unless `direction: rtl` says otherwise, whatever its first // strong character: pinned (on every paragraph) when that would read as right to left. - if self.css.prop(t, "direction").is_some_and(|d| d.trim() == "rtl") { + if self.prop(t, "direction").is_some_and(|d| d.trim() == "rtl") { obj.edit_paras(None, |pa| pa.direction = Some(ParaDirection::RightToLeft)); } else if obj.plain_text().split('\n').any(|p| vectorcraft_text::paragraph_is_rtl(p, None)) { obj.edit_paras(None, |pa| pa.direction = Some(ParaDirection::LeftToRight)); @@ -1400,3 +1662,6 @@ fn assemble(cells: &[Cell], breaks: &[Break]) -> (Runs, Vec) { } (out, bytes) } + +#[cfg(test)] +mod tests; diff --git a/crates/svg/src/import/text/tests.rs b/crates/svg/src/import/text/tests.rs new file mode 100644 index 0000000000..1508997dcc --- /dev/null +++ b/crates/svg/src/import/text/tests.rs @@ -0,0 +1,24 @@ +use super::*; + +/// The texts [`prepare`] builds from `body` and how many times it read a ``'s path. +fn prepared(body: &str) -> (usize, u64, Vec) { + let svg = format!(r#"{body}"#); + let xml = roxmltree::Document::parse(&svg).unwrap(); + let mut warnings = vec![]; + let before = TEXT_PATH_READS.get(); + let (_, slots) = prepare(&svg, &xml, None, 96.0, &mut warnings); + (slots.texts.len(), TEXT_PATH_READS.get() - before, warnings) +} + +/// Every `` of a text on a path follows the path the text's own build read. +#[test] +fn uses_of_a_text_on_a_path_read_the_path_once() { + let uses: String = (101..=150).map(|i| format!(r##""##)).collect(); + let (texts, reads, warnings) = + prepared(&format!(r##"Path{uses}"##)); + assert_eq!(texts, 51, "the text and one per : {warnings:?}"); + assert_eq!(reads, 1); + // A path with no usable data is not read again either. + let (texts, reads, _) = prepared(&format!(r##"Path{uses}"##)); + assert_eq!((texts, reads), (51, 1)); +} diff --git a/crates/svg/tests/it/clip_text.rs b/crates/svg/tests/it/clip_text.rs index 07773dd5f8..a2dcb46156 100644 --- a/crates/svg/tests/it/clip_text.rs +++ b/crates/svg/tests/it/clip_text.rs @@ -183,6 +183,20 @@ fn a_use_of_a_text_in_a_clip_path_clips_where_the_use_places_it() { assert!(glyphs.x0 >= 200.0 - 1e-6 && glyphs.y1 <= 100.0 + 1e-6 && glyphs.y0 > 40.0, "{glyphs:?}"); } +#[test] +fn a_use_of_a_text_in_a_clip_path_passes_its_properties_on_to_the_text() { + let d = open( + r##"HI + + "##, + ); + let (clip, _) = clip_group(&top(&d)[0]); + let NodeKind::Text(t) = &clip.kind else { panic!("{:?}", clip.kind) }; + assert!(near(t.runs[0].style.size, 100.0, 1e-9), "{}", t.runs[0].style.size); + assert_eq!(t.runs[0].style.font_style, "Bold"); + assert!(clip_bounds(clip).height() > 50.0, "{:?}", clip_bounds(clip)); +} + #[test] fn a_clip_path_on_a_use_of_a_text_is_in_the_use_s_user_space() { // The text's transform moves its glyphs to x 200…; the ``'s clip keeps x < 250 of its own diff --git a/crates/svg/tests/it/import_text.rs b/crates/svg/tests/it/import_text.rs index 8b0d0338c6..b25dc23678 100644 --- a/crates/svg/tests/it/import_text.rs +++ b/crates/svg/tests/it/import_text.rs @@ -191,6 +191,89 @@ fn hidden_text_is_skipped_and_use_instances_are_kept() { assert!(t[1].xf.translation().to_point().distance(Point::new(50.0, 30.0)) < 1e-9); } +/// SVG 2 §5.6 (https://www.w3.org/TR/SVG2/struct.html#UseElement): the element a `` shows +/// inherits from the ``, not from where it is defined. +#[test] +fn a_use_of_a_text_passes_its_properties_on_to_the_text() { + let d = open( + r##"HI + + "##, + ); + let t = texts(&d); + assert_eq!(t.len(), 3, "{t:?}"); + let s: Vec = t.iter().map(|t| t.first_style()).collect(); + assert!(close(s[0].size, 100.0, 1e-9), "{}", s[0].size); + assert_eq!(s[0].fill, Paint::solid(vectorcraft_color::Color::rgb8(255, 0, 0))); + assert!(close(s[1].size, 12.0, 1e-9), "the defaults, not the 's 30: {}", s[1].size); + assert_eq!(s[1].font_style, "Bold"); + assert_eq!(s[1].fill, Paint::solid(vectorcraft_color::Color::BLACK)); + assert!(close(s[2].size, 20.0, 1e-9), "{}", s[2].size); + assert!(t[0].xf.translation().to_point().distance(Point::new(0.0, 50.0)) < 1e-9, "{:?}", t[0].xf); + assert!(t[1].xf.translation().to_point().distance(Point::new(200.0, 50.0)) < 1e-9, "{:?}", t[1].xf); + assert!(t[2].xf.translation().to_point().distance(Point::new(0.0, 250.0)) < 1e-9, "{:?}", t[2].xf); + // The ``'s opacity stays on the object it becomes. + let faded: Vec<&Node> = art(&d).into_iter().filter(|n| (n.opacity - 0.5).abs() < 1e-6).collect(); + assert_eq!(faded.len(), 1, "{faded:?}"); + // The text's own properties win over the ``'s. + let d = open(r##"HI"##); + let s = only_text(&d).first_style(); + assert!(close(s.size, 20.0, 1e-9), "{}", s.size); + assert_eq!(s.fill, Paint::solid(vectorcraft_color::Color::rgb8(0, 0, 255))); +} + +/// A `` inside the text it shows does not make the text inherit from itself. +#[test] +fn a_use_inside_the_text_it_shows_opens() { + let d = open(r##"HI"##); + assert!(texts(&d).iter().all(|t| close(t.first_style().size, 20.0, 1e-9)), "{:?}", texts(&d)); +} + +/// Past 1 MiB of text built for `` elements, the rest show their text with its own properties. +#[test] +fn uses_of_a_long_text_stop_taking_their_properties_past_the_limit() { + let long = "a".repeat(350_000); + let uses: String = (1..=4).map(|i| format!(r##""##, i * 10)).collect(); + let (d, w) = import_with_report(&svg(&format!(r##"{long}{uses}"##))).unwrap(); + let sizes: Vec = texts(&d).iter().map(|t| t.first_style().size).collect(); + assert_eq!(sizes.len(), 4, "{sizes:?}"); + assert!(close(sizes[0], 10.0, 1e-9) && close(sizes[1], 20.0, 1e-9), "{sizes:?}"); + assert!(close(sizes[2], 12.0, 1e-9) && close(sizes[3], 12.0, 1e-9), "{sizes:?}"); + assert_eq!(w.iter().filter(|w| w.contains("KiB of text and text paths shown through elements")).count(), 1, "{w:?}"); +} + +/// The path a text follows counts toward that limit: each `` of the text lays it out along +/// the whole path again. +#[test] +fn uses_of_a_text_on_a_long_path_stop_taking_their_properties_past_the_limit() { + let d = format!("M0 0{}", "h1".repeat(350_000)); + let uses: String = (1..=4).map(|i| format!(r##""##, i * 10)).collect(); + let body = format!(r##"HI{uses}"##); + let (d, w) = import_with_report(&svg(&body)).unwrap(); + let sizes: Vec = texts(&d).iter().map(|t| t.first_style().size).collect(); + assert_eq!(sizes.len(), 4, "{sizes:?}"); + assert!(close(sizes[0], 10.0, 1e-9) && close(sizes[1], 20.0, 1e-9), "{sizes:?}"); + assert!(close(sizes[2], 12.0, 1e-9) && close(sizes[3], 12.0, 1e-9), "{sizes:?}"); + assert_eq!(w.iter().filter(|w| w.contains("KiB of text and text paths shown through elements")).count(), 1, "{w:?}"); + // The same text on a short path is within the limit however many `` elements show it. + let uses: String = (1..=300).map(|i| format!(r##""##)).collect(); + let body = format!(r##"HI{uses}"##); + let (d, w) = import_with_report(&svg(&body)).unwrap(); + assert!(texts(&d).iter().enumerate().all(|(i, t)| close(t.first_style().size, (i + 1) as f64, 1e-9)), "{w:?}"); + assert!(!w.iter().any(|w| w.contains("shown through elements")), "{w:?}"); +} + +/// Many `` elements of one text that inherit the same properties each get it. +#[test] +fn many_uses_of_a_text_in_one_group_all_take_its_properties() { + let uses: String = (0..200).map(|i| format!(r##""##)).collect(); + let d = open(&format!(r##"HI{uses}"##)); + let t = texts(&d); + assert_eq!(t.len(), 200); + assert!(t.iter().all(|t| close(t.first_style().size, 40.0, 1e-9))); + assert!(t[199].xf.translation().to_point().distance(Point::new(199.0, 50.0)) < 1e-9, "{:?}", t[199].xf); +} + #[test] fn text_path_becomes_type_on_a_path() { let d = open( diff --git a/crates/ui-egui/src/font_menu/tests.rs b/crates/ui-egui/src/font_menu/tests.rs index a054794379..259408711c 100644 --- a/crates/ui-egui/src/font_menu/tests.rs +++ b/crates/ui-egui/src/font_menu/tests.rs @@ -21,12 +21,13 @@ fn the_thai_filter_lists_fonts_with_thai() { /// stay clear of it. #[test] fn the_scroll_bar_leaves_the_stars_clear() { - // Every family starred, so each row draws its star. + // Every family starred, so each row draws its star. Tests running alongside add fonts and + // composite fonts to the process-wide database, which the menu lists too: the ★ filter keeps + // the rows to the families starred here. let all = vectorcraft_text::FontDb::global().families(); - let families = all.len(); let ctx = egui::Context::default(); let area = egui::Rect::from_min_size(egui::pos2(0.0, 0.0), egui::vec2(300.0, 160.0)); - let mut st = super::MenuState::default(); + let mut st = super::MenuState { favorites_only: true, ..Default::default() }; let look = super::MenuLook { samples: false, favorites: &all, ..Default::default() }; let mut stars = vec![]; for _ in 0..3 { @@ -45,6 +46,7 @@ fn the_scroll_bar_leaves_the_stars_clear() { }) .collect(); } + let families = st.rows.len(); assert!(!stars.is_empty() || families == 0, "{families} families, no row stars drawn"); // The rows don't fill the list without enough families to scroll, and then there is no bar. if families as f32 * look.row > area.height() { @@ -62,6 +64,13 @@ fn family(name: &str) -> Row { Row::Family(name.to_string()) } +/// The families the font menu lists that stay listed, in its order: the menu's list without the +/// composite fonts at its top, which tests running alongside define and remove in the process-wide +/// database. +fn lasting_families() -> Vec { + vectorcraft_text::FontDb::global().family_list().iter().filter(|f| !f.starts_with('.')).cloned().collect() +} + #[test] fn recent_fonts_are_first_available_unique_and_expandable() { let names: Vec<_> = ["Alpha", "Bravo", "Charlie"].map(str::to_string).into(); @@ -86,7 +95,7 @@ fn recent_fonts_are_first_available_unique_and_expandable() { #[test] fn recent_section_rebuilds_and_arrows_skip_its_headings() { - let names = vectorcraft_text::FontDb::global().menu_family_list(); + let names = lasting_families(); if names.len() < 3 { return; } @@ -135,7 +144,7 @@ fn recent_section_rebuilds_and_arrows_skip_its_headings() { #[test] fn recent_fonts_obey_search_and_favorite_filters() { - let names = vectorcraft_text::FontDb::global().menu_family_list(); + let names = lasting_families(); if names.len() < 3 { return; } @@ -176,7 +185,7 @@ fn recent_fonts_obey_search_and_favorite_filters() { #[test] fn opening_on_another_font_keeps_the_recent_section_visible() { - let names = vectorcraft_text::FontDb::global().menu_family_list(); + let names = lasting_families(); let Some(current) = names.last() else { return }; let recent = vec![names[0].clone()]; let ctx = egui::Context::default(); @@ -244,7 +253,7 @@ fn the_wheel_steps_one_family_per_notch_and_stops_at_the_ends() { /// others in the database's order, under the favourites (and other) filters. #[test] fn the_wheel_follows_the_menus_order_and_filters() { - let names = vectorcraft_text::FontDb::global().menu_family_list(); + let names = lasting_families(); if names.len() < 3 { return; } @@ -770,8 +779,9 @@ fn the_variable_filter_lists_only_variable_families() { assert_eq!(frame(&mut st, vec![], &favorites).0, [TWO_AXIS_FAMILY]); st.favorites_only = false; // Off again: every family is listed. + let lasting = lasting_families(); let again = click(&mut st, &favorites); assert!(!st.variable_only); - assert_eq!(again, frame(&mut st, vec![], &[]).0); + assert!(lasting.iter().all(|f| has(&again, f)), "{again:?}"); assert!(has(&again, TWO_AXIS_FAMILY) && has(&again, "Source Sans 3") && has(&again, "JetBrains Mono"), "{again:?}"); } diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index 42740ea6cd..4906a0ae0b 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -5871,6 +5871,7 @@ theme Appearance Външен вид Ruby… Руби… Ruby Руби Geometric Guides Геометрични водачи + Optical Effects Оптични ефекти Geometric Patterns Геометрични мостри Grit Textures Зърнести текстури Ease In Плавно начало diff --git a/crates/ui-egui/src/i18n/bs.tsv b/crates/ui-egui/src/i18n/bs.tsv index 7a210989a2..a349a50a94 100644 --- a/crates/ui-egui/src/i18n/bs.tsv +++ b/crates/ui-egui/src/i18n/bs.tsv @@ -5888,6 +5888,7 @@ mark-style Japanese Japanski Ruby… Ruby… Ruby Ruby Geometric Guides Geometrijske vodilice + Optical Effects Optički efekti Geometric Patterns Geometrijski uzorci Grit Textures Zrnaste teksture Ease In Ublaži ulaz diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index 61e58a0b58..037a2560b7 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -2938,6 +2938,7 @@ digits Malayalam Malaiàlam Opens network connections Obre connexions de xarxa Open… Obre… Optical Òptic + Optical Effects Efectes òptics Optical Margin Alignment Alineació òptica de marges Dynamic Text Text dinàmic Dynamic Text Presets Valors predefinits de text dinàmic diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 4243a1d992..2b0b3b5c11 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -6057,6 +6057,7 @@ mark-style Japanese Japonské Ruby… Ruby… Ruby Ruby Geometric Guides Geometrická vodítka + Optical Effects Optické efekty Geometric Patterns Geometrické vzorky Grit Textures Zrnité textury Ease In Pozvolný začátek diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index 1ffecdcd54..640c0cf986 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -5886,6 +5886,7 @@ swatch Bright Violet Kraftig violet Ruby… Ruby… Ruby Ruby Geometric Guides Geometriske hjælpelinjer + Optical Effects Optiske effekter Geometric Patterns Geometriske mønstre Grit Textures Kornede teksturer Ease In Langsom start diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 07db00a917..33b23e1ad6 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -5646,6 +5646,7 @@ effect Soft Weich Ruby… Ruby… Ruby Ruby Geometric Guides Geometrische Hilfslinien + Optical Effects Optische Effekte Geometric Patterns Geometrische Muster Grit Textures Körnige Texturen Ease In Langsam starten diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 4f0bcd69f3..3a2d702b29 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -6008,6 +6008,7 @@ effect Points Σημεία Ruby… Ruby… Ruby Ruby Geometric Guides Γεωμετρικοί οδηγοί + Optical Effects Οπτικά εφέ Geometric Patterns Γεωμετρικά μοτίβα Grit Textures Κοκκώδεις υφές Ease In Ομαλή αρχή diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 31ab2e43ea..b2ed2c8331 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -5999,6 +5999,7 @@ mark-style Japanese Japonesas Ruby… Ruby… Ruby Ruby Geometric Guides Guías geométricas + Optical Effects Efectos ópticos Geometric Patterns Motivos geométricos Grit Textures Texturas granuladas Ease In Entrada suave diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index 54936048dc..916831777b 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -6087,6 +6087,7 @@ swatch Bright Violet Kirkas violetti Ruby… Ruby… Ruby Ruby Geometric Guides Geometriset apuviivat + Optical Effects Optiset tehosteet Geometric Patterns Geometriset kuviot Grit Textures Rakeiset tekstuurit Ease In Hidas alku diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 2fa01b22e4..cc3b1ca31f 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -6052,6 +6052,7 @@ theme Appearance Apparence Ruby… Ruby… Ruby Ruby Geometric Guides Repères géométriques + Optical Effects Effets optiques Geometric Patterns Motifs géométriques Grit Textures Textures granuleuses Ease In Accélération progressive diff --git a/crates/ui-egui/src/i18n/hi.tsv b/crates/ui-egui/src/i18n/hi.tsv index 55a2813689..9d31b2d5f1 100644 --- a/crates/ui-egui/src/i18n/hi.tsv +++ b/crates/ui-egui/src/i18n/hi.tsv @@ -3194,6 +3194,7 @@ antiAlias Sharp शार्प Opens network connections नेटवर्क कनेक्शन ओपन करें OpenType Features ओपन टाइप सुविधाएँ Optical ऑप्टिकल + Optical Effects ऑप्टिकल इफ़ेक्ट्स Optical Margin Alignment ऑप्टिकल मार्जिन संरेखण Dynamic Text डायनैमिक टेक्स्ट Dynamic Text Presets डायनैमिक टेक्स्ट प्रीसेट diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 7b504512fd..9027b75a04 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -6040,6 +6040,7 @@ imageTrace Outline Körvonal Ruby… Ruby… Ruby Ruby Geometric Guides Geometriai segédvonalak + Optical Effects Optikai effektusok Geometric Patterns Geometriai mintázatok Grit Textures Szemcsés textúrák Ease In Lassú kezdés diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index 6b9f5624d7..36bc86481a 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -5886,6 +5886,7 @@ swatch Bright Violet Violet cerah Ruby… Ruby… Ruby Ruby Geometric Guides Panduan Geometris + Optical Effects Efek Optis Geometric Patterns Pola Geometris Grit Textures Tekstur Butiran Ease In Awal Lambat diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index b891f757c5..55ee43874f 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -5680,6 +5680,7 @@ lighting Light Luce Ruby… Ruby… Ruby Ruby Geometric Guides Guide geometriche + Optical Effects Effetti ottici Geometric Patterns Motivi geometrici Grit Textures Texture granulose Ease In Inizio graduale diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 16d452e862..988f17bb4e 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -5730,6 +5730,7 @@ lighting Light ライト Ruby… ルビ… Ruby ルビ Geometric Guides 幾何ガイド + Optical Effects オプティカル効果 Geometric Patterns 幾何パターン Grit Textures 粒状テクスチャ Ease In イーズイン diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index a6ac658ad2..fc6e7152f6 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -5997,6 +5997,7 @@ imageTrace Outline კონტური Ruby… რუბი… Ruby რუბი Geometric Guides გეომეტრიული მიმმართველები + Optical Effects ოპტიკური ეფექტები Geometric Patterns გეომეტრიული პატერნები Grit Textures მარცვლოვანი ტექსტურები Ease In ნელი დასაწყისი diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index dc4b36f0ff..6c7badf0d3 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -5641,6 +5641,7 @@ lighting Light 광원 Ruby… 루비… Ruby 루비 Geometric Guides 기하학적 안내선 + Optical Effects 광학 효과 Geometric Patterns 기하학적 패턴 Grit Textures 입자 텍스처 Ease In 서서히 시작 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index bdbc3ddeea..30f33f1bd6 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -5937,6 +5937,7 @@ swatch Bright Violet Felviolet Ruby… Ruby… Ruby Ruby Geometric Guides Geometrische hulplijnen + Optical Effects Optische effecten Geometric Patterns Geometrische patronen Grit Textures Korrelige texturen Ease In Langzaam beginnen diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 6c23e85d9d..172a5183cb 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -5911,6 +5911,7 @@ mark-style Japanese Japońskie Ruby… Ruby… Ruby Ruby Geometric Guides Geometryczne linie pomocnicze + Optical Effects Efekty optyczne Geometric Patterns Wzorki geometryczne Grit Textures Tekstury ziarniste Ease In Łagodny początek diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index b72baa59c1..f2f212e142 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -5972,6 +5972,7 @@ mark-style Japanese Japonesas Ruby… Ruby… Ruby Ruby Geometric Guides Guias geométricas + Optical Effects Efeitos ópticos Geometric Patterns Padrões geométricos Grit Textures Texturas granuladas Ease In Entrada suave diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 3d86b7edab..78fc696f80 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -5666,6 +5666,7 @@ lighting Light Свет Ruby… Руби… Ruby Руби Geometric Guides Геометрические направляющие + Optical Effects Оптические эффекты Geometric Patterns Геометрические узоры Grit Textures Зернистые текстуры Ease In Плавное начало diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index 5f1ef8ca72..4b5dc41d00 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -6079,6 +6079,7 @@ font-script Japanese Japanska Ruby… Ruby… Ruby Ruby Geometric Guides Geometriska stödlinjer + Optical Effects Optiska effekter Geometric Patterns Geometriska mönster Grit Textures Korniga texturer Ease In Långsam start diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index 75e49a4985..6a678a4d7f 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -6052,6 +6052,7 @@ axis Y Axis Y ekseni Snap to Tangent Teğete Yasla Snap to Perpendicular Dikeye Yasla Geometric Guides Geometrik Kılavuzlar + Optical Effects Optik Efektler Geometric Patterns Geometrik Desenler Grit Textures Grenli Dokular @msg `method` must be classic or perceptual `method` classic veya perceptual olmalıdır diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index d69044cab3..3306c6a5d7 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -5660,6 +5660,7 @@ lighting Light Світло Ruby… Рубі… Ruby Рубі Geometric Guides Геометричні напрямні + Optical Effects Оптичні ефекти Geometric Patterns Геометричні візерунки Grit Textures Зернисті текстури Ease In Плавний початок diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index 81363292f7..75ae6ba12d 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -5661,6 +5661,7 @@ theme Appearance Giao diện Ruby… Ruby… Ruby Ruby Geometric Guides Đường dẫn hình học + Optical Effects Hiệu ứng quang học Geometric Patterns Họa tiết hình học Grit Textures Kết cấu hạt Ease In Chậm lúc đầu diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index d5082bffca..264114e3ad 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -5894,6 +5894,7 @@ lighting Light 光源 Ruby… 注音… Ruby 注音 Geometric Guides 几何参考线 + Optical Effects 光学效果 Geometric Patterns 几何图案 Grit Textures 颗粒纹理 Ease In 缓入 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 83233958b0..82c484b082 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -5698,6 +5698,7 @@ lighting Light 光源 Ruby… 注音… Ruby 注音 Geometric Guides 幾何參考線 + Optical Effects 光學效果 Geometric Patterns 幾何圖樣 Grit Textures 顆粒紋理 Ease In 緩入 diff --git a/crates/ui-egui/src/io.rs b/crates/ui-egui/src/io.rs index dff4fa0dd2..fb4a106465 100644 --- a/crates/ui-egui/src/io.rs +++ b/crates/ui-egui/src/io.rs @@ -15,8 +15,12 @@ use crate::{FilePick, Services, VectorcraftApp, dialogs}; mod export; mod open; +mod read_only_save; mod save; +#[cfg(test)] +mod tests_read_only_save; + pub use export::{EXPORT_CANCELLED, cancel_exports, export, export_for_screens, export_pdf, hide_exports, reveal, save_command_output}; pub(crate) use export::{ export_files, exported_with_notes, file_url, open_in_app, open_original, reveal_path, run_to_file, save_command_output_named, @@ -29,6 +33,7 @@ pub use open::{ }; #[cfg(test)] pub(crate) use open::{open_file, opened_with_notes}; +pub(crate) use save::save_current; pub use save::{ask_format_options, ask_pdf_options, remember_active_artboard, remember_view, save, save_pdf}; pub(crate) use save::{export_path, is_web, notes_of, report_saved, show_with_notes, target_path, write_named, write_reporting, write_to}; #[cfg(test)] diff --git a/crates/ui-egui/src/io/read_only_save.rs b/crates/ui-egui/src/io/read_only_save.rs new file mode 100644 index 0000000000..2927e0c0c6 --- /dev/null +++ b/crates/ui-egui/src/io/read_only_save.rs @@ -0,0 +1,64 @@ +//! The continuation of ordinary File > Save when its own location refuses writes (#6146). +//! It reuses Save As, including extension and overwrite checks, under its own picker entry. + +use std::rc::Rc; + +use serde_json::{Value, json}; +use vectorcraft_engine::cmd::fileio::{self, SaveMode, SavePlan}; + +use crate::picks::{self, Entry}; +use crate::{FilePick, VectorcraftApp}; + +pub(super) struct Retry { + path: Option, + pick: FilePick, + params: Value, +} + +impl Retry { + pub(super) fn new(plan: &SavePlan, folder: Option) -> Self { + let name = plan.path.as_deref().map(fileio::file_name).unwrap_or_else(|| plan.name.clone()); + let mut params = json!({ "format": plan.format.id, "options": plan.options }); + if let Some(modified) = plan.modified { + params["modified"] = json!(modified); + } + Self { path: plan.path.clone(), pick: FilePick { name, folder, filters: vec![] }, params } + } + + /// None when this continuation is stale or another picker prevents it. The existing failed-save + /// dialog then stays in charge; there is no queue of deferred Save As requests. + pub(super) fn instead(self, app: &mut VectorcraftApp, uid: u64) -> Option> { + if app.automation_asked() || app.services.pick_save.is_none() || app.picks.is_waiting() || super::save::is_web(app) { + return None; + } + let index = app.session.documents().iter().position(|st| st.uid == uid && st.path == self.path)?; + if app.session.active_index() != Some(index) && app.run("document.activate", json!({ "index": index })).is_err() { + return None; + } + let name = &self.pick.name; + let status = format!("Cannot write to the location of “{name}”; choose where to save it"); + app.status(status.clone()); + let save = Rc::new(move |app: &mut VectorcraftApp| { + // An agent can retarget this document while the sheet is open. Its answer must not + // replace that later choice. The picker itself already checks the document's uid. + if !app.session.active().is_some_and(|st| st.uid == uid && st.path == self.path) { + return Err("cancelled".into()); + } + super::save::save_suggesting(app, SaveMode::SaveAs, &self.params, false, Some(&self.pick)) + }); + let again = save.clone(); + let entry = Entry::Call(Rc::new(move |app| { + if let Err(e) = again(app) { + app.status(e); + } + })); + let r = picks::as_own_entry(app, entry, |app| save(app)); + Some(match r { + Err(e) if e == "cancelled" && app.picks.is_waiting() => { + app.status(status); + Ok(json!({ "pending": "fileDialog" })) + } + r => r, + }) + } +} diff --git a/crates/ui-egui/src/io/save.rs b/crates/ui-egui/src/io/save.rs index d9d475e76f..ab11de0abc 100644 --- a/crates/ui-egui/src/io/save.rs +++ b/crates/ui-egui/src/io/save.rs @@ -241,6 +241,31 @@ fn plan(app: &VectorcraftApp, mode: SaveMode, p: &Value) -> Result Save alone offers another destination when its own location refuses writes. +/// Save Changes during Close/Quit continues to use [`save`]. +pub(crate) fn save_current(app: &mut VectorcraftApp, p: &Value) -> Result { + save_current_in(app, p, fileio::documents_dialog_folder()) +} + +/// The same entry with a resolved Documents folder (also permits deterministic host-free tests). +pub(super) fn save_current_in(app: &mut VectorcraftApp, p: &Value, folder: Option) -> Result { + remember_view(app); + let first = plan(app, SaveMode::Save, &acknowledged(p))?; + let own = !app.automation_asked() + && !is_web(app) + && p.get("path").is_none_or(Value::is_null) + && first.path.is_some() + && app.session.active().is_some_and(|st| !st.converted && !st.recovered && st.path == first.path && st.format == first.format.id); + if !own { + return save(app, SaveMode::Save, p, true); + } + if let Some(r) = first.path.as_deref().and_then(|path| ask_before_losing(app, path, "file.save", p)) { + return Ok(r); + } + let retry = super::read_only_save::Retry::new(&first, folder); + write_plan(app, first, ("file.save", p.clone()), Some(retry)) +} + /// File → Save, Save As…, Save a Copy…, Save as Template… and the options dialogs' OK. `p` is the /// engine command's `{path?, format?, options?, svg?}`. /// @@ -253,6 +278,17 @@ fn plan(app: &VectorcraftApp, mode: SaveMode, p: &Value) -> Result, /// path?}` while a dialog is open. pub fn save(app: &mut VectorcraftApp, mode: SaveMode, p: &Value, ask_options: bool) -> Result { + save_suggesting(app, mode, p, ask_options, None) +} + +/// Reuse Save As with a captured name and folder, without changing the document before writing. +pub(super) fn save_suggesting( + app: &mut VectorcraftApp, + mode: SaveMode, + p: &Value, + ask_options: bool, + suggestion: Option<&FilePick>, +) -> Result { remember_view(app); // Writing over the file the document was read from is asked about here, not refused. let first = plan(app, mode, &acknowledged(p))?; @@ -264,7 +300,7 @@ pub fn save(app: &mut VectorcraftApp, mode: SaveMode, p: &Value, ask_options: bo if ask && matches!(mode, SaveMode::SaveAs | SaveMode::Copy) && matches!(first.format.id, "svg" | "svgz") { return ask_format_options(app, mode, first.format, &path); } - return write_plan(app, first, (save_command(mode), with_path(p, &path))); + return write_plan(app, first, (save_command(mode), with_path(p, &path)), None); } if is_web(app) && ask_options { return Ok(open_options(app, mode.command(), first.format, &first.name, true)); @@ -273,25 +309,35 @@ pub fn save(app: &mut VectorcraftApp, mode: SaveMode, p: &Value, ask_options: bo SaveMode::Template => fileio::template_filters(first.format.id), _ => fileio::save_filters(first.format.id), }); - let pick = FilePick { name: first.name.clone(), folder: first.folder.clone(), filters }; - let picked = pick_finished(app, &pick, |picked, file_type| with_picked_type(picked, &first.name, file_type, first.format, mode))?; + let pick = FilePick { + name: suggestion.map_or_else(|| first.name.clone(), |s| s.name.clone()), + folder: suggestion.map_or_else(|| first.folder.clone(), |s| s.folder.clone()), + filters, + }; + let picked = pick_finished(app, &pick, |picked, file_type| with_picked_type(picked, &pick.name, file_type, first.format, mode))?; + // Only the read-only retry carries options for the original format through Save As. + // A different format must use its own normal options flow instead. + let changed_retry_format = suggestion.is_some() && fileio::format_for_name(&picked).is_some_and(|f| f.id != first.format.id); // The picked file type wins over a `format` param. let mut q = if p.is_object() { p.clone() } else { json!({}) }; if let Some(o) = q.as_object_mut() { o.remove("format"); + if changed_retry_format { + o.remove("options"); + } o.insert("path".into(), json!(picked)); } let chosen = plan(app, mode, &acknowledged(&q))?; if let Some(r) = chosen.path.as_deref().and_then(|path| ask_before_losing(app, path, save_command(mode), &q)) { return Ok(r); } - if ask && asks_options(mode, chosen.format) { + if (ask || changed_retry_format) && asks_options(mode, chosen.format) { let path = chosen.path.clone().unwrap_or_default(); let r = ask_format_options(app, mode, chosen.format, &path); crate::dialogs::opened_by_panel(app); return r; } - write_plan(app, chosen, (save_command(mode), q)) + write_plan(app, chosen, (save_command(mode), q), None) } /// `p` (an object, else none) with `path`. @@ -471,7 +517,12 @@ pub(crate) fn saved_status(path: &str, r: &Value) -> String { /// A file it writes beside the one at its path that is there already, and that the document's last /// save did not write, is asked about before anything is written; `again` (a command and its params) /// runs the save again once the question's OK allows it ([`ask_to_replace_files`]). -fn write_plan(app: &mut VectorcraftApp, plan: SavePlan, again: (&'static str, Value)) -> Result { +fn write_plan( + app: &mut VectorcraftApp, + plan: SavePlan, + again: (&'static str, Value), + retry: Option, +) -> Result { let uid = app.session.active().ok_or("no document")?.uid; // Saves of one document finish in order. if app.background.busy_with(uid) { @@ -497,28 +548,60 @@ fn write_plan(app: &mut VectorcraftApp, plan: SavePlan, again: (&'static str, Va let enabled = app.session.prefs.background_save && target.is_some(); // The work writes the file; the copy makes it the document's own afterwards. let done = job.clone(); + // Capture the caller before background work leaves the request: it must never open a panel + // or error dialog later, when answering_control has been reset. + let by_control = app.automation_asked(); + let location_refused = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)); + let refused_by_work = location_refused.clone(); + let own_path = target.clone(); + let save_write = app.services.save_write.clone().filter(|_| !is_web(app)); + let save_stream = app.services.save_stream.clone().filter(|_| job.streams() && !is_web(app)); // A native file streams into the file as it is encoded (desktop; not to a download). let stream = app.services.stream_shared.clone().filter(|_| job.streams() && !is_web(app)); let (exists, allowed) = checks_replaced(app); let known: Vec = allowed.iter().chain(app.picks.written_beside.get(&uid).into_iter().flatten()).cloned().collect(); let work = move |write: Writer| { - match stream { - Some(stream) => job.write_streamed(&mut |p, fill| stream(p, fill).map_err(EngineError::Other)), - None => job.write_unless( - |paths| { - let path = job.plan().path.as_deref().unwrap_or_default(); - let replaced = replaced_files(exists.as_ref(), path, &known, paths.iter().copied()); - (!replaced.is_empty()).then(|| json!({ "replaces": replaced })) - }, - |p, b| write(p, b).map_err(EngineError::Other), - ), - } - .map_err(|e| e.to_string()) + let mut own_refused = false; + let mut observed = |p: &str, r: vectorcraft_engine::Result<()>| { + own_refused = own_path.as_deref() == Some(p) && matches!(&r, Err(EngineError::ReadOnlyLocation(_))); + r + }; + let check = |paths: &[&str]| { + let path = job.plan().path.as_deref().unwrap_or_default(); + let replaced = replaced_files(exists.as_ref(), path, &known, paths.iter().copied()); + (!replaced.is_empty()).then(|| json!({ "replaces": replaced })) + }; + let r = match (save_stream, save_write, stream) { + (Some(stream), _, _) => job.write_streamed(&mut |p, fill| observed(p, stream(p, fill))), + (_, Some(write), _) => job.write_unless(check, |p, b| observed(p, write(p, b))), + (_, _, Some(stream)) => job.write_streamed(&mut |p, fill| stream(p, fill).map_err(EngineError::Other)), + _ => job.write_unless(check, |p, b| write(p, b).map_err(EngineError::Other)), + }; + r.map_err(|e| { + if let EngineError::ReadOnlyLocation(reason) = &e { + refused_by_work.store(own_refused, std::sync::atomic::Ordering::Relaxed); + if !by_control { + return reason.clone(); + } + } + e.to_string() + }) }; let failed = target.clone().unwrap_or_default(); let then = move |app: &mut VectorcraftApp, r: Result| { + if r.is_err() + && location_refused.load(std::sync::atomic::Ordering::Relaxed) + && let Some(retry) = retry + && let Some(r) = retry.instead(app, uid) + { + return r; + } // Not written: the document keeps its file, format and modified state, and the user is told. - let r = r.inspect_err(|e| report_write_error(app, false, &failed, e))?; + let r = r.inspect_err(|e| { + if !by_control { + report_write_error(app, false, &failed, e); + } + })?; // Nothing written either: asked first. if let Some(files) = r.get("replaces").and_then(Value::as_array) { let files: Vec = files.iter().filter_map(Value::as_str).map(str::to_string).collect(); diff --git a/crates/ui-egui/src/io/tests_read_only_save.rs b/crates/ui-egui/src/io/tests_read_only_save.rs new file mode 100644 index 0000000000..d7cc4647ac --- /dev/null +++ b/crates/ui-egui/src/io/tests_read_only_save.rs @@ -0,0 +1,357 @@ +//! Injected writers and picker answers: no native panel or filesystem permissions are needed. + +use std::io::ErrorKind; +use std::sync::{Arc, Mutex, mpsc}; + +use serde_json::{Value, json}; +use vectorcraft_engine::{EngineError, READ_ONLY_LOCATION, Session}; + +use super::{fileio, save}; +use crate::control::ControlRequest; +use crate::tests::picks::off_the_ui_thread; +use crate::{Services, VectorcraftApp, background, dialogs, picks}; + +const DOCUMENTS: &str = "/home/artist/Documents"; + +#[derive(Default)] +struct Files { + original: String, + attempts: Vec, + kind: Option, + fail_second: bool, +} + +fn write(files: &Mutex, path: &str) -> vectorcraft_engine::Result<()> { + let mut files = files.lock().unwrap(); + files.attempts.push(path.into()); + if path == files.original || files.fail_second { + let reason = format!("{path}: injected refusal"); + return Err(match files.kind { + Some(ErrorKind::PermissionDenied | ErrorKind::ReadOnlyFilesystem) => EngineError::ReadOnlyLocation(reason), + _ => EngineError::Other(reason), + }); + } + Ok(()) +} + +fn app(format: &str, kind: ErrorKind) -> (VectorcraftApp, Arc>) { + let files = Arc::new(Mutex::new(Files { original: format!("/locked/Poster.{format}"), kind: Some(kind), ..Default::default() })); + let typed = files.clone(); + let ordinary = files.clone(); + let services = Services { + save_write: Some(Arc::new(move |path, _| write(&typed, path))), + write: Some(Box::new(move |path, _| { + write(&ordinary, path).map_err(|e| match e { + EngineError::ReadOnlyLocation(reason) => reason, + e => e.to_string(), + }) + })), + ..Default::default() + }; + let mut app = VectorcraftApp::new(Session::new(), services); + app.run("file.new", json!({"width": 40, "height": 40})).unwrap(); + app.run("shape.rectangle", json!({"x": 1, "y": 1, "width": 10, "height": 10})).unwrap(); + let st = app.session.doc_mut().unwrap(); + st.path = Some(files.lock().unwrap().original.clone()); + st.format = fileio::format(format).unwrap().id; + st.save_options = if format == "svg" { + json!({"decimals": 2, "preserveEditing": false}) + } else { + json!({"pdfCompatible": false, "compress": true, "preview": false}) + } + .as_object() + .unwrap() + .clone(); + (app, files) +} + +fn state(app: &VectorcraftApp) -> (Option, bool, &'static str, Value) { + let st = app.session.doc().unwrap(); + (st.path.clone(), st.is_dirty(), st.format, json!(st.save_options)) +} + +fn poll(app: &mut VectorcraftApp) { + picks::poll(app, &egui::Context::default()); +} + +fn normal_error(app: &VectorcraftApp) { + let d = app.ui.dialog.as_ref().expect("a persistent error dialog"); + assert_eq!(d.kind, dialogs::confirm::MESSAGE); + assert!(d.str("detail").contains("injected refusal")); + assert!(!d.str("detail").contains(READ_ONLY_LOCATION)); + assert!(app.ui.told.is_empty(), "no duplicate error dialog"); +} + +#[test] +fn readonly_fallback_prefills_save_as_and_cancel_keeps_the_failed_save_state() { + for kind in [ErrorKind::PermissionDenied, ErrorKind::ReadOnlyFilesystem] { + let (mut app, files) = app("svg", kind); + let before = state(&app); + let (shown, _) = off_the_ui_thread(&mut app.services); + let r = save::save_current_in(&mut app, &json!({"modified": null}), Some(DOCUMENTS.into())).unwrap(); + assert_eq!(r["pending"], "fileDialog"); + let requests = shown.requests(); + let [picks::PickRequest::Save(pick)] = requests.as_slice() else { panic!("expected one save panel: {requests:?}") }; + assert_eq!(pick.name, "Poster.svg"); + assert_eq!(pick.folder.as_deref(), Some(DOCUMENTS)); + assert_eq!(pick.filters.first(), crate::FilePick::filters(fileio::save_filters("svg")).first()); + assert!(app.ui.status.contains("choose where to save it")); + assert_eq!(state(&app), before); + assert!(app.ui.dialog.is_none()); + shown.answer(&[]); + poll(&mut app); + assert_eq!(state(&app), before); + assert_eq!(files.lock().unwrap().attempts, ["/locked/Poster.svg"]); + assert!(!app.picks.is_waiting()); + } +} + +#[test] +fn readonly_save_as_retargets_only_after_write_and_keeps_options() { + let (mut app, files) = app("svg", ErrorKind::PermissionDenied); + let before = state(&app); + let uid = app.session.doc().unwrap().uid; + let (shown, _) = off_the_ui_thread(&mut app.services); + assert_eq!(app.run("file.save", json!({})).unwrap()["pending"], "fileDialog"); + // Hold the second write after the picker answers: the chosen path is not yet the document's. + let (entered, writing) = mpsc::channel(); + let (release, released) = mpsc::channel(); + let released = Mutex::new(released); + let f = files.clone(); + app.services.save_write = Some(Arc::new(move |path, _| { + entered.send(()).unwrap(); + released.lock().unwrap().recv().unwrap(); + write(&f, path) + })); + app.services.write_shared = Some(Arc::new(|_, _| panic!("typed saves must use their own writer"))); + app.session.prefs.background_save = true; + shown.answer(&["/home/artist/Documents/Poster.svg"]); + poll(&mut app); + writing.recv().unwrap(); + assert_eq!(state(&app), before); + release.send(()).unwrap(); + background::wait_all(&mut app); + let st = app.session.document_mut(uid).unwrap(); + assert_eq!(st.path.as_deref(), Some("/home/artist/Documents/Poster.svg")); + assert!(!st.is_dirty()); + assert_eq!(st.format, before.2); + assert_eq!(json!(st.save_options), before.3); + assert_eq!(files.lock().unwrap().attempts, ["/locked/Poster.svg", "/home/artist/Documents/Poster.svg"]); + assert!(shown.requests().is_empty() && app.ui.dialog.is_none()); +} + +#[test] +fn readonly_second_failure_shows_one_dialog_without_reopening_the_picker() { + let (mut app, files) = app("svg", ErrorKind::PermissionDenied); + files.lock().unwrap().fail_second = true; + let before = state(&app); + let (shown, _) = off_the_ui_thread(&mut app.services); + app.run("file.save", json!({})).unwrap(); + shown.answer(&["/elsewhere/Poster.svg"]); + poll(&mut app); + normal_error(&app); + assert_eq!(state(&app), before); + assert!(shown.requests().is_empty() && !app.picks.is_waiting()); + assert_eq!(files.lock().unwrap().attempts, ["/locked/Poster.svg", "/elsewhere/Poster.svg"]); +} + +#[test] +fn readonly_other_errors_and_explicit_destinations_keep_the_dialog() { + for (command, params, kind) in [ + ("file.save", json!({}), ErrorKind::StorageFull), + ("file.save", json!({}), ErrorKind::NotFound), + ("file.save", json!({}), ErrorKind::Other), + ("file.save", json!({"path": "/locked/Poster.svg"}), ErrorKind::PermissionDenied), + ("file.saveAs", json!({"path": "/locked/Poster.svg", "options": {"decimals": 2}}), ErrorKind::PermissionDenied), + ("file.saveCopy", json!({"path": "/locked/Poster.svg", "options": {"decimals": 2}}), ErrorKind::PermissionDenied), + ("file.exportAs", json!({"path": "/locked/Poster.svg", "format": "svg"}), ErrorKind::PermissionDenied), + ] { + let (mut app, _) = app("svg", kind); + let before = state(&app); + // A fallback would return pending, so no inline/native dialog is necessary here. + app.services.pick_save = Some(Box::new(move |_| panic!("{command}: no fallback"))); + assert!(app.run(command, params).is_err(), "{command}"); + normal_error(&app); + assert_eq!(state(&app), before); + } +} + +#[test] +fn readonly_control_returns_the_prefix_and_never_opens_a_panel_even_after_completion() { + for wait in [true, false] { + let (mut app, _) = app("svg", ErrorKind::PermissionDenied); + app.services.pick_save = Some(Box::new(|_| panic!("automation never picks"))); + app.services.write_shared = Some(Arc::new(|_, _| panic!("typed writer required"))); + app.session.prefs.background_save = true; + let before = state(&app); + let (tx, rx) = mpsc::channel(); + app = app.with_control(rx); + let (req, reply) = ControlRequest::new("app.save", json!({"wait": wait})); + tx.send(req).unwrap(); + app.drain_control(&egui::Context::default()); + let reply = reply.try_recv().unwrap(); + if wait { + assert_eq!(reply["ok"], false); + assert!(reply["error"].as_str().unwrap().starts_with("READ_ONLY_LOCATION: "), "{reply}"); + } else { + assert_eq!(reply["result"]["background"], true); + assert!(!app.ui.answering_control); + background::wait_all(&mut app); + assert!(app.ui.status.contains("READ_ONLY_LOCATION: ")); + } + assert_eq!(state(&app), before); + assert!(app.ui.dialog.is_none() && app.ui.told.is_empty() && !app.picks.is_waiting()); + } +} + +#[test] +fn readonly_automation_continuation_keeps_the_prefix_and_never_opens_a_panel() { + let (mut app, files) = app("svg", ErrorKind::PermissionDenied); + let before = state(&app); + app.services.pick_save = Some(Box::new(|_| panic!("automation never picks"))); + assert!(!app.automation_is_acting()); + let r = picks::as_automated(&mut app, true, |app| app.run("file.save", json!({}))); + assert!(r.unwrap_err().starts_with("READ_ONLY_LOCATION: ")); + assert_eq!(state(&app), before); + assert_eq!(files.lock().unwrap().attempts, ["/locked/Poster.svg"]); + assert!(app.ui.dialog.is_none() && app.ui.told.is_empty() && !app.picks.is_waiting()); + assert!(!app.automation_asked()); +} + +#[test] +fn readonly_background_completion_resumes_the_original_uid_without_repeating_save() { + for streamed in [false, true] { + let format = if streamed { "vectorcraft" } else { "svg" }; + let (mut app, files) = app(format, ErrorKind::ReadOnlyFilesystem); + let original = files.lock().unwrap().original.clone(); + let uid = app.session.doc().unwrap().uid; + let (shown, _) = off_the_ui_thread(&mut app.services); + if streamed { + let f = files.clone(); + app.services.save_stream = Some(Arc::new(move |path, fill| { + let mut bytes = Vec::new(); + fill(&mut bytes).unwrap(); + write(&f, path) + })); + app.services.save_write = Some(Arc::new(|_, _| panic!("native save must stream"))); + } + app.services.write_shared = Some(Arc::new(|_, _| panic!("typed writer required"))); + app.session.prefs.background_save = true; + assert_eq!(app.run("file.save", json!({})).unwrap()["background"], true); + app.run("file.new", json!({"width": 20, "height": 20})).unwrap(); + let other = app.session.doc().unwrap().uid; + background::wait_all(&mut app); + assert_eq!(app.session.doc().unwrap().uid, uid); + assert_eq!(shown.requests().len(), 1); + // Switch again while the native panel is open: its answer belongs to the original uid. + app.run("document.activate", json!({"index": 1})).unwrap(); + let chosen = format!("/elsewhere/Poster.{format}"); + shown.answer(&[&chosen]); + poll(&mut app); + background::wait_all(&mut app); + assert_eq!(app.session.doc().unwrap().uid, uid); + assert_eq!(app.session.doc().unwrap().path.as_deref(), Some(chosen.as_str())); + assert!(!app.session.doc().unwrap().is_dirty()); + assert!(app.session.document_mut(other).unwrap().path.is_none()); + assert_eq!(files.lock().unwrap().attempts, [original, chosen]); + assert!(shown.requests().is_empty()); + } +} + +#[test] +fn readonly_busy_picker_and_save_changes_keep_the_existing_error_flow() { + let (mut app, _) = app("svg", ErrorKind::PermissionDenied); + let (shown, _) = off_the_ui_thread(&mut app.services); + app.run("file.saveAs", json!({})).unwrap_err(); + assert_eq!(shown.requests().len(), 1); + assert!(app.run("file.save", json!({})).is_err()); + normal_error(&app); + assert_eq!(shown.requests().len(), 1); + shown.answer(&[]); + poll(&mut app); + app.ui.dialog = None; + let st = app.session.doc().unwrap(); + app.ui.dialog = Some(crate::state::Dialog::new(crate::unsaved::KIND, json!({"index": 0, "uid": st.uid, "then": "close"}))); + assert!(crate::unsaved::confirm(&mut app).is_err()); + normal_error(&app); + assert!(shown.requests().is_empty()); + assert_eq!(app.session.documents().len(), 1); + assert!(app.session.doc().unwrap().is_dirty()); +} + +#[test] +fn readonly_changed_format_asks_for_its_options_before_writing() { + let (mut app, files) = app("svg", ErrorKind::PermissionDenied); + let before = state(&app); + let (shown, _) = off_the_ui_thread(&mut app.services); + assert_eq!(app.run("file.save", json!({})).unwrap()["pending"], "fileDialog"); + shown.answer(&["/elsewhere/Poster.pdf"]); + poll(&mut app); + let dialog = app.ui.dialog.as_ref().expect("the picked format's Save dialog"); + assert_eq!(dialog.kind, "savePdf"); + assert_eq!(dialog.str("path"), "/elsewhere/Poster.pdf"); + assert_eq!(state(&app), before); + assert_eq!(files.lock().unwrap().attempts, ["/locked/Poster.svg"]); +} + +/// Typed saves keep the sidecar preflight both before the refused write and at the retry's +/// destination. Confirming the ordinary Save still permits its one read-only fallback. +#[test] +fn readonly_typed_save_checks_sidecars_before_writing_and_after_retry() { + for in_background in [false, true] { + let (mut app, files) = app("vectorcraft", ErrorKind::PermissionDenied); + app.run("artboard.new", json!({"x": 100, "y": 0, "width": 40, "height": 40, "name": "B"})).unwrap(); + app.session.doc_mut().unwrap().save_options.insert("separateArtboards".into(), json!(true)); + let before = state(&app); + let there = ["/locked/Poster-B.vectorcraft", "/elsewhere/Poster-B.vectorcraft"]; + app.services.exists = Some(Arc::new(move |path| there.contains(&path))); + app.services.write_shared = Some(Arc::new(|_, _| panic!("typed writer required"))); + app.session.prefs.background_save = in_background; + let (shown, _) = off_the_ui_thread(&mut app.services); + + app.run("file.save", json!({})).unwrap(); + background::wait_all(&mut app); + let d = app.ui.dialog.as_ref().expect("ask before writing the existing artboard file"); + assert_eq!(d.kind, dialogs::confirm::REPLACE_FILES); + assert_eq!(d.fields["files"], json!(["/locked/Poster-B.vectorcraft"])); + assert!(files.lock().unwrap().attempts.is_empty()); + assert!(shown.requests().is_empty()); + assert_eq!(state(&app), before); + dialogs::cancel(&mut app); + assert_eq!(state(&app), before); + assert!(files.lock().unwrap().attempts.is_empty()); + + app.run("file.save", json!({})).unwrap(); + background::wait_all(&mut app); + dialogs::confirm(&mut app).unwrap(); + background::wait_all(&mut app); + assert_eq!(files.lock().unwrap().attempts, ["/locked/Poster.vectorcraft"]); + assert_eq!(state(&app), before); + let requests = shown.requests(); + let [picks::PickRequest::Save(pick)] = requests.as_slice() else { panic!("expected one fallback panel: {requests:?}") }; + assert_eq!(pick.name, "Poster.vectorcraft"); + + shown.answer(&["/elsewhere/Poster.vectorcraft"]); + poll(&mut app); + background::wait_all(&mut app); + let d = app.ui.dialog.as_ref().expect("the retry also checks its destination's artboard file"); + assert_eq!(d.kind, dialogs::confirm::REPLACE_FILES); + assert_eq!(d.fields["files"], json!(["/elsewhere/Poster-B.vectorcraft"])); + assert_eq!(files.lock().unwrap().attempts, ["/locked/Poster.vectorcraft"]); + assert_eq!(state(&app), before); + dialogs::confirm(&mut app).unwrap(); + background::wait_all(&mut app); + assert_eq!( + files.lock().unwrap().attempts, + [ + "/locked/Poster.vectorcraft", + "/elsewhere/Poster.vectorcraft", + "/elsewhere/Poster-Artboard-1.vectorcraft", + "/elsewhere/Poster-B.vectorcraft" + ] + ); + assert_eq!(app.session.doc().unwrap().path.as_deref(), Some("/elsewhere/Poster.vectorcraft")); + assert!(!app.session.doc().unwrap().is_dirty()); + assert!(shown.requests().is_empty() && app.ui.dialog.is_none()); + } +} diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index be668e1e4f..1c64655f73 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -122,8 +122,8 @@ pub use craft_ui::docking::Monitor as DockMonitor; #[cfg(test)] pub(crate) use services::browsable_url; pub use services::{ - DownloadFn, ExistsFn, FileFilter, FilePick, Inbox, NativeInputHook, OpenUrlFn, PickOpen, PickSave, PickedInbox, ReadFn, RevealFn, Services, - WriteFn, + DownloadFn, ExistsFn, FileFilter, FilePick, Inbox, NativeInputHook, OpenUrlFn, PickOpen, PickSave, PickedInbox, ReadFn, RevealFn, SaveStreamFn, + SaveWriteFn, Services, WriteFn, }; use state::{GeometryStamp, InputGeneration, PendingScreenshot, QueuedScreenshot}; pub(crate) use state::{InputFrame, take_frame_once}; diff --git a/crates/ui-egui/src/menus/run.rs b/crates/ui-egui/src/menus/run.rs index de3261de1b..72e6c25acb 100644 --- a/crates/ui-egui/src/menus/run.rs +++ b/crates/ui-egui/src/menus/run.rs @@ -165,7 +165,8 @@ fn run_ui_part1(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option SaveMode::Copy, _ => SaveMode::Template, }; - io::save(app, mode, p, true).map(|mut r| { + let saved = if mode == SaveMode::Save { io::save_current(app, p) } else { io::save(app, mode, p, true) }; + saved.map(|mut r| { vectorcraft_engine::cmd::fileio::leave_out_unreadable_save_paths(&mut r, p.get("path").and_then(Value::as_str)); r }) diff --git a/crates/ui-egui/src/menus/ui_commands.rs b/crates/ui-egui/src/menus/ui_commands.rs index f435bfff22..b05da18e12 100644 --- a/crates/ui-egui/src/menus/ui_commands.rs +++ b/crates/ui-egui/src/menus/ui_commands.rs @@ -40,7 +40,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[ "file.save", "Save", "Cmd+S", - "{path?, format?, options?, svg?: {…SVG options}} = document.save, written through the app (a document saved as SVG, PDF or .ai, or opened from one Save can write back, saves as that again, with the same options); with no path known (never saved, converted) the Save As panel asks first", + "{path?, format?, options?, svg?: {…SVG options}} = document.save, written through the app (a document saved as SVG, PDF or .ai, or opened from one Save can write back, saves as that again, with the same options); with no path known (never saved, converted) the Save As panel asks first; ordinary Save to the document's existing file refused by filesystem permissions or a read-only volume opens Save As in Documents with the same name, format and options; cancelling keeps the original path and modified state. Explicit destinations and Close/Quit keep their existing error dialog. Automation gets READ_ONLY_LOCATION: followed by the reason and no panel", ), ( "file.place", diff --git a/crates/ui-egui/src/panels/library_panel/tests.rs b/crates/ui-egui/src/panels/library_panel/tests.rs index f28e7e5540..10acd4603e 100644 --- a/crates/ui-egui/src/panels/library_panel/tests.rs +++ b/crates/ui-egui/src/panels/library_panel/tests.rs @@ -155,9 +155,10 @@ fn find_filters_list_view_and_arrows_step_through_the_libraries() { assert!(ctx.read_response(tile_id(panel, "#00FF00")).is_none()); let first = SWATCH_IDS[0]; let last = SwatchLibraries::list(&app).pop().unwrap().id; - assert_eq!(last, "grit-textures", "gradient and then pattern libraries come after the swatch libraries"); + assert_eq!(last, "optical-effects", "gradient and then pattern libraries come after the swatch libraries"); assert_eq!(neighbour::(&app, "radial-glows", 1).as_deref(), Some("geometric-patterns")); assert_eq!(neighbour::(&app, "geometric-patterns", 1).as_deref(), Some("grit-textures")); + assert_eq!(neighbour::(&app, "grit-textures", 1).as_deref(), Some("optical-effects")); assert_eq!(neighbour::(&app, first, -1), Some(last), "wraps around"); assert_eq!(neighbour::(&app, "web-safe-216", 1).as_deref(), Some(SWATCH_IDS[1])); } diff --git a/crates/ui-egui/src/picks.rs b/crates/ui-egui/src/picks.rs index d2b0a26d3f..ea1b51e049 100644 --- a/crates/ui-egui/src/picks.rs +++ b/crates/ui-egui/src/picks.rs @@ -223,6 +223,15 @@ pub(crate) fn as_entry(app: &mut VectorcraftApp, entry: impl FnOnce() -> Entr r } +/// Run a continuation under its own entry: a refused Save's panel must resume Save As, not retry +/// the original Save. Restore the enclosing entry for any work it does afterwards. +pub(crate) fn as_own_entry(app: &mut VectorcraftApp, entry: Entry, f: impl FnOnce(&mut VectorcraftApp) -> R) -> R { + let outer = app.picks.entry.replace(entry); + let r = f(app); + app.picks.entry = outer; + r +} + /// Run `f` as handling the answer of a file dialog automation opened when `automated` (and inside /// one already, as that): [`VectorcraftApp::automation_asked`] is true meanwhile. pub(crate) fn as_automated(app: &mut VectorcraftApp, automated: bool, f: impl FnOnce(&mut VectorcraftApp) -> R) -> R { diff --git a/crates/ui-egui/src/services.rs b/crates/ui-egui/src/services.rs index 3e74ad394f..2a1ffdb5d2 100644 --- a/crates/ui-egui/src/services.rs +++ b/crates/ui-egui/src/services.rs @@ -43,6 +43,13 @@ pub type ReadHeadFn = Box Result, String>>; pub type WriteFn = Box Result<(), String>>; +/// A save writer that keeps typed filesystem failures, on either the UI or background thread. +pub type SaveWriteFn = Arc vectorcraft_engine::Result<()> + Send + Sync>; + +/// A typed save writer for native files encoded directly into the output. +pub type SaveStreamFn = + Arc std::io::Result<()>) -> vectorcraft_engine::Result<()> + Send + Sync>; + /// Shared, so Background Save and Export can check from a worker thread too. pub type ExistsFn = Arc bool + Send + Sync>; @@ -140,6 +147,10 @@ pub struct Services { /// stream into the file as they are encoded instead of being held whole in memory first. /// Without it they are encoded, then written with `write` / `write_shared`. pub stream_shared: Option, + /// Typed writers for document saves only. Without them the existing string writers are used + /// and failures keep their normal dialog. Downloads never use these callbacks. + pub save_write: Option, + pub save_stream: Option, /// Open a file (or a folder) in the system's default app for it (desktop: Edit Original, Show /// Package). Without it (the web) those answer with an error. pub open_file: Option, diff --git a/crates/ui-egui/src/tests/nativemenu.rs b/crates/ui-egui/src/tests/nativemenu.rs index 65bd01999d..689ae8a60c 100644 --- a/crates/ui-egui/src/tests/nativemenu.rs +++ b/crates/ui-egui/src/tests/nativemenu.rs @@ -683,48 +683,51 @@ fn the_app_bar_hides_its_menus_with_the_mac_menu_bar() { /// answers, the menus are as before and work again. #[test] fn the_menu_bar_is_inert_while_a_file_dialog_is_open() { - let mut m = Mac::new(true); - m.app.services.dialogs_are_sheets = true; - let (shown, _) = crate::tests_picks::off_the_ui_thread(&mut m.app.services); - m.app.run("shape.rectangle", json!({"x": 200, "y": 150, "width": 20, "height": 20})).unwrap(); - m.app.run("select.none", json!({})).unwrap(); - m.frame(vec![]); let rows = |bar: &MenuBar| bar.items().iter().map(|it| (it.command, it.params.clone(), it.enabled)).collect::>(); - let before = m.fake.last(); - let system = [native_menu::HIDE, native_menu::HIDE_OTHERS, native_menu::MINIMIZE]; - assert!(system.iter().all(|c| find(&before, c).enabled)); - assert!(m.app.run("file.open", json!({})).is_err()); - assert!(m.app.file_dialog_open()); - m.frame(vec![]); - let inert = m.fake.last(); - for it in inert.items() { - let kept = it.command.is_some_and(|c| system.contains(&c)); - assert_eq!(it.enabled, kept, "{:?} {} while the dialog is open", it.command, it.label); - } - // A click that raced the menu going inert isn't run; Minimize is. - let rulers = (m.app.ui.view.rulers, find(&before, "view.rulers").clone()); - m.fake.report(native_menu::Event::Click(rulers.1.clone())); - m.frame(vec![]); - assert_eq!(m.app.ui.view.rulers, rulers.0); - m.fake.report(native_menu::Event::Click(find(&inert, native_menu::MINIMIZE).clone())); - assert!(m.frame(vec![]).viewport_output[&egui::ViewportId::ROOT].commands.contains(&egui::ViewportCommand::Minimized(true))); - // A key equivalent that raced it doesn't reach egui. - let mut k = crate::shortcuts::parse("Cmd+A").unwrap(); - k.modifiers.mac_cmd = k.modifiers.command; - m.fake.report(native_menu::Event::Key(k)); - let mut raw = egui::RawInput::default(); - m.app.raw_input_hook(&m.ctx, &mut raw); - assert!(!raw.events.iter().any(|e| matches!(e, Event::Key { .. })), "{:?}", raw.events); - m.key_equivalent("Cmd+A"); - assert_eq!(m.selected(), 0, "⌘A selected nothing"); - // Cancelled: the menus are as before, and work again. - shown.answer(&[]); - m.frame(vec![]); - m.frame(vec![]); - assert!(!m.app.file_dialog_open()); - // Every row from before is back as it was (tests running alongside may add plug-in rows). - let after = rows(&m.fake.last()); - assert!(rows(&before).iter().all(|r| after.contains(r)), "the menus are as before"); + // The menus read before the dialog and after it are compared, so the whole run is undisturbed: + // a test running alongside can add or remove a composite font or a plug-in in between. + let (mut m, (before, after, rulers)) = undisturbed(|m| { + m.app.services.dialogs_are_sheets = true; + let (shown, _) = crate::tests_picks::off_the_ui_thread(&mut m.app.services); + m.app.run("shape.rectangle", json!({"x": 200, "y": 150, "width": 20, "height": 20})).unwrap(); + m.app.run("select.none", json!({})).unwrap(); + m.frame(vec![]); + let before = m.fake.last(); + let system = [native_menu::HIDE, native_menu::HIDE_OTHERS, native_menu::MINIMIZE]; + assert!(system.iter().all(|c| find(&before, c).enabled)); + assert!(m.app.run("file.open", json!({})).is_err()); + assert!(m.app.file_dialog_open()); + m.frame(vec![]); + let inert = m.fake.last(); + for it in inert.items() { + let kept = it.command.is_some_and(|c| system.contains(&c)); + assert_eq!(it.enabled, kept, "{:?} {} while the dialog is open", it.command, it.label); + } + // A click that raced the menu going inert isn't run; Minimize is. + let rulers = (m.app.ui.view.rulers, find(&before, "view.rulers").clone()); + m.fake.report(native_menu::Event::Click(rulers.1.clone())); + m.frame(vec![]); + assert_eq!(m.app.ui.view.rulers, rulers.0); + m.fake.report(native_menu::Event::Click(find(&inert, native_menu::MINIMIZE).clone())); + assert!(m.frame(vec![]).viewport_output[&egui::ViewportId::ROOT].commands.contains(&egui::ViewportCommand::Minimized(true))); + // A key equivalent that raced it doesn't reach egui. + let mut k = crate::shortcuts::parse("Cmd+A").unwrap(); + k.modifiers.mac_cmd = k.modifiers.command; + m.fake.report(native_menu::Event::Key(k)); + let mut raw = egui::RawInput::default(); + m.app.raw_input_hook(&m.ctx, &mut raw); + assert!(!raw.events.iter().any(|e| matches!(e, Event::Key { .. })), "{:?}", raw.events); + m.key_equivalent("Cmd+A"); + assert_eq!(m.selected(), 0, "⌘A selected nothing"); + // Cancelled: the menus are as before, and work again. + shown.answer(&[]); + m.frame(vec![]); + m.frame(vec![]); + assert!(!m.app.file_dialog_open()); + (rows(&before), rows(&m.fake.last()), rulers) + }); + // Every row from before is back as it was. + assert!(before.iter().all(|r| after.contains(r)), "the menus are as before"); m.fake.report(native_menu::Event::Click(rulers.1)); m.frame(vec![]); assert_eq!(m.app.ui.view.rulers, !rulers.0); diff --git a/docs/control-protocol.md b/docs/control-protocol.md index a076cea1e3..003dcab9dd 100644 --- a/docs/control-protocol.md +++ b/docs/control-protocol.md @@ -111,7 +111,7 @@ the two can still win that race. | `app.export` with `useArtboards` | `{path, useArtboards: true, range? \| artboards?, …}` | Export As: PNG, JPEG and WebP write one file per chosen artboard (default all; SVG does so for a `range`), `-.`, and return `{path, files: [path…]}`; a PDF keeps them as pages of `path`. `useArtboards: false` covers the bounds of the visible art. The menu's Export As… is `file.exportAs`: the `exportAs` dialog (`format`, `useArtboards`, `all`, `range`), then a save dialog and the format's options: `pngOptions` / `jpgOptions` / `webpOptions`, `svgOptions`, or `savePdf` for a PDF with Use Artboards | | `ui.dialog.*` on `gradientStop` | `{field: "color" \| "opacity" \| "location", value}` | double-clicking a stop on the Gradient tool's annotator opens its popover (fields `index`, `x`, `y`, `tab`); set `color` (hex), `opacity` or `location` (percentages) and `ui.dialog.confirm` to apply them to the selected stop. `gradient.selectStop {index}` picks the stop the annotator, the panels and Delete/←/→ (`ui.key`) act on | | `engine.execute` `ui.savePdfDialog` | `{path?, preset?, range?, …document.exportPdf options}` | opens the Save PDF dialog (`file.export.pdf` with no params opens it too). Its fields are the `document.exportPdf` options: set `compatibility` with `ui.dialog.set {field: "compatibility", value: "1.5"}`, a whole section with `{field: "compression", value: {…}}`, the artboards with `__allArtboards: false` and `range`, the visible section with `__section` (General, Compression, Marks and Bleeds, Output, Advanced, Security, Summary). `ui.dialog.confirm` writes `path` (else asks for one), returns `{path, bytes, warnings}` and opens the file when `viewAfterSaving` is set | -| `file.save` / `file.saveAs` / `file.saveCopy` / `file.saveAsTemplate` (via `engine.execute`) | `{path?, format?, options?, svg?}` | Save writes the document's own file in its own format (a document opened from or saved as SVG/PDF saves as that again; the result's `warnings` say what the format loses). With `path` the file is written at once (Save As or a Copy to an SVG path without SVG options first opens SVG Options, below). Without one (never saved, a converted older file, Save As, a Copy, a Template) the app shows a save panel with one file type per save format (`vectorcraft`, `template`, `pdf`, `svg`, `svgz`, `ai`; for a Template, `template` and `ait`), then the format's options: SVG opens the `svgOptions` dialog (below; returns `{"pending": "svgOptions", "path"}`), PDF the `savePdf` dialog (its `__save` field names the save command); `ui.dialog.confirm` finishes the save. A name typed in the panel without the file type's extension gets it, and a name the panel returns with the extension of a format the panel does not list has it replaced (in a native save, `card.png` and `card.ait` are saved as `card.vectorcraft`, and `notes.v2`, whose extension names no format, is saved as `notes.v2.vectorcraft`); when a file of that name exists, which the panel did not ask about, the save (and every export or other write through a save panel) answers `cancelled` and opens a `replaceFile` dialog (fields `message`, `detail`, `path`): `ui.dialog.confirm` writes that file, `ui.dialog.cancel` writes nothing. A save or export that writes other files beside that path (a file per artboard, linked images, image maps) checks them before it writes anything: when any of them exists, it writes nothing and opens a `replaceFiles` dialog (fields `message`, `detail` (each file's name when there are several), `files`, `__command`, `__params`; the reply is `{"pending": "replaceFiles"}`, or comes once a Background Save or Export ends): `ui.dialog.confirm` runs the save or export again and replaces them, `ui.dialog.cancel` writes nothing; a dialog open when a background job asks stays open, and the question follows it. A save does not ask about the files the document's last Save or Save As in this session wrote. On the web a `saveOptions` dialog (fields `path`, `format`) first names the file and picks the format. `file.formatOptions {format?}` reads the options a save would use. `file.revert` opens a `confirm` dialog (`{"pending": "confirm"}`); `ui.dialog.confirm` reloads the saved file in the same tab | +| `file.save` / `file.saveAs` / `file.saveCopy` / `file.saveAsTemplate` (via `engine.execute`) | `{path?, format?, options?, svg?}` | Save writes the document's own file in its own format (a document opened from or saved as SVG/PDF saves as that again; the result's `warnings` say what the format loses). With `path` the file is written at once (Save As or a Copy to an SVG path without SVG options first opens SVG Options, below). Without one (never saved, a converted older file, Save As, a Copy, a Template) the app shows a save panel with one file type per save format (`vectorcraft`, `template`, `pdf`, `svg`, `svgz`, `ai`; for a Template, `template` and `ait`), then the format's options: SVG opens the `svgOptions` dialog (below; returns `{"pending": "svgOptions", "path"}`), PDF the `savePdf` dialog (its `__save` field names the save command); `ui.dialog.confirm` finishes the save. A name typed in the panel without the file type's extension gets it, and a name the panel returns with the extension of a format the panel does not list has it replaced (in a native save, `card.png` and `card.ait` are saved as `card.vectorcraft`, and `notes.v2`, whose extension names no format, is saved as `notes.v2.vectorcraft`); when a file of that name exists, which the panel did not ask about, the save (and every export or other write through a save panel) answers `cancelled` and opens a `replaceFile` dialog (fields `message`, `detail`, `path`): `ui.dialog.confirm` writes that file, `ui.dialog.cancel` writes nothing. A save or export that writes other files beside that path (a file per artboard, linked images, image maps) checks them before it writes anything: when any of them exists, it writes nothing and opens a `replaceFiles` dialog (fields `message`, `detail` (each file's name when there are several), `files`, `__command`, `__params`; the reply is `{"pending": "replaceFiles"}`, or comes once a Background Save or Export ends): `ui.dialog.confirm` runs the save or export again and replaces them, `ui.dialog.cancel` writes nothing; a dialog open when a background job asks stays open, and the question follows it. A save does not ask about the files the document's last Save or Save As in this session wrote. On the web a `saveOptions` dialog (fields `path`, `format`) first names the file and picks the format. `file.formatOptions {format?}` reads the options a save would use. `file.revert` opens a `confirm` dialog (`{"pending": "confirm"}`); `ui.dialog.confirm` reloads the saved file in the same tab. Location refusals return `READ_ONLY_LOCATION: ` without a fallback panel for automation; see [Save location errors](#save-location-errors). | | SVG Options (`engine.execute` of `file.export.svg`, `file.saveAs`, `file.saveCopy`) | `{}` / `{path?, svg?: {…}}` | `file.export.svg` without params, and Save As or Save a Copy to an `.svg`/`.svgz` path without SVG options, open the `svgOptions` dialog (`ui.dialog.set` its fields: `styling`, `outlineText`, `images`, `objectIds`, `decimals`, `minify`, `responsive`, `fewerTspans`, `metadata`, `preserveEditing`, `useArtboards`, `allArtboards`, `range`, `showCode`, `profile` (`svg11`/`tiny12`), `encoding` (`utf8`/`utf16`/`latin1`), `embedFonts`; `ui.dialog.confirm` exports or saves and remembers the choices). With `svg: {…}` (the `document.formats` SVG options) they run directly. `app.save {path?, svg?}` takes the same options; a document saved as SVG saves as SVG again with the same options | | `engine.execute` `file.place` | `{paths?}` / `{path \| name+dataBase64, …file.place params}` | with no file, picks files and opens the `place` dialog; with `paths`, opens it for them (fields `files`, `link`, `template`, `replace`; `__replace` says whether Replace applies, `__info` describes each file). `ui.dialog.confirm` places one file centered in the view (or in place of the selected object) and loads the place cursor with several (`file.place.queue`). In the desktop app the dialog reads its files off the UI thread: `__info` is empty until they are read, `ui.dialog.confirm` returns `{background: true, status}`, `ui.inspect` lists the files still being read in `placing`, and the file is placed (or the cursor loaded) once it is read. With a file, it runs `file.place` centered in the view, at once. Files dropped on the window with no document open open. On the web, files dropped on the canvas are placed at the drop point (linked; Shift embeds) and files dropped off it (the tab bar) open, as in Illustrator; desktop drops carry no pointer position, so documents (native, SVG, PDF, `.ai`, EPS, DXF, EMF/WMF) open as tabs of their own and pictures and text are placed in the middle of the view. Swatch libraries and color books (`.vcswatches`, `.gpl`, `.ase`, `.acb`), graphic style libraries (`.vcstyles`), Libraries panel files (`.vclibrary`), presets (`.vcflattener`, `.vcpdfpresets`, `.vcprintpresets`, `.vcperspective`, `.vcexportpresets`), plug-ins (`.wasm`) and shortcut sets (`.kys`, in Import Shortcut Set) open wherever they are dropped, as File › Open opens them. With the cursor loaded (`ui.inspect` → `tool: "place"`, `toolOptions: {count, current, name, names}`), `ui.pointer` clicks and drags place files (in the desktop app each file is read off the UI thread and placed once it is read, and `ui.inspect` lists it in `placing` until then) and `ui.key` ←/→/↑/↓ and Escape cycle and discard them | | `app.export` raster options | `{path?, format: png \| jpg \| webp, ppi?, width?, height?, background?, antiAlias?, interlaced?, quality?}` | `ppi` sets the pixel size (72 = one pixel per point) and is stored in PNG and JPEG files (WebP files store no resolution); `width` or `height` sets the size of each file in pixels instead (`width` wins), and PNG and JPEG files store the resolution that results; `background` is `transparent`, `white`, `black` or `"#rrggbb"`; `antiAlias` is `none`, `art` or `type` (text snapped to pixels); `interlaced` writes an Adam7 PNG. The PNG Options dialog (`pngOptions`; `jpgOptions` / `webpOptions` for the other raster formats) has the same fields: fill them with `ui.dialog.set` and press OK with `ui.dialog.confirm` | @@ -1102,11 +1102,32 @@ hidden tab (whose frames the browser pauses) answers when it is shown again. | 2026-11-07 | minor | Documented OpenType feature values in text commands (`"salt=2"`, the font's alternate number) and that runs differing only in features shape as one piece of text. | | 2026-10-31 | minor | Confining file access: on Windows a `:` in a name after the drive (an NTFS alternate data stream) is refused with the device names (#3604). | | 2026-10-25 | minor | Panel docking on craft-ui's layout: `ui.docking`, the floating and dock parameters, native floating windows. | -| 2026-10-24 | minor | Documented the brush library panel commands (`window.brushLibrary`, `.other`, `ui.saveBrushLibrary`, `window.userBrushLibrary1`…`10`). | -| 2026-10-24 | minor | Documented `ui.screenshot`'s 64 MiB inline-PNG reply cap: with `path` the file is still written and `pngBase64` is omitted when over the cap; a path-less `data: true` request fails before encoding. | | 2026-10-25 | minor | Documented that Object → Expand… is dimmed, and `ui.expandDialog` refuses, while the selection has an appearance Expand Appearance expands (`object.expand.info`'s `appearance`, #3089). | | 2026-10-25 | minor | Documented that Transform Each opens on the values it was last confirmed with and that its Copy button confirms with `copy: true` (#1732). | +| 2026-10-24 | minor | Documented the brush library panel commands (`window.brushLibrary`, `.other`, `ui.saveBrushLibrary`, `window.userBrushLibrary1`…`10`). | +| 2026-10-24 | minor | Documented `ui.screenshot`'s 64 MiB inline-PNG reply cap: with `path` the file is still written and `pngBase64` is omitted when over the cap; a path-less `data: true` request fails before encoding. | | 2026-10-23 | minor | Documented the symbol library panel commands (`window.symbolLibrary`, `.other`, `ui.saveSymbolLibrary`, `window.userSymbolLibrary1`…`10`). | +| 2026-10-18 | minor | Documented `window.vectorcraft`, the web build's control channel. | | 2026-10-13 | minor | Documented count, pending-event and per-frame request budgets (#1709). | | 2026-10-13 | minor | Documented transport deadlines and connection-slot recovery (#1726). | -| 2026-10-18 | minor | Documented `window.vectorcraft`, the web build's control channel. | + +## Save location errors + +A save refused by the filesystem with PermissionDenied or ReadOnlyFilesystem returns +`READ_ONLY_LOCATION: `. The reason retains the path and system message. This is a +stable prefix within the existing error string, not a new response field or JSON-RPC code. +An automation-root denial, an encoding failure, and an error retaining recovery copies do +not use this code. Choose a writable destination and explicitly save there; automation never +opens the fallback Save As panel. Failed saves keep the original document path and modified +state; multi-file saves may already have written other output files. + +`app.save` with `wait: true` returns the write failure in its reply (`save_file` already asks +for this on the desktop). Without waiting, an accepted background save still replies before +completion; any later failure opens no panel. MCP retains its usual tool-name prefix around +the error text. The interactive fallback applies only to ordinary File > Save, not Close/Quit. + +## Revision history + +| Date | Change | +|---|---| +| 2026-11-11 | Document READ_ONLY_LOCATION for saves (#6146); no response-shape change. | diff --git a/docs/development.md b/docs/development.md index e7831adf1b..37f2b770ac 100644 --- a/docs/development.md +++ b/docs/development.md @@ -1,5 +1,6 @@ # Development +> **Last updated:** 2026-11-12 · **Change:** Tests canonicalize temporary roots and normalize Windows verbatim prefixes before comparing preset refusal paths or probing a case-variant script folder (#6860); production access checks unchanged. > **Last updated:** 2026-11-12 · **Change:** minor (clipboard failure helper follows its Linux/BSD and test callers; previously: Hindi catalog and zero/one plural rule; desktop Devanagari fallback and optional craft-fonts UI face; native review pending). > **Last updated:** 2026-11-01 · **Change:** Linux: Automatic Linux Display Server opens the window through XWayland when a pen is attached on a Wayland session, so the pen draws (#4967); `VECTORCRAFT_NATIVE_WAYLAND=1` keeps Wayland. > **Last updated:** 2026-10-31 · **Change:** files opened while the app runs open in its window on Windows and Linux (#4892); `--new-instance`. @@ -512,6 +513,7 @@ Scores and hour estimates are unchanged. | Date | Change | Summary | |---|---|---| +| 2026-11-12 | minor | Tests (#6860): compare preset refusal roots after canonicalization and Windows display normalization; canonicalize the script fixture base before constructing `MAIN`. This keeps macOS `/var` → `/private/var` aliases from masking the case-sensitivity checks; no production-code change. | | 2026-11-12 | minor | Compile the clipboard failure helper only for Linux/BSD worker callers and unit tests; keep production macOS free of the unused helper without suppressing workspace dead-code warnings. | | 2026-11-06 | minor | Localisation: Hindi catalog, zero/one plurals, desktop and web font requirements; AI/MT draft, native review pending. | | 2026-10-31 | minor | Files opened while the app runs open in its window on Windows and Linux (#4892); `--new-instance` opts out. | diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index b99adf420e..7a59146afa 100644 --- a/docs/file-format-parity.md +++ b/docs/file-format-parity.md @@ -15,6 +15,8 @@ from the public specifications, and users' own `.ai` files are used locally only ## Summary +Ordinary File > Save offers Save As when its own location refuses writes with a terminal permission/read-only error (#6146). The picker suggests the original filename and format, keeps the original options when the format is unchanged, and opens the chosen format’s Save options when it changes. It starts in Documents (home or host default if unavailable). Cancel leaves the document modified at its original path. Explicit Save As/Copy/export, recovery failures and Close/Quit keep their existing error dialog. Non-UI saves return `READ_ONLY_LOCATION: ` without opening a panel. Focused UI regressions passed; engine regression execution and native panel acceptance pending; format scores unchanged. + | | Illustrator | VectorCraft | Notes | |---|---:|---:|---| | Formats opened or placed | ~30 | 30 | missing: FreeHand, CorelDRAW, DWG, PICT, PS | @@ -341,10 +343,12 @@ Stop opacity on translated artboards remains outside this regression coverage. | Date | Change | Summary | |---|---|---| +| 2026-11-12 | minor | Save refusal follow-up (#6146): changed format opens its Save options; unchanged format retains original options. Focused UI regressions passed, including the format-change case that failed before the fix; native panel acceptance pending. Scores and estimates unchanged. | | 2026-11-11 | minor | Newly imported SVG/SVGZ titles use the imported-text rules: generated Untitled names and control-only titles give the filename fallback, other control characters are removed, and titles are limited to 65,536 characters. Native and Preserve Editing titles remain unchanged (#3626). | | 2026-11-11 | minor | Native and Preserve Editing restores keep an existing File Info title exactly, including repeated spaces; imported-text fallback titles alone are normalized (#3626). | | 2026-11-03 | minor | An imported document's own title goes into File Info only (#3626): an SVG's root ``, a PDF's, `.ai`'s or `.ait`'s XMP `dc:title` else its Info `Title` is File Info's title (`metadata.title` in the native format, left out when unset so other documents save unchanged and older versions ignore it; written by exports); the document's name, which the tab, Save As and the default export names use, stays the file's name. | | 2026-11-11 | minor | `.ai` and EPS import, and what the canvas, every export and `.ai` save draw: type's strokes in an envelope (its characters' and those on its own appearance) paint along the bent glyph outlines at their own weight, Distort Appearance on or off, as the drawn art of users' `.ai` files strokes them (used locally, never committed: there the drawn art keeps those strokes as stroked paths at the type's own weights, and its ring is as wide wherever the grid squeezes or stretches the type). With Distort Appearance, VectorCraft bent them with the art, as their filled outlines, so an envelope of stroked type in a squeezed or stretched grid didn't draw as its drawn art and kept it. The same holds in an envelope inside an envelope. Paths' strokes, geometry effects, opacity masks, linear gradients and pattern fills bend as before. On 224 of our own files: envelopes of type that open live go from 135 in 37 files to 154 in 45, and no file loses one; no file changes whether it opens from its layers or its PDF part; renders at 40 ppi change in 31 files, by at most 2.3% of their pixels, 19 closer to the file's own page and 12 further, 11 of those where an envelope of type now opens live in place of its drawn art (the page's own art) and one by 4 pixels. Tests `envelope_distort::type_strokes_paint_at_their_own_weight_in_a_squeezed_envelope`, `a_stroke_on_types_appearance_paints_at_its_own_weight_in_a_squeezed_envelope`, `a_paths_stroke_still_bends_in_a_squeezed_envelope`, `tests_native::an_envelope_of_stroked_type_whose_drawn_art_strokes_the_bent_outlines_at_the_weight_opens_live` (#6644) | +| 2026-11-11 | minor | Ordinary Save to a location refusing writes offers Save As (#6146); cancellation preserves the original path and modified state; automation receives READ_ONLY_LOCATION. Scores and estimates unchanged. Validation pending. | | 2026-11-11 | minor | `.ai` and EPS import, and what SVG export, PDF export and `.ai` save draw: centred and right-aligned lines of tracked type are aligned without the tracking after their last character, as a `.ai`'s own page aligns them (see type-parity.md, same date). In 224 users' files (used locally, never committed), 63 render differently: 54 closer to the page, 7 further, 1 alike and 1 that couldn't be compared; among them the median share of pixels that differ from the page went from 4.4% to 1.0%. Of the 7: one now opens from its layers instead of its PDF part (they differ from the page by less than the import's 5% limit now; type note 3.8%); one has a centred line whose spacing inside it differs from the page's, which now centres on the page's line while half of it no longer lies on the page's glyphs (type note 1.4% to 2.4%); five moved by less than 0.05 of a percentage point, in type the page draws in other glyphs. The type note shows a smaller share in 6 files; 2 more envelopes of type open live (129 to 131, none fewer in any file). SVG import: `text-anchor` aligns a line's advance, which ends after its last character's letter spacing, so a centred or right-aligned letter-spaced line moves back by half or all of it and stays where a reader draws it; SVG export, which anchors lines so, reads back unchanged (test `import_text::anchored_letter_spaced_lines_stay_where_a_reader_draws_them`). Affinity import: centred or right-aligned tracked artistic text moves by half or all of its last character's tracking, as layout now aligns it (unverified against Affinity, whose files we don't have), and differently aligned paragraphs line up by the widths layout aligns them by (`affinity_features::artistic_text_aligns_its_lines_within_the_longest`). Scores and estimates unchanged. (#6610) | | 2026-11-11 | minor | `.ai` and EPS import: while an envelope's contents are edited, the editing data keeps its mesh (a mesh's, a top object's or a warp's grid) in its dictionary, and in some files that mesh lies away from the drawn art by a move, the contents with the drawn art, as if the mesh hadn't followed the envelope when it moved: the envelope drew its contents elsewhere and kept its drawn art. A mesh or top object saved in Edit Contents (or a warp of type tried on its saved grid) whose mesh draws the contents as large as the drawn art, within half a point over width and height together, but further from it than that, is now tried again, after every other way and with one more of the import's checks, with its mesh moved by as far as the drawn art's box lies from the box of what it draws; it opens live there where it then draws as the drawn art. In users' files, used locally and never committed, only envelopes saved in Edit Contents draw elsewhere than their drawn art, and nothing in their dictionaries says by how much they moved. Tests `ai::tests::an_envelope_in_edit_contents_whose_mesh_stayed_behind_opens_live_on_its_drawn_art`, `tests_native::a_warp_of_type_in_edit_contents_whose_grid_stayed_behind_opens_on_its_drawn_art` (#6570) | | 2026-11-11 | minor | `.ai` save (PDF-compatible editing copy and legacy) and `.ai`/EPS open: a layer's `Lb` colour index follows the file's numbering, where Magenta, Cyan and Yellow are 4, 5 and 6 and `LAYER_COLORS` has Yellow, Magenta and Cyan; Yellow, Magenta and Cyan layers no longer come out as Magenta, Cyan and Yellow, and the other 24 presets are unchanged (#6502, from the index and RGB pairs in users' files). `LAYER_COLORS` keeps its order, so `.vectorcraft` files and the Layers panel don't change. Scores and estimates unchanged. | @@ -371,14 +375,14 @@ Stop opacity on translated artboards remains outside this regression coverage. | 2026-11-07 | minor | EPS import: an embedded Type 1 font's encrypted part (`eexec`, [Adobe Type 1 Font Format](https://adobe-type-tools.github.io/font-tech-notes/pdfs/T1_SPEC.pdf), chapter 7) is no longer only skipped: its charstrings are read, and the font dictionary gets the `CharStrings` dictionary the part would have given it. Its type then takes its characters from its glyph names (the standard and ISO Latin-1 encodings by their codes' names, PLRM 3rd ed., Appendix E), its widths move the current point and space the live type as tracking (as Type 42 fonts do), `stringwidth` and `charpath` read its glyphs, glyphs whose names say no character are drawn from its outlines, and a composite font's Type 1 descendants draw their glyphs. A font found by name, re-encoded with glyph names at other codes (codes 1 to 12 for `T`, `r`, `a`… in pdftocairo's subsets), sets the characters its names say instead of its codes read as Latin-1 (such type was left out). | | 2026-11-07 | minor | EPS import: CID-keyed fonts (PLRM 3rd ed., §5.11; [Adobe Technical Note #5014](https://adobe-type-tools.github.io/font-tech-notes/pdfs/5014.CIDFont_Spec.pdf)) draw from the font data the file carries instead of stopping the program at `composefont` or the CIDInit operators: CMaps built with `begincmap` … `endcmap` (codespace ranges, CID and notdef mappings, `usefont`, `usecmap`; mappings to base-font codes are not read) and the predefined `Identity-H` and `Identity-V`; `composefont` and `FMapType` 9 fonts, whose codes take as many bytes as their codespace range says, an invalid or unmapped code falling back as §5.11.5 says; Type 0 CIDFonts' Type 1 charstrings, read by `StartData` (binary or hexadecimal) or from a `GlyphDirectory`, with their FDArray dictionaries' matrices and subroutines (`Subrs` or the binary data's subroutine table); Type 2 CIDFonts' TrueType glyphs through `CIDMap` (strings, a dictionary or an offset); `Metrics` widths; vertical writing mode set downwards. A CIDFont the file doesn't carry draws nothing, with a warning; FontSet (Compact Font Format) data is skipped. | | 2026-11-07 | minor | EPS import: a plain Type 42 font that carries its TrueType font ([Adobe Technical Note #5012](https://adobe-type-tools.github.io/font-tech-notes/pdfs/5012.Type42_Spec.pdf)) gives each code its character by its glyph name (single characters, `uniXXXX`, the standard encoding's and Windows-1252's names) or, when the name says nothing (`/g12`), by the glyph `CharStrings` gives it in the font's own character map or `post` names, instead of reading the code as Latin-1 (a Euro at code 128 was dropped); the font's own widths move the current point (`stringwidth`, `show`), `charpath` outlines its glyphs, and a string with a glyph that stands for no character is drawn from the font's outlines. Live type keeps the spacing the file gives it: where the show operator's numbers (`ashow`, `widthshow`, `xshow`, `yshow`, `xyshow`: PLRM 3rd ed., chapter 8) or the embedded font's widths differ from the installed or fallback font's advances, each character's tracking makes up the difference, as PDF import does, so justified type in a font that isn't installed no longer drifts (pdftocairo and pdftops EPS of a justified PDF page set in a missing font: 10.2% and 9.0% of the page differing from Ghostscript's render, now 6.2% and 5.8%). Type under `nulldevice` no longer shows. | -| 2026-11-06 | minor | EPS import: the buffers an image decoder makes for itself (its sample planes, its copies of the strings a procedure data source hands back, and the split buffers of an interleaved masked image) stop counting against the PostScript reader's 1 GiB memory budget once the image is decoded, as they are freed then; the pixels kept, and anything the program's own procedures allocate, still count. A user's Illustrator 25.2 EPS with 71 CMYK images (used locally, not committed) used to stop with "the program uses too much memory" and come in as its screen preview; it now opens with its art. The budget itself is unchanged. Scores and estimates unchanged. | | 2026-11-07 | minor | EPS import: a sampled function (type 0) of two or more inputs took the nearest sample, so a function-based shading (type 1) over one came in as flat blocks; it is now interpolated between its samples (multilinearly), and with `Order 3` by cubics through them (Catmull-Rom, kept in the function's range), as the PostScript Language Reference (3rd ed., §3.10.1, type 0 functions) defines. An axial or radial shading whose function is a linearly interpolated sampled function of one input has a stop at each sample, not 33 evenly spaced ones. Tests `sampled_functions_interpolate_linearly_or_by_cubics`, `mesh_and_function_shadings_become_gradient_meshes`; a 2 × 2 function-based shading is 1.2% from Ghostscript's render (it was 45.9%); scores unchanged | | 2026-11-07 | minor | EPS and `.ai` type from the text document keeps its paragraphs' tab stops (positions and alignments; leaders aren't read), at most 1,024 a paragraph and 2^20 a story, past which the default stops apply with a warning. A user's `.ai` (used locally, not committed) whose price lists came in on the default stops now lays them out as its page has them (its type 0.8% from its page, from 1.5%; what differs is hyphenation in Hungarian and type in a font that isn't installed). Scores and estimates unchanged. | | 2026-11-07 | minor | EPS and `.ai` type from the text document: auto leading stays auto at the paragraph's share (it was the first two lines' distance for every line, so 10 pt lines under a 15 pt heading came in 18 pt apart), the paragraph's hyphenation options are read, and area type of one frame whose last lines the file sets in too short a frame for them here may get a frame taller by its lines' deepest descent plus 0.05 pt, when that shows more of them. A user's `.ai` (used locally, not committed) whose type differed from its page by 6.6%, so that it opened from its PDF part, now opens from its layers (4.9%; type with tab stops in right-aligned paragraphs still differs, a separate issue) (#6137). Scores and estimates unchanged. | +| 2026-11-07 | minor | EPS and `.ai` editing data: opacity mask art that sets no paint of its own paints with the paint before the mask's dictionary, not the black a dictionary's state starts with. A logo of white letters masked by a rectangle with no colour no longer opens hidden: a user's `.ai` (used locally, not committed) shows its six logos. Scores and estimates unchanged. | +| 2026-11-06 | minor | EPS import: the buffers an image decoder makes for itself (its sample planes, its copies of the strings a procedure data source hands back, and the split buffers of an interleaved masked image) stop counting against the PostScript reader's 1 GiB memory budget once the image is decoded, as they are freed then; the pixels kept, and anything the program's own procedures allocate, still count. A user's Illustrator 25.2 EPS with 71 CMYK images (used locally, not committed) used to stop with "the program uses too much memory" and come in as its screen preview; it now opens with its art. The budget itself is unchanged. Scores and estimates unchanged. | | 2026-11-06 | minor | EPS and `.ai` editing data: a path or compound path whose art style stacks several fills and strokes (solid colours and gradients, each with its own visibility, opacity and blend mode, under effects any object takes) comes in as itself with that appearance instead of its drawn look. Each gradient must be its own swatch, placed as the drawn look places it; a stack that repeats a swatch, or whose look doesn't paint its gradients, keeps the drawn look, as do fills and strokes with effects of their own. | | 2026-11-06 | minor | `.ai` and EPS import: Art brushes whose objects are in the older form (first number 1, one field fewer and 8 numbers at the end) come into the Brushes panel with their art instead of being left out with a note. In a survey of 302 users' EPS files (used locally, not committed), 652 Art brushes in 188 of the 197 files with the note were left out this way; afterwards 55 files have the note, all for Pattern brushes. Test `an_older_art_brush_comes_in_with_its_art` (crate `vectorcraft-eps`); scores unchanged | | 2026-11-06 | minor | EPS import: composite (Type 0) fonts with the 8/8 mapping (`FMapType` 2: PLRM 3rd ed., §5.10) draw a Type 42 descendant's glyphs from the TrueType outlines its `sfnts` carry (Adobe Technical Note #5012), spaced by their own widths or the show operator's numbers, and `charpath` and `stringwidth` read them; descendants found by name set their characters as type at the composite font's size instead of a thousand times it. EPS from `pdftops` of a PDF with embedded TrueType fonts showed nothing, or one black shape, where its type was. | -| 2026-11-07 | minor | EPS and `.ai` editing data: opacity mask art that sets no paint of its own paints with the paint before the mask's dictionary, not the black a dictionary's state starts with. A logo of white letters masked by a rectangle with no colour no longer opens hidden: a user's `.ai` (used locally, not committed) shows its six logos. Scores and estimates unchanged. | | 2026-11-06 | minor | EPS and `.ai` editing data: an art style's stroke aligned inside or outside its path (Stroke panel › Align Stroke, written as its own `Adobe Stroke Offset` filter) comes in as a stroke with that alignment, so objects whose style has one keep the style live instead of coming in as their drawn look. Without its inside/outside switch the drawn look stays. | | 2026-11-06 | minor | EPS and `.ai` editing data: `Q` restores the painting state but not the transparency (`Xy`), which the writer only sets when the next object's differs from the last one written. A Screen glow followed by a clipping group set back to Normal no longer leaves the type after the group in Screen: two users' Illustrator 26.5 and 30 EPS posters (used locally, not committed) now show their gold heading and crescent, and both open from their layers (2.5% and 2.7% from the page before, under the limit after). Scores and estimates unchanged. | | 2026-11-06 | minor | EPS and `.ai` editing data: a blend whose spine was never edited, written with its keys and no spine part, comes in as a live blend along the straight lines between its keys' centres (in its record's order) instead of as its drawn steps. Checked on six users' Illustrator EPS files (used locally, not committed): every blend in four of them now comes in live, their renders unchanged; two still have blends the reader can't match to their drawn steps. Scores and estimates unchanged. | @@ -403,8 +407,8 @@ Stop opacity on translated artboards remains outside this regression coverage. | 2026-10-31 | minor | `.ai`/EPS editing data: type made from the file's text document took each character attribute its run doesn't set from the document's base attributes, not from the character style the run names (`[Normal Character Style]` or another of the file's, and the styles each is based on). Where they differ the type came in wrong: a user's file whose base fill is white and whose Normal Character Style is black came in with its text white, so most of its type was missing wherever the page's own type didn't stand in for it. A run now takes its own attributes, then its character style's, then the document's ([Character and paragraph styles](https://helpx.adobe.com/illustrator/using/character-paragraph-styles.html)). Test `import::tests_native::type_from_the_text_document_takes_its_character_styles_attributes`; checked locally against 377 users' files: none newly opens from its PDF part, and with #4791 that file opens from its layers (6.3% → 1.1% from its page); scores unchanged | | 2026-10-31 | minor | SVG export (and Object › Expand, EPS, DXF, EMF and the other outputs that cut a gradient mesh into flat pieces): each patch was cut into 8 × 8 quads whatever its colours, so a patch of one colour became 64 identical pieces. Neighbouring quads are now joined into square blocks wherever the colour changes by no more than about one 8-bit step across the block, so a flat patch is one piece and a patch that shades strongly keeps all 64; a block's outline runs through its quads' corners, and a patch that folds over itself is not joined. A synthetic page of 400 meshes (6 × 6 patches, half flat) exports 468,000 pieces instead of 921,600 (35 MB instead of 66 MB), and resvg draws the two files identically. Tests `mesh_export` (crate `vectorcraft-svg`), `tests::live::mesh_expand_to_flat_pieces`; scores unchanged | | 2026-10-31 | minor | PDF import (and a `.ai`'s PDF part): a transparency group's knockout and isolate flags (`/K`, `/I`; ISO 32000-1, 11.4.6.3) come from the interpreter as it pushes the group. They were read from the file in order and matched to the groups pushed, but a group that adds nothing (full opacity, Normal, no mask, neither flag) is drawn straight onto its parent without being pushed, so every group after one lost its own flags and took another's; and a page drawing form groups more than 10,000 times failed to import (the list of their flags was capped), which it no longer does. A blend's steps written as a knockout group of falling opacity drew as solid dark shapes: a user's `.ai` with 143 such bubbles now draws them as light rings, as poppler does (its bubble area 4.9% → 1.2% of pixels off from poppler's render); 16 other users' PDFs with transparency groups (44 pages) draw the same. Test `tests::import_fidelity::knockout_groups_after_a_group_drawn_straight_keep_their_flag`; scores unchanged | -| 2026-10-30 | minor | `.ai`/EPS editing data: a blend takes the transparency (opacity, blend mode, knockout; blends are written with knockout on) in the state block after its record, as a group takes the one after its end; it was left for the next object, so a blend's steps of falling opacity drew over each other instead of knocking each other out, and every such blend came in too dark (a user's file has 143, its bubbles came in as dark rings). Both a live blend and drawn steps kept as a group take it. Test `import::ai::tests::a_blends_transparency_follows_its_record`; checked locally against that file: it comes 1.3% → 0.6% nearer its page, though a mesh seen through a mesh-shaped opacity mask still keeps it just over the limit. When a blend is the layer's last object and the layer's own state block (`7 () XW`, then `LB`) follows, the state is the layer's and the blend keeps its own, as for meshes (#4748); scores unchanged | | 2026-10-31 | minor | PDF (and `.ai`) with Illustrator editing data saved with only some of its artboards: its pages are those artboards, while the editing data keeps them all, and the layers were compared with the first page on the first artboard, so a page of the second artboard came in as the PDF part, without its layers. With fewer pages than artboards, the first page is now taken to be the artboard of its size whose art draws most like it (up to 16 weighed), and the pages' type goes to that artboard and the ones after it. All artboards come in, as before. Test `import::tests_native::a_pdf_of_its_second_artboard_is_compared_on_that_artboard`; scores unchanged | +| 2026-10-30 | minor | `.ai`/EPS editing data: a blend takes the transparency (opacity, blend mode, knockout; blends are written with knockout on) in the state block after its record, as a group takes the one after its end; it was left for the next object, so a blend's steps of falling opacity drew over each other instead of knocking each other out, and every such blend came in too dark (a user's file has 143, its bubbles came in as dark rings). Both a live blend and drawn steps kept as a group take it. Test `import::ai::tests::a_blends_transparency_follows_its_record`; checked locally against that file: it comes 1.3% → 0.6% nearer its page, though a mesh seen through a mesh-shaped opacity mask still keeps it just over the limit. When a blend is the layer's last object and the layer's own state block (`7 () XW`, then `LB`) follows, the state is the layer's and the blend keeps its own, as for meshes (#4748); scores unchanged | | 2026-10-30 | minor | `.ai`/EPS editing data: a blend takes the transparency (opacity, blend mode, knockout; blends are written with knockout on) in the state block after its record, as a group takes the one after its end; it was left for the next object, so a blend's steps of falling opacity drew over each other instead of knocking each other out, and every such blend came in too dark (a user's file has 143, its bubbles came in as dark rings). Both a live blend and drawn steps kept as a group take it. Test `import::ai::tests::a_blends_transparency_follows_its_record`; checked locally against that file: it comes 1.3% → 0.6% nearer its page, though a mesh seen through a mesh-shaped opacity mask still keeps it just over the limit; scores unchanged | | 2026-10-30 | minor | `.ai`/EPS editing data: a gradient mesh with an opacity mask takes the transparency (`Xy`) written after the mask's dictionary, as one without a mask takes the one right after its `/End X!`; the operators of the mask's art (on `%_` lines) hid it, so the state was read as the next object's and each masked mesh in a run drew with the blend mode of the one before it (a Multiply shading drawn Normal, an Overlay tint drawn Multiply over the art below). Test `import::ai::tests::a_masked_gradient_mesh_takes_the_transparency_written_after_its_mask`; checked locally against a user's files: one opens from its layers 0.1% from its page (it was 0.6%, just inside the limit), another comes 1.6% → 1.3% nearer; scores unchanged | | 2026-10-30 | minor | `.ai`/EPS editing data: a layer's own state block (`Xy`, `Xd`, `7 () XW` before `LB`) can have the stroke style (`J`, `j`, `w`, `M`, `d`, `XR`) between its `Xy` and `Xd`; read as the art moving on, it lost the layer's opacity, so a layer at 17% drew opaque over the art under it and the file came in as its page. When the layer's last object is a gradient mesh, the mesh keeps its own state through the stroke style too (#4748). An object's own state block (`6 () XW`) still isn't the layer's. Of 408 users' files whose editing data was checked, 3 have such a block: an EPS that now opens from its layers 0.5% from its page (it was 18.8%), one whose layer now draws at 85% as its page does, and one whose layers are all at 100%; the others draw as before. Test `import::ai::tests::a_layers_state_keeps_through_the_stroke_style_written_in_it`; scores unchanged | @@ -428,9 +432,9 @@ Stop opacity on translated artboards remains outside this regression coverage. | 2026-10-30 | minor | `.ai` import: a PDF part counts as an empty page only when nothing on it was skipped (an image that can't be decoded draws nothing too), so such a page keeps its notes and is checked against the editing data as before; with `textAs: "outlines"`, a file read from its editing data alone (an empty page, or the text-only placeholder of #1387) has its type outlined. Tests in `tests_aiimport.rs`; scores and hours unchanged | | 2026-10-30 | minor | `.ai` import: a file whose PDF part is an empty page opens from its editing data where that reads, instead of opening blank; scores unchanged | | 2026-10-30 | minor | `.ai` import: a file whose PDF part's pages are cropped to its art rather than its artboards no longer comes in as that PDF part (each page a copy of all the art) but from its layers, with its artboards, where they draw as the page does; own-fixture test in `tests_aiimport`; 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-29 | minor | `.ai` and Illustrator EPS import: the editing data is read up to an eighth of installed memory, from 512 MiB to 2 GiB on 64-bit builds (it was 512 MiB on every machine), so a layered file whose embedded rasters unpack past 512 MiB keeps its layers, groups and text on a machine with 8 GiB or more instead of opening as its page or PDF part (#3510, #4628). Measured: 1.1 GB of editing data peaked 1.1 GB above the start; data past the limit is still refused (a 2 MB file unpacking to 2.2 GB stops in under a second). 32-bit and web builds keep 256 MiB. Own-fixture tests `import::ai::tests::editing_data_past_512_mib_is_read_where_memory_allows`, `the_editing_data_limit_follows_installed_memory`; scores and hours unchanged | | 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-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 | @@ -456,15 +460,15 @@ Stop opacity on translated artboards remains outside this regression coverage. | 2026-10-16 | minor | SVG: CSS classes on imported elements (`class="hero"`) are kept on the object and written back on export, beside the generated `cls-N` classes (#2414); scores unchanged (estimated) | | 2026-10-14 | minor | Legacy `.ai` of version 7 runs as a PostScript program: its prolog is a procedure set of VectorCraft's own that gives each operator the writer uses its meaning (paths, colours, compound paths, clipping groups, layers that don't print, images; gradients flat, type not set), and the page trailer's `annotatepage` (§3 of the specification) marks nothing; every file the writer's tests write is run through VectorCraft's PostScript reader and must draw what the document it was written from draws (#868) | | 2026-10-14 | minor | SVG import: `:not()` selectors (#1799, #1557), stacked descendant selectors over deep nesting no longer stall (#1834, #1703), linked files within the automation roots again (#1870); DXF export writes the VPORT view direction once (#1789); nesting capped at 32 objects with self-using symbols and patterns opened without the offending instance (#1852); deep and self-referencing PDF and `.ai` data (#1871), decompression bombs (#1873), self-drawing brushes (#1872), hostile EPS, Affinity and native data (#1863) and huge ids (#1862) handled; a failed save is reported in a dialog and the old file is kept (#1865, #1685, #1693, #1694); type inside symbols gets its layout bounds back on opening a native file (#1833, #1738). Scores unchanged (estimated) | -| 2026-10-13 | minor | Pinned PngSuite HTTPS files verified by SHA-256 before publication; bounded downloads replace archive extraction (#1729). Scores and hours unchanged. | | 2026-10-14 | minor | EPS import: a stitched shading keeps its hard colour changes (#1654, #1653); WMF/EMF import refuses an oversized embedded PNG or JPEG from its header, before any pixels are decoded (#1776); PDF open no longer takes exponential time on a crafted name tree with shared nodes when it looks for the editing data (#1781); The size limits on a native file's embedded data also apply to files read from memory, not only streamed ones (#1783); Saving over a private file on Unix no longer leaves a readable temporary copy beside it while the file is written (#1778); An Export for Screens suffix can no longer place files outside the export folder (#1777); Package refuses a folder where it would overwrite the open document or one of its linked files (#1786); ruzstd 0.9.1 fixes a decompression bomb (RUSTSEC-2026-0344) reachable from EPS/`.ai` and Affinity import (#1807); File Info dates are stamped only after a save succeeds, so a failed save no longer changes them (#1774); Linked files that change during a drag are updated after the drag instead of being missed (#1773); scores unchanged | +| 2026-10-13 | minor | Pinned PngSuite HTTPS files verified by SHA-256 before publication; bounded downloads replace archive extraction (#1729). Scores and hours unchanged. | | 2026-10-13 | minor | SVGs that move objects with a CSS transform (`translate(100px, 0)`, `rotate(45deg)`) or style type with the `font` shorthand import as drawn (#1565); SVG symbols whose shapes use percentage lengths import at the size and place they are drawn, the percentages being of the symbol's `viewBox` (#1596); Inkscape's flowed text (`flowRoot`) opens as area type in its rectangle, a paragraph per `flowPara`, instead of being dropped, and a region that can't be read is left out with a warning (#1569, fixes #1568); The warning for an unsupported SVG filter names the filter, its primitives and the object, and says the object is drawn without it (#1599, fixes #1598); The pages of an Inkscape multi-page SVG become artboards, named by their labels (#1603, fixes #1601); SVG export with CSS classes or style entities stays fast in documents with many distinct styles (#1567); PDF and `.ai` import: evenly letter-spaced or tightened type on a path keeps its length along the path instead of closing up to the font's advances (#1586), and type on a path whose stroke is written glyph by glyph comes in as one text object with its stroke, not one object per glyph (#1585); The ICC profiles embedded in JPEG, TIFF and PSD exports have every tag on a 4-byte boundary (ICC.1 §7.1.2), so strict readers accept them (#1589, fixes #1584); GIF, PNG-8 and other indexed exports find each pixel's palette colour through an index instead of a scan, with identical output (#1576); Saving to a network drive on macOS falls back to a plain fsync when the share refuses a full flush (#1625, part of #1620; not confirmed on a real share), and a save still succeeds when a folder refuses the temporary file, the rename over the document or copying permissions: the content is written elsewhere first and copied in, and a failed copy keeps the temporary file and names it (#1649, part of #1620); A document opened by a relative path finds its relatively linked images, and `vectorcraft-cli convert` lists what reading the input left out as `importWarnings` (#1606, fixes #1604); A zero-length subpath draws its round cap as a dot and its projecting cap as a square, so dots in imported `.ai` and SVG files show (#1619, fixes #1613); scores unchanged (estimated) | | 2026-10-13 | minor | PDF.R1: editable PDF import uses the shared pinned reader; layer and overprint scans preserve state across content windows and reject known incomplete input. Synthetic streaming, decoder-limit and dependency-source regressions added; scores and hours unchanged. | | 2026-10-13 | minor | PDF.W1: PDF export and print composition use the shared guarded writer; drawing, colour policy and editable import stay app-owned. Metadata regression coverage and immutable dependency checks added; scores and hours unchanged. | | 2026-10-13 | minor | Native documents with a lot of path data (traced or generated art, in layers, symbols or patterns) are saved as compact JSON, about a fifth of the size; small documents stay pretty-printed (#1492); Paste takes a PDF or EPS another app copied as editable vector art: from the macOS pasteboard (with SVG), EPS on Windows beside PDF, and `.eps` files copied in a file manager, with the new command `clipboard.importEps` (#1538, fixes #1499); Opening or placing a file with an image too large to decode says so, and the Links panel shows it on the row (#1540, part of #1479); a multi-artboard `.ai` with notes beside its artboards opens from its editing data, a page's art past its artboard no longer lying over the page before it (#1528); a `.ai` gradient whose stops are listed upwards keeps each midpoint on the stop that starts its part of the ramp (#1530); SVG text on a path follows basic shapes (circle, ellipse, rect, line, polyline, polygon) as their SVG 2 equivalent paths (#1531, fixes #1527); PDFs whose alpha soft masks are drawn as images import with the mask instead of empty art (#1551); EPS and PostScript type set in Type 42 (TrueType) fonts imports at its size instead of 1000 times too large (#1553); PDF and EPS import keep a clipping path whose bounding box covers the page but whose shape doesn't (#1544, fixes #1542) (the `.ai` row for #1528 was written by that PR); scores unchanged (estimated) | | 2026-10-13 | minor | `.ai` read as its PDF part: a page whose art reaches past its artboard to the left (notes beside a design) moves right in the row until that art clears the page before it, so it is no longer taken for that page's art; a multi-artboard `.ai` with such notes opened as its PDF part ("the art on them differs from the page's"), without the art outside its artboards, and now opens from its editing data | -| 2026-10-12 | minor | Legacy `.ai` of version 7 (Save As › `ai` › Version: Legacy (version 7), #868): written from the published specification, keeping layers, groups, compound paths, clipping groups, gradients and images, one artboard per file chosen in an artboard picker; type as editable text objects (point, area and on a path, fonts by PostScript name, Windows-1252), or outlines (Text: Outlines); hidden objects, which the format can't hide, in a hidden layer of their own, omitted or made visible (Hidden Objects); images with see-through pixels flattened with the art under them or omitted (Transparent Images). The `.ai` reader reads legacy type (`To` … `TO`) and `Ar`. `vectorcraft-cli convert` takes `--ai-version 7`, `--hidden-objects` and `--transparent-images`; scores unchanged (estimated) | | 2026-10-13 | minor | TIFF: grayscale files with an alpha channel or other extra channels place, relink and open, read as CMYK files with alpha already were (8 and 16 bits, either polarity; #1489, part of #1466); the rows for #1434 (strokes kept apart from a faded or masked fill, a `.ai`'s RGB numbers) and #1435 (SVG knockout groups without masks, `--decimals`) were written by those PRs; scores unchanged (estimated) | +| 2026-10-12 | minor | Legacy `.ai` of version 7 (Save As › `ai` › Version: Legacy (version 7), #868): written from the published specification, keeping layers, groups, compound paths, clipping groups, gradients and images, one artboard per file chosen in an artboard picker; type as editable text objects (point, area and on a path, fonts by PostScript name, Windows-1252), or outlines (Text: Outlines); hidden objects, which the format can't hide, in a hidden layer of their own, omitted or made visible (Hidden Objects); images with see-through pixels flattened with the art under them or omitted (Transparent Images). The `.ai` reader reads legacy type (`To` … `TO`) and `Ar`. `vectorcraft-cli convert` takes `--ai-version 7`, `--hidden-objects` and `--transparent-images`; scores unchanged (estimated) | | 2026-10-12 | minor | TIFF is available in Export for Screens and Asset Export with shared format settings; scores and hours unchanged | | 2026-10-12 | minor | Affinity import: a text frame too short for its first line (a 64 pt title in a 54 pt frame) is made taller from its top, with a warning in the open result, instead of showing nothing (#1423), as `.ai` area type is (#1392); the spacing Affinity itself fits the line with isn't read; scores unchanged | | 2026-10-12 | minor | `.ai` editing data: a compound path takes the paint, opacity, blend mode and fill rule of its last path (the last painted one when that has none), so gradient-filled compound paths no longer come in black, and paths grouped inside a compound path stay its paths (#1426, fixes #1410); hand-written editing data test; scores unchanged | diff --git a/docs/gaps.md b/docs/gaps.md index 97e7bd1705..ae53828640 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -34,29 +34,29 @@ Artboard Options forwards the active Artboard tool's Scale Artwork with Artboard | Date | Change | Summary | |---|---|---| -| 2026-11-03 | minor | A double-click on a ruler adds a guide at the pointer (#5239): a vertical one on the top ruler, a horizontal one on the left ruler; scores and estimates unchanged. | | 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-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-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. | +| 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 | G15: long open strokes with an undashed width profile are outlined near the view while keeping profile positions on the full path; a 200,000-segment outline took 59.95 ms before and 8.44 ms after in local median runs. Closed and dashed profile strokes remain whole; estimate unchanged. | | 2026-11-05 | minor | G15: Direct Selection reuses one document hit result when choosing the cursor over a path; a 200,000-anchor path's segment cursor took 58.15 ms before and 29.33 ms after in local median runs. Cursor behavior is unchanged; estimate unchanged. | | 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. | | 2026-11-05 | minor | G12: Hyphenation › Default Language sets the language of new type unless Document Setup chose one, stored as a tag; each run hyphenates in its own language, English only so far, so 2 Preferences options stay unused (#394) | | 2026-11-05 | minor | Compound Shapes add-on (beyond Illustrator): Object › Plug-ins › Compound Shapes lists Make, Release and Expand Compound Shape, and holds a setting, off by default, that makes the Pathfinder panel's Shape Mode buttons make a compound shape on a click and give the permanent result on Alt-click (the default is the reverse). Selected members of a compound shape always take the clicked mode. The setting is `window.pathfinder.compoundShapes`; no existing behavior changes while it is off. 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-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. | +| 2026-11-03 | minor | A double-click on a ruler adds a guide at the pointer (#5239): a vertical one on the top ruler, a horizontal one on the left ruler; 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-03 | minor | New gradients use CMYK default stops in CMYK documents, including Gradient tool creation. Scores and estimates unchanged. | | 2026-11-03 | minor | Help Bar has its own Window menu toggle and workspace visibility setting. 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-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 | 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. | @@ -337,7 +337,7 @@ macOS cursor delivery (#2292) is narrowed to a native host adapter using the exi selected with Direct Selection (#1839, #1824); Snap to Grid wins over Snap to Pixel (#1840, #1816 unconfirmed); Redo during a drag keeps the drag (#1856, #1719); a template layer dims its images by Dim Images instead of fading the whole layer (#1864, #1683); a failed save or export is reported in a dialog that names the file and stays until - closed, the document left modified (#1865, #1685). + closed, the document left modified (#1865, #1685). Ordinary File > Save now offers Save As for terminal permission/read-only refusals (#6146), unless another picker is open; recovery errors retain the dialog. Regression execution pending; G11 estimate unchanged. - **Done:** the Shape Builder offers the empty areas closed shapes enclose as regions, such as the middle of four circles that touch, and merges them with the regions around them (#1691). - **Done (#2523):** Shape Builder Gap Detection shares Live Paint's spatially indexed gap finder, @@ -864,11 +864,12 @@ trap, and the defaults (0.25 pt, 100%, 40%) are VectorCraft's own, the pages giv | 2026-11-07 | minor | G4: the Glyphs panel's Alternates for Current Selection applies the OpenType feature that draws a variant to that letter alone (`salt=2`; the style commands take `tag=N`; runs that differ only in their features shape as one piece of text), so an Arabic word stays joined, and Show › Entire Font lists the unencoded glyphs (#5514). Estimate unchanged. | | 2026-11-03 | minor | An imported document's own title (an SVG's root `<title>`, a PDF's XMP `dc:title` else its Info `Title`) goes into File Info only; the document's name stays the file's name (#3626); no scored gap or estimate reassessed. | | 2026-11-11 | minor | Eyedropper copies effective character dimensions between transformed text objects (#6404); scores and estimates unchanged. | +| 2026-11-11 | minor | Ordinary Save to a location refusing writes offers Save As (#6146); cancellation preserves the original path and modified state; automation receives READ_ONLY_LOCATION. Scores and estimates unchanged. Validation pending. | | 2026-11-11 | minor | G5: built-in Energy Beams brush library (Laser Bolt, Plasma Arc, Charge Wave, Spark Trail); Missing trimmed; estimate 5–15 → 4–14 h; MCP Brush libraries list Energy Beams. | | 2026-11-11 | minor | G5: built-in Skill Icons symbol library (Attack, Defend, Heal, Buff, Ult); Missing trimmed; estimate 6–16 → 5–15 h; MCP Symbol libraries list Skill Icons. | -| 2026-11-10 | minor | G6: MCP catalogue (#6460): headless lists `UI_COMMANDS` (`ui: true`), eight prompts, effect vs command ids, 33 tools, commands/reference; estimate unchanged. | | 2026-11-11 | minor | G5: built-in Status Effects graphic style library (Poison Tint, Freeze Edge, Burn Aura, Stun Pulse, Stealth Fade, Shield Buff); Missing trimmed; estimate 7–17 → 6–16 h; MCP docs list UI Frames and Map Markers under Symbol libraries. | | 2026-11-11 | minor | G5: built-in Inventory Icons symbol library (Sword, Shield, Potion, Key, Gem; no default 9-slice); Missing trimmed; estimate 8–18 → 7–17 h. | +| 2026-11-10 | minor | G6: MCP catalogue (#6460): headless lists `UI_COMMANDS` (`ui: true`), eight prompts, effect vs command ids, 33 tools, commands/reference; estimate unchanged. | | 2026-11-10 | minor | G4 type: Edit › Edit Custom Dictionary… adds, deletes and changes the custom dictionary's words, #4936; the Check Spelling dialog remains; scores and estimates unchanged. | | 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. | @@ -895,16 +896,17 @@ trap, and the defaults (0.25 pt, 100%, 40%) are VectorCraft's own, the pages giv | 2026-11-05 | minor | G15: a document's first frame renders on the render worker (#4155); the longest UI-thread stall opening that file went from 8.7 s to 64 ms. Estimate unchanged. | | 2026-11-05 | minor | G15: a reduced image is shrunk before it is separated into ink planes (#4155); first frame 9.2 s → 1.6 s, sharp frame after a pan 3.9 s → 8 ms on that file. Estimate unchanged. | | 2026-11-05 | minor | G7: document tabs are dragged along the tab strip to reorder them (`document.move`, #5663). Estimate unchanged. | +| 2026-11-03 | minor | Symbol editing shows draggable 9-slice guides within the artwork (#5280). Guides outside the artwork remain unsupported. The G5 estimate remains 14–24 h. | | 2026-11-02 | minor | G15: the renderer keeps a compound path's assembled path between frames and drops unused geometry entries after a few frames however few there are. Estimate unchanged. | | 2026-11-02 | minor | G15: hit testing culls paths and compound paths by their control points' box and tests a compound path's outline and fill against only its members near the click; the same objects are hit. Estimate unchanged. | -| 2026-11-03 | minor | Symbol editing shows draggable 9-slice guides within the artwork (#5280). Guides outside the artwork remain unsupported. The G5 estimate remains 14–24 h. | -| 2026-10-31 | minor | G12: Units › East Asian Type acts (#394): Q and Ha (a quarter of a millimetre each) are units that preference offers; 4 Preferences options remain stored but not acted on. Estimate unchanged. | | 2026-11-01 | minor | G5: dynamic symbols (#4938): Symbol Options › Symbol Type, per-instance fill, stroke, opacity and blend for objects of a dynamic symbol's art that stay linked (redefining keeps them, Break Link bakes them, Reset and Static drop them; `symbol.instance.setAppearance`, `symbol.instance.reset`; `crates/engine/src/tests/dynamic_symbols.rs`); on-canvas Direct Selection of the parts and `.ai` round trip remain. Estimate 15–26 → 14–24 h. | | 2026-11-01 | minor | G13: Artboard background colors in output (#4937): raster exports (a transparent background too), PDF, SVG and EPS pages, `.ai` files' PDF pages, Export for Screens and print (and its preview) draw each artboard's color behind its art, inside the artboard and in the color's own model; Export for Screens has Include Artboard Fill beside Include Bleed (on by default, remembered; `includeArtboardFill`, also on `document.export` and `file.print`); editing data keeps the color as the artboard's, and Export for Screens' document rows keep it too; estimate unchanged. | | 2026-11-01 | minor | G5: Select › Start Global Edit on a group takes the groups like it (the same objects at the same places, moved or scaled; Appearance, Size and the artboard options apply) and gives an edit made to an object inside one to the object at its place in each of the others, in proportion, as one undo step (#4939); rotated copies and Shift-click exclusion remain. Estimate unchanged. | +| 2026-10-31 | minor | G12: Units › East Asian Type acts (#394): Q and Ha (a quarter of a millimetre each) are units that preference offers; 4 Preferences options remain stored but not acted on. Estimate unchanged. | | 2026-10-31 | minor | G4: PDF import finds an installed font whose PDF name holds hyphens (`Noto-Sans-Bold` is Noto Sans, Bold: the longest part before a hyphen that names an installed family) and a composite font by its CIDFont's name rather than its base name, which ends in the CMap's (`Name-Identity-H`, [ISO 32000-1 §9.7.6.1](https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf)); a missing font is split only before a style part (`Helvetica-BoldOblique`), so it is reported as `MS PGothic`, not `MS`. Estimate unchanged. | | 2026-10-31 | minor | G13: Direct Selection snaps the end of an existing straight line, dragged by its end anchor, square to straight paths and grazing curves (Smart Guides › Geometric Guides: Snap to Perpendicular and Snap to Tangent), the case the public Snap to Perpendicular and Snap to Tangent pages add to drawing with the Line Segment and Pen tools (#2530). Scores and estimates unchanged. | | 2026-10-31 | minor | G8: a gradient mesh read from a file is budgeted: at most about a million quads per tessellation, and a mesh with more patches than that is malformed (#3592); estimate unchanged. | +| 2026-10-31 | minor | G1: Grid Repeat Options has a Preview checkbox (#2651); estimate unchanged. | | 2026-10-30 | minor | G9: `.ai`/EPS editing data: a blend takes the transparency (opacity, blend mode, knockout) in its state block after its record, so its steps knock each other out as the page draws them; a user's file comes in 0.6% from its page instead of 1.3%. Scores and estimates unchanged. | | 2026-10-30 | minor | G1: Object › Path › Average & Join… (`path.averageJoin`, Cmd+Alt+Shift+J) moves two open path ends to their midpoint and joins them there, the joined anchor a corner or a smooth point as its dialog's Points choice asks, in one undo step; the "Average and Join in one step" entry is done. Estimate unchanged | | 2026-10-30 | minor | G12: Preferences audit (#394): 5 options are stored but not acted on (Open Documents As Tabs, Units › East Asian Type, Hyphenation › Default Language, Scratch Disks, Legacy vector clipboard), each waiting on a subsystem; Highlight Alternate Glyphs, Real-time Drawing and the Devices options act. Estimate unchanged. | @@ -912,16 +914,19 @@ trap, and the defaults (0.25 pt, 100%, 40%) are VectorCraft's own, the pages giv | 2026-10-29 | minor | G10: the Linux Flatpak on GNOME stops the clipboard retry storm (retried with a growing wait, logged once). Estimate unchanged. | | 2026-10-29 | minor | G12: 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. No entry closed, estimates unchanged. | | 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-28 | minor | Flare ray rotation follows vertical travel at one degree per document point, independent of pointer event count. Shift freezes the current orientation; Space also freezes rotation on the first update. This is the chosen application interaction rate. Scores and estimates unchanged. | +| 2026-10-28 | minor | Drop Shadow Darkness subtracts the normalized amount from RGB source channels or adds it to CMYK/Gray black before profile conversion and blur; near-100% becomes gray black, with shadow opacity separate. SVG keeps live filters for uniform sRGB sources and renders shadows for other RGB profiles, source models and gradients. Gradient-stop adjustment is an application rule; spot/custom colors and composite-source parity remain open. Scores and estimates unchanged. | +| 2026-10-28 | minor | Blob Brush Shift locks after clear axial movement, keeps earlier samples fixed, and waits without an off-axis trail. Activation uses raw press displacement in logical screen pixels, with exact quarter-turn boundaries. Explicit application rule; scores and estimates unchanged. | +| 2026-10-28 | minor | Flare Tool Options opened by a canvas click previews its settings at that click. Reset refreshes the enabled preview while keeping the dialog open. Cancel discards preview art and tentative options; OK accepts one flare in one Undo step. Editing existing flare artwork remains outside this dialog scope. Scores and 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-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. | | 2026-10-25 | minor | Floating panels open in OS windows of their own on the desktop (inside the window on the web and on Wayland); saved layouts from earlier versions convert to the shared docking layout (#4135); scores and estimates unchanged (estimated) | -| 2026-10-24 | minor | Recolor Artwork › Assign: double-clicking a row's new color opens the Color Picker over the dialog; its OK sets the row's new color in the model it was picked in (a CMYK mix stays CMYK, RGB becomes CMYK in a CMYK document), Cancel leaves the row as it was. A Lab new color is kept, but a Lab color can't be typed yet: the picker's Lab fields work in RGB, as for fill and stroke (part of #4028; Lab entry, swatch libraries and the row menu remain). Scores and estimates unchanged. | | 2026-10-25 | minor | G12: Pathfinder panel › Pathfinder Options… (Precision, Remove Redundant Points, Divide and Outline Will Remove Unpainted Artwork), for #3426; no entry closed, estimate unchanged | | 2026-10-25 | minor | G12: Artboards panel › Delete Empty Artboards (`artboard.deleteEmpty`); no entry closed, estimate unchanged | | 2026-10-25 | minor | G12: the Transform panel menu's Transform Object Only / Pattern Only / Both (`transformPanelPatterns`) choose what the Transform panel and the Properties panel's Transform section transform (until a choice, General › Transform Pattern Tiles as before), and Use Registration Point for Symbol (`useRegistrationPointForSymbol`) measures and transforms one selected symbol instance from its registration point; estimate unchanged | +| 2026-10-24 | minor | Recolor Artwork › Assign: double-clicking a row's new color opens the Color Picker over the dialog; its OK sets the row's new color in the model it was picked in (a CMYK mix stays CMYK, RGB becomes CMYK in a CMYK document), Cancel leaves the row as it was. A Lab new color is kept, but a Lab color can't be typed yet: the picker's Lab fields work in RGB, as for fill and stroke (part of #4028; Lab entry, swatch libraries and the row menu remain). Scores and estimates unchanged. | | 2026-10-24 | minor | G1: Live Paint Make, Merge and the Live Paint Bucket refuse type, images and other objects that aren't paths, changing nothing, instead of hiding them among the group's sources; Properties shows Merge Live Paint only for a selection it can take. No entry closed, estimates unchanged. | | 2026-10-24 | minor | G1: Properties: Blend Options and Merge Live Paint in Quick Actions, the Width Profile list and an applied effect's Delete icon in Appearance, and the kind and radius of corners selected with Direct Selection, as the public Properties panel overview lists them. No entry closed, estimates unchanged. | | 2026-10-24 | minor | Docs catch-up: G11's three Missing lines (left by merges, naming fixed bugs) become one: SVG units reverting to points (#864) and a Transform effect in a fill's or stroke's own effect list (#885) remain; Shape Builder regions (#937, closed), PDF text placement (#722), the bezier drag preview (#834) and interior anchor delete are done. G2's pre-#2883 "the Effect Gallery dialog" Missing and 4–6 h estimate lines and a duplicate Done line removed. Estimates unchanged. | @@ -948,17 +953,15 @@ trap, and the defaults (0.25 pt, 100%, 40%) are VectorCraft's own, the pages giv | 2026-10-23 | minor | G4: OpenType SVG colour fonts draw and export as vector art; colour fonts are done but for images, scripts and text inside SVG glyph documents and palette variables; estimate unchanged. | | 2026-10-23 | minor | G12: General › Constrain Angle acts for the arrow keys, Shift drags (selection tools, Free Transform, slices, Reshape, Measure, Pen handles) and new rectangles, ellipses, grids, type, polygons, stars and size-dialog shapes (#394); no entry closed, estimate unchanged | | 2026-10-23 | minor | G4: Glyph Scaling leaves hung punctuation at its width and Standard burasagari counts a hung line start as room (`crates/text/src/tests/hang.rs`). No entry closed, estimates unchanged. | -| 2026-10-20 | minor | G4: a paragraph's Auto Leading also drives the Character panel's Leading, the leading nudge and Fill with Placeholder Text; estimate unchanged | -| 2026-10-22 | minor | G11: #834 done (a tool drag's preview could miss the position where the pointer came back into the window and the move in the release's frame, so the release committed a stale handle or anchor); estimate unchanged | | 2026-10-23 | minor | G1: a path moved by a press on its stroke or beside its edge (Selection, Group Selection, Direct Selection), and a straight segment Direct Selection drags, snap by the nearest point of the path, so the path lands exactly on a ruler guide with "align" instead of the pressed spot landing there and the path a few pixels beside it (#3701). No entry closed, estimates unchanged. | -| 2026-10-22 | minor | G11: the Transform effect has Transform Objects, Transform Patterns and Scale Strokes & Effects, its checkboxes visible (#885); canvas, export and Expand Appearance agree. A Transform in a fill's or stroke's own effects ignores the two options. Estimate unchanged. | | 2026-10-23 | minor | G4: COLR colour fonts (v0 and v1) draw and export as vector art; paints documents lack (sweep gradients, Porter-Duff compositing, radial gradients from a circle, repeating gradients) draw the glyph as an image; OpenType SVG fonts remain; estimate unchanged. | -| 2026-10-18 | minor | G11: Shape Builder on art of thousands of segments (traced outlines, type outlines): lines cut it again past 4,096 segments, where the planar map used to give up and the tool offered whole shapes (#937's reproduced case; the planar map's edge-source and face lookups were quadratic in the segments, now indexed, and the cap is 65,536); estimate unchanged until confirmed on the reporter's file | | 2026-10-23 | minor | Fix #3705 in part: drawing modes apply to pastes (Draw Inside puts Paste, Paste in Place and Paste on All Artboards inside the object drawn into; Draw Behind puts Paste in Place and Paste on All Artboards behind the selection), and Shift+D goes from Draw Behind back to Draw Normal when Draw Inside can't draw into the selection instead of stopping at Draw Behind; scores and estimates unchanged. | | 2026-10-23 | minor | G7: Artboard Options' Show Video Safe Areas (action-safe 3.5% and title-safe 5% in from each edge, EBU R 95, canvas only) and Video Ruler Pixel Aspect Ratio (`artboard.setProps`/`artboard.new` `showVideoSafeAreas`, `videoRulerPixelAspectRatio`), and View › Rulers › Show Video Rulers (`view.videoRulers`), which counts the active artboard in those pixels (#2849); video rulers leave G7's missing list; estimates unchanged. | | 2026-10-23 | minor | Artboard Options: Show Center Mark and Show Cross Hairs (`artboard.setProps`/`artboard.new` `showCenterMark`, `showCrossHairs`) draw a center cross and lines from the middle of each side through the center on the canvas only, in one undo step; video safe areas, video rulers and the pixel aspect ratio remain missing; scores and estimates unchanged. | | 2026-10-23 | minor | G1: Simplify of part of a path: with anchors or segments of a path direct-selected, Object › Path › Simplify (slider, Auto-Simplify and dialog) refits only each run of selected segments between its end anchors and leaves the rest of the path exactly as it was; estimates unchanged | | 2026-10-23 | minor | G11: Simplify: Object › Path › Simplify opens its slider at the Auto-Simplify level for the selection, previewing that result, instead of at 1 pt; partial-path simplify remains missing; estimates unchanged | +| 2026-10-22 | minor | G11: #834 done (a tool drag's preview could miss the position where the pointer came back into the window and the move in the release's frame, so the release committed a stale handle or anchor); estimate unchanged | +| 2026-10-22 | minor | G11: the Transform effect has Transform Objects, Transform Patterns and Scale Strokes & Effects, its checkboxes visible (#885); canvas, export and Expand Appearance agree. A Transform in a fill's or stroke's own effects ignores the two options. Estimate unchanged. | | 2026-10-22 | minor | G8: stacked geometry effects make at most 2 million anchors of a path and about 8 million lines to draw: Roughen, Zig Zag, Free Distort, Twist, the warps and Transform build within it, and an effect that would still pass it is left out (#3578). No entry closed, estimates unchanged. | | 2026-10-22 | minor | G13: gradient Method (30.0), Classic or Perceptual, in the Gradient panel, on the canvas, in raster exports and as images in SVG and PDF exports (with Dither, a Perceptual fill takes its noise); G13 estimate 14–24 h → 13–23 h. | | 2026-10-22 | minor | G8: SVG images in SVG images share the depth cap (any data URL type usvg reads as SVG) and SVGZ images one 512 MB unpacking cap; past either, an image is left out with a warning (#3562, #3563). No entry closed, estimates unchanged. | @@ -976,6 +979,7 @@ trap, and the defaults (0.25 pt, 100%, 40%) are VectorCraft's own, the pages giv | 2026-10-21 | minor | Tangent and straight-segment perpendicular snapping use saved Smart Guides Geometric Guides preferences and Control bar quick access (#2537); no View submenu. Scores and estimates unchanged. | | 2026-10-21 | minor | 3D: original Standard, Diffuse, Top Left and Right lighting presets; command access, effective-setting recognition and preview/history; scores and estimates unchanged | | 2026-10-21 | minor | G1: one selected clip group's Properties panel has Edit Clipping Path and Edit Contents at its top. No entry closed, estimates unchanged. | +| 2026-10-20 | minor | G4: a paragraph's Auto Leading also drives the Character panel's Leading, the leading nudge and Fill with Placeholder Text; estimate unchanged | | 2026-10-20 | minor | G1: for the selection tools, a press within the selection tolerance of a clipping path's outline (a clip group's, or a layer's clipping mask) hits the clipping path before the art it clips, so Direct Selection takes its segments and the Selection tool the clip group even where the art lies over the mask's edge; Select Behind lists the same; the eyedropper and the tools that act on what is drawn are unchanged. Doc and tool regression tests. No entry closed, estimates unchanged. | | 2026-10-20 | minor | Polygon and Star exact-size placement remembers the chosen count for later drags and dialog openings; Cancel preserves the previous count; existing opening gestures retained; scores and hours unchanged. | | 2026-10-20 | minor | Live trace result bounds and selection highlights exclude the retained bitmap except in source-image views; scores and hours unchanged. | @@ -989,14 +993,16 @@ trap, and the defaults (0.25 pt, 100%, 40%) are VectorCraft's own, the pages giv | 2026-10-19 | minor | Cached canvas dimensions match the renderer limit before cache-key creation, view construction and worker submission. Regression coverage; scores and estimates unchanged. | | 2026-10-19 | minor | Cached canvas reprojection preserves scale during resize, rotation and DPI changes. Contributor original-file acceptance confirmed; scores and estimates unchanged. | | 2026-10-19 | minor | Color Picker layout (#2008): compact controls, centered header, stacked actions and aligned inputs; screen-reader channel values, current shared button names and styling, screen sampler and model-by-model Tab order retained. Narrow windows scroll vertically. Documentation correction: #1692 was completed upstream before #2008; the Direct Selection alignment gap remains open. Scores and hours unchanged. | +| 2026-10-19 | minor | G13: blend easing (#1124): Ease In / Ease Out and separate Color Ease In / Out in Blend Options, saved with the blend; on-canvas handles and easing in the Blend panel remain. Estimate unchanged | +| 2026-10-19 | minor | G13: tangent and perpendicular snapping for lines drawn from an anchor (Pen, Curvature, Line Segment, with Smart Guides on), View › Snapping with every snap option, and the `view.snapping` command (#2530); estimate unchanged | +| 2026-10-18 | minor | G11: Shape Builder on art of thousands of segments (traced outlines, type outlines): lines cut it again past 4,096 segments, where the planar map used to give up and the tool offered whole shapes (#937's reproduced case; the planar map's edge-source and face lookups were quadratic in the segments, now indexed, and the cap is 65,536); estimate unchanged until confirmed on the reporter's file | | 2026-10-18 | minor | G11: Shape Builder Gap Detection (#2523): Shape Builder Tool Options with a Gap Length of Small, Medium, Large or Custom bridges small gaps between open paths for the regions they bound; the other Shape Builder options stay missing. The stale gap line on deleting an interior anchor of an open path is removed (the behaviour was already implemented). Estimates unchanged | | 2026-10-18 | minor | Mirror Repeat shows its axis when selected (#2877): drag the line or center handle to move it, drag an end handle to rotate it (Shift: 45°), Escape cancels and each drag is one undo step. Engine and headless canvas gesture regressions passed; native move, rotation and Escape screenshots reviewed. Formatting, Clippy, WASM and repository checks passed; the undo label reuses translated Repeat Options. (#2877) Scores and hours unchanged. | | 2026-10-18 | minor | Bounding-box handles take priority where a selected Mirror Repeat axis crosses them; a Top-handle regression confirms scaling of the default repeat. Center and end handles retain their move and rotation behavior. G1 scope and estimate unchanged. | -| 2026-10-19 | minor | G13: blend easing (#1124): Ease In / Ease Out and separate Color Ease In / Out in Blend Options, saved with the blend; on-canvas handles and easing in the Blend panel remain. Estimate unchanged | -| 2026-10-19 | minor | G13: tangent and perpendicular snapping for lines drawn from an anchor (Pen, Curvature, Line Segment, with Smart Guides on), View › Snapping with every snap option, and the `view.snapping` command (#2530); estimate unchanged | | 2026-10-17 | minor | G11: #722 fixed (PDF text slid on open where the file spaced its glyphs other than the font's metrics: kerning, word and character spacing, `TJ` adjustments, text rise, narrow vertical glyphs; justified text boxes stayed lines); estimate unchanged | | 2026-10-17 | minor | Blend spine Anchor Point conversions, selection/stacking priority and Shift regression coverage (#2544); scores and hours unchanged. Windows native canvas checks and screenshots reviewed in PR #2643. | | 2026-10-17 | minor | G12: Background Export progress dialog with Cancel and Continue in Background (#2564); no entry closed, estimates unchanged | +| 2026-10-17 | minor | Colored Inflate: shared membrane normals and smooth RGB vector ramps replace flat polygon bands; channel groups are batched per solid-fill surface. White-light Inflate retains its original polygons, paint composition and seam joins; the 0.05 seam minimum is restored. Scores and hour estimates unchanged. | | 2026-10-16 | minor | Native documents restore transparency grid visibility (#1968); save/open regression covers on/off, old files and SVG; scores and hours unchanged (estimated) | | 2026-10-16 | minor | G6: MCP cancellation, progress notifications and a busy answer during long exports (#2165, #2171, #2172); no entry closed, estimates unchanged | | 2026-10-16 | minor | G11: PDF export of a CMYK document reports its RGB colours and non-CMYK images, with a count, in the export warnings (#2162); no entry closed, estimates unchanged | @@ -1007,31 +1013,30 @@ trap, and the defaults (0.25 pt, 100%, 40%) are VectorCraft's own, the pages giv | 2026-10-16 | minor | G2: Effect › Sketch › Bas Relief, Charcoal, Photocopy and Stamp (#2527); measured Photoshop filters 37/57 with Cutout, Neon Glow and Poster Edges, 20 remaining (18–30 h). | | 2026-10-16 | minor | G2: Effect › Sketch › Halftone Pattern (Size, Contrast, Pattern Type Circle, Dot or Line; black ink on white paper, Size in points at any Document Raster Effects resolution); measured Photoshop filters 30/57, 27 remaining (22–38 h). | | 2026-10-16 | minor | G1: Smart Guides line extension for a dragged end of a straight segment, at any angle (#2120); estimate unchanged | -| 2026-10-15 | minor | G1: no busy cursor during long operations, listed; estimate unchanged | -| 2026-10-17 | minor | Colored Inflate: shared membrane normals and smooth RGB vector ramps replace flat polygon bands; channel groups are batched per solid-fill surface. White-light Inflate retains its original polygons, paint composition and seam joins; the 0.05 seam minimum is restored. Scores and hour estimates unchanged. | | 2026-10-16 | minor | 3D lighting: one to ten directional lights, a selectable light list, Add Light and Delete Light, independent settings and artboard gestures; ambient applied once and smooth RGB vector export; scores and hour estimates unchanged | | 2026-10-16 | minor | 3D lighting: opaque RGB Color for the shared directional light, with neutral ambient, preview/reset controls and smooth vector Revolve shading; scores and hour estimates unchanged | +| 2026-10-16 | minor | G13: Object › Create Gradient Mesh on an embedded image (#1676); a linked image is refused. No entry closed, estimates unchanged | +| 2026-10-15 | minor | G1: no busy cursor during long operations, listed; estimate unchanged | +| 2026-10-15 | minor | G13: Object › Intertwine (#1763): Make, Edit, Release, the Intertwine tool (hover, click, drawn path) and `object.intertwine.arrange` (toggle, front, back). Bring Forward / Send Backward stay missing; adding objects through the Layers panel is untested. No entry closed, estimates unchanged | +| 2026-10-15 | minor | G11: Transform Each Reflect X flips left-right and Reflect Y top-bottom (#1910). Estimates unchanged. | +| 2026-10-15 | minor | Artboard tool: closing a clean active tab restores the displayed board before Properties Delete. Nine targeted checks passed; three original close-route captures reviewed; scores and hours unchanged. | | 2026-10-14 | minor | G16: Brazilian Portuguese (`pt-br`) catalog complete under the tests (#1914); three key languages remain (Hindi, Arabic, Indonesian), 55–90 → 50–85 h | | 2026-10-14 | minor | G6: dataset CSV import/export (`dataset.import` / `dataset.export`); remaining: scripting surface, Batch, graph variables; estimate 6–12 → 4–8 h | | 2026-10-14 | minor | G6: image variables (`VariableKind::Image`, `variable.makeImageDynamic`, path values on `dataset.select` keeping bounds); remaining: scripting surface, Batch, graph variables, dataset import/export; estimate 8–14 → 6–12 h | | 2026-10-14 | minor | Effect > Warp: style, orientation and percent controls; focused checks passed and original captures reviewed; scores and hours unchanged | | 2026-10-14 | minor | G11: open-path anchor Delete removes adjoining segments without bridging the gap; explicit Remove Anchors keeps its reconnect/refit behavior. Scores and hours unchanged. | -| 2026-10-16 | minor | G13: Object › Create Gradient Mesh on an embedded image (#1676); a linked image is refused. No entry closed, estimates unchanged | -| 2026-10-15 | minor | G13: Object › Intertwine (#1763): Make, Edit, Release, the Intertwine tool (hover, click, drawn path) and `object.intertwine.arrange` (toggle, front, back). Bring Forward / Send Backward stay missing; adding objects through the Layers panel is untested. No entry closed, estimates unchanged | | 2026-10-14 | minor | MCP `add_text` exposes the existing `vertical` option for point, area and path type; horizontal remains the default; scores and hours unchanged. | | 2026-10-14 | minor | Vertical fullwidth question and exclamation marks keep their nominal glyph upright when the font has no vertical alternate; scores and hours unchanged. | | 2026-10-14 | minor | G1: Curvature curve geometry (the Curvature tool's curve: 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°), and the ends of an open path curve in, so uneven spacing neither overshoots nor loops; #1236, in part); neighbour re-curving on existing paths remains open; estimates unchanged | -| 2026-10-15 | minor | G11: Transform Each Reflect X flips left-right and Reflect Y top-bottom (#1910). Estimates unchanged. | | 2026-10-14 | minor | Quick Image Trace presets reveal the adjustment panel with Advanced open; Properties reopens it. Sliders keep their preset row until release so the re-trace is committed. UI regressions added; scores and remaining estimates unchanged. | | 2026-10-14 | minor | Revolve: automatic smooth vector shading, default-on Cap with continuous curved rims, and bounded seam joins; angular cut sides remain open. Scores and hour estimates unchanged at this rounding | | 2026-10-14 | minor | No entry closed. G8 and G11 record the later merges of 2026-10-14 (#1852, #1862–#1865, #1871–#1873, #1839, #1840, #1856); #1683 (Dim Images) and #1685 (failed-save dialog) are done by #1864 and #1865. Gaps noted for reporters: G1 Effect › Warp through Warp Options (#1760), Color Picker Tab order (#1692), aligning to a key anchor (#1636; the public Align page does not say how the key anchor is chosen); G7 custom toolbars (#1759), workspace panel widths (#1753); G11 Shape Builder regions between closed shapes (#1691), deleting an interior anchor of an open path; G13 Intertwine (#1763), gradient mesh from an embedded image (#1676), Image Trace radial gradients (draft #1849). Average and Join with a corner or smooth choice was already listed (G1). G12: a line two merges had left twice is there once, without Appearance of Black for output, which acts since #1750. Estimates unchanged | | 2026-10-14 | minor | Existing-guide Escape cancellation: eight app-input regressions, the Direct Selection Escape control and current full CI passed; scores and hours unchanged. | -| 2026-10-15 | minor | Artboard tool: closing a clean active tab restores the displayed board before Properties Delete. Nine targeted checks passed; three original close-route captures reviewed; scores and hours unchanged. | | 2026-10-14 | minor | Artboard tab changes align the tool with the displayed board; same-tab multiselection and Move/Scale Artwork options preserved. Three tab regressions and historical full CI on `a0671777` passed; scores and hours unchanged. | | 2026-10-14 | minor | Width-point finite-arithmetic follow-up: atomicity, history and native round-trip regressions and current full CI passed; scores and hours unchanged. | +| 2026-10-14 | minor | No entry closed. G11: Align and Distribute act on the anchor points selected with Direct Selection: the points of one path align to each other, not to the artboard, and Distribute spaces them (#1641, fixes #1636; aligning to a key anchor is still missing); Delete removes every selected anchor of a closed path (it used to remove the wrong ones), removes objects selected whole along with the selected anchors of others, and keeps the remainder selected so a second Delete removes it (#1674); In isolation mode Direct Selection and Group Selection pick only inside the isolated object (#1666); Escape cancels a Direct Selection drag (#1803, #1743); Escape while typing ends the edit, keeps the type selected and switches to the Selection tool (#1802, #1686, #1656); A filled Pencil path keeps a stroke of None (#1804, part of #1679); In the Pathfinder panel a group selected alone counts its nested shapes, so an expanded stroked object keeps the Shape Modes enabled (#1642, #1635); Object › Expand turns radial, grid and mirror repeats into groups of their instances (#1765, #1756); Select › Same matches type on its characters' fill and stroke (#1746, #1737); Roughen with Smooth keeps turn-backs smooth instead of making cusps that draw as spikes (#1646, #1643); Scribble draws one zig-zag per region, with no lines across holes and notches; convex shapes are unchanged (#1651, #1648). G8: WMF/EMF import refuses an oversized embedded PNG or JPEG from its header, before any pixels are decoded (#1776); PDF open no longer takes exponential time on a crafted name tree with shared nodes when it looks for the editing data (#1781); The size limits on a native file's embedded data also apply to files read from memory, not only streamed ones (#1783); Saving over a private file on Unix no longer leaves a readable temporary copy beside it while the file is written (#1778); An Export for Screens suffix can no longer place files outside the export folder (#1777); Package refuses a folder where it would overwrite the open document or one of its linked files (#1786); ruzstd 0.9.1 fixes a decompression bomb (RUSTSEC-2026-0344) reachable from EPS/`.ai` and Affinity import (#1807); Scatter graph data near the largest number is refused with a message instead of hanging the app (#1780); One very long Liquify stroke segment can no longer create an unbounded number of dabs (#1779); MCP limits: 16 MiB lines, 64-message batches, 256-step `command_batch` with 8 MiB of kept results and 256 MiB control replies (#1764, fixes #1727); MCP requires the JSON-RPC 2.0 envelope and matches control replies by id, and an app refusal without an id reports the app's reason (#1768, fixes #1728). G10, unconfirmed: Tool cursors are painted into the window on Wayland (#1652; not run on Linux, #1645 stays open until confirmed); On Windows the custom title bar's window stays truly borderless, so the pointer lines up with what is drawn on systems where the hidden caption offset it (#1791, #1758; awaiting the reporter's confirmation). A clause repeated in two G11 bullets is there once. Estimates unchanged | | 2026-10-13 | minor | Pinned PngSuite HTTPS files verified by SHA-256 before publication; bounded downloads replace archive extraction (#1729). Scores and hours unchanged. | | 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 | No entry closed. G11: Align and Distribute act on the anchor points selected with Direct Selection: the points of one path align to each other, not to the artboard, and Distribute spaces them (#1641, fixes #1636; aligning to a key anchor is still missing); Delete removes every selected anchor of a closed path (it used to remove the wrong ones), removes objects selected whole along with the selected anchors of others, and keeps the remainder selected so a second Delete removes it (#1674); In isolation mode Direct Selection and Group Selection pick only inside the isolated object (#1666); Escape cancels a Direct Selection drag (#1803, #1743); Escape while typing ends the edit, keeps the type selected and switches to the Selection tool (#1802, #1686, #1656); A filled Pencil path keeps a stroke of None (#1804, part of #1679); In the Pathfinder panel a group selected alone counts its nested shapes, so an expanded stroked object keeps the Shape Modes enabled (#1642, #1635); Object › Expand turns radial, grid and mirror repeats into groups of their instances (#1765, #1756); Select › Same matches type on its characters' fill and stroke (#1746, #1737); Roughen with Smooth keeps turn-backs smooth instead of making cusps that draw as spikes (#1646, #1643); Scribble draws one zig-zag per region, with no lines across holes and notches; convex shapes are unchanged (#1651, #1648). G8: WMF/EMF import refuses an oversized embedded PNG or JPEG from its header, before any pixels are decoded (#1776); PDF open no longer takes exponential time on a crafted name tree with shared nodes when it looks for the editing data (#1781); The size limits on a native file's embedded data also apply to files read from memory, not only streamed ones (#1783); Saving over a private file on Unix no longer leaves a readable temporary copy beside it while the file is written (#1778); An Export for Screens suffix can no longer place files outside the export folder (#1777); Package refuses a folder where it would overwrite the open document or one of its linked files (#1786); ruzstd 0.9.1 fixes a decompression bomb (RUSTSEC-2026-0344) reachable from EPS/`.ai` and Affinity import (#1807); Scatter graph data near the largest number is refused with a message instead of hanging the app (#1780); One very long Liquify stroke segment can no longer create an unbounded number of dabs (#1779); MCP limits: 16 MiB lines, 64-message batches, 256-step `command_batch` with 8 MiB of kept results and 256 MiB control replies (#1764, fixes #1727); MCP requires the JSON-RPC 2.0 envelope and matches control replies by id, and an app refusal without an id reports the app's reason (#1768, fixes #1728). G10, unconfirmed: Tool cursors are painted into the window on Wayland (#1652; not run on Linux, #1645 stays open until confirmed); On Windows the custom title bar's window stays truly borderless, so the pointer lines up with what is drawn on systems where the hidden caption offset it (#1791, #1758; awaiting the reporter's confirmation). A clause repeated in two G11 bullets is there once. Estimates unchanged | | 2026-10-13 | minor | No entry closed. Pathfinder's Trim, Merge and Crop remove the objects' strokes, and Merge joins same-fill objects whatever their strokes were, as the public Pathfinder page describes (#1572, fixes #1570); Warp › Arc (the effect and Envelope › Make with Warp) bends in the object's real proportions: a circular arc with concentric edges and no fold-over at 100%, so Arc on art that isn't square draws differently from before (#1581, fixes #1578); Offset Path with an inset larger than the shapes says the offset leaves nothing, instead of asking to select paths (#1591, fixes #1590); A zero-length subpath draws its round cap as a dot and its projecting cap as a square, so dots in imported `.ai` and SVG files show (#1619, fixes #1613); Area type in a non-rectangular shape moves a word too wide for the top lines down to the first line that holds it instead of breaking it (#1617, fixes #1615); Tool flyouts are as wide as their longest translated name and shortcut need, so names no longer run into shortcuts in Spanish and other languages (#1650, fixes #1616); Saving to a network drive on macOS falls back to a plain fsync when the share refuses a full flush (#1625, part of #1620; not confirmed on a real share), and a save still succeeds when a folder refuses the temporary file, the rename over the document or copying permissions: the content is written elsewhere first and copied in, and a failed copy keeps the temporary file and names it (#1649, part of #1620); Smart Guides: a point drawn with the Pen or a shape tool, and an anchor dragged with Direct Selection, snaps to where paths and ruler guides cross, labelled "intersect" (#1658, part of #1602; a moved selection's own anchors don't); estimates unchanged | | 2026-10-13 | minor | G1/M11: Artboard panel deletion, protected deletion keys and navigator/tool synchronization before and after deletion; history normalization preserves valid multiselection; history regressions and full workspace CI pass; scores and hours unchanged | | 2026-10-13 | minor | G4: mixed paragraph alignment indication implemented; headless regressions, full integration CI and visual review pass; other mixed paragraph fields and estimates unchanged, no gap entry closed | @@ -1039,9 +1044,9 @@ trap, and the defaults (0.25 pt, 100%, 40%) are VectorCraft's own, the pages giv | 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 | G1: Average and Join in one step with a corner or smooth choice is listed as missing (the ui-parity row said done; the command does not exist); Join merges the selected anchor points that lie on one another, as after Average, into one anchor (#1582, fixes #1533); Lock Guides also locks the objects made into guides: they can't be selected, moved or deleted (#1543); they are drawn in the guide colour and style of Preferences, and a dragged selection snaps to ruler guides with Smart Guides on (#1583, fixes #1534); Edit › Keyboard Shortcuts lists Edit › Duplicate and the other commands that are in no menu but run without parameters (panel-menu commands such as Break Link to Symbol), so they can be given shortcuts (#1549, #1580, fixes #1539). G11: Make Compound Path on a group (such as outlined type) keeps the paint of its backmost path instead of drawing nothing, and compound paths joined into one keep their holes (#1537); a round corner next to a curved side curves smoothly instead of sticking out of the shape and the Smooth tool smooths only the stretch of path under the drag (#1547, fixes #1512); Raster effects (blur, shadow, glow, grain) drawn after a pattern, radial or freeform gradient fill export at their place and size (#1541, fixes #1536). G9: a multi-artboard `.ai` with notes beside its artboards opens from its editing data, a page's art past its artboard no longer lying over the page before it (#1528); a `.ai` gradient whose stops are listed upwards keeps each midpoint on the stop that starts its part of the ramp (#1530); SVG text on a path follows basic shapes (circle, ellipse, rect, line, polyline, polygon) as their SVG 2 equivalent paths (#1531, fixes #1527); PDFs whose alpha soft masks are drawn as images import with the mask instead of empty art (#1551); EPS and PostScript type set in Type 42 (TrueType) fonts imports at its size instead of 1000 times too large (#1553); PDF and EPS import keep a clipping path whose bounding box covers the page but whose shape doesn't (#1544, fixes #1542); Paste takes a PDF or EPS another app copied as editable vector art: from the macOS pasteboard (with SVG), EPS on Windows beside PDF, and `.eps` files copied in a file manager, with the new command `clipboard.importEps` (#1538, fixes #1499); Opening or placing a file with an image too large to decode says so, and the Links panel shows it on the row (#1540, part of #1479). Native documents with a lot of path data (traced or generated art, in layers, symbols or patterns) are saved as compact JSON, about a fifth of the size; small documents stay pretty-printed (#1492); the font scan on Linux also reads the folders fontconfig's configuration lists, such as TeX fonts under `/usr/share/texmf/fonts` (#1548, fixes #1529; not verified on Linux); Polish catalog (#1498); estimates unchanged, no gap entry closed | | 2026-10-13 | minor | Interaction: Direct Selection takes the selected object's anchor where anchors of overlapping objects coincide (#1508), sliders in dialogs show their rail (#1483), a template layer stays in Preview in Outline view (#1486), a tab stop in the Tabs panel drags from where the press landed (#1458); type: commands and MCP `add_text` report area type that does not fit its frame (#1471); 3D and Materials on outlined type and other shapes of many small curves (#1481), Twist 0° to 360° (#1524), twisted walls keep their faces (#1525); print-size images draw, export and crop (#1517); `vectorcraft-cli perf` reports load and memory on Windows (#1502; the idle-machine run is still missing); French catalog complete again (#1511); `graph.setData`'s CSV keeps each value exactly (#1485); estimates unchanged, no gap entry closed | +| 2026-10-13 | minor | G1: Split Into Grid has its documented options (#1462), Transform Objects in the Transform dialogs and Transform Each and Reflect across an angle (#1448, #1460), the Pathfinder panel works on a group selected alone (#1450; one plain path alone is not settled), a focused field follows its value (#1457), selected text in value fields is readable (#1465 in part); G9: grayscale TIFFs with an alpha channel or extra channels place (#1466 in part), strokes kept apart from a faded or masked fill on PDF and `.ai` import (#1434); G4: Fit Headline in a frame one line tall (#1444), frames beyond a billion points hold no text (#1451); G6: commands refuse a parameter key they don't take (#1442); G10: a window surface past 8192 px no longer closes the app (#1459 in part); G15: edits and Alt-drags of thousands of objects (#1429 in part); G16: 128 more Czech strings (#1461); estimates unchanged | | 2026-10-12 | minor | Width Point Edit: Shift-double-click enables adjoining adjustment; plain double-click keeps it off; scores and hours unchanged | | 2026-10-12 | minor | G9: Save As `.ai` › Version: Legacy (version 7) writes the PostScript form from the published version 7 specification (#868): layers, groups, compound paths, clipping groups, type as text objects (or outlines), gradients and images, one artboard per file; hidden objects in a hidden layer, omitted or shown; transparent images flattened or omitted; legacy type read on open; it waits for users' testing, so the estimate is unchanged | -| 2026-10-13 | minor | G1: Split Into Grid has its documented options (#1462), Transform Objects in the Transform dialogs and Transform Each and Reflect across an angle (#1448, #1460), the Pathfinder panel works on a group selected alone (#1450; one plain path alone is not settled), a focused field follows its value (#1457), selected text in value fields is readable (#1465 in part); G9: grayscale TIFFs with an alpha channel or extra channels place (#1466 in part), strokes kept apart from a faded or masked fill on PDF and `.ai` import (#1434); G4: Fit Headline in a frame one line tall (#1444), frames beyond a billion points hold no text (#1451); G6: commands refuse a parameter key they don't take (#1442); G10: a window surface past 8192 px no longer closes the app (#1459 in part); G15: edits and Alt-drags of thousands of objects (#1429 in part); G16: 128 more Czech strings (#1461); estimates unchanged | | 2026-10-12 | minor | TIFF is available in Export for Screens and Asset Export with shared format settings; scores and hours unchanged | | 2026-10-12 | minor | Character styles retain explicit Auto leading, propagate resets and preserve local leading overrides; scores and hours unchanged | | 2026-10-12 | minor | G3: Extrude & Bevel, Inflate and Rotate with Position presets ([three-d.md](three-d.md), #1407); materials, lights, shadows, Classic, Turntable and export remain, 45–75 → 25–45 h | @@ -1060,7 +1065,6 @@ trap, and the defaults (0.25 pt, 100%, 40%) are VectorCraft's own, the pages giv | 2026-10-12 | minor | DXF reads control-point SPLINE closure (#1299 follow-up after #1320); open curves stay open and curves retained; rational/non-rational import and export versions regression-tested; scores/hours unchanged | | 2026-10-12 | minor | G16: Vietnamese catalog added (complete under the tests); three key languages remain (Hindi, Arabic, Indonesian), 55–90 h | | 2026-10-12 | minor | G16: Korean catalog added (complete under the tests); four key languages remain, 60–100 h | -| 2026-10-11 | minor | Czech render-site follow-up: file-dialog filters, diagnostics, preset numbering, contributor tooltips/plurals, gradient grammar and contextual printer mark styles; estimates unchanged | | 2026-10-12 | minor | Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged | | 2026-10-12 | minor | Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged | | 2026-10-12 | minor | G11: clear stale component refs after Simplify/Add Anchor Points; preview and point counts give visible feedback (#1272); estimates unchanged | @@ -1076,6 +1080,7 @@ trap, and the defaults (0.25 pt, 100%, 40%) are VectorCraft's own, the pages giv | 2026-10-12 | minor | G1: painted strokes regain the existing default after None/zero-width inheritance (#1253); estimate unchanged | | 2026-10-12 | minor | G11: Divide uses open cutting paths without taking their paint (#1307); implicit filled closure, holes and history regression-tested; estimates unchanged | | 2026-10-12 | minor | G11: numeric fields keep the text-drag start instead of replacing it with select-all on press (#1326, in part); keyboard and click selection regression-tested; estimates unchanged; short movements below the click threshold remain open | +| 2026-10-11 | minor | Czech render-site follow-up: file-dialog filters, diagnostics, preset numbering, contributor tooltips/plurals, gradient grammar and contextual printer mark styles; estimates unchanged | | 2026-10-11 | minor | G15: large-selection edits, cached live effects and faster Pathfinder (#1173); a 25k-path perf run meets every budget; estimate unchanged | | 2026-10-11 | minor | G4: Middle Eastern digit types done (Character panel menu › Digits); estimate unchanged | | 2026-10-11 | minor | G4: proportional vertical metrics (#966) done; estimate unchanged | @@ -1087,10 +1092,6 @@ trap, and the defaults (0.25 pt, 100%, 40%) are VectorCraft's own, the pages giv | 2026-10-10 | minor | G9 evidence counted from the tracker (28 `.ai`/EPS issues, 20 fixed); impact narrowed to handing files back | | 2026-10-10 | minor | Alpha blockers checked against the core-workflow gate: none | | 2026-10-10 | major | Moved from the ROADMAP's "Where we're lacking"; kept ids G1–G8, added G9–G19 (file formats, stability, bugs, preferences, 30.x additions, pen hardware, performance, localization, ecosystem, AI, accessibility) and re-ranked | -| 2026-10-28 | minor | Flare ray rotation follows vertical travel at one degree per document point, independent of pointer event count. Shift freezes the current orientation; Space also freezes rotation on the first update. This is the chosen application interaction rate. Scores and estimates unchanged. | -| 2026-10-28 | minor | Drop Shadow Darkness subtracts the normalized amount from RGB source channels or adds it to CMYK/Gray black before profile conversion and blur; near-100% becomes gray black, with shadow opacity separate. SVG keeps live filters for uniform sRGB sources and renders shadows for other RGB profiles, source models and gradients. Gradient-stop adjustment is an application rule; spot/custom colors and composite-source parity remain open. Scores and estimates unchanged. | -| 2026-10-28 | minor | Blob Brush Shift locks after clear axial movement, keeps earlier samples fixed, and waits without an off-axis trail. Activation uses raw press displacement in logical screen pixels, with exact quarter-turn boundaries. Explicit application rule; scores and estimates unchanged. | -| 2026-10-28 | minor | Flare Tool Options opened by a canvas click previews its settings at that click. Reset refreshes the enabled preview while keeping the dialog open. Cancel discards preview art and tentative options; OK accepts one flare in one Undo step. Editing existing flare artwork remains outside this dialog scope. Scores and estimates unchanged. | > **Tab-drag status (2026-10-22):** updated from upstream, including the Turkish catalog and all three tab-drag translations requested in #3479; Selection-tool reset regression-checked and clipboard evidence scoped to internal state; drag lifecycle, insertion and regression contracts documented; behavior, scores and estimates unchanged. diff --git a/docs/mcp.md b/docs/mcp.md index dd94ebebcc..d1f28cfa1f 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -1,5 +1,7 @@ # VectorCraft MCP server +> **Last updated:** 2026-11-11 · **Change:** Save location-refusal error (#6146); existing response shapes retained. + `vectorcraft-cli mcp` runs a [Model Context Protocol](https://modelcontextprotocol.io) server on stdio (newline-delimited JSON-RPC 2.0; see [Protocol](#protocol) for version negotiation). Agents use it to draw, inspect and look at VectorCraft documents. @@ -541,7 +543,7 @@ applied colors still join the list while it is hidden. | `open_panel` | `{panel}` | Remote only. `panel` is a panel id (`layers`, `swatches`, `colorGuide`, …, as `window.panel` takes) or its display label (`"Color Guide"`), in any case. | | `screenshot` | `{path?, scale?, artboard?, window?, max_side?, traceViews?}` | Returns MCP image content (`image/png`, base64) plus a text block. Renders the artboard with the pixels of a PNG `export` with `background: "white"` at the same scale; `traceViews:true` draws Image Trace objects in the view set with `imageTrace.setView`, as in the app's window; `window:true` captures the app window (remote only), as `ui_screenshot` does, and `max_side` (1–4096, window captures only) scales the returned image down as described for `ui_screenshot`. With `path` the backend (the headless session or the app) writes the PNG there too; a window capture written there keeps its full size with `max_side`. | | `open_file` | `{path, importer?, params?}` | Opens any readable file as a new active document: `.vectorcraft`/`.drawcraft`, `.vctemplate`, `.svg`/`.svgz`, `.pdf`/`.ai`, `.ait`, `.eps`/`.epsf`, `.ps`, `.dxf`, `.emf`, `.wmf`, HP-GL (`.plt`/`.hpgl`/`.hgl`/`.hpg`, and a file of an extension no other format reads that starts with plotter instructions), PNG (`.png`/`.pns`), JPEG, GIF, WebP, TIFF, BMP (`.bmp`/`.dib`/`.rle`), Targa (`.tga`/`.vda`/`.icb`/`.vst`), PCX, Pixar (`.pxr`), JPEG 2000 (`.jpf`/`.jpx`/`.jp2`/`.j2k`/`.j2c`/`.jpc`), HEIC/HEIF (`.heic`/`.heif`), JPEG XL (`.jxl`), PSD (`.psd`/`.pdd`)/PSB (an image opens as a document of its pixel size; a PSD or PSB file with its layers as layers, or as its merged image through `run_command document.open` with `layers: false`; a JPEG XL file as its first frame). Templates (`.vctemplate`, `.ait`) open as a new untitled document. PDF, `.ai` and SVG files saved with Preserve Editing reopen as the document they carry. The reply is `document.open`'s: its `warnings` say what didn't come in as it was (an EPS whose PostScript can't be read opens as its preview, and the warning names the PostScript error, the operator and the procedure). A file of an extension only an installed importer plug-in lists opens with that importer ([Plug-ins](#plug-ins)); `importer: "plugin.<id>"` opens the file with the importer it names, which must list the file's extension, also one a built-in format reads (in the app, as File › Open With does), and `params` gives that importer's parameters (their defaults when omitted). `run_command document.formats` lists the formats. | -| `save_file` | `{path?, compress?, version?, preview?, precision?, options?}` | Runs `document.save`: the document's own file in its own format (native `.vectorcraft` unless it was opened from or saved as SVG, PDF or a restorable `.ai`; then `warnings` say what that format loses). A path's extension picks the format (`.vectorcraft`, `.vctemplate`, `.pdf`, `.svg`, `.svgz`, `.ai`: a PDF carrying the native document, which reopens editable; `.eps`: by default one file whose bounding box fits the visible art, the first artboard when no art shows; `options.useArtboards: true` makes the chosen artboard the page and, with two or more chosen, the file at the path is the central file of the visible art and each artboard is also written to `<name>_<artboard>.eps` beside it, listed in the reply's `files`). For a native file, `compress: true` gzips it (default: the `useCompression` preference, Preferences › File Handling › Use Compression), `version` (1 to 4) writes it for an older VectorCraft (1 and 2 are never compressed, and `version: 2` with `compress: true` is an error), `preview: true` embeds a PNG of the first artboard, and `precision` (0 to 9) rounds path anchors and handles to that many decimal places for that save only (colors, transforms and type stay exact). `options` passes the format's save options as `run_command document.formats` lists them. When `compress`, `version`, `preview` or `precision` is given both at the top level and in `options` with different values, the call is an error (`` `compress: true` and `options.compress: false` disagree: give the option once ``) and nothing is written. A key in `options` that no save format takes is an error, and nothing is written (see Parameter keys). The save drops each option the written format doesn't read. The reply's `warnings` then name each such option given (for example `` `compress` was not used: SVG files don't read it `` for an `.svg` path, and `` `options.preview` was not used: … `` for a key in `options`; a `.ai` file reads `compress` but not `version`, `preview` or `precision`). For a `.ai` file, these warnings name the kind of `.ai` written: a legacy .ai (version 7) for `aiVersion: "7"`, a non-PDF-compatible .ai for `pdfCompatible: false`, and a PDF-compatible .ai otherwise, including a save that reuses the options the document was last saved with. `run_command document.save` and the other save commands refuse any option given beside the path and in `options` with different values, and give the same warnings for a top-level `compress`, `version`, `preview` or `precision` and for each key in `options` that the format doesn't read. The headless session and the app receive the same `app.save` params, and the app is also sent `wait: true`: the call returns once the file is written, or with the error (such as a path outside the write root), even with Background Save on. When the save asks first (the question before replacing a file that opened with losses, or on macOS and Linux the file dialog for an untitled document without `path`), the reply is `{pending}` or `"cancelled"` as before, and the save that runs after the answer uses the Background Save preference. | +| `save_file` | `{path?, compress?, version?, preview?, precision?, options?}` | Runs `document.save`: the document's own file in its own format (native `.vectorcraft` unless it was opened from or saved as SVG, PDF or a restorable `.ai`; then `warnings` say what that format loses). A path's extension picks the format (`.vectorcraft`, `.vctemplate`, `.pdf`, `.svg`, `.svgz`, `.ai`: a PDF carrying the native document, which reopens editable; `.eps`: by default one file whose bounding box fits the visible art, the first artboard when no art shows; `options.useArtboards: true` makes the chosen artboard the page and, with two or more chosen, the file at the path is the central file of the visible art and each artboard is also written to `<name>_<artboard>.eps` beside it, listed in the reply's `files`). For a native file, `compress: true` gzips it (default: the `useCompression` preference, Preferences › File Handling › Use Compression), `version` (1 to 4) writes it for an older VectorCraft (1 and 2 are never compressed, and `version: 2` with `compress: true` is an error), `preview: true` embeds a PNG of the first artboard, and `precision` (0 to 9) rounds path anchors and handles to that many decimal places for that save only (colors, transforms and type stay exact). `options` passes the format's save options as `run_command document.formats` lists them. When `compress`, `version`, `preview` or `precision` is given both at the top level and in `options` with different values, the call is an error (`` `compress: true` and `options.compress: false` disagree: give the option once ``) and nothing is written. A key in `options` that no save format takes is an error, and nothing is written (see Parameter keys). The save drops each option the written format doesn't read. The reply's `warnings` then name each such option given (for example `` `compress` was not used: SVG files don't read it `` for an `.svg` path, and `` `options.preview` was not used: … `` for a key in `options`; a `.ai` file reads `compress` but not `version`, `preview` or `precision`). For a `.ai` file, these warnings name the kind of `.ai` written: a legacy .ai (version 7) for `aiVersion: "7"`, a non-PDF-compatible .ai for `pdfCompatible: false`, and a PDF-compatible .ai otherwise, including a save that reuses the options the document was last saved with. `run_command document.save` and the other save commands refuse any option given beside the path and in `options` with different values, and give the same warnings for a top-level `compress`, `version`, `preview` or `precision` and for each key in `options` that the format doesn't read. The headless session and the app receive the same `app.save` params, and the app is also sent `wait: true`: the call returns once the file is written, or with the error (such as a path outside the write root), even with Background Save on. When the save asks first (the question before replacing a file that opened with losses, or on macOS and Linux the file dialog for an untitled document without `path`), the reply is `{pending}` or `"cancelled"` as before, and the save that runs after the answer uses the Background Save preference. Location refusals return `READ_ONLY_LOCATION: <reason>` without a fallback panel for automation; see [Save location errors](#save-location-errors). | | `export` | `{path?, format?, scale?, artboard?, range?, selection?, outlineText?, options?}` | `svg`, `svgz`, `pdf`, `ai` (the PDF-compatible `.ai` that Save writes, which carries the document when every artboard is written in order), `eps`, `dxf`, `emf`, `wmf`, `hpgl` (a `.plt` plot file for plotters and cutters), `png`, `jpg`, `webp`, `gif`, `png8` (an indexed `.png`), `tiff`, `bmp`, `tga`, `psd` (layered), `txt` (the document's text), `vectorcraft` or `template` (a native template). The tool's `format` enum and `document.formats` list them, generated from the engine's format table. When `format` is omitted, it comes from the path's extension. PDF and `.ai` write one page per artboard: all of them, or `artboard` (0-based) / `range` (`"1-3, 5"`, 1-based). SVG and SVGZ write one file per chosen artboard. EPS writes one file of the visible art; with `artboard`, `range`, `options.artboards` or `options: {"useArtboards": true}` it writes one file per chosen artboard, after a central file of the visible art when there are two or more, and `useArtboards: false` keeps the one file. Rasters, DXF, EMF, WMF and HP-GL write one artboard, or one file per chosen artboard with `options: {"useArtboards": true}`. Rasters also take `options: {"rect": [x, y, width, height]}` (page points, y down): that area of the page at the given `ppi`/`scale`, without an artboard and without changing the document; it is refused with `artboard`, `artboards`, `range` or `useArtboards`, with a non-positive or non-finite size, and for non-raster formats. `options` carries more format options (e.g. `{"quality": 80}` for JPEG). `selection: true` exports the selected objects cropped to their bounds (the reply adds their `bounds`, and reports `format` and the encoder's `warnings` as a whole-document export does); `outlineText: true` writes SVG text as paths. Template layers are left out, live effects are kept, and exporting `vectorcraft` never changes the document's path. Without `path` the bytes come back as `dataBase64`. Both backends run the same `document.export` call. | | `add_text` | `{text, x?, y?, width?, height?, fit?, fitMinPercent?, path?, mode?, pathEffect?, size?, font?, color?, justify?, vertical?}` | Point type at (x, y) (`justify`: `left`, `center` or `right` starts, centres or ends each line on x); area type with `width`/`height` (`fit`: `none`, `autoHeight`, `shrinkText`, as `text.areaOptions`); or `path` + `mode` (`area`/`onPath`) to flow text in or along a path, with `pathEffect` (`rainbow`, `skew`, `3dRibbon`, `stairStep`, `gravity`; SVG export writes type on a path with any effect other than `rainbow` as outlines, with a warning); `justify` (also `auto`, `justifyAll`) aligns the paragraphs of every kind, as `text.setStyle` does later. `vertical: true` uses the existing vertical writing mode for every kind (default `false`, horizontal). Returns `{id}`, with `overflow: true` when the text doesn't fit its frame or path. | | `apply_effect` | `{effect?, params?, ids?}` | Appends a live effect. Without `effect`, returns the effect catalogue with parameters and defaults. | @@ -3484,9 +3486,10 @@ in the `swatch.list` shape (`library` is an id or a name). `swatch.library.add { into the document as one undo step: a colour group's name brings the whole group, a swatch's name the swatch alone, no names the whole library; swatches the document already has (same name and paint) are reported under `existing` and not added again. `apply: "fill"|"stroke"` also applies the first one, in the same undo step (what clicking a -swatch in the library panel does). The pattern libraries Geometric Patterns (`geometric-patterns`) and Grit Textures -(`grit-textures`: seamless black grain, grit, sand, speckle, stipple, scratch and fleck tiles), category `patterns`, -hold pattern swatches drawn in code; adding one also adds its pattern, as pasting art painted with it does +swatch in the library panel does). The pattern libraries Geometric Patterns (`geometric-patterns`), Grit Textures +(`grit-textures`: seamless black grain, grit, sand, speckle, stipple, scratch and fleck tiles) and Optical Effects +(`optical-effects`: VectorCraft's own black wave, ring, warped-check, pinched-stripe, chevron and lens tiles), +category `patterns`, hold pattern swatches drawn in code; adding one also adds its pattern, as pasting art painted with it does (the same pattern the document has is reused, and a name another pattern or swatch has gets a number). `swatch.resetDefaults {replace?}` brings back the missing default swatches. @@ -9999,3 +10002,24 @@ Path Options' Preview does (#5360). {"name":"run_command","arguments":{"command":"addon.type.releasePath","params":{}}} {"name":"run_command","arguments":{"command":"addon.type.releasePath","params":{"mode":"path","keepOriginal":true}}} ``` + +## Save location errors + +A save refused by the filesystem with PermissionDenied or ReadOnlyFilesystem returns +`READ_ONLY_LOCATION: <reason>`. The reason retains the path and system message. This is a +stable prefix within the existing error string, not a new response field or JSON-RPC code. +An automation-root denial, an encoding failure, and an error retaining recovery copies do +not use this code. Choose a writable destination and explicitly save there; automation never +opens the fallback Save As panel. Failed saves keep the original document path and modified +state; multi-file saves may already have written other output files. + +`app.save` with `wait: true` returns the write failure in its reply (`save_file` already asks +for this on the desktop). Without waiting, an accepted background save still replies before +completion; any later failure opens no panel. MCP retains its usual tool-name prefix around +the error text. The interactive fallback applies only to ordinary File > Save, not Close/Quit. + +## Revision history + +| Date | Change | +|---|---| +| 2026-11-11 | Document READ_ONLY_LOCATION for saves (#6146); no response-shape change. | diff --git a/docs/roadmap.md b/docs/roadmap.md index d7df0b6b52..32c98a0a5f 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -48,33 +48,33 @@ M7 type: Touch Type moves a right-to-left character along its line and up from i | Date | Change | Summary | |---|---|---| -| 2026-11-07 | minor | M4/M7: `.ai`/EPS type from the text document keeps its paragraphs' tab stops, 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 from the left edge (#6147). Scores and estimates unchanged. | -| 2026-11-07 | minor | M4/M7: `.ai`/EPS type from the text document keeps auto leading at the paragraph's share, the paragraph's hyphenation options and 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 as a wrapped line is (#6137); a user's `.ai` that opened from its PDF part now opens from its layers. 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-10 | minor | M13 / G6: MCP catalogue (#6460): headless lists UI commands, eight prompts, 33 tools, effect vs command ids; scores and estimates unchanged. | | 2026-11-08 | minor | Character Styles panel: a double-click on a user style's row opens its Character Style Options (`ui.charStyleOptions`); the 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. | | 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-10 | minor | M13 / G6: MCP catalogue (#6460): headless lists UI commands, eight prompts, 33 tools, effect vs command ids; scores and estimates unchanged. | +| 2026-11-07 | minor | M4/M7: `.ai`/EPS type from the text document keeps its paragraphs' tab stops, 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 from the left edge (#6147). Scores and estimates unchanged. | +| 2026-11-07 | minor | M4/M7: `.ai`/EPS type from the text document keeps auto leading at the paragraph's share, the paragraph's hyphenation options and 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 as a wrapped line is (#6137); a user's `.ai` that opened from its PDF part now opens from its layers. 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-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-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 | 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-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. | +| 2026-11-05 | minor | Panels: a floating panel window hides under a modal dialog and drops out of the top level under an open menu, so neither the Colour Picker or Recolor Artwork nor the menu bar is covered, and it comes back at its palette level after (#5238); scores and estimates unchanged. | | 2026-11-04 | minor | M7: Select Text by Font takes a scope: the document, chosen artboards or chosen layers; Find Font's Find In offers Document, Artboard (the active one) and Layers (the selected ones, else the current one) (#5529, a VectorCraft addition); estimates unchanged. | | 2026-11-04 | minor | M7: Find Font's Replace With Font lists family and style rows with a search, and the list and the document's font list scroll on their own (#4972); scores and estimates unchanged. | | 2026-11-04 | minor | Selection Tools add-on: Select by Artboards selects the objects overlapping, fully inside or fully outside several chosen artboards, from a dialog that lists, searches and multi-selects the artboards and fits one in the window on double-click (#5525, #5548, #5549, a VectorCraft addition); scores and estimates unchanged. | | 2026-11-04 | minor | M4: raster/PSD exports default to document resolution; CLI run/convert expose --ppi, matching the engine and MCP precedence. Scores and hours unchanged. | -| 2026-11-05 | minor | Panels: a floating panel window hides under a modal dialog and drops out of the top level under an open menu, so neither the Colour Picker or Recolor Artwork nor the menu bar is covered, and it comes back at its palette level after (#5238); scores and estimates unchanged. | | 2026-11-04 | minor | New Document: the units chosen in Preset Details belong to the chosen preset; Recent and Saved cards keep the units their document or preset was made in, and the built-in tabs still follow the chosen units (#2078). Scores and estimates unchanged. | | 2026-11-03 | minor | M1: Enclosing Lasso provides whole-object enclosure selection as an add-on with no default shortcut or Basic toolbar slot; estimates unchanged. | | 2026-11-03 | minor | New gradients use CMYK default stops in CMYK documents, including Gradient tool creation. Scores and estimates unchanged. | | 2026-11-03 | minor | Help Bar has its own Window menu toggle and workspace visibility setting. Scores and estimates unchanged. | +| 2026-11-03 | minor | M7: Find Font replacement sources Document, Recent and System are shipped (#5217); estimates unchanged. | +| 2026-11-03 | minor | M7: composition spacing remains along the line when characters rotate; 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-10-30 | minor | M4: G9: `.ai`/EPS editing data: a blend takes the transparency (opacity, blend mode, knockout) in its state block after its record, so its steps knock each other out as the page draws them; a user's file comes in 0.6% from its page instead of 1.3%. Scores and estimates unchanged. | | 2026-10-30 | minor | M4: File formats: `.ai`/EPS editing-data fidelity from an audit of a user's files: All Caps and Small Caps (#4708); placed files at their box's corner, kept where they draw closer to the page (#4664, #4723, #4742); pages cropped to the art, with `UserUnit` (#4658, #4727); gradient mesh corners with handle weights of 0 and the state written after a mesh (#4738, #4744, #4748); type on a path spaced as the file lays it out (#4743); synthetic regressions in `crates/eps`; scores and estimates unchanged. | -| 2026-11-03 | minor | M7: Find Font replacement sources Document, Recent and System are shipped (#5217); estimates unchanged. | | 2026-10-30 | minor | Raster effects: bounded length fields in effect dialogs clamp on commit and reject invalid inputs visibly (#4638); estimates unchanged. | -| 2026-11-03 | minor | M7: composition spacing remains along the line when characters rotate; estimates 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. | @@ -143,7 +143,7 @@ native wording review, layout checks and a web Devanagari font remain. [Notes](l | M1 | Selection, transform, layers, MCP (common command tools, strict keys, session resources and version-aware result hints) | ✅ mostly done (document-tab artwork drag with lossless resource transfer and one target Undo; malformed MCP app replies discard the connection without replay, #1761; bounded control socket waits and connection-slot recovery, #1726; bounded synthetic control input, #1709; no-op anchor Align keeps Undo, Redo and saved state (#1180 follow-up); transform reference point snaps to anchors/centres; rotated objects keep a rotated bounding box and their angle; Free Transform, Scale and Reflect along a rotated box pending; the Transform fields take one Tab each, the W/H link after them) | 4–6 | | M2 | Drawing tools + smart guides | ✅ mostly done (#3380 deletion creates independent selected path pieces and keeps bounds-fitted gradients in place; endpoint highlighting remains open; Polygon/Star exact-size count persistence, with repeated-drag, tool-switch and Cancel regression coverage; Curvature point markers without direction handles (#1236, in part); Direct Selection segment click/Delete preserves neighboring edges, #1278; Flare, Reshape, Live Corners widget dragging and per-corner radii and kinds landed, on any path's corners; Shaper rough-shape recognition and orientation steps, the remaining Pen modifier nuances; Shaper Groups with editable sources and separate fill/stroke edits landed; custom ruler origin with drag preview, active-artboard reset, relative X/Y fields and native round-trip (#1212); construction widget/refinements pending); a shape just drawn moves by its centre point and turns from the zone outside a corner with its tool still active (#5477); Tangent and straight-segment perpendicular snapping use saved Smart Guides Geometric Guides preferences and Control bar quick access (#2537), including the existing Snap to Glyph and Alignment Guides toggles (#6164); no View submenu; Copy and Cut with the Pen or Curvature tool take the anchors a Cmd/Ctrl-lent Direct Selection picked (#6123) | 12–18 | | M3 | Paint & appearance (swatches, color, gradient, stroke, appearance, transparency, styles) | ✅ done (M3.7–M3.98): swatches, groups, libraries, tints, Lab spots and Registration; Eyedropper copies transformed character dimensions (#6404) and styles highlighted text only (atomic transparency rejection); Eyedropper reads visible locked and outside-isolation sources (#1251); Color Picker, Color panel, Color Guide, Color Themes, Edit Colors and Recolor Artwork (screen-bounded Edit chip list, #3416); gradients that follow every transform, the annotator, linked stops, freeform gradients, focal points, gradients on strokes and Expand; visible strokes when paint is added to inherited None/zero-width art (#1253); stroke geometry, arrowheads, dashes fitted to corners, stroke on type, width profiles and the Width tool (Shift-double-click enables adjoining adjustment in Width Point Edit; finite-arithmetic follow-up regressions and full CI passed ([Validation evidence](target-app-parity.md#evidence-what-ships-today-by-area))), Scale Strokes and preview bounds; Appearance targeting, container appearance, target circles, Expand Appearance; knockout, isolation, blend accuracy, CMYK blending, overprint, Attributes, masks and clipping, Flatten Transparency; graphic styles, links and libraries; CSS library export preserves fractional linear and radial gradient stop positions. Left: freeform gradients in SVG/PDF, mixed spot/process gradients in SVG and PDF gradients mixing a spot color with RGB or Lab stops export as stops or process colors; PDF export writes dithered gradient fills with spot stops, and Perceptual spot gradients with RGB or Lab stops other than white or more than 4,096 stops on their curve, as images in process colors; confirm the Scale Strokes & Effects default; Recolor model selection is available through the engine and MCP only (#2520); the dialog retains its existing controls | 4–8 | -| M4 | Files (native, SVG, PDF, raster, Export for Screens, clipboard interop) | PDF authored ActualText preserved once in editable import (#6165; first-glyph line/style, bounded input; estimates unchanged); PDF export keeps harakat and zero-advance Noto Arabic dot glyphs in one compatible batch per layout line, with per-glyph vertical offsets (#6309). `crates/pdf/src/tests/realtext.rs` (`mark_text_*`) checks decoded cluster mappings, mixed Arabic/Latin/digits, contour placement against outline export, vertical columns, differing path rotations and PDF/X-1a/3/4. Editable-text import preserves authored ActualText separately (#6165), with cross-line/style layout limits; separately styled or differently rotated clusters and external viewer search/copy remain outside this export-only evidence. `.ai`/EPS type from the text document keeps its paragraphs' tab stops (#6147); `.ai`/EPS type from the text document keeps auto leading per line, its hyphenation options, and 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 (#6137); ✅ done (M4.14–M4.98 on 2026-10-05): Save As/Save a Copy/Revert/templates, Data Recovery and background save, SVG import and export fidelity (units, text, symbols, filters, SVGZ, Preserve Editing, SVG Options), shared guarded PDF writer and windowed reader ([pdf-backend.md](pdf-backend.md)), PDF import (colours, layers, masks, editable text, security) and export (presets, marks, bleed, ICC and output intent, raster effects, subset fonts, PDF/X-1a, X-3 and X-4, PDF layers, overprint, page thumbnails, fast web view, PDF 1.3), Place and Links, Package, EPS and DXF in and out (EPS legacy mesh corners, vector opacity masks and raster page comparison verified; closed SPLINE flags round-trip and circular paths export as native CIRCLE/ARC, #1299), EMF/WMF, TIFF/BMP/Targa/PSD/GIF/PNG-8 export, Print and print presets, slices, Save for Web, TIFF in Export for Screens and Asset Export, clipboard flavours, File Info; AF.1–AF.4 add Affinity import (native layers, artboards, vectors, text and images from Affinity 1–3 files; the embedded preview only as a warned fallback; AF.4 expands current `.af` fixtures and every-board round-trip validation); EPS Save and Save As keep an opened EPS as EPS with its page frame and complete editable VectorCraft document (2026-10-26; G9 import fidelity unchanged). Left: DWG (use DXF), PSD placement as layers, overprint read back from PDF, Affinity export, polish; SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts | 3–6 (existing scope; Affinity export unestimated) | +| M4 | Files (native, SVG, PDF, raster, Export for Screens, clipboard interop) | Save refusal continuation (#6146): ordinary Save offers Save As; unchanged format keeps options, changed format opens its Save options; cancellation/automation isolation; focused UI regressions passed, native acceptance pending; PDF authored ActualText preserved once in editable import (#6165; first-glyph line/style, bounded input; estimates unchanged); PDF export keeps harakat and zero-advance Noto Arabic dot glyphs in one compatible batch per layout line, with per-glyph vertical offsets (#6309). `crates/pdf/src/tests/realtext.rs` (`mark_text_*`) checks decoded cluster mappings, mixed Arabic/Latin/digits, contour placement against outline export, vertical columns, differing path rotations and PDF/X-1a/3/4. Editable-text import preserves authored ActualText separately (#6165), with cross-line/style layout limits; separately styled or differently rotated clusters and external viewer search/copy remain outside this export-only evidence. `.ai`/EPS type from the text document keeps its paragraphs' tab stops (#6147); `.ai`/EPS type from the text document keeps auto leading per line, its hyphenation options, and 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 (#6137); ✅ done (M4.14–M4.98 on 2026-10-05): Save As/Save a Copy/Revert/templates, Data Recovery and background save, SVG import and export fidelity (units, text, symbols, filters, SVGZ, Preserve Editing, SVG Options), shared guarded PDF writer and windowed reader ([pdf-backend.md](pdf-backend.md)), PDF import (colours, layers, masks, editable text, security) and export (presets, marks, bleed, ICC and output intent, raster effects, subset fonts, PDF/X-1a, X-3 and X-4, PDF layers, overprint, page thumbnails, fast web view, PDF 1.3), Place and Links, Package, EPS and DXF in and out (EPS legacy mesh corners, vector opacity masks and raster page comparison verified; closed SPLINE flags round-trip and circular paths export as native CIRCLE/ARC, #1299), EMF/WMF, TIFF/BMP/Targa/PSD/GIF/PNG-8 export, Print and print presets, slices, Save for Web, TIFF in Export for Screens and Asset Export, clipboard flavours, File Info; AF.1–AF.4 add Affinity import (native layers, artboards, vectors, text and images from Affinity 1–3 files; the embedded preview only as a warned fallback; AF.4 expands current `.af` fixtures and every-board round-trip validation); EPS Save and Save As keep an opened EPS as EPS with its page frame and complete editable VectorCraft document (2026-10-26; G9 import fidelity unchanged). Left: DWG (use DXF), PSD placement as layers, overprint read back from PDF, Affinity export, polish; SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts | 3–6 (existing scope; Affinity export unestimated) | | M5 | Performance | 🟡 background render + caches + MT done; `vectorcraft-cli bench` and `vectorcraft-cli perf` (budget suite); opaque solid-fill knockout paths draw once in hard-edge/supersampled raster export, with analytical-AA compositing unchanged; file format v2 opens 3× faster (50k paths: 722 → 244 ms); raster effects (glows, shadows, blur, feather) no longer force the whole frame single-threaded (filtered offscreen per effect, verified equal to the single-threaded reference); Craquelure/Mosaic Tiles/Stained Glass distance searches optimized with pixel-invariance checks and the `texture_drag_perf` probe (#1257); effect-heavy demos need a clean-machine benchmark; dirty-region rendering, GPU backend spike pending | 15–25 | | M6 | Path operations (Pathfinder, Shape Builder, offset…) | ✅ mostly done (live compound shapes: Make, Alt-click a Shape Mode, member modes, Release, Expand (#4522); single-result Pathfinder names and SVG IDs retained (#1293); Divide open-cutting-path geometry, appearance and history verified (#1307; existing segment cap remains); Simplify preview/counts and rewritten-path component selection verified (#1272; the slider opens at the Auto-Simplify level; a Direct Selection part of a path simplifies on its own); Compound appearance and member fill-rule edits verified (#1197); Boolean precision on almost-horizontal edges fixed and the property tests made deterministic; Shape Builder gap detection, large-offset bug open); Shape Builder Gap Detection shares Live Paint’s spatial gap finder and temporary merge bridges (#2526); Custom is capped at 72 pt in the existing options dialog | 3–6 | | 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, and the Edit Custom Dictionary dialog (#4936; the Check Spelling dialog remains); 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); 🟡 Hanging punctuation keeps its published amounts under Glyph Scaling, and Standard burasagari counts a hung line start as room; Fullwidth `!` / `?` keep an upright fallback in vertical type; Hyphenation… and Justification… per paragraph (#2310, #2755), Auto Leading also driving the Character panel's Leading, the Alt+↑/↓ leading nudge and Fill with Placeholder Text; 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 (feature-valued alternates and unencoded glyphs applied in one Undo step while keeping Arabic joining, #5514), OpenType panel (including font-defined Stylistic Sets), 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 | @@ -192,7 +192,9 @@ separate. Scores and hours unchanged. [Behavior and command details](development |---|---|---| | 2026-11-12 | minor | M7: Glyphs panel applies feature-valued alternates and unencoded glyphs while keeping Arabic joining (#5514). Scores and estimates unchanged. | | 2026-11-12 | minor | M12: Live Paint Bucket fills faces crossed in one drag (#6373). Scores and estimates unchanged. | +| 2026-11-12 | minor | Save refusal follow-up (#6146): changed format opens its Save options; unchanged format retains original options. Focused UI regressions passed, including the format-change case that failed before the fix; native panel acceptance pending. Scores and estimates unchanged. | | 2026-11-11 | minor | Eyedropper copies effective character dimensions between transformed text objects (#6404); scores and estimates unchanged. | +| 2026-11-11 | minor | Ordinary Save to a location refusing writes offers Save As (#6146); cancellation preserves the original path and modified state; automation receives READ_ONLY_LOCATION. Scores and estimates unchanged. Validation pending. | | 2026-11-11 | minor | M10/G5: Energy Beams built-in brush library; MCP docs catch-up; estimates unchanged | | 2026-11-11 | minor | M10/G5: Skill Icons built-in symbol library; MCP docs catch-up; estimates unchanged | | 2026-11-10 | minor | M10/G5: Status Effects built-in graphic style library; MCP docs catch-up for UI Frames and Map Markers; estimates unchanged | @@ -203,17 +205,17 @@ separate. Scores and hours unchanged. [Behavior and command details](development | 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. | +| 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-10 | minor | Vector Halftone Preview leaves the dots unobscured and keeps the selection box (#6369). Scores and estimates unchanged. | | 2026-11-09 | minor | M7: Auto Spell Check (#4936); the dialogs and the right-click Spelling menu remain; scores and estimates unchanged. | | 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`. | | 2026-11-09 | minor | PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged. Decoded stream and placement regressions; reader ActualText (#6165), styled clusters and external viewer validation remain separate. | | 2026-11-09 | minor | Interim macOS canvas cursor delivery (#2292): existing artwork in aligned 1×/2× AppKit representations, visible system fallback and host-owned restoration. Headless routing, bitmap and ownership coverage; native timing, menus, focus/handback, mixed displays, Spotlight and full-screen Spaces remain unverified. Scores and estimates unchanged. | | 2026-11-09 | minor | M7: the custom dictionary commands and Ignore All (#4936); the dialogs and Auto Spell Check remain; scores and estimates unchanged. | | 2026-11-09 | minor | M4/G9: EPS import: 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. | +| 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 | 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-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. | @@ -221,13 +223,17 @@ separate. Scores and hours unchanged. [Behavior and command details](development | 2026-11-04 | minor | New Document: the units chosen in Preset Details belong to the chosen preset; Recent and Saved cards keep their own units; scores and estimates unchanged. | | 2026-11-03 | minor | M7: the OpenType panel offers the selected font's available Stylistic Sets with custom or numbered names; estimates unchanged. | | 2026-11-03 | minor | M1: Enclosing Lasso adds freehand whole-object selection under Object › Plug-ins; estimates unchanged. | -| 2026-11-02 | minor | Font-family fields in Character, Properties and Control step selected text with the wheel (#5253), with one undo step per notch; scores and estimates unchanged. | | 2026-11-03 | minor | M7 records Find Font's shipped replacement-source chooser (#5217); estimates unchanged. | | 2026-11-03 | minor | Symbol editing shows draggable 9-slice guides (#5280). Guides outside the artwork remain unsupported. Scores and hours remain unchanged. | | 2026-11-03 | minor | M7: Touch Type moves right-to-left characters along their line while keeping text editable; first-logical-character restriction retained. Estimates unchanged. | -| 2026-10-30 | patch | M5: opaque solid-fill knockout paths avoid redundant erase/add layers in hard-edge frames and supersampled raster export; exact-pixel regression coverage retains analytical-AA rounding and translucent knockout behavior; scores and estimates unchanged | | 2026-11-03 | minor | M7: retain composition trimming in rotated character placement and rotation metadata; estimates unchanged. | +| 2026-11-02 | minor | Font-family fields in Character, Properties and Control step selected text with the wheel (#5253), with one undo step per notch; scores and estimates unchanged. | +| 2026-10-30 | patch | M5: opaque solid-fill knockout paths avoid redundant erase/add layers in hard-edge frames and supersampled raster export; exact-pixel regression coverage retains analytical-AA rounding and translucent knockout behavior; scores and 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-28 | minor | Flare ray rotation follows vertical travel at one degree per document point, independent of pointer event count. Shift freezes the current orientation; Space also freezes rotation on the first update. This is the chosen application interaction rate. Scores and estimates unchanged. | +| 2026-10-28 | minor | Drop Shadow Darkness subtracts the normalized amount from RGB source channels or adds it to CMYK/Gray black before profile conversion and blur; near-100% becomes gray black, with shadow opacity separate. SVG keeps live filters for uniform sRGB sources and renders shadows for other RGB profiles, source models and gradients. Gradient-stop adjustment is an application rule; spot/custom colors and composite-source parity remain open. Scores and estimates unchanged. | +| 2026-10-28 | minor | Blob Brush Shift locks after clear axial movement, keeps earlier samples fixed, and waits without an off-axis trail. Activation uses raw press displacement in logical screen pixels, with exact quarter-turn boundaries. Explicit application rule; scores and estimates unchanged. | +| 2026-10-28 | minor | Flare Tool Options opened by a canvas click previews its settings at that click. Reset refreshes the enabled preview while keeping the dialog open. Cancel discards preview art and tentative options; OK accepts one flare in one Undo step. Editing existing flare artwork remains outside this dialog scope. Scores and estimates 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-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. | @@ -236,12 +242,10 @@ separate. Scores and hours unchanged. [Behavior and command details](development | 2026-10-23 | minor | M2/G1: a double-click on the Line Segment, Arc, Rectangular Grid or Polar Grid tool opens its Tool Options (#2691, in part); estimates unchanged | | 2026-10-23 | minor | G12 and M14: General › Constrain Angle acts for the arrow keys, Shift drags and new objects (#394); estimates unchanged | | 2026-10-23 | minor | M7/G4: Glyph Scaling leaves hung punctuation at its width and Standard burasagari counts a hung line start as room; paragraph-level hanging tests. Scores and hours unchanged. | -| 2026-10-20 | minor | M7: a paragraph's Auto Leading also drives the Character panel's Leading, the leading nudge and Fill with Placeholder Text; estimates unchanged | -| 2026-10-22 | minor | #834 (bezier drag preview stalled after the pointer re-entered the window, and missed the move in the release's frame) fixed; G11 keeps its 8–15 h | -| 2026-10-22 | minor | The Transform effect has Transform Objects, Transform Patterns and Scale Strokes & Effects (#885). | -| 2026-10-18 | minor | #937's reproduced case fixed (Shape Builder lines cut art past 4,096 segments); G11 keeps 8–15 h until confirmed on the reporter's file | | 2026-10-23 | minor | M6: Simplify of part of a path: with anchors or segments of a path direct-selected, Object › Path › Simplify (slider, Auto-Simplify and dialog) refits only each run of selected segments between its end anchors; estimates unchanged | | 2026-10-23 | minor | M6: Simplify: Object › Path › Simplify opens its slider at the Auto-Simplify level for the selection, previewing that result, instead of at 1 pt; partial-path simplify remains missing; estimates unchanged | +| 2026-10-22 | minor | #834 (bezier drag preview stalled after the pointer re-entered the window, and missed the move in the release's frame) fixed; G11 keeps its 8–15 h | +| 2026-10-22 | minor | The Transform effect has Transform Objects, Transform Patterns and Scale Strokes & Effects (#885). | | 2026-10-22 | minor | G2: the Effect Gallery shipped (#2883): Effect › Effect Gallery… browses the Artistic, Brush Strokes, Distort, Sketch, Stylize and Texture filters, edits each filter's settings, builds an ordered stack (add, reorder, hide, remove) with a live canvas preview, and OK applies it as separate appearance effects in one undo step; remaining: filter thumbnails and a preview area inside the dialog; G2 estimate 4–6 → 1–2 h; Raster effects 82% → 85% (power-user view 49% → 52%), estimated, a modest move because the named missing dialog landed without thumbnails or an in-dialog preview. | | 2026-10-22 | minor | G2: Effect › Sketch › Conté Crayon (Foreground Level, Background Level, Texture, Scaling, Relief, Light Direction, Invert; a black crayon in the dark areas and a white one in the light areas on textured mid-grey paper; ranges and defaults unverified VectorCraft choices); measured Photoshop-style filters 57/57, the Effect Gallery remains (4–6 h). | | 2026-10-22 | minor | Recolor Artwork Edit chips scroll within the available screen height (#3416); small palettes retain their tab height; scores and estimates unchanged. | @@ -250,13 +254,14 @@ separate. Scores and hours unchanged. [Behavior and command details](development | 2026-10-21 | minor | M14: workspaces keep the layout changes made in them across switches and restarts until Reset <name> (UI.7); estimates unchanged | | 2026-10-21 | minor | M14: panels dock by dragging into any dock group (tabs or a top/bottom split) or the icon column; dock tabs close from their context menu (UI.6); estimates unchanged | | 2026-10-21 | minor | M14: a double-click on a dock tab collapses its group to its tabs and expands it again (UI.8); estimates unchanged | -| 2026-10-20 | minor | M14: workspaces keep the layout changes made in them across switches and restarts until Reset <name> (UI.7); estimates unchanged | | 2026-10-21 | minor | M3: highlighted-text Eyedropper sampling preserves ranges and rejects unsupported source/target transparency atomically; regression-tested; scores and estimates unchanged. | | 2026-10-21 | minor | Shape Builder Gap Detection shares Live Paint’s spatial gap finder and temporary merge bridges (#2526); Custom is capped at 72 pt in the existing options dialog. Scores and estimates unchanged. | | 2026-10-21 | minor | Recolor model selection is available through the engine and MCP only (#2520); the dialog retains its existing controls. Scores and estimates unchanged. | | 2026-10-21 | minor | Tangent and straight-segment perpendicular snapping use saved Smart Guides Geometric Guides preferences and Control bar quick access (#2537); no View submenu; Recolor model selection is available through the engine and MCP only (#2520); the dialog retains its existing controls. Scores and estimates unchanged. | | 2026-10-21 | minor | SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts. Scores and estimates unchanged. | | 2026-10-21 | minor | 3D: original Standard, Diffuse, Top Left and Right lighting presets; command access, effective-setting recognition and preview/history; scores and estimates unchanged | +| 2026-10-20 | minor | M7: a paragraph's Auto Leading also drives the Character panel's Leading, the leading nudge and Fill with Placeholder Text; estimates unchanged | +| 2026-10-20 | minor | M14: workspaces keep the layout changes made in them across switches and restarts until Reset <name> (UI.7); estimates unchanged | | 2026-10-20 | minor | Polygon and Star exact-size placement remembers the chosen count for later drags and dialog openings; Cancel preserves the previous count; existing opening gestures retained; scores and hours unchanged. | | 2026-10-20 | minor | M12: consolidated three duplicate milestone rows into one, retaining distinct Image Trace and Graphs details and the quick-preset wording correction; status, scores and 6–10 h estimate unchanged. | | 2026-10-20 | minor | Move the live-trace bounds/highlights note into the Image Trace milestone description and restore the Graphs label-formatting code span; implementation, scores and hours unchanged. | @@ -264,10 +269,11 @@ separate. Scores and hours unchanged. [Behavior and command details](development | 2026-10-20 | minor | M3.57: CSS swatch libraries preserve fractional stop positions in linear and radial gradients; synthetic color-crate regression. Scores and hours unchanged. | | 2026-10-19 | minor | Cache status-readout sample widths per UI, language, document unit and pixels per point; regressions check reuse across live frames and refresh after display-setting changes. Scores and estimates unchanged. | | 2026-10-19 | minor | Status messages and readouts use stable responsive allocations sized for localized labels and ordinary coordinates. Contributor original-file acceptance confirmed; scores and estimates unchanged. | -| 2026-10-18 | minor | M4/DXF #1299: circular cubic paths export as native CIRCLE/ARC where stroke semantics permit; R12 weighted strokes retain POLYLINE width and closed partial arcs with an implicit closing chord are not promoted to CIRCLE. Scores and hours unchanged. | | 2026-10-19 | minor | Cached canvas dimensions match the renderer limit before cache-key creation, view construction and worker submission. Regression coverage; scores and estimates unchanged. | | 2026-10-19 | minor | Cached canvas reprojection preserves scale during resize, rotation and DPI changes. Contributor original-file acceptance confirmed; scores and estimates unchanged. | | 2026-10-19 | minor | Color Picker layout (#2008): compact controls, centered header, stacked actions and aligned inputs; screen-reader channel values, current shared button names and styling, screen sampler and model-by-model Tab order retained. Narrow windows scroll vertically; scores and hours unchanged. | +| 2026-10-18 | minor | #937's reproduced case fixed (Shape Builder lines cut art past 4,096 segments); G11 keeps 8–15 h until confirmed on the reporter's file | +| 2026-10-18 | minor | M4/DXF #1299: circular cubic paths export as native CIRCLE/ARC where stroke semantics permit; R12 weighted strokes retain POLYLINE width and closed partial arcs with an implicit closing chord are not promoted to CIRCLE. Scores and hours unchanged. | | 2026-10-18 | minor | M4: PDF Place and clipboard paste preserve live text by default; explicit outlines on Place remain available. Synthetic regressions; estimates unchanged. | | 2026-10-18 | minor | Mirror Repeat shows its axis when selected (#2877): drag the line or center handle to move it, drag an end handle to rotate it (Shift: 45°), Escape cancels and each drag is one undo step. Engine and headless canvas gesture regressions passed; native move, rotation and Escape screenshots reviewed. Formatting, Clippy, WASM and repository checks passed; the undo label reuses translated Repeat Options. (#2877) Scores and hours unchanged. | | 2026-10-18 | minor | Selected Mirror Repeat bounding-box handles take priority over the axis line; a Top-handle regression confirms the default repeat scales vertically, while center and end handles retain their move and rotation behavior. Headline scores and estimates unchanged. | @@ -279,6 +285,7 @@ separate. Scores and hours unchanged. [Behavior and command details](development | 2026-10-17 | minor | M12: maximal-disc pruning and junction merging, measured in the centre-lines class; whole-blob selection, scores and hours unchanged. | | 2026-10-17 | minor | M12: exact per-blob distances and Guo–Hall thinning preserve more stroke ends in the centre-lines class; whole-blob selection, scores and hours unchanged. | | 2026-10-17 | minor | M12: code-drawn centre-line scenes and metrics added to the ignored quality harness; current Create Strokes baseline in `image-trace-quality.md`. Tests only; scores, missing features and estimates unchanged. | +| 2026-10-17 | minor | Colored Inflate: shared membrane normals and smooth RGB vector ramps replace flat polygon bands; channel groups are batched per solid-fill surface. White-light Inflate retains its original polygons, paint composition and seam joins; the 0.05 seam minimum is restored. Scores and hour estimates unchanged. | | 2026-10-16 | minor | Image Trace: a loop or junction-returning piece whose fit collapsed to one cubic keeps the fewest segments that hold its area and course instead of every positioned point, so a higher Paths no longer gives fewer segments on code-drawn noisy images (six falling runs to none; one fall with another cause remains). Scores and hours unchanged. | | 2026-10-16 | minor | Image Trace: a notch corner is cut only when the straight edges beside it lie in the shared piece, so both regions of the piece get the same points (code-drawn noisy colour images: 93 uncovered sample points to 0). Scores and hours unchanged. | | 2026-10-16 | minor | Image Trace: a crack's sub-pixel shift is the mean of the readings from its two sides, exactly antisymmetric, so abutting regions get the same points on thin slivers (a code-drawn noisy ripple went from 482–1,228 uncovered sample points per trace to 0). Scores and hours unchanged. | @@ -287,13 +294,13 @@ separate. Scores and hours unchanged. [Behavior and command details](development | 2026-10-16 | minor | EPS editing data: legacy mesh corners, vector opacity masks and compound member paint retained; raster page comparison filters minification without relaxing limits; scores and hours unchanged. Synthetic EPS/parser/minification regressions; user EPS/SVG/JPG kept local. | | 2026-10-16 | minor | Correction to M12: quick Image Trace presets trace at once; the Image Trace button opens the adjustment controls (<https://helpx.adobe.com/illustrator/using/image-trace-results-optimization.html>). Wording only; status unchanged. | | 2026-10-16 | minor | Batch exporting in Export for Screens (#1935): a file name pattern (document and item names, a counter, the date), folders per format, item or both, Layers and Selection tabs with thumbnails, EPS, AI and VectorCraft rows, a PDF row with printer's marks, and export presets (built-in Logo Package and Print-Ready PDF, saved ones); scores and hours unchanged (estimated) | -| 2026-10-15 | minor | Image Trace retains nonzero positioned boundaries when a fixed-end returning piece has fewer than two fitted cubics. Source-generated tests check survival, winding, shared-edge reversal and Noise controls; see `image-trace-quality.md`. Scores and hours unchanged. | | 2026-10-16 | minor | G2: Effect › Artistic › Smudge Stick, Sponge and Watercolor (original approximations with their documented options); measured Photoshop filters 36/57 with Cutout, Neon Glow and Poster Edges, 21 remaining (19–31 h). | | 2026-10-16 | minor | G2: Effect › Sketch › Bas Relief, Charcoal, Photocopy and Stamp (#2527); measured Photoshop filters 37/57 with Cutout, Neon Glow and Poster Edges, 20 remaining (18–30 h). | | 2026-10-16 | minor | G2: Effect › Sketch › Halftone Pattern (Size, Contrast, Pattern Type Circle, Dot or Line; black ink on white paper, Size in points at any Document Raster Effects resolution); measured Photoshop filters 30/57, 27 remaining (22–38 h). | -| 2026-10-17 | minor | Colored Inflate: shared membrane normals and smooth RGB vector ramps replace flat polygon bands; channel groups are batched per solid-fill surface. White-light Inflate retains its original polygons, paint composition and seam joins; the 0.05 seam minimum is restored. Scores and hour estimates unchanged. | | 2026-10-16 | minor | 3D lighting: one to ten directional lights, a selectable light list, Add Light and Delete Light, independent settings and artboard gestures; ambient applied once and smooth RGB vector export; scores and hour estimates unchanged | | 2026-10-16 | minor | 3D lighting: opaque RGB Color for the shared directional light, with neutral ambient, preview/reset controls and smooth vector Revolve shading; scores and hour estimates unchanged | +| 2026-10-15 | minor | Image Trace retains nonzero positioned boundaries when a fixed-end returning piece has fewer than two fitted cubics. Source-generated tests check survival, winding, shared-edge reversal and Noise controls; see `image-trace-quality.md`. Scores and hours unchanged. | +| 2026-10-15 | minor | Artboard tool: closing a clean active tab restores the displayed board before Properties Delete. Nine targeted checks passed; three original close-route captures reviewed; scores and hours unchanged. | | 2026-10-14 | minor | Effect > Warp: style, orientation and percent controls; focused checks passed and original captures reviewed; scores and hours unchanged | | 2026-10-14 | minor | G11: open-path anchor Delete removes adjoining segments without bridging the gap; explicit Remove Anchors keeps its reconnect/refit behavior. Scores and hours unchanged. | | 2026-10-14 | minor | MCP `add_text` exposes the existing `vertical` option for point, area and path type; horizontal remains the default; scores and hours unchanged. | @@ -304,13 +311,12 @@ separate. Scores and hours unchanged. [Behavior and command details](development | 2026-10-14 | minor | Corner Radius has a link button in Properties and Transform: unlinked, a live rectangle shows a radius field for each corner (#1682); With the Width tool, double-clicking a stroke opens Width Point Edit to create a width point there, and Shift-double-click on a width point opens it with Adjust Adjoining Width Points ticked (#1814, #1630); Holding Alt while dragging a guide out of a ruler switches it between horizontal and vertical (#1782), and a marquee with the Selection or Direct Selection tool also selects the unlocked ruler guides it crosses (#1836); The Transform panel's, Properties panel's and Control bar's X/Y/W/H show and edit only the anchor points selected with Direct Selection (#1839, fixes #1824); With Snap to Grid on, anchors and objects snap to the grid even when Snap to Pixel is also ticked (#1840; #1816 waits for the reporter); Artboards are deleted the same way from the Artboards panel, its menu, Properties and Delete/Backspace (#1631); The toolbar shows no group labels by default (a preference brings them back) and runs on in one or two columns; the All Tools drawer lists the tools by flyout group with their shortcuts, has a menu (Reset, Basic, Advanced) and a Show section for the Fill/Stroke, Coloring, Draw Mode and Screen Mode controls (#1751); An action clicked in the Actions panel stays selected, so Play Current Selection and Delete Selection act on it (#1853, part of #1629); Redo during a drag keeps the drag as a step (#1856, #1719); Preferences are written as soon as OK is pressed (#1857; #1747 waits for the reporter); Preferences › Appearance of Black › Printing / Exporting and Units › Identify Objects By act (#1750, #1809); The paragraph alignment buttons show none selected for paragraphs with different alignments (#1784); redefining a character style to Auto leading updates the text that uses it and keeps local overrides (#1633); New and Redefine Character Style take the format of the selected text range, also while typing (#1634); Type inside symbols, pattern tiles, graph designs and opacity masks gets its layout bounds back when a native file opens, so warped type in a symbol looks the same after save and reopen (#1833, #1738); Editing, renaming, deleting or merging a global swatch reaches the art of opacity masks (#1830, #1735); macOS: Save As, Save a Copy and the first Save show their sheet again; the sheet's Format menu (#866, reopened) is withdrawn until it can be set before the sheet opens (#1855); Rendering: copies, symbol instances and repeats that share an appearance keep their stroke outlines between frames (#1667); the canvas no longer reads its whole picture back for each non-isolated group that blends (#1687); envelopes, blends, repeats and meshes redraw when a pattern, symbol, font or colour setting they show changes (#1688); Outline view draws from geometry kept between frames (#1689); CMYK documents with transparency compose their ink planes on several threads (#1755); an edit re-flows a threaded story only when it changes one of the story's frames (#1792); opening a file reads its linked files and lays out its type on the opening thread (#1767); Instances of imported symbols are measured, selected, aligned, culled and exported by their art instead of a 20 pt square (#1744); Export for Screens and the Asset Export panel offer TIFF (#1632); SVG import looks style rules up by id, class and type (#1690), applies `:not()` selectors (#1799, #1557), no longer stalls on stacked descendant selectors over deep nesting (#1834, #1703) and reads linked files within the automation roots again (#1870); DXF export writes the VPORT view direction once (#1789); Untrusted files: nesting is capped at 32 objects in SVG, PDF, EPS and native files and symbols or patterns that use themselves open without the offending instance or paint (#1852); deeply nested PDF and `.ai` data, page trees holding themselves and functions or colour spaces built on themselves (#1871), decompression bombs and impossible predictor parameters (#1873, #1700, #1701), brushes that draw themselves (#1872) and hostile EPS, Affinity and native data (#1863: #1715, #1714, #1704, #1699) no longer crash or exhaust the app; the save fallback keeps the old file until the new one is in (#1695); Files whose ids sit at their largest value, a failed `command.batch`, control requests that timed out and images too large as RGBA for the Eyedropper are handled (#1862: #1718, #1697, #1725, #1717); Effects: wide shadows and glows at high zoom stay within the raster budget, stacked Transform effects are bounded, 3D keeps the area a nonzero compound path fills, pattern tiles redraw when the art they show changes, and a template layer dims its images by Dim Images (#1864: #1706, #1705, #1724, #1723, #1683); A failed save or export is reported in a dialog that names the file and stays until closed (#1865, #1685); a browser download that can't start fails the save (#1693); a file that finishes opening waits while a dialog is open (#1694); krilla's text shaping is no longer built in (#1874, #1730); the CI gate compiles the web app for wasm (#1875); milestone rows and estimates unchanged | | 2026-10-14 | minor | M12 row: each documented Image Trace preset has an enhanced version (#1847) | | 2026-10-14 | minor | Existing-guide Escape cancellation: eight app-input regressions, the Direct Selection Escape control and current full CI passed; scores and hours unchanged. | -| 2026-10-15 | minor | Artboard tool: closing a clean active tab restores the displayed board before Properties Delete. Nine targeted checks passed; three original close-route captures reviewed; scores and hours unchanged. | | 2026-10-14 | minor | Artboard tab changes align the tool with the displayed board; same-tab multiselection and Move/Scale Artwork options preserved. Three tab regressions and historical full CI on `a0671777` passed; scores and hours unchanged. | | 2026-10-14 | minor | Width-point finite-arithmetic follow-up: atomicity, history and native round-trip regressions and current full CI passed; scores and hours unchanged. | -| 2026-10-13 | minor | Pinned PngSuite HTTPS files verified by SHA-256 before publication; bounded downloads replace archive extraction (#1729). Scores and hours unchanged. | -| 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 | Align and Distribute act on the anchor points selected with Direct Selection: the points of one path align to each other, not to the artboard, and Distribute spaces them (#1641, fixes #1636; aligning to a key anchor is still missing); Delete removes every selected anchor of a closed path (it used to remove the wrong ones), removes objects selected whole along with the selected anchors of others, and keeps the remainder selected so a second Delete removes it (#1674); In isolation mode Direct Selection and Group Selection pick only inside the isolated object (#1666); Escape cancels a Direct Selection drag (#1803, #1743); Escape while typing ends the edit, keeps the type selected and switches to the Selection tool (#1802, #1686, #1656); A filled Pencil path keeps a stroke of None (#1804, part of #1679); With the Pen, a press that moves under 3 screen pixels is a click (a corner without handles), and a Precise Cursors command toggles the existing preference and can take a shortcut (#1663); The [ and ] keys reach the Blob Brush, the Eraser and the Symbolism tools in the app (#1734, part of #1661); In the Pathfinder panel a group selected alone counts its nested shapes, so an expanded stroked object keeps the Shape Modes enabled (#1642, #1635); Object › Expand turns radial, grid and mirror repeats into groups of their instances (#1765, #1756); Select › Same matches type on its characters' fill and stroke (#1746, #1737); Fields commit what was typed when you click away or move to another field, as Enter does; Escape still discards it (#1787); Up and Down move through the command search results and Enter runs the highlighted one (#1798, #1757); Slider rails show on Medium Dark (#1808, #1662); On Windows the custom title bar's window stays truly borderless, so the pointer lines up with what is drawn on systems where the hidden caption offset it (#1791, #1758; awaiting the reporter's confirmation); Tool cursors are painted into the window on Wayland (#1652; not run on Linux, #1645 stays open until confirmed); Roughen with Smooth keeps turn-backs smooth instead of making cusps that draw as spikes (#1646, #1643); Scribble draws one zig-zag per region, with no lines across holes and notches; convex shapes are unchanged (#1651, #1648); EPS import: a stitched shading keeps its hard colour changes (#1654, #1653); WMF/EMF import refuses an oversized embedded PNG or JPEG from its header, before any pixels are decoded (#1776); PDF open no longer takes exponential time on a crafted name tree with shared nodes when it looks for the editing data (#1781); The size limits on a native file's embedded data also apply to files read from memory, not only streamed ones (#1783); Saving over a private file on Unix no longer leaves a readable temporary copy beside it while the file is written (#1778); An Export for Screens suffix can no longer place files outside the export folder (#1777); Package refuses a folder where it would overwrite the open document or one of its linked files (#1786); ruzstd 0.9.1 fixes a decompression bomb (RUSTSEC-2026-0344) reachable from EPS/`.ai` and Affinity import (#1807); File Info dates are stamped only after a save succeeds, so a failed save no longer changes them (#1774); Linked files that change during a drag are updated after the drag instead of being missed (#1773); Scatter graph data near the largest number is refused with a message instead of hanging the app (#1780); One very long Liquify stroke segment can no longer create an unbounded number of dabs (#1779); A drag whose last preview failed is no longer recorded as a committed command in the journal that actions and scripts read (#1775); Tests: text import fuzzing no longer searches installed fonts (#1785); Tests: render tests that add fonts no longer race the frame cache tests (#1795); `text.create`, `text.createInPath` and the MCP `add_text` tool take `justify`, so point type can be centred on or end at its point (#1752, #1748); MCP limits: 16 MiB lines, 64-message batches, 256-step `command_batch` with 8 MiB of kept results and 256 MiB control replies (#1764, fixes #1727); MCP requires the JSON-RPC 2.0 envelope and matches control replies by id, and an app refusal without an id reports the app's reason (#1768, fixes #1728); The MCP server reconnects to the app before a call when the connection was closed or holds stray bytes, after an app restart or an idle expiry (#1739); Image Trace, measured in `image-trace-quality.md`: snap Curves To Lines moves no end of a line by more than 1.6 px, so long lines a degree or two off stay as the image has them (#1647: Technical Drawing's largest distance 4.21 px to 1.46); flat Logo takes the tones a scan's uneven paper makes of one colour for one colour (#1657: on scans 470 paths and 21 px off to 8.5 paths and 0.09 px); milestone rows and estimates unchanged | | 2026-10-14 | minor | Malformed JSON app replies drop the MCP connection without replay and report uncertain completion (#1761); upstream notification and id-less refusal behavior preserved; scores and estimates unchanged. | +| 2026-10-13 | minor | Pinned PngSuite HTTPS files verified by SHA-256 before publication; bounded downloads replace archive extraction (#1729). Scores and hours unchanged. | +| 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-13 | minor | SVGs that move objects with a CSS transform (`translate(100px, 0)`, `rotate(45deg)`) or style type with the `font` shorthand import as drawn (#1565); SVG symbols whose shapes use percentage lengths import at the size and place they are drawn, the percentages being of the symbol's `viewBox` (#1596); Inkscape's flowed text (`flowRoot`) opens as area type in its rectangle, a paragraph per `flowPara`, instead of being dropped, and a region that can't be read is left out with a warning (#1569, fixes #1568); The warning for an unsupported SVG filter names the filter, its primitives and the object, and says the object is drawn without it (#1599, fixes #1598); The pages of an Inkscape multi-page SVG become artboards, named by their labels (#1603, fixes #1601); SVG export with CSS classes or style entities stays fast in documents with many distinct styles (#1567); PDF and `.ai` import: evenly letter-spaced or tightened type on a path keeps its length along the path instead of closing up to the font's advances (#1586), and type on a path whose stroke is written glyph by glyph comes in as one text object with its stroke, not one object per glyph (#1585); The ICC profiles embedded in JPEG, TIFF and PSD exports have every tag on a 4-byte boundary (ICC.1 §7.1.2), so strict readers accept them (#1589, fixes #1584); GIF, PNG-8 and other indexed exports find each pixel's palette colour through an index instead of a scan, with identical output (#1576); Saving to a network drive on macOS falls back to a plain fsync when the share refuses a full flush (#1625, part of #1620; not confirmed on a real share), and a save still succeeds when a folder refuses the temporary file, the rename over the document or copying permissions: the content is written elsewhere first and copied in, and a failed copy keeps the temporary file and names it (#1649, part of #1620); A document opened by a relative path finds its relatively linked images, and `vectorcraft-cli convert` lists what reading the input left out as `importWarnings` (#1606, fixes #1604); A zero-length subpath draws its round cap as a dot and its projecting cap as a square, so dots in imported `.ai` and SVG files show (#1619, fixes #1613); Pathfinder's Trim, Merge and Crop remove the objects' strokes, and Merge joins same-fill objects whatever their strokes were, as the public Pathfinder page describes (#1572, fixes #1570); Offset Path with an inset larger than the shapes says the offset leaves nothing, instead of asking to select paths (#1591, fixes #1590); The Symbols panel's New Symbol opens Symbol Options to name the symbol first, and Alt-click skips it (#1566, fixes #1560); Tool flyouts are as wide as their longest translated name and shortcut need, so names no longer run into shortcuts in Spanish and other languages (#1650, fixes #1616); Smart Guides: a point drawn with the Pen or a shape tool, and an anchor dragged with Direct Selection, snaps to where paths and ruler guides cross, labelled "intersect" (#1658, part of #1602; a moved selection's own anchors don't); The Transform panel shows a selected ruler guide's X or Y, and typing a value moves it (#1659, part of #1597). This is not established by the documentation we could find: the public help pages on guides, rulers and the Transform, Control and Properties panels (read again on 2026-10-14) describe selecting and dragging unlocked guides and the panels' X and Y for a selection, not a guide's position in a panel, and the one public statement of it is a user's [community answer](https://community.adobe.com/questions-652/adding-a-new-custom-guide-785586), which is not documentation. It is kept as a convenience pending the owner's decision, with no parity claim and nothing counted towards a score; Select Behind (Ctrl/Cmd-click) tests each object once, so a deep stack of overlapping objects no longer slows it; same objects in the same order (#1571); Commands that take `ids` run on the objects given with nothing selected, over MCP, the control channel and the CLI too (#1577, fixes #1575); Zig Zag spaces its ridges evenly along each segment, so lines and curves get a regular wave in Corner and Smooth (#1558); Warp › Arc (the effect and Envelope › Make with Warp) bends in the object's real proportions: a circular arc with concentric edges and no fold-over at 100%, so Arc on art that isn't square draws differently from before (#1581, fixes #1578); `effect.apply` and `effect.setParams` give an error for a misspelt or wrongly typed effect parameter instead of running the effect with its default (#1623, #1592); Area type in a non-rectangular shape moves a word too wide for the top lines down to the first line that holds it instead of breaking it (#1617, fixes #1615); Stylistic sets (ss01–ss20), stylistic and titling alternates, proportional and lining figures, and superscript and subscript glyphs can be turned on as OpenType features of a character style (`text.setStyle {features}`), and the OpenType panel keeps them (#1611, fixes #1607); The Polish translator confirmed the Polish rows the maintainers had added after the catalog was written (#1614); milestone rows and estimates unchanged | | 2026-10-13 | minor | PDF.R1: editable PDF import uses the shared pinned reader; layer and overprint scans preserve state across content windows and reject known incomplete input. Synthetic streaming, decoder-limit and dependency-source regressions added; scores and hours unchanged. | | 2026-10-13 | minor | PDF.W1: PDF export and print composition use the shared guarded writer; drawing, colour policy and editable import stay app-owned. Metadata regression coverage and immutable dependency checks added; scores and hours unchanged. | @@ -323,9 +329,9 @@ separate. Scores and hours unchanged. [Behavior and command details](development | 2026-10-13 | minor | M12 row: Image Trace has 15 presets and the Ignore Color, Shapes, Auto Grouping and Gradients options (#1574, #1609, #1618, #1627); its speed figure, measured before these, is dropped for the measurements in `image-trace-quality.md` | | 2026-10-13 | minor | The interface is available in Polish (`pl`): menus, panels, dialogs and messages, with one/few/many plurals; AI-drafted, native review pending (#1498); Native documents with a lot of path data (traced or generated art, in layers, symbols or patterns) are saved as compact JSON, about a fifth of the size; small documents stay pretty-printed (#1492); Paste takes a PDF or EPS another app copied as editable vector art: from the macOS pasteboard (with SVG), EPS on Windows beside PDF, and `.eps` files copied in a file manager, with the new command `clipboard.importEps` (#1538, fixes #1499); Opening or placing a file with an image too large to decode says so, and the Links panel shows it on the row (#1540, part of #1479); a multi-artboard `.ai` with notes beside its artboards opens from its editing data, a page's art past its artboard no longer lying over the page before it (#1528); a `.ai` gradient whose stops are listed upwards keeps each midpoint on the stop that starts its part of the ramp (#1530); SVG text on a path follows basic shapes (circle, ellipse, rect, line, polyline, polygon) as their SVG 2 equivalent paths (#1531, fixes #1527); PDFs whose alpha soft masks are drawn as images import with the mask instead of empty art (#1551); EPS and PostScript type set in Type 42 (TrueType) fonts imports at its size instead of 1000 times too large (#1553); PDF and EPS import keep a clipping path whose bounding box covers the page but whose shape doesn't (#1544, fixes #1542); Make Compound Path on a group (such as outlined type) keeps the paint of its backmost path instead of drawing nothing, and compound paths joined into one keep their holes (#1537); a round corner next to a curved side curves smoothly instead of sticking out of the shape and the Smooth tool smooths only the stretch of path under the drag (#1547, fixes #1512); Join merges the selected anchor points that lie on one another, as after Average, into one anchor (#1582, fixes #1533); Lock Guides also locks the objects made into guides: they can't be selected, moved or deleted (#1543); they are drawn in the guide colour and style of Preferences, and a dragged selection snaps to ruler guides with Smart Guides on (#1583, fixes #1534); Edit › Keyboard Shortcuts lists Edit › Duplicate and the other commands that are in no menu but run without parameters (panel-menu commands such as Break Link to Symbol), so they can be given shortcuts (#1549, #1580, fixes #1539); Raster effects (blur, shadow, glow, grain) drawn after a pattern, radial or freeform gradient fill export at their place and size (#1541, fixes #1536); the font scan on Linux also reads the folders fontconfig's configuration lists, such as TeX fonts under `/usr/share/texmf/fonts` (#1548, fixes #1529; not verified on Linux); estimates unchanged | | 2026-10-13 | minor | Direct Selection takes the selected object's anchor where anchors of overlapping objects coincide (#1508); commands and MCP `add_text` report area type that does not fit its frame (#1471), a tab stop in the Tabs panel drags from where the press landed (#1458); M9: 3D and Materials on outlined type and other shapes of many small curves (#1481), Twist 0° to 360° (#1524), twisted walls keep their faces (#1525); print-size images draw, export and crop (#1517), `vectorcraft-cli perf` reports load and memory on Windows (#1502); sliders in dialogs show their rail (#1483), a template layer stays in Preview in Outline view (#1486), French catalog complete again (#1511), a Hungarian message in the informal address (#1474); `graph.setData`'s CSV keeps each value exactly (#1485); estimates unchanged | +| 2026-10-13 | minor | M6: Split Into Grid has its documented options (#1462), the Pathfinder panel works on a group selected alone (#1450); M2: Transform Objects in the Transform dialogs and Transform Each, Reflect across an angle (#1448, #1460); M4: grayscale TIFFs with an alpha channel or extra channels place (#1466 in part), strokes kept apart from a faded or masked fill on PDF and `.ai` import (#1434), SVG knockout groups without masks (#1435); M7: Fit Headline in a frame one line tall (#1444), frames beyond a billion points hold no text (#1451); M13: commands refuse a parameter key they don't take (#1442); M5: edits and Alt-drags of thousands of objects (#1429 in part); M14: a focused field follows its value (#1457), selected text in value fields is readable (#1465 in part), a window surface past 8192 px no longer closes the app (#1459 in part); estimates unchanged | | 2026-10-12 | minor | Width Point Edit: Shift-double-click enables adjoining adjustment; plain double-click keeps it off; scores and hours unchanged | | 2026-10-12 | minor | Exchange with Illustrator users: Save As `.ai` › Version: Legacy (version 7) keeps layers, groups and type as text, one artboard per file (#868); estimate unchanged until users test it | -| 2026-10-13 | minor | M6: Split Into Grid has its documented options (#1462), the Pathfinder panel works on a group selected alone (#1450); M2: Transform Objects in the Transform dialogs and Transform Each, Reflect across an angle (#1448, #1460); M4: grayscale TIFFs with an alpha channel or extra channels place (#1466 in part), strokes kept apart from a faded or masked fill on PDF and `.ai` import (#1434), SVG knockout groups without masks (#1435); M7: Fit Headline in a frame one line tall (#1444), frames beyond a billion points hold no text (#1451); M13: commands refuse a parameter key they don't take (#1442); M5: edits and Alt-drags of thousands of objects (#1429 in part); M14: a focused field follows its value (#1457), selected text in value fields is readable (#1465 in part), a window surface past 8192 px no longer closes the app (#1459 in part); estimates unchanged | | 2026-10-12 | minor | TIFF is available in Export for Screens and Asset Export with shared format settings; scores and hours unchanged | | 2026-10-12 | minor | Character styles retain explicit Auto leading, propagate resets and preserve local leading overrides; scores and hours unchanged | | 2026-10-12 | minor | M9: Extrude & Bevel, Inflate and Rotate (initial 3D types, [three-d.md](three-d.md)) | @@ -336,7 +342,6 @@ separate. Scores and hours unchanged. [Behavior and command details](development | 2026-10-12 | minor | M11: Artboard object clicks create a fitted artboard (#1227); estimate unchanged | | 2026-10-12 | minor | Pathfinder single-result booleans preserve source names and SVG IDs (#1293); paint-order, grouped/compound sources, unchanged geometry/appearance and Undo/Redo regression tests; scores and estimates unchanged | | 2026-10-12 | minor | DXF reads control-point SPLINE closure (#1299 follow-up after #1320); open curves stay open and curves retained; rational/non-rational import and export versions regression-tested; scores/hours unchanged | -| 2026-10-11 | minor | Czech render-site follow-up: file-dialog filters, diagnostics, preset numbering, contributor tooltips/plurals, gradient grammar and contextual printer mark styles; estimates unchanged | | 2026-10-12 | minor | Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged | | 2026-10-12 | minor | Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged | | 2026-10-12 | minor | M6: Simplify preview/counts and valid selection after topology changes (#1272); auto/slider/advanced UI remains open; estimates unchanged | @@ -350,26 +355,23 @@ separate. Scores and hours unchanged. [Behavior and command details](development | 2026-10-12 | minor | M3: Eyedropper reads visible locked and outside-isolation sources (#1251); estimates unchanged | | 2026-10-12 | minor | M2: independent segment selection/display and geometry-preserving Delete (#1278); estimates unchanged | | 2026-10-12 | minor | M3: adding paint to an unpainted zero-width stroke restores 1 pt (#1253); estimates unchanged | +| 2026-10-12 | minor | M14: native macOS save Format menu (#866); filename regression tests, actual SVG save and PDF selection/cancel verified; estimates unchanged | +| 2026-10-12 | minor | M6: compound paths inherit the backmost appearance; selected members read and change the compound fill rule (#1197); estimates unchanged | +| 2026-10-12 | minor | M6: Divide accepts open cutting paths (#1307), preserving filled-face appearance and Undo; the existing segment cap remains; estimates unchanged | +| 2026-10-12 | minor | M14: numeric-field focus and text-drag selection regression-tested (#1326, in part); estimates unchanged; short movements below the click threshold remain open | +| 2026-10-11 | minor | Czech render-site follow-up: file-dialog filters, diagnostics, preset numbering, contributor tooltips/plurals, gradient grammar and contextual printer mark styles; estimates unchanged | | 2026-10-11 | minor | M12: the Graph Data worksheet. Scores/hours unchanged. | | 2026-10-11 | minor | M12: graph label formatting kept through regeneration. Scores/hours unchanged. | | 2026-10-11 | minor | M12: Graph Type's widths past 100%, First Row/Column in Front, Draw Filled Lines. Scores/hours unchanged. | | 2026-10-11 | minor | M12: Graph Type's Add Drop Shadow and Add Legend Across Top. Scores/hours unchanged. | | 2026-10-11 | minor | M12: Graph Data's Transpose and Switch x/y, `|` line breaks in graph labels, pie Legend/Position/Sort. Scores/hours unchanged. | | 2026-10-11 | minor | M12: Object › Graph › Design… and Marker… (graph designs, marker designs); column designs remain. Scores/hours unchanged. | -| 2026-10-12 | minor | M14: native macOS save Format menu (#866); filename regression tests, actual SVG save and PDF selection/cancel verified; estimates unchanged | -| 2026-10-12 | minor | M6: compound paths inherit the backmost appearance; selected members read and change the compound fill rule (#1197); estimates unchanged | -| 2026-10-12 | minor | M6: Divide accepts open cutting paths (#1307), preserving filled-face appearance and Undo; the existing segment cap remains; estimates unchanged | -| 2026-10-12 | minor | M14: numeric-field focus and text-drag selection regression-tested (#1326, in part); estimates unchanged; short movements below the click threshold remain open | | 2026-10-11 | minor | M9: PDF raster-effect resolution-cap warnings verified with real capped/uncapped exports; scores and hours unchanged | | 2026-10-11 | minor | #893 (Shape Builder seams on grids of lines) fixed; G11 keeps #937 and its 8–15 h | | 2026-10-10 | minor | M7: document-point character sizes, leading and baseline shift, and resettable horizontal stretching after object/group scaling (#1034). Optional group font-size preservation remains separate; scores/hours unchanged. | | 2026-10-10 | minor | M9 row from main: initial Revolve | | 2026-10-10 | minor | Added the Alpha gate table (core-workflow gate): all six workflows work end to end, `.ai` exchange partial but not blocking; stage stays alpha | | 2026-10-10 | major | Created from ROADMAP.md's milestone table and "Where we're going"; added Current focus and the beta list | -| 2026-10-28 | minor | Flare ray rotation follows vertical travel at one degree per document point, independent of pointer event count. Shift freezes the current orientation; Space also freezes rotation on the first update. This is the chosen application interaction rate. Scores and estimates unchanged. | -| 2026-10-28 | minor | Drop Shadow Darkness subtracts the normalized amount from RGB source channels or adds it to CMYK/Gray black before profile conversion and blur; near-100% becomes gray black, with shadow opacity separate. SVG keeps live filters for uniform sRGB sources and renders shadows for other RGB profiles, source models and gradients. Gradient-stop adjustment is an application rule; spot/custom colors and composite-source parity remain open. Scores and estimates unchanged. | -| 2026-10-28 | minor | Blob Brush Shift locks after clear axial movement, keeps earlier samples fixed, and waits without an off-axis trail. Activation uses raw press displacement in logical screen pixels, with exact quarter-turn boundaries. Explicit application rule; scores and estimates unchanged. | -| 2026-10-28 | minor | Flare Tool Options opened by a canvas click previews its settings at that click. Reset refreshes the enabled preview while keeping the dialog open. Cancel discards preview art and tentative options; OK accepts one flare in one Undo step. Editing existing flare artwork remains outside this dialog scope. Scores and estimates unchanged. | > **Tab-drag status (2026-10-22):** updated from upstream, including the Turkish catalog and all three tab-drag translations requested in #3479; Selection-tool reset regression-checked and clipboard evidence scoped to internal state; drag lifecycle, insertion and regression contracts documented; behavior, scores and estimates unchanged. diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 5694ef7d98..a86564b6a4 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -51,26 +51,24 @@ Artboard Options forwards the active Artboard tool's Scale Artwork with Artboard | Date | Change | Summary | |---|---|---| -| 2026-11-03 | minor | A double-click on a ruler adds a guide at the pointer (#5239): a vertical one on the top ruler, a horizontal one on the left ruler; 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. | | 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. | | 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 | 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-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-07 | minor | Pen and Curvature tool with Cmd/Ctrl held: Edit › Copy and Cut take the anchors and segments the lent Direct Selection picked, not the whole path; with the Pen's own last anchor selected Copy still takes the whole path (#6123); scores and estimates unchanged | | 2026-11-07 | minor | Live Corners: with Direct Selection, while every corner of a path is selected (the path selected whole), a Shift-click on a corner widget picks that corner alone, as the first click of "click one widget, then Shift-click the others" does, instead of deselecting it and hiding its widget (#5188). The other corners' widgets then hide: a Shift-click on another corner's anchor adds that corner and shows its widget, and a Shift-click on a showing widget deselects its corner. Adding a corner by Shift-clicking its widget while it is hidden stays open (#1232). Test: `corners::tests::a_shift_click_on_a_widget_of_a_whole_path_picks_its_corner`. Scores and estimates unchanged. | | 2026-11-07 | minor | Live Corners: with Direct Selection, while every corner of a path is selected (the path selected whole), a Shift-click on a corner widget picks that corner alone, as the first click of "click one widget, then Shift-click the others" does, instead of deselecting it and hiding its widget (#5188). Once some corners are picked, Shift-click still adds or removes one. Test: `corners::tests::a_shift_click_on_a_widget_of_a_whole_path_picks_its_corner`. 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 | The canvas keeps selection outlines, anchors and handles between frames, so an idle frame with a large selection redraws them as one mesh; scores and estimates unchanged | | 2026-11-07 | minor | Stroke gradients along or across a variable-width stroke: where the stroke crosses or runs close by itself, a narrow part paints over a wide part only where the two overlap, instead of in a square as wide as the stroke gets anywhere (#5960). Each slice reaches as far as the stroke is wide there (for a dashed stroke, wherever a cap or dot can reach, found in one pass along the path however densely it is flattened). Tests: `stroke::tests_gradient::a_narrow_pass_over_a_wide_one_paints_only_where_they_overlap`, `a_narrow_pass_beside_a_wide_one_leaves_its_edge_alone`, `a_densely_flattened_dashed_pass_beside_a_wide_one_leaves_its_edge_alone`, `widest_ahead_is_the_widest_within_the_window`. 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-07 | minor | Type: paint-only Arabic harakat keep joining forms; a matching character-level pass assigns a later in-cluster run only for GDEF mark glyphs on harakat characters (otherwise cluster start); Insert Kashidas tatweels take the base letter's paint (`tests::harakat`). Scores and estimates unchanged. Marks may retain letter paint without GDEF mark classes or with some AAT/`morx` ligatures. | | 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 | 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-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. | @@ -86,40 +84,42 @@ Artboard Options forwards the active Artboard tool's Scale Artwork with Artboard | 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); a printing layer's name is upright, and Print on again makes it upright again; scores and estimates unchanged. | | 2026-11-05 | minor | G12: Hyphenation › Default Language sets the language of new type unless Document Setup chose one, stored as a tag; each run hyphenates in its own language, English only so far, so 2 Preferences options stay unused (#394) | | 2026-11-05 | minor | Type Tools add-on: Release Type on a Path (a VectorCraft addition, #5532): evidence added; scores unchanged. | -| 2026-11-04 | minor | Path Precision Tools add-on (a VectorCraft addition): Set Corners… and `addon.pathtools.cornerSet` `minAngle`/`maxAngle` round only the corners whose angle is in a range (#5545); evidence added; scores unchanged. | -| 2026-11-04 | minor | Path Precision Tools add-on (a VectorCraft addition): Set Corners… and `addon.pathtools.cornerSet` `every` round all, the odd-numbered or the even-numbered anchors of each path (#5546); evidence added; scores unchanged. | -| 2026-11-04 | minor | Transform Each's Scale has the documented Relative and Absolute radio buttons (`object.transformEach` `relative`: percentages of each object's size, or the width and height it becomes, measured on each object's own bounds) and the link that keeps the two scales in proportion; the panel lists Scale, Move, Rotate, the Options in two columns, the Reference Point and Preview (level with Copy, Cancel and OK) in the documented order. Scores and estimates unchanged. | | 2026-11-05 | minor | PDF import: a DeviceN gradient of a spot ink over process plates keeps the ink and the plates, and PDF export writes it as a DeviceN shading again (#3235); scores unchanged | | 2026-11-05 | minor | Optical kerning moves each pair part of the way toward the font's own stem spacing, with a minimum gap and tighter limits, and leaves ideographs, kana, fullwidth forms and dashes on the font's spacing, so text is no longer squeezed (#5336). Scores and estimates unchanged. | | 2026-11-05 | minor | Gradient panel: Delete Stop sits at the right end of the ramp, so it shows whenever the ramp does instead of under the stop fields, where a short panel hid it (#5403); scores and estimates unchanged | +| 2026-11-05 | minor | Floating panels keep out of the main window's way: hidden under a modal dialog, and out of the top level while a menu is open (#5238); scores and estimates unchanged. | +| 2026-11-04 | minor | Path Precision Tools add-on (a VectorCraft addition): Set Corners… and `addon.pathtools.cornerSet` `minAngle`/`maxAngle` round only the corners whose angle is in a range (#5545); evidence added; scores unchanged. | +| 2026-11-04 | minor | Path Precision Tools add-on (a VectorCraft addition): Set Corners… and `addon.pathtools.cornerSet` `every` round all, the odd-numbered or the even-numbered anchors of each path (#5546); evidence added; scores unchanged. | +| 2026-11-04 | minor | Transform Each's Scale has the documented Relative and Absolute radio buttons (`object.transformEach` `relative`: percentages of each object's size, or the width and height it becomes, measured on each object's own bounds) and the link that keeps the two scales in proportion; the panel lists Scale, Move, Rotate, the Options in two columns, the Reference Point and Preview (level with Copy, Cancel and OK) in the documented order. Scores and estimates unchanged. | | 2026-11-04 | minor | Record Select Text by Font's artboard and layer scope (#5529, a VectorCraft addition); scores and estimates unchanged. | -| 2026-11-07 | minor | Type: paint-only Arabic harakat keep joining forms; a matching character-level pass assigns a later in-cluster run only for GDEF mark glyphs on harakat characters (otherwise cluster start); Insert Kashidas tatweels take the base letter's paint (`tests::harakat`). Scores and estimates unchanged. Marks may retain letter paint without GDEF mark classes or with some AAT/`morx` ligatures. | | 2026-11-04 | minor | Type core: a Type-range Copy or Cut publishes styled art alongside plain text, so canvas Paste keeps font family, size and color after editing ends (#5636); scores and estimates unchanged. | | 2026-11-04 | minor | Selection Tools add-on: Select by Artboards selects the objects overlapping, fully inside or fully outside several chosen artboards, from a dialog that lists, searches and multi-selects the artboards and fits one in the window on double-click (#5525, #5548, #5549, a VectorCraft addition); scores and estimates unchanged. | | 2026-11-04 | minor | Raster/PSD defaults honor document resolution; CLI run/convert accept --ppi, matching MCP options.ppi precedence. Synthetic regressions added; scores and hours unchanged. | -| 2026-11-05 | minor | Floating panels keep out of the main window's way: hidden under a modal dialog, and out of the top level while a menu is open (#5238); scores and estimates unchanged. | | 2026-11-04 | minor | New Document: the units chosen in Preset Details belong to the chosen preset, as the public page lists the pane's options 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 (#2078). Scores and estimates unchanged. | | 2026-11-04 | minor | Document Tools add-on (a VectorCraft addition): Batch Trace Folder under Object › Plug-ins (`addon.document.batchTraceFolder`, #4921); evidence added; scores unchanged. | | 2026-11-04 | minor | Object › Expand turns selected symbol instances into their art as Break Link places it, one level deep, with the Expand dialog's Fill and Stroke reaching that art. Scores and estimates unchanged. | +| 2026-11-04 | minor | Record Find Font's replacement rows: family and style together, a search over them, and a scroll state of their own (#4972); scores and estimates unchanged. | +| 2026-11-03 | minor | A double-click on a ruler adds a guide at the pointer (#5239): a vertical one on the top ruler, a horizontal one on the left ruler; 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-03 | minor | The OpenType panel offers the selected font's stylistic sets by custom or numbered name, and allows several to be active. Scores and estimates unchanged. | | 2026-11-03 | minor | Actions panel: an expanded action's steps can be excluded from playback and Batch… by clearing their checkmarks; action and set checkmarks include or exclude all their steps, and exclusions survive restart, duplication and `.vcactions` save/load. Scores and estimates unchanged. | | 2026-11-03 | minor | Correct the Type core missing list and remove duplicate target and Type core entries: Hyphenation and Justification options are complete; the remaining Every-line Composer fidelity limits stay listed. Record the Kashidas paragraph lengths and per-character switch in the feature history (#5404). Scores and estimates unchanged. | | 2026-11-03 | minor | Correct the Type core missing list: Hyphenation and Justification options are complete; the remaining Every-line Composer fidelity limits stay listed. Record the Kashidas paragraph lengths and per-character switch in the feature history (#5404). Scores and estimates unchanged. | | 2026-11-03 | minor | Ligature caret stops use GDEF contour-point positions in horizontal and vertical type at the font's default variation location; invalid point indices keep the evenly divided fallback. Scores and estimates unchanged. | -| 2026-11-04 | minor | Record Find Font's replacement rows: family and style together, a search over them, and a scroll state of their own (#4972); scores and estimates unchanged. | | 2026-11-03 | minor | Spiral decay applies once per full wind of four segments; newly drawn spirals with the same settings wind more broadly than earlier builds. Existing paths are unchanged. Scores and estimates unchanged. | | 2026-11-03 | minor | New gradients use CMYK default stops in CMYK documents, including Gradient tool creation. Scores and estimates unchanged. | | 2026-11-03 | minor | Type Tools add-on: Text Tweaker (a VectorCraft addition, #4924): evidence added; scores unchanged. | | 2026-11-03 | minor | Transform previews valid typed values while numeric fields remain focused. Effect dialogs use the object, group, or layer targeted in Appearance. | | 2026-11-03 | minor | Help Bar has its own Window menu toggle and workspace visibility setting. Scores and estimates unchanged. | -| 2026-11-02 | minor | Legacy `.ai` saves (version 7) write type with rotated characters as outlines that keep each character's rotation, with a warning unless `outlineText` is on (#5186); scores unchanged. | | 2026-11-03 | minor | Record shipped Find Font replacement sources: Document, Recent and System (#5217); scores and estimates unchanged. | -| 2026-10-31 | minor | Object › Path › Smooth smooths a path along its length before refitting it, so Auto-Smooth takes a shaky line to one smooth curve instead of leaving its jags, and with anchor points direct-selected it smooths only that section; scores unchanged. | | 2026-11-03 | minor | Enclosing Lasso adds freehand whole-object enclosure selection under Object › Plug-ins; scores and estimates unchanged. | -| 2026-10-31 | minor | New Document: More Settings has the Save Preset button beside the name too, saving its settings (artboard layout included) as a preset (#4893); scores unchanged. | | 2026-11-03 | minor | Touch Type moves right-to-left characters along their line when a preceding logical character is on the same line; direction rules are VectorCraft's choices. Scores and estimates unchanged. | -| 2026-10-31 | minor | Object › Rasterize in a CMYK document with its CMYK colour model makes a CMYK image of the art's ink amounts (as CMYK exports draw them), so the Properties panel says CMYK and CMYK exports keep its inks; a transparent background becomes the image's opacity mask (#4882); scores unchanged. | | 2026-11-03 | minor | Composition spacing stays along the line under Character Rotation, including the turning point and unrotated placement; 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-02 | minor | Legacy `.ai` saves (version 7) write type with rotated characters as outlines that keep each character's rotation, with a warning unless `outlineText` is on (#5186); scores unchanged. | +| 2026-10-31 | minor | Object › Path › Smooth smooths a path along its length before refitting it, so Auto-Smooth takes a shaky line to one smooth curve instead of leaving its jags, and with anchor points direct-selected it smooths only that section; scores unchanged. | +| 2026-10-31 | minor | New Document: More Settings has the Save Preset button beside the name too, saving its settings (artboard layout included) as a preset (#4893); scores unchanged. | +| 2026-10-31 | minor | Object › Rasterize in a CMYK document with its CMYK colour model makes a CMYK image of the art's ink amounts (as CMYK exports draw them), so the Properties panel says CMYK and CMYK exports keep its inks; a transparent background becomes the image's opacity mask (#4882); scores unchanged. | | 2026-10-31 | minor | Export for Screens is laid out as Illustrator's: Artboards and Assets tabs with a grid or list of thumbnails, Clear Selection and Prefix under them, Create Sub-folders by Scale or Format (on by default), Export PDFs As Single File or Multiple Files (`pdfs`), iOS and Android rows, the format presets in a ☰ menu, and a Selected / files-to-export count; the naming pattern, Layers and Selection pages and folders list stay in `document.exportForScreens` for scripts and MCP; scores unchanged. | | 2026-10-31 | minor | Export for Screens is laid out as the reference app's: Artboards and Assets tabs with a grid or list of thumbnails, Clear Selection and Prefix under them, Create Sub-folders by Scale or Format (on by default), Export PDFs As Single File or Multiple Files (`pdfs`), iOS and Android rows, the format presets in a ☰ menu, and a Selected / files-to-export count; the naming pattern, Layers and Selection pages and folders list stay in `document.exportForScreens` for scripts and MCP; scores unchanged. | | 2026-10-31 | minor | Artwork Cleanup add-on: Join Point Text (`addon.cleanup.joinText`) joins stacked one-line point type into one object, a paragraph per line keeping its baseline (#4826); scores unchanged. | @@ -242,7 +242,7 @@ dominate; 3D, graphs and automation are specialist). They sum to 105. Feature de | 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 PDF marked-text batching covered by M4 evidence (#6309); cross-line/style ActualText layout (#6165) and styled-cluster limits remain.| 10–20 | estimated (lowered from 85%: open issues); DXF control-point SPLINE import closure regression-tested (#1299 follow-up after #1320); DXF export's Include Placed Images and Include Fill Hatches options, off in CNC / CAM (`images_and_hatches_are_left_out_independently_and_the_cnc_preset_leaves_out_both`; score, gaps and hours unchanged); 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. PDF ActualText preserved in editable import (#6165; bounded synthetic coverage below); score/hours 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 PDF marked-text batching covered by M4 evidence (#6309); cross-line/style ActualText layout (#6165) and styled-cluster limits remain.| 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. PDF ActualText preserved in editable import (#6165; bounded synthetic coverage below); score/hours 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 PDF marked-text batching covered by M4 evidence (#6309); cross-line/style ActualText layout (#6165) and styled-cluster limits remain.| 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. PDF ActualText preserved in editable import (#6165; bounded synthetic coverage below); score/hours 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 PDF marked-text batching covered by M4 evidence (#6309); cross-line/style ActualText layout (#6165) and styled-cluster limits remain.| 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. PDF ActualText preserved in editable import (#6165; bounded synthetic coverage below); score/hours unchanged. Save refusal continuation (#6146): `READ_ONLY_LOCATION`, Save As on ordinary UI Save only; regression execution pending; score/hours 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) | | 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) | | Guides, grids, smart guides, snapping, rulers | 3 | 80% | "intersect" smart guides for a moved selection's anchors (drawn points and dragged anchors snap to intersections, #1658), distance readouts while drawing | 4–7 | estimated (lowered from 85%: target moved); Control bar snapping quick access includes Snap to Glyph and Alignment Guides (#6164); new ruler-guide Alt orientation implemented; global/artboard ruler modes (`view.globalRulers`) and video rulers (View › Rulers › Show Video Rulers, #2849) done; status-bar X/Y uses the active ruler origin and General units; score unchanged | @@ -555,6 +555,8 @@ since 2026-10-01. ## Evidence: what ships today, by area +- **Save refusal (#6146):** ordinary File > Save to its own location offers Save As after a terminal permission/read-only refusal, suggesting the original name and format, preserving options for the unchanged format and opening the chosen format’s Save options on a change. Cancel keeps the failed-save state. Busy pickers, recovery-copy errors, other failures, explicit destinations and Close/Quit keep the existing dialog. Engine, UI and MCP regression cases are in `cmd/fileio/tests_readonly.rs`, `io/tests_read_only_save.rs` and MCP `tests/fileio.rs`; focused UI regressions passed; engine regression execution and native panel acceptance pending. Scores and hours unchanged. + - **PDF authored replacement text (#6165):** `crates/pdf/src/tests/actual_text.rs` covers replacement once with suffix placement, PDF text-string decoding, empty and nested owners, sibling spans, page reset, multiple text-show operations, style/line changes, fill-and-stroke, reader windows and bounded input. Authored logical-order stretches are protected while adjacent ordinary text still undergoes reconstruction. The replacement takes its first placed glyph's style and line; its characters are not redistributed over later lines/styles. Exact mixed-direction paragraph layout is not claimed. No export changes; scores and hours stay at 75% / 10–20 h. @@ -1306,7 +1308,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur Double-clicking the Objects on Path tool opens Objects on Path Tool Options ([Objects on Path](https://helpx.adobe.com/illustrator/using/objects-on-path.html)): the nine-point Pivot grid and the Rotate field set the attachment point and angle of the objects attached next, and groups already attached keep theirs; Reset puts the center and 0° back. Save keeps the options across tool switches and saves them with the preferences (`tool.setOption {tool: "objectsOnPath"}`). - **Paint and appearance (M3):** - **CSS swatch libraries:** fractional linear and radial gradient stop positions are preserved, so nearby stops such as 12.5% and 12.890625% remain distinct instead of collapsing to 13%. Reference: [CSS Images 3 color stops](https://www.w3.org/TR/css-images-3/#color-stop-syntax). The original synthetic regression is `palette_io::tests::css_preserves_fractional_gradient_stop_positions` in `crates/color/src/palette_io.rs`. Scores and hours are unchanged. - - **Swatches:** names follow the colour model and are unique; Swatch Options (process/spot, Global, Gray/RGB/HSB/CMYK/Web, live preview); editing a global or spot swatch recolours every linked fill, stroke and text run in one undo step; multi-select and delete with confirmation; New Swatch and New Color Group dialogs (a group from the selected artwork's colours); swatches in colour groups convert, separate and count like any other. The Swatches panel selects ranges and whole groups, has a find field, and drags swatches to reorder, regroup or paint art; Add Used Colors, Select All Unused, Merge, Ungroup and Sort by Kind; a CMYK document keeps applied colours in CMYK. Swatch libraries: nine generated colour libraries, seven gradient libraries (among them Organic Fades, soft perceptually mixed multi-stop fades that thin out to transparent, and Holographic, seven opaque linear foils of narrow pastel and pearl bands and seven radial glows that fade out to a clear edge) and two pattern libraries (Geometric Patterns, and Grit Textures with eight seamless grain, speckle, stipple, scratch and fleck tiles; their swatches bring their patterns) in a library panel, saved as `.vcswatches`, `.gpl`, swatch exchange (`.ase`) or CSS, and loaded from `.vcswatches`, `.gpl` and `.ase` files (`.ase` both ways: RGB, CMYK, Lab and Gray; global, spot and process; groups) and from color books (`.acb`, read only: the RGB, CMYK or Lab colors of a book such as a Pantone book a user owns, named with its prefix and suffix, spot unless it says process, #831); `swatch.library.copyToUser` copies a loaded library into the User Defined folder without replacing a file, and a file with the same extension and bytes as a User Defined library opens as that library. Tints of global and spot colours ("Name 40%") stay linked and separate as a percentage of their plate. Spot colours can be defined in Lab (Spot Colors options). A built-in [Registration] swatch prints on every plate. + - **Swatches:** names follow the colour model and are unique; Swatch Options (process/spot, Global, Gray/RGB/HSB/CMYK/Web, live preview); editing a global or spot swatch recolours every linked fill, stroke and text run in one undo step; multi-select and delete with confirmation; New Swatch and New Color Group dialogs (a group from the selected artwork's colours); swatches in colour groups convert, separate and count like any other. The Swatches panel selects ranges and whole groups, has a find field, and drags swatches to reorder, regroup or paint art; Add Used Colors, Select All Unused, Merge, Ungroup and Sort by Kind; a CMYK document keeps applied colours in CMYK. Swatch libraries: nine generated colour libraries, seven gradient libraries (among them Organic Fades, soft perceptually mixed multi-stop fades that thin out to transparent, and Holographic, seven opaque linear foils of narrow pastel and pearl bands and seven radial glows that fade out to a clear edge) and three pattern libraries (Geometric Patterns; Grit Textures with eight seamless grain, speckle, stipple, scratch and fleck tiles; and Optical Effects with eight of VectorCraft's own seamless wave, ring, warped-check, stripe, chevron and lens tiles; their swatches bring their patterns) in a library panel, saved as `.vcswatches`, `.gpl`, swatch exchange (`.ase`) or CSS, and loaded from `.vcswatches`, `.gpl` and `.ase` files (`.ase` both ways: RGB, CMYK, Lab and Gray; global, spot and process; groups) and from color books (`.acb`, read only: the RGB, CMYK or Lab colors of a book such as a Pantone book a user owns, named with its prefix and suffix, spot unless it says process, #831); `swatch.library.copyToUser` copies a loaded library into the User Defined folder without replacing a file, and a file with the same extension and bytes as a User Defined library opens as that library. Tints of global and spot colours ("Name 40%") stay linked and separate as a percentage of their plate. Spot colours can be defined in Lab (Spot Colors options). A built-in [Registration] swatch prints on every plate. Editing, renaming, deleting or merging a global swatch also changes the colors linked to it in graph designs, the art of brushes and the tile that pattern editing's Cancel puts back, in the same undo step as the rest of the art; Default Swatches with replace unlinks them there too. Add Used Colors (`swatch.addUsedColors`) and Select All Unused (`swatch.unused`) look colors, swatch names, links, tints and gradients up in hash sets instead of searching every swatch and color for each color or linked paint (New Color Group from artwork too), and New Swatch with several colors (`swatch.new`), New Color Group (`swatch.newGroup`) and Edit Color Group (`swatch.editGroup`) name their new swatches from one set of the names already taken. The new swatches keep their names, order and links, and Select All Unused lists the same swatches. Evidence: `add_used_colors_adds_each_new_color_once_with_free_names`, `add_used_colors_global_links_every_paint_of_a_new_color`, `unused_matches_links_tints_colors_gradients_and_patterns_exactly`, `new_swatches_from_colors_are_numbered_after_names_already_taken`, `new_color_group_numbers_its_colors_after_the_group_and_its_moved_swatches`, `new_color_group_from_artwork_lists_each_color_once_with_free_names` and `edit_color_group_numbers_extra_colors_after_the_names_it_keeps` in `crates/engine/src/tests/swatchcmds.rs`; `free_swatch_names_match_naming_each_swatch_as_it_is_added` and `free_swatch_names_match_new_swatch_names_as_swatches_are_added` in `crates/doc/src/swatches.rs`. - **Colorized images (#1466):** a fill color applied to a selected grayscale or 1-bit image (`paint.setFill`) colorizes it with black, white, a process color or a spot color, and refuses a process color when the image has an alpha channel ([Convert color modes](https://web.archive.org/web/20251220223850/https://helpx.adobe.com/illustrator/desktop/manage-colors/select-and-adjust-colors/convert-color-modes.html), "Colorize Grayscale or 1-bit images"); black and white are accepted there too. The canvas draws each pixel in the color as far as the pixel is dark, with white pixels transparent; PDF export writes the image in the color (a spot color as its ink, Registration in every ink) with the image as its soft mask; Separations Preview and printed separations show it on its color's plates; native files keep the color. SVG writes the fill color through a coverage mask (or a colorized PNG in Tiny 1.2); EPS writes binary coverage as an image mask in the color's ink, and a grayscale image as ink samples scaled by coverage, with white pixels left out at Level 3 and gray midtones written over white. @@ -2082,14 +2084,17 @@ Edit › Copy and Cut put the selected ruler guides on the clipboard, with the s | 2026-11-03 | minor | File Info on open: an imported document's own title (an SVG's root `<title>`, a PDF's, `.ai`'s or `.ait`'s XMP `dc:title` else its Info `Title`) goes into File Info only; the document's name (tab, Save As, default export names) stays the file's name (#3626); scores and estimates unchanged. | | 2026-11-12 | minor | Type: kerning at the Type tool's caret at the start of a line (the text's, a paragraph's, or after a forced line break) sets the space before the character after it (`kerningBefore`), which moves it and the rest of its line; it did nothing at the text's start and kerned the invisible line break at a paragraph's (#5111). Scores and estimates unchanged. | | 2026-11-12 | minor | Live Paint Bucket fills faces crossed in one drag (#6373). Scores and estimates unchanged. | +| 2026-11-12 | minor | Save refusal follow-up (#6146): changed format opens its Save options; unchanged format retains original options. Focused UI regressions passed, including the format-change case that failed before the fix; native panel acceptance pending. Scores and estimates unchanged. | | 2026-11-11 | minor | Rulers: the zero point dragged out of the rulers' corner snaps as a drawn point does (Smart Guides, Snap to Point and the ruler guides, pixels, the grid); evidence updated; scores unchanged. | | 2026-11-11 | minor | Eyedropper copies effective character dimensions between transformed text objects (#6404); scores and estimates unchanged. | | 2026-11-11 | minor | Envelopes: with Distort Appearance, type's strokes (characters' and appearance) paint along the bent glyph outlines at their own weight, as users' `.ai` files draw them; paths' strokes, geometry effects, gradients and patterns bend as before; also in an envelope inside an envelope; opacity masks bend as other art; Expand keeps type's strokes as strokes; the visual bounds of envelopes, blends and repeats and the reach of symbol art count type's character strokes as type's own bounds do. Changes how VectorCraft's own envelopes of stroked type draw. Advanced art evidence and `.ai` import evidence updated (#6644); scores and estimates unchanged. | +| 2026-11-11 | minor | Ordinary Save to a location refusing writes offers Save As (#6146); cancellation preserves the original path and modified state; automation receives READ_ONLY_LOCATION. Scores and estimates unchanged. Validation pending. | | 2026-11-11 | minor | Bundled content: built-in Energy Beams brush library (Laser Bolt, Plasma Arc, Charge Wave, Spark Trail); brush libraries evidence updated; ~44% → ~46%; more built-in brush libraries remain. | | 2026-11-11 | minor | Bundled content: built-in Skill Icons symbol library (Attack, Defend, Heal, Buff, Ult); symbol libraries evidence updated; ~42% → ~44%; more built-in symbol libraries remain. | -| 2026-11-10 | minor | Automation: MCP tool count 33; headless `command_list` lists `UI_COMMANDS` (`ui: true`); eight prompts; effect-id vs command-id clarified (#6460); scores and hours unchanged. | | 2026-11-11 | minor | Bundled content: built-in Status Effects graphic style library; style libraries evidence updated; ~40% → ~42%; MCP Symbol libraries list UI Frames and Map Markers; more built-in graphic-style libraries remain. | | 2026-11-11 | minor | Bundled content: built-in Inventory Icons symbol library (Sword, Shield, Potion, Key, Gem); symbol libraries evidence updated; ~38% → ~40%; more built-in symbol libraries remain. | +| 2026-11-11 | minor | Guides and snapping: a Shift move slides the selection along its line onto a ruler guide by its edges and centre, as a move without Shift does (#4312); a guide a nearly parallel line only reaches a long way off pulls nothing. Score and hours unchanged (estimated). | +| 2026-11-10 | minor | Automation: MCP tool count 33; headless `command_list` lists `UI_COMMANDS` (`ui: true`); eight prompts; effect-id vs command-id clarified (#6460); scores and hours unchanged. | | 2026-11-10 | minor | Type advanced: the Edit Custom Dictionary dialog, #4936; evidence added; the Check Spelling dialog remains; scores and hours unchanged. | | 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. | @@ -2097,6 +2102,8 @@ Edit › Copy and Cut put the selected ruler guides on the clipboard, with the s | 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. | +| 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-10 | minor | Vector Halftone Preview leaves the dots unobscured and keeps the selection box (#6369). Scores and estimates unchanged. | | 2026-11-09 | minor | Canvas: a 3D effect changed on type or a group redraws where its solid was, measured by the effect's art through the renderer that drew the frame; exports and Object › Rasterize take the solid of 3D type; Fit to Artwork Bounds and per-artboard SVG export measure all artboards through one renderer; a 3D effect on a fill or stroke that builds nothing no longer overflows the stack on the canvas, in Object › Rasterize and in raster and EPS exports (evidence in Feature revisions). Scores and estimates unchanged. | | 2026-11-09 | minor | Bundled content: built-in Game Trails brush library (Speed Line, Dash Trail, Burst Ray, Zigzag); brush libraries evidence updated; ~32% → ~34%; more built-in brush libraries remain. | | 2026-11-09 | minor | Type advanced: Auto Spell Check (`edit.autoSpellCheck`), #4936; evidence added; the dialogs and the right-click Spelling menu remain; scores and hours unchanged. | @@ -2110,14 +2117,13 @@ Edit › Copy and Cut put the selected ruler guides on the clipboard, with the s | 2026-11-09 | minor | File formats: EPS import: 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. | | 2026-11-08 | minor | Type advanced: spell check engine, `text.spelling.check` lists unknown words per run language at `text.editRange` byte offsets and `text.spelling.suggest` gives suggestions, from craft-proof (#4936); evidence added; the dialog, Auto Spell Check and the custom dictionary remain; scores and hours 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-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 | 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 | File formats and Type core: `.ai`/EPS type keeps its paragraphs' tab stops, 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 (#6147); evidence added; scores unchanged. | | 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-10 | minor | Held numeric spinner arrows keep stepping past the click timeout and stop on release (#6307). Scores and estimates unchanged. | | 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. | +| 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-06 | minor | Direct Selection: with Snap to Grid, a dragged straight segment lands its anchors on the grid (#3835); evidence added; scores unchanged. | | 2026-11-06 | minor | Snap to Glyph › Glyph Bounds: the corners of each glyph's box are points a drawn point lands on, also on a curved side (`B`), and the box turns with rotated type, its edges then Angular Guides (#5112; [Snap to Glyph options](https://helpx.adobe.com/illustrator/desktop/design-with-text/special-characters-glyphs/snap-to-glyph-options.html)); scores and estimates unchanged. | | 2026-11-06 | minor | Drop Shadow: a swatch beside Color opens the Color Picker over the dialog, as the other effect colour fields do (#5364); correction: the #5365 entry listed Drop Shadow, whose Color is drawn outside the shared grid and had no swatch; scores unchanged. | @@ -2127,24 +2133,23 @@ Edit › Copy and Cut put the selected ruler guides on the clipboard, with the s | 2026-11-06 | minor | Properties in Edit Artboards: a Transform section (reference point, X, Y, W, H, preset, orientation, Move and Scale Artwork) above the Artboards section (name, Artboard Options, Fill), X and Y of the shared reference point (#5219); evidence updated; scores unchanged. | | 2026-11-06 | minor | Scrubby labels: the Control bar's Stroke: and Opacity: links and the Properties panel's Stroke link scrub their fields; a click still opens the panel (#4088); evidence updated; scores unchanged. | | 2026-11-06 | minor | Control bar: Paragraph: link and Align left/center/right while type is selected, trimmed from the bar's right end after X/Y/W/H and the Align buttons (#5372); evidence added; scores 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. | -| 2026-11-05 | minor | View & navigation: document tabs are reordered by dragging them along the tab strip (`document.move`, #5663); evidence added; scores unchanged. | -| 2026-10-30 | minor | Effects: Color Halftone's Max Radius is 4–127 px of the Document Raster Effects resolution, shown in px whatever the units; older documents open converted, and files saved for older app versions hold points (#3260); scores unchanged. | -| 2026-11-05 | minor | Compound Shapes add-on (beyond Illustrator): Object › Plug-ins › Compound Shapes lists Make, Release and Expand Compound Shape, and holds a setting, off by default, that makes the Pathfinder panel's Shape Mode buttons make a compound shape on a click and give the permanent result on Alt-click (the default is the reverse). Selected members of a compound shape always take the clicked mode. The setting is `window.pathfinder.compoundShapes`; no existing behavior changes while it is off. Scores and estimates unchanged. | -| 2026-11-01 | minor | Batch Trace Folder (`imageTrace.batchFolder`, #4921); evidence added; scores unchanged. | | 2026-11-06 | minor | Shape tools: a shape just drawn can be moved and turned with its tool still active — a press on the selection's centre point (Attributes › Show Center) moves the selection with the Selection tool's move arrow, snapping and Alt copy, and a press in the rotate zone just outside a bounding-box corner turns it about the centre with its rotate cursor, as the Selection tool's do (#5477); the Selection tool takes the centre point too, so a press there moves an unfilled shape instead of starting a marquee; evidence: `a_shape_tool_moves_the_selection_by_its_centre_point_and_turns_it_from_a_corner` (`crates/tools/src/shape/tests.rs`) and `a_press_on_the_centre_point_moves_an_unfilled_shape` (`select/tests.rs`); 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 (each field's scrub label was a second tab stop, invisible because only the label is drawn there, and the W/H link between W and H in the Control bar was one too, and now comes after the fields); evidence: `one_tab_moves_from_a_field_to_the_next` and `the_control_bar_link_toggles_by_click` in `crates/ui-egui/src/panels/tests_constrain.rs`; scores and estimates unchanged | -| 2026-11-01 | minor | Copy and paste ruler guides (#3226); evidence added; scores unchanged. | +| 2026-11-05 | minor | View & navigation: document tabs are reordered by dragging them along the tab strip (`document.move`, #5663); evidence added; scores unchanged. | +| 2026-11-05 | minor | Compound Shapes add-on (beyond Illustrator): Object › Plug-ins › Compound Shapes lists Make, Release and Expand Compound Shape, and holds a setting, off by default, that makes the Pathfinder panel's Shape Mode buttons make a compound shape on a click and give the permanent result on Alt-click (the default is the reverse). Selected members of a compound shape always take the clicked mode. The setting is `window.pathfinder.compoundShapes`; no existing behavior changes while it is off. 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. | | 2026-11-04 | minor | Area type vertical alignment buttons in the Properties panel's Paragraph section and the Control panel, off for columns, rows and threaded frames (#3759); evidence added; scores unchanged. | | 2026-11-03 | minor | Enclosing Lasso is an add-on with no default shortcut or Basic toolbar slot. Scores and estimates remain unchanged. | -| 2026-11-02 | minor | Wheel input over closed font-family fields changes selected text (#5253); public documentation and application choices distinguished; scores and estimates unchanged. | | 2026-11-03 | minor | Find Font's replacement-source chooser offers Document, Recent and System (#5217); scores and estimates unchanged. | | 2026-11-03 | minor | SVG imports preserve exported ellipses as four-anchor paths (#5243). Inline graphics use manual kerning (#5245). Symbol editing shows draggable 9-slice guides (#5280). Recolor Artwork offers smooth and segmented wheels and color bars (#5249). Scores and hours remain unchanged. | -| 2026-11-01 | minor | Repeat: a double-click isolates it to edit its source art, text included (#5054); evidence added; scores unchanged. | | 2026-11-03 | minor | Touch Type supports along-line RTL movement while retaining the first-logical-character restriction; scores and estimates unchanged. | -| 2026-11-01 | minor | Live Corners: Up/Down while dragging a corner widget step the corner kind (#2898); evidence added; scores unchanged. | | 2026-11-03 | minor | Rotated characters retain line-axis composition trimming; scores and estimates unchanged. | +| 2026-11-02 | minor | Wheel input over closed font-family fields changes selected text (#5253); public documentation and application choices distinguished; scores and estimates unchanged. | +| 2026-11-01 | minor | Batch Trace Folder (`imageTrace.batchFolder`, #4921); evidence added; scores unchanged. | +| 2026-11-01 | minor | Copy and paste ruler guides (#3226); evidence added; scores unchanged. | +| 2026-11-01 | minor | Repeat: a double-click isolates it to edit its source art, text included (#5054); evidence added; scores unchanged. | +| 2026-11-01 | minor | Live Corners: Up/Down while dragging a corner widget step the corner kind (#2898); evidence added; scores unchanged. | | 2026-11-01 | minor | Shortcuts for Effect menu items (#3296); evidence added; scores unchanged. | | 2026-11-01 | minor | Color Effects add-on (a VectorCraft addition): Find Nearest Swatch (#4923); evidence added; scores unchanged. | | 2026-11-01 | minor | Create Swatch Legend add-on under Object › Plug-ins › Color Effects (`addon.color.swatchLegend`, #4919); evidence added; scores unchanged. | @@ -2169,13 +2174,13 @@ Edit › Copy and Cut put the selected ruler guides on the clipboard, with the s | 2026-10-31 | minor | Path Precision Tools add-on: the Shared Edge Merge Tool and Keep Shared Points Together; evidence added; scores unchanged. | | 2026-10-31 | minor | UI chrome: Units › East Asian Type acts (#394): Q and Ha (a quarter of a millimetre each) are units that preference offers; 4 Preferences options remain stored but unused; evidence added; score and hours unchanged (estimated). | | 2026-10-31 | minor | Path Precision Tools add-on: the Corner Tool and Slide Anchor Along Path; evidence added; scores unchanged. | -| 2026-10-30 | patch | Performance: opaque solid-fill knockout paths avoid redundant erase/add compositing in hard-edge frames, including supersampled raster export; analytical antialiasing keeps its existing rounding; scores and estimates unchanged | | 2026-10-31 | minor | Path Precision Tools add-on (a VectorCraft addition): live Path Effects; evidence added; scores unchanged. | | 2026-10-31 | minor | Artwork Cleanup add-on (a VectorCraft addition): text tools; evidence added; scores unchanged. | +| 2026-10-30 | minor | Effects: Color Halftone's Max Radius is 4–127 px of the Document Raster Effects resolution, shown in px whatever the units; older documents open converted, and files saved for older app versions hold points (#3260); scores unchanged. | +| 2026-10-30 | patch | Performance: opaque solid-fill knockout paths avoid redundant erase/add compositing in hard-edge frames, including supersampled raster export; analytical antialiasing keeps its existing rounding; 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 | 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 | @@ -2203,20 +2208,24 @@ Edit › Copy and Cut put the selected ruler guides on the clipboard, with the s | 2026-10-29 | minor | Libraries panel: art dragged off the canvas and swatches dragged in (colours, also another library's, gradients and patterns) are added where they are dropped, as items dragged within the list move: on another item of their kind, just before or after it by the pointer's half and in its group; on a group, into it; away from the items, ungrouped at the end as before (`library.add` takes `group` and `before`/`after`). Tests: `tests::library::a_new_item_is_added_where_it_is_dropped`, `panels::libraries::tests::art_and_colours_dropped_from_elsewhere_go_where_they_are_dropped`; the library fuzz test adds with them. Scores and hours unchanged. | | 2026-10-29 | minor | Path operations: 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; evidence added; score and hours unchanged (estimated). | | 2026-10-29 | minor | Object › Path › Smooth… with its slider and Auto-Smooth (#4115) added to the evidence; score and hours unchanged. | +| 2026-10-28 | minor | The center drag turns only the rays by one degree per document point of increasing pointer y and back at the same rate for decreasing y. This is an application rule independent of event frequency. Shift keeps the current turn. Space freezes rotation on every sample, including the first; the initial samples establish size until the gesture begins, after which Space translates the flare without changing its size or turn. Releasing a modifier resumes from the current pointer position without catching up skipped rotation. Scores and remaining estimates unchanged. | +| 2026-10-28 | minor | Drop Shadow Darkness subtracts the normalized amount from RGB source channels or adds it to CMYK/Gray black before profile conversion and blur; near-100% becomes gray black, with shadow opacity separate. SVG keeps live filters for uniform sRGB sources and renders shadows for other RGB profiles, source models and gradients. Gradient-stop adjustment is an application rule; spot/custom colors and composite-source parity remain open. Scores and estimates unchanged. | +| 2026-10-28 | minor | Blob Brush Shift locks after clear axial movement, keeps earlier samples fixed, and waits without an off-axis trail. Activation uses raw press displacement in logical screen pixels, with exact quarter-turn boundaries. Explicit application rule; scores and estimates unchanged. | +| 2026-10-28 | minor | Flare Tool Options opened by a canvas click previews its settings at that click. Reset refreshes the enabled preview while keeping the dialog open. Cancel discards preview art and tentative options; OK accepts one flare in one Undo step. Editing existing flare artwork remains outside this dialog scope. Scores and estimates 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-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. | -| 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). | | 2026-10-25 | minor | Floating panels open in OS windows of their own on the desktop (inside the window on the web and on Wayland); saved layouts from earlier versions convert to the shared docking layout (#4135); scores and estimates unchanged (estimated) | | 2026-10-25 | minor | Expand Appearance keeps a path's strokes as stroked paths (brushed strokes still become brush art) and Object › Expand… is dimmed while the selection has appearance attributes, so a stroked line with an effect expands to a stroked line before Expand outlines it (#3089, public doc cited). Scores and hours unchanged. | -| 2026-10-24 | minor | UI chrome: Preferences › General › Corner Radius is the Rounded Rectangle tool's radius and follows the radius set while drawing (#394); score unchanged at 80% (estimated), ~15 Preferences options still stored but unused. | | 2026-10-25 | minor | Transform Each opens on the values it was last confirmed with (Copy aside) and has a Copy button instead of a Copy checkbox (#1732, public doc cited for Copy; the remembered values are a maintainer's call on the issue). Scores and hours unchanged. | -| 2026-10-24 | minor | Recolor Artwork › Assign: double-clicking a row's new color opens the Color Picker over the dialog; its OK sets the row's new color in the model it was picked in (a CMYK mix stays CMYK, RGB becomes CMYK in a CMYK document), Cancel leaves the row as it was. A Lab new color is kept, but a Lab color can't be typed yet: the picker's Lab fields work in RGB, as for fill and stroke (part of #4028; Lab entry, swatch libraries and the row menu remain). Scores and estimates unchanged. | | 2026-10-25 | minor | Path operations: Pathfinder panel › Pathfinder Options… (Precision, Remove Redundant Points, Divide and Outline Will Remove Unpainted Artwork, #3426); evidence added, scores unchanged | | 2026-10-25 | minor | Artboards: the Artboards panel's Delete Empty Artboards (`artboard.deleteEmpty`); evidence added, scores unchanged | | 2026-10-25 | minor | UI: the Transform panel menu's Transform Object Only / Pattern Only / Both and Use Registration Point for Symbol act in the Transform panel and the Properties panel's Transform section; evidence added; scores unchanged. | +| 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. | +| 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). | +| 2026-10-24 | minor | UI chrome: Preferences › General › Corner Radius is the Rounded Rectangle tool's radius and follows the radius set while drawing (#394); score unchanged at 80% (estimated), ~15 Preferences options still stored but unused. | +| 2026-10-24 | minor | Recolor Artwork › Assign: double-clicking a row's new color opens the Color Picker over the dialog; its OK sets the row's new color in the model it was picked in (a CMYK mix stays CMYK, RGB becomes CMYK in a CMYK document), Cancel leaves the row as it was. A Lab new color is kept, but a Lab color can't be typed yet: the picker's Lab fields work in RGB, as for fill and stroke (part of #4028; Lab entry, swatch libraries and the row menu remain). Scores and estimates unchanged. | | 2026-10-24 | minor | Live Paint: Make, Merge and the Live Paint Bucket refuse type, images and other objects that aren't paths, with a message to convert them first, and change nothing; before, they vanished among the group's hidden sources (public doc cited). Scores and hours unchanged. | | 2026-10-24 | minor | Properties: the overview's contextual controls: Blend Options and Merge Live Paint in Quick Actions, the Width Profile list and an applied effect's Delete icon in Appearance, the kind and radius of Direct-Selected corners (public doc cited) (G1). Scores and hours unchanged. | | 2026-10-24 | minor | Commands: a command whose params take `ids?\|id?` (or `id?, ids?: … (instead of id)`) takes `id` as `ids` of one with nothing selected too, instead of refusing it as "nothing selected" (29 such commands needed a selection); Invert, Complement, Gradient Vector and Make Pixel Perfect act on the object given as `id`; an `id` or `ids` that is malformed or names no object is refused under the command's name instead of the selection standing in for it (`plugin.run`'s required `id`, the plug-in's, is not an object) (`tests_ids_without_selection::id_does_what_ids_of_one_does_with_nothing_selected`, `a_bad_id_is_refused_as_the_same_ids_are_and_changes_nothing`); scores unchanged | @@ -2240,19 +2249,17 @@ Edit › Copy and Cut put the selected ruler guides on the clipboard, with the s | 2026-10-23 | minor | UI chrome: General › Constrain Angle acts for the arrow keys, Shift drags and new rectangles, ellipses, grids, type, polygons, stars and size-dialog shapes (#394); score unchanged at 80% (estimated), hours unchanged | | 2026-10-23 | minor | SVG: attributes in other applications' XML namespaces are kept on their objects from import to export and in `.vectorcraft` files, as `data-*` attributes are (the rest of #522); SVG and engine regressions. Scores and hours unchanged. | | 2026-10-23 | minor | Type: hanging punctuation keeps its published amounts under Glyph Scaling, and Standard burasagari counts a hung line start as room (G4); stale "Optical Margin Alignment" removed from the Type core row's remaining items. Scores and hours unchanged. | -| 2026-10-20 | minor | Type core: Hyphenation… and Justification… no longer listed as missing (done in #2310, #2755); evidence: a paragraph's Auto Leading drives the Character panel's Leading, the leading nudge and Fill with Placeholder Text; scores unchanged | -| 2026-10-22 | minor | Drawing tools: a Pen, Anchor Point or Direct Selection drag's preview no longer stalls (#834: the position where the pointer re-entered the window could be skipped, and the move in the release's frame was lost); score and hours unchanged (estimated) | | 2026-10-23 | minor | Guides and snapping: a path moved by a press on its stroke or beside its edge (Selection, Group Selection, Direct Selection), and a straight segment Direct Selection drags, snap by the nearest point of the path, so the path lands exactly on a ruler guide with "align" instead of the pressed spot landing there and the path a few pixels beside it (#3701); `tests_prefs::a_move_held_by_a_paths_stroke_lands_the_path_on_a_ruler_guide`, `tests_prefs::a_straight_segment_dragged_by_direct_selection_lands_on_a_ruler_guide`. Score and hours unchanged. | -| 2026-11-11 | minor | Guides and snapping: a Shift move slides the selection along its line onto a ruler guide by its edges and centre, as a move without Shift does (#4312); a guide a nearly parallel line only reaches a long way off pulls nothing. Score and hours unchanged (estimated). | -| 2026-10-22 | minor | Live vector effects: the Transform effect has Transform Objects, Transform Patterns and Scale Strokes & Effects, its checkboxes visible (#885); the area row now lists only the options on a fill's or stroke's own Transform effect. Score and hours unchanged (estimated). | | 2026-10-23 | minor | COLR colour fonts (v0 and v1) draw and export as vector art; evidence added; scores unchanged. | -| 2026-10-22 | minor | Libraries panel: the buttons beside the library picker show the items as icons (the default) or in a list, colours and graphics as rows with their names (a colour's hex value too), dragged and dropped by row; graphic thumbnails are shrunk to the size they are drawn at, so thin strokes stay whole. Tests: `panels::libraries::tests::the_list_view_shows_names_and_takes_drops_by_row`, `the_panel_draws_its_libraries`. Scores and hours unchanged. | -| 2026-10-18 | minor | Shape Builder on art of thousands of segments (traced outlines, type outlines): lines cut it again past 4,096 segments, where the planar map used to give up and the tool offered whole shapes (#937's reproduced case; the planar map's edge-source and face lookups were quadratic in the segments, now indexed, and the cap is 65,536); Path operations stays 80% and Shape Builder 70% until confirmed on the reporter's file (estimated) | | 2026-10-23 | minor | Fix #3705 in part: drawing modes apply to pastes (Draw Inside puts Paste, Paste in Place and Paste on All Artboards inside the object drawn into; Draw Behind puts Paste in Place and Paste on All Artboards behind the selection), and Shift+D goes from Draw Behind back to Draw Normal when Draw Inside can't draw into the selection instead of stopping at Draw Behind; scores and estimates unchanged. | | 2026-10-23 | minor | Artboard Options: Show Video Safe Areas (action-safe 3.5% and title-safe 5% in from each edge, EBU R 95, canvas only) and Video Ruler Pixel Aspect Ratio (`artboard.setProps`/`artboard.new` `showVideoSafeAreas`, `videoRulerPixelAspectRatio`), per artboard and in the native format; View › Rulers › Show Video Rulers (`view.videoRulers`) counts the active artboard in those pixels (#2849); evidence added; Layers/artboards 78% and Guides/rulers 80% unchanged (estimated). | | 2026-10-23 | minor | Artboard Options: Show Center Mark and Show Cross Hairs (`artboard.setProps`/`artboard.new` `showCenterMark`, `showCrossHairs`) draw a center cross and lines from the middle of each side through the center on the canvas only, in one undo step; video safe areas, video rulers and the pixel aspect ratio remain missing; scores and estimates unchanged. | | 2026-10-23 | minor | Path operations: Simplify of part of a path: with anchors or segments of a path direct-selected, Object › Path › Simplify (slider, Auto-Simplify and dialog) refits only each run of selected segments between its end anchors; no longer listed as missing; evidence added; score and hours unchanged (estimated). | | 2026-10-23 | minor | Path operations: Simplify: Object › Path › Simplify opens its slider at the Auto-Simplify level for the selection, previewing that result, instead of at 1 pt; partial-path simplify remains missing; score and hours unchanged (estimated). | +| 2026-10-23 | minor | Width-profile outlines sample adaptively: two samples per profile segment, refined where the width bends, and only where the flattened path does not already sample the width. The sampled edges are fitted with G1 cubics within a tenth of the flattening tolerance (0.001 pt by default) instead of one cubic per flatten step. On the reviewer's 1,500-ellipse fixture (20–200 pt ellipses, 3–12 pt weights) the PDF falls to 3.59 MB and the SVG to 6.79 MB (reported main baselines 4.48 MB and 9.30 MB, about 0.80× and 0.73×), and `profiled_outlines_stay_compact` guards the path-element count. Scores and estimates unchanged. | +| 2026-10-22 | minor | Drawing tools: a Pen, Anchor Point or Direct Selection drag's preview no longer stalls (#834: the position where the pointer re-entered the window could be skipped, and the move in the release's frame was lost); score and hours unchanged (estimated) | +| 2026-10-22 | minor | Live vector effects: the Transform effect has Transform Objects, Transform Patterns and Scale Strokes & Effects, its checkboxes visible (#885); the area row now lists only the options on a fill's or stroke's own Transform effect. Score and hours unchanged (estimated). | +| 2026-10-22 | minor | Libraries panel: the buttons beside the library picker show the items as icons (the default) or in a list, colours and graphics as rows with their names (a colour's hex value too), dragged and dropped by row; graphic thumbnails are shrunk to the size they are drawn at, so thin strokes stay whole. Tests: `panels::libraries::tests::the_list_view_shows_names_and_takes_drops_by_row`, `the_panel_draws_its_libraries`. Scores and hours unchanged. | | 2026-10-22 | minor | Colour and gradients: Illustrator 30.0's gradient Method, Classic or Perceptual (Gradient panel menu, canvas and raster exports, SVG/PDF as images at the raster effects resolution, native file); score and hours unchanged (estimated). | | 2026-10-22 | minor | G2: the Effect Gallery shipped (#2883): Effect › Effect Gallery… browses the Artistic, Brush Strokes, Distort, Sketch, Stylize and Texture filters, edits each filter's settings, builds an ordered stack (add, reorder, hide, remove) with a live canvas preview, and OK applies it as separate appearance effects in one undo step; remaining: filter thumbnails and a preview area inside the dialog; G2 estimate 4–6 → 1–2 h; Raster effects 82% → 85% (power-user view 49% → 52%), estimated, a modest move because the named missing dialog landed without thumbnails or an in-dialog preview. | | 2026-10-22 | minor | Dimension tool: in a CMYK document a dimension's lines, arrowheads and label are drawn in CMYK, black as K only (`0 0 0 1`) and other colours through the active colour settings, instead of an RGB black that printed as a four-ink rich black (#3233); RGB documents unchanged; `dimensions::dimension_colours_follow_the_document_colour_model`. Scores and hours unchanged. | @@ -2262,15 +2269,15 @@ Edit › Copy and Cut put the selected ruler guides on the clipboard, with the s | 2026-10-22 | minor | G2: Effect › Sketch › Conté Crayon (Foreground Level, Background Level, Texture, Scaling, Relief, Light Direction, Invert; a black crayon in the dark areas and a white one in the light areas on textured mid-grey paper; ranges and defaults unverified VectorCraft choices); measured Photoshop-style filters 57/57, the Effect Gallery remains (4–6 h); area score unchanged (49%, estimated). | | 2026-10-22 | minor | File formats: Affinity import gains master pages, tables, paragraph formats, text scale, shadows, glows, outlines (also on stroked shapes), overlays, underlines, strikethrough and editable embedded documents, strokes styled None left out, CMYK pixel layers through the document's CMYK profile, text placed as Affinity places it (AF.5, from storytold/designcraft#291); synthetic, corpus and local-thumbnail tests; score and hours unchanged (estimated). Hostile files stay bounded: one budget for the whole read (layers, pixels, characters, table cells, embedded documents included), a work budget for overlays, capped master page copies, objects past `MAX_COORD` left out; the import fuzzer and limit tests cover tables, paragraphs, effects, overlays, master pages, CMYK pixels and embedded documents. | | 2026-10-22 | minor | Recolor Artwork Edit chips scroll within the available screen height (#3416); small palettes retain their tab height; scores and estimates unchanged. | -| 2026-10-21 | minor | Width-profile outlines offset samples added inside a flattened chord along the source curve's exact tangent, so a zoomed outline no longer jogs at flatten vertices; a regression test covers added samples. Scores and estimates unchanged. | | 2026-10-22 | minor | Font pickers list recently applied families first (#3515), using the existing recent-font limit; search, filters, previews and keyboard selection share the same rows. Scores and estimates unchanged. | +| 2026-10-22 | minor | Ruby (ふりがな) for live text: `type.ruby` sets/removes a reading over a byte range, above or below (right of the column in vertical type), aligned, sized and overhanging or squeezed; saved natively, laid out with line growth, drawn on the canvas and in SVG/PDF (outlines where ruby is present); scores and hours unchanged | +| 2026-10-21 | minor | Width-profile outlines offset samples added inside a flattened chord along the source curve's exact tangent, so a zoomed outline no longer jogs at flatten vertices; a regression test covers added samples. Scores and estimates unchanged. | | 2026-10-21 | minor | Plug-ins: filters keep data with the document (`documentData`), as its foreign data, so the native file needs no section of its own (G17); engine and unit regressions. Scores and hours unchanged. | | 2026-10-21 | minor | Print: PDF/X-1a and PDF/X-3 flattening in a CMYK document rasterizes as ink amounts, so CMYK colours under an image with see-through pixels keep their numbers, K included, instead of going through RGB (#2160); own-fixture test; scores and hours unchanged (estimated) | | 2026-10-21 | minor | Blend panel Type and Ramp control step easing with a separate color Shift (#2539); Blend Options retains spacing and orientation; polynomial curves are original approximation. Scores and estimates unchanged. | | 2026-10-21 | minor | UI chrome: workspaces keep the layout changes made in them when switching away and back, and across restarts; Reset <name> returns to the saved layout (UI.7); score and hours unchanged (estimated) | | 2026-10-21 | minor | UI chrome: a panel group dragged onto a dock group's tabs joins it, onto its upper or lower half makes a new group above or below it, onto the icon column becomes icons (the tabbed group's panels too); dock tabs close from their context menu (UI.6); score and hours unchanged (estimated) | | 2026-10-21 | minor | UI chrome: a double-click on a dock tab collapses its group to its tabs and expands it again, as floating groups already did (UI.8); scores and hours unchanged (estimated) | -| 2026-10-20 | minor | UI chrome: workspaces keep the layout changes made in them when switching away and back, and across restarts; Reset <name> returns to the saved layout (UI.7); score and hours unchanged (estimated) | | 2026-10-21 | minor | M3: highlighted-text Eyedropper sampling preserves ranges and rejects unsupported source/target transparency atomically; regression-tested; scores and estimates unchanged. | | 2026-10-21 | minor | Shape Builder Gap Detection shares Live Paint’s spatial gap finder and temporary merge bridges (#2526); Custom is capped at 72 pt in the existing options dialog. Scores and estimates unchanged. | | 2026-10-21 | minor | Recolor model selection is available through the engine and MCP only (#2520); the dialog retains its existing controls. Scores and estimates unchanged. | @@ -2278,12 +2285,12 @@ Edit › Copy and Cut put the selected ruler guides on the clipboard, with the s | 2026-10-21 | minor | SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts. Scores and estimates unchanged. | | 2026-10-21 | minor | Snap to Glyph (#3124) removed from View navigation missing lists and menu stubs; scores and hours unchanged at this rounding | | 2026-10-21 | minor | Type core: System Layout (Character panel menu, whole document, off by default) is done; scores and hours unchanged | -| 2026-10-22 | minor | Ruby (ふりがな) for live text: `type.ruby` sets/removes a reading over a byte range, above or below (right of the column in vertical type), aligned, sized and overhanging or squeezed; saved natively, laid out with line growth, drawn on the canvas and in SVG/PDF (outlines where ruby is present); scores and hours unchanged | -| 2026-10-23 | minor | Width-profile outlines sample adaptively: two samples per profile segment, refined where the width bends, and only where the flattened path does not already sample the width. The sampled edges are fitted with G1 cubics within a tenth of the flattening tolerance (0.001 pt by default) instead of one cubic per flatten step. On the reviewer's 1,500-ellipse fixture (20–200 pt ellipses, 3–12 pt weights) the PDF falls to 3.59 MB and the SVG to 6.79 MB (reported main baselines 4.48 MB and 9.30 MB, about 0.80× and 0.73×), and `profiled_outlines_stay_compact` guards the path-element count. Scores and estimates unchanged. | | 2026-10-21 | minor | 3D: original Standard, Diffuse, Top Left and Right lighting presets; command access, effective-setting recognition and preview/history; scores and estimates unchanged | | 2026-10-21 | minor | Properties: one selected clip group has Edit Clipping Path and Edit Contents at the top of the panel (public doc cited) (G1). Scores and hours unchanged. | | 2026-10-21 | minor | Print: the Print dialog's Media list carries the Japanese papers — JIS B4 (257 × 364 mm), JIS B5 (182 × 257 mm) and JIS B6 next to the ISO B4/B5, which keep their ISO 216 sizes, plus はがき, 往復はがき, 封筒 長形3号/長形4号/角形2号/角形3号 and 四六判; the JSON id or the dialog label names a paper, any case and spacing. Print score and hours unchanged. | | 2026-10-21 | minor | Type core: Fractional Widths (Character panel menu, per type object, on by default) is done; scores and hours unchanged | +| 2026-10-20 | minor | Type core: Hyphenation… and Justification… no longer listed as missing (done in #2310, #2755); evidence: a paragraph's Auto Leading drives the Character panel's Leading, the leading nudge and Fill with Placeholder Text; scores unchanged | +| 2026-10-20 | minor | UI chrome: workspaces keep the layout changes made in them when switching away and back, and across restarts; Reset <name> returns to the saved layout (UI.7); score and hours unchanged (estimated) | | 2026-10-20 | minor | Polygon and Star exact-size placement remembers the chosen count for later drags and dialog openings; Cancel preserves the previous count; existing opening gestures retained; scores and hours unchanged. | | 2026-10-20 | minor | Live trace result bounds and selection highlights exclude the retained bitmap except in source-image views; scores and hours unchanged. | | 2026-10-20 | minor | M3.57: CSS swatch libraries preserve fractional stop positions in linear and radial gradients; synthetic color-crate regression. Scores and hours unchanged. | @@ -2298,17 +2305,18 @@ Edit › Copy and Cut put the selected ruler guides on the clipboard, with the s | 2026-10-19 | minor | Status messages and readouts use stable responsive allocations sized for localized labels and ordinary coordinates. Contributor original-file acceptance confirmed; scores and estimates unchanged. | | 2026-10-19 | minor | Cached canvas dimensions match the renderer limit before cache-key creation, view construction and worker submission. Regression coverage; scores and estimates unchanged. | | 2026-10-19 | minor | Cached canvas reprojection preserves scale during resize, rotation and DPI changes. Contributor original-file acceptance confirmed; scores and estimates unchanged. | -| 2026-10-18 | minor | PDF Place and clipboard paste use Open's live-text choice instead of forcing outlines; synthetic regressions cover page selection and explicit outlines. Scores and hours unchanged. | -| 2026-10-18 | minor | File formats: PDF invisible text (rendering mode 3, a scanned page's recognized text) opens as type with no fill or stroke instead of being dropped without a warning, and characters with no fill or stroke export as invisible text, so a scanned PDF opened and saved keeps its searchable text (#2675); own-fixture tests; scores and hours unchanged (estimated) | | 2026-10-19 | minor | Effect rasters cropped to the view or made coarser are kept while the view stays the same: a drag over a 500 pt blur draws it from the still frame on this thread and on the worker, and an interaction stays on the worker that drew the frame shown (#2558). Scores and hours unchanged. | | 2026-10-19 | minor | Color Picker layout (#2008): compact controls, centered header, stacked actions and aligned inputs; screen-reader channel values, current shared button names and styling, screen sampler and model-by-model Tab order retained. Narrow windows scroll vertically; scores and hours unchanged. | +| 2026-10-19 | minor | Blends: ease in/out and separate colour easing in Blend Options (#1124); on-canvas handles and easing in the Blend panel remain. Symbols, blends, envelopes score unchanged (estimated) | +| 2026-10-19 | minor | Guides, grids, smart guides, snapping: tangent and perpendicular snapping for lines drawn from an anchor (Pen, Curvature, Line Segment) and View › Snapping with every snap option, #2530; both only in part, so score and hours unchanged (estimated) | +| 2026-10-18 | minor | Shape Builder on art of thousands of segments (traced outlines, type outlines): lines cut it again past 4,096 segments, where the planar map used to give up and the tool offered whole shapes (#937's reproduced case; the planar map's edge-source and face lookups were quadratic in the segments, now indexed, and the cap is 65,536); Path operations stays 80% and Shape Builder 70% until confirmed on the reporter's file (estimated) | +| 2026-10-18 | minor | PDF Place and clipboard paste use Open's live-text choice instead of forcing outlines; synthetic regressions cover page selection and explicit outlines. Scores and hours unchanged. | +| 2026-10-18 | minor | File formats: PDF invisible text (rendering mode 3, a scanned page's recognized text) opens as type with no fill or stroke instead of being dropped without a warning, and characters with no fill or stroke export as invisible text, so a scanned PDF opened and saved keeps its searchable text (#2675); own-fixture tests; scores and hours unchanged (estimated) | | 2026-10-18 | minor | Shape Builder: Gap Detection (Small 3 pt, Medium 6 pt, Large 12 pt, Custom) in the new Shape Builder Tool Options bridges small gaps between open paths for the regions they bound (#2523); Path operations stays 80% (estimated) while #937 is open | | 2026-10-18 | minor | Mirror Repeat shows its axis when selected (#2877): drag the line or center handle to move it, drag an end handle to rotate it (Shift: 45°), Escape cancels and each drag is one undo step. Engine and headless canvas gesture regressions passed; native move, rotation and Escape screenshots reviewed. Formatting, Clippy, WASM and repository checks passed; the undo label reuses translated Repeat Options. (#2877) Scores and hours unchanged. | | 2026-10-18 | minor | Bounding-box handles take priority where a selected Mirror Repeat axis crosses them: the Top handle scales the repeat, while the center still moves it and the ends still rotate it. Headless canvas regression confirms vertical scaling without width change; scores and hours unchanged. | -| 2026-10-19 | minor | Blends: ease in/out and separate colour easing in Blend Options (#1124); on-canvas handles and easing in the Blend panel remain. Symbols, blends, envelopes score unchanged (estimated) | -| 2026-10-19 | minor | Guides, grids, smart guides, snapping: tangent and perpendicular snapping for lines drawn from an anchor (Pen, Curvature, Line Segment) and View › Snapping with every snap option, #2530; both only in part, so score and hours unchanged (estimated) | -| 2026-10-17 | minor | File formats: PDF text opens where the file draws it (#722: glyphs slid where the file's spacing differed from the font's metrics: kerning, word and character spacing, `TJ` adjustments, text rise, narrow vertical glyphs; justified text boxes stayed lines); score 75% and hours unchanged (estimated) | | 2026-10-18 | minor | Status-bar pointer X/Y follows the active global/artboard ruler origin and General units; three painted-text regressions cover origins, mode/artboard/reset/unit changes and read-only state; scores and hours unchanged. | +| 2026-10-17 | minor | File formats: PDF text opens where the file draws it (#722: glyphs slid where the file's spacing differed from the font's metrics: kerning, word and character spacing, `TJ` adjustments, text rise, narrow vertical glyphs; justified text boxes stayed lines); score 75% and hours unchanged (estimated) | | 2026-10-17 | minor | Blend spine Anchor Point conversions, selection/stacking priority and Shift regression coverage (#2544); scores and hours unchanged. Windows native canvas checks and screenshots reviewed in PR #2643. | | 2026-10-17 | minor | Evidence: Artboard Options has X and Y beside a reference point locator, counted from the ruler origin (#1876); scores unchanged | | 2026-10-17 | minor | Image Trace: Auto Color keeps rare but distinct colours drawn as regions of their own (stems, highlights, near-white on pale backgrounds), at most six, protected from mixed-colour merging and Noise: every key colour on 66 of 66 flat test logos (was 39); scans unchanged. Scores and hours unchanged. | @@ -2319,6 +2327,8 @@ Edit › Copy and Cut put the selected ruler guides on the clipboard, with the s | 2026-10-17 | minor | M12: code-drawn centre-line scenes and metrics added to the ignored quality harness; current Create Strokes baseline in `image-trace-quality.md`. Tests only; scores, missing features and estimates unchanged. | | 2026-10-17 | minor | Plug-ins: Install Plug-in keeps the module in a Plug-ins folder next to the preferences, installed again at startup (#2541); scores unchanged | | 2026-10-17 | minor | Evidence: Group Selection picks and moves one path of a compound path (#2441, in part); scores unchanged | +| 2026-10-17 | minor | Evidence: Group Selection picks one path of a compound path (#2441, in part); scores unchanged | +| 2026-10-17 | minor | Colored Inflate: shared membrane normals and smooth RGB vector ramps replace flat polygon bands; channel groups are batched per solid-fill surface. White-light Inflate retains its original polygons, paint composition and seam joins; the 0.05 seam minimum is restored. Scores and hour estimates unchanged. | | 2026-10-16 | minor | Create Gradient Mesh on an embedded image (#1676): colours sampled as area averages, transform kept, linked images refused. Scores and hours unchanged | | 2026-10-16 | minor | The Move dialog opens on the offset of the last drag or Move and with the Copy, Transform Objects and Preview it was last confirmed with (Transform Patterns sets the preference it starts from), so Enter, Enter repeats a move or a copy; scores and hours unchanged | | 2026-10-16 | minor | Image Trace: a loop or junction-returning piece whose fit collapsed to one cubic keeps the fewest segments that hold its area and course instead of every positioned point, so a higher Paths no longer gives fewer segments on code-drawn noisy images (six falling runs to none; one fall with another cause remains). Scores and hours unchanged. | @@ -2327,7 +2337,6 @@ Edit › Copy and Cut put the selected ruler guides on the clipboard, with the s | 2026-10-16 | minor | Image Trace Flat Logo: line art whose only solid area is the paper (thin strokes, small marks) is traced from its strokes' cores instead of returning a blank success with no paths; anti-aliased edge shades fold into their ink, and paper with ink on under 1% of it (specks, grain) stays blank; code-drawn regressions `thin_line_art_on_paper_is_traced_not_blank`, `anti_aliased_coloured_lettering_has_only_its_inks`, `sparse_marks_on_paper_are_not_promoted_to_colours`, `grainy_blank_paper_has_no_ink`. Score and hours unchanged (estimated) | | 2026-10-16 | minor | Image Trace: the contour walk works out each turn from the pixels as it goes instead of following a precomputed table of per-corner directions; identical loops on all 65,536 4 × 4 masks and 400 seeded masks, and no slower (whole 1 MP traces 1–5% faster). Scores and hours unchanged. | | 2026-10-16 | minor | Native documents restore transparency grid visibility (#1968); save/open regression covers on/off, old files and SVG; scores and hours unchanged (estimated) | -| 2026-10-17 | minor | Evidence: Group Selection picks one path of a compound path (#2441, in part); scores unchanged | | 2026-10-16 | minor | Agent automation: MCP cancellation, progress notifications and busy answers during long exports (#2165, #2171, #2172); scores and hours unchanged | | 2026-10-16 | minor | EPS editing data: legacy mesh corners, vector opacity masks and compound member paint retained; raster page comparison filters minification without relaxing limits; scores and hours unchanged. Synthetic EPS/parser/minification regressions; user EPS/SVG/JPG kept local. | | 2026-10-16 | minor | Type: localized legacy family names remain available when typographic names are English-only (#2208); loaded and catalog paths tested. Scores and estimates unchanged. | @@ -2337,10 +2346,10 @@ Edit › Copy and Cut put the selected ruler guides on the clipboard, with the s | 2026-10-16 | minor | G2: Effect › Sketch › Bas Relief, Charcoal, Photocopy and Stamp (#2527); measured Photoshop filters 37/57 with Cutout, Neon Glow and Poster Edges, 20 remaining (18–30 h). Scores unchanged. | | 2026-10-16 | minor | G2: Effect › Sketch › Halftone Pattern (Size, Contrast, Pattern Type Circle, Dot or Line; black ink on white paper, Size in points at any Document Raster Effects resolution); measured Photoshop filters 30/57, 27 remaining (22–38 h). | | 2026-10-16 | minor | Symbols: Symbol Options › Export Type (Graphic / Movie Clip) with Registration; Symbols score 85% → 87% (estimated) | -| 2026-10-15 | minor | Evidence: 36 more tool states get cursors of their own (node editing, Anchor Point, Curvature, side-shear drawn; the rest borrowed until drawn), listed in `crates/tools/src/cursors.toml`; scores unchanged | -| 2026-10-17 | minor | Colored Inflate: shared membrane normals and smooth RGB vector ramps replace flat polygon bands; channel groups are batched per solid-fill surface. White-light Inflate retains its original polygons, paint composition and seam joins; the 0.05 seam minimum is restored. Scores and hour estimates unchanged. | | 2026-10-16 | minor | 3D lighting: one to ten directional lights, a selectable light list, Add Light and Delete Light, independent settings and artboard gestures; ambient applied once and smooth RGB vector export; scores and hour estimates unchanged | | 2026-10-16 | minor | 3D lighting: opaque RGB Color for the shared directional light, with neutral ambient, preview/reset controls and smooth vector Revolve shading; scores and hour estimates unchanged | +| 2026-10-15 | minor | Evidence: 36 more tool states get cursors of their own (node editing, Anchor Point, Curvature, side-shear drawn; the rest borrowed until drawn), listed in `crates/tools/src/cursors.toml`; scores unchanged | +| 2026-10-15 | minor | Artboard tool: closing a clean active tab restores the displayed board before Properties Delete. Nine targeted checks passed; three original close-route captures reviewed; scores and hours unchanged. | | 2026-10-14 | minor | Localization: Brazilian Portuguese (`pt-br`) catalog completed under the tests (#1914); Localization 71% → 74% (measured), hours 55–90 → 50–85; full readiness unchanged (~62%) | | 2026-10-14 | minor | Automation: dataset CSV import/export; headline Automation ~62% → ~65%, detail ~68% → ~72%, hours 6–12 → 4–8 / 5–9 → 4–7 (estimated) | | 2026-10-14 | minor | Automation: image variables (`kind: image`, `variable.makeImageDynamic`, path values on datasets that keep object bounds); headline Automation ~60% → ~62%, detail ~65% → ~68%, hours 8–14 → 6–12 / 6–10 → 5–9 (estimated) | @@ -2357,15 +2366,14 @@ Edit › Copy and Cut put the selected ruler guides on the clipboard, with the s | 2026-10-14 | minor | Image Trace: closed outlines keep their interior when straightening would collapse them; generated mark/hole/winding/Noise regressions; score and hours unchanged | | 2026-10-14 | minor | Image Trace, measured in `image-trace-quality.md`: Shapes rework (#1845), square stroke caps (#1846), enhanced presets (#1847), gradients grown over noise and soft rims (#1848). Score and hours unchanged (estimated) | | 2026-10-14 | minor | Existing-guide Escape cancellation: eight app-input regressions, the Direct Selection Escape control and current full CI passed; scores and hours unchanged. | -| 2026-10-15 | minor | Artboard tool: closing a clean active tab restores the displayed board before Properties Delete. Nine targeted checks passed; three original close-route captures reviewed; scores and hours unchanged. | | 2026-10-14 | minor | Artboard tab changes align the tool with the displayed board; same-tab multiselection and Move/Scale Artwork options preserved. Three tab regressions and historical full CI on `a0671777` passed; scores and hours unchanged. | | 2026-10-14 | minor | Width-point finite-arithmetic follow-up: atomicity, history and native round-trip regressions and current full CI passed; scores and hours unchanged. | -| 2026-10-13 | minor | Pinned PngSuite HTTPS files verified by SHA-256 before publication; bounded downloads replace archive extraction (#1729). Scores and hours unchanged. | -| 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 | Tools, UI and effects: Align and Distribute act on the anchor points selected with Direct Selection: the points of one path align to each other, not to the artboard, and Distribute spaces them (#1641, fixes #1636; aligning to a key anchor is still missing); Delete removes every selected anchor of a closed path (it used to remove the wrong ones), removes objects selected whole along with the selected anchors of others, and keeps the remainder selected so a second Delete removes it (#1674); In isolation mode Direct Selection and Group Selection pick only inside the isolated object (#1666); Escape cancels a Direct Selection drag (#1803, #1743); Escape while typing ends the edit, keeps the type selected and switches to the Selection tool (#1802, #1686, #1656); A filled Pencil path keeps a stroke of None (#1804, part of #1679); With the Pen, a press that moves under 3 screen pixels is a click (a corner without handles), and a Precise Cursors command toggles the existing preference and can take a shortcut (#1663); The [ and ] keys reach the Blob Brush, the Eraser and the Symbolism tools in the app (#1734, part of #1661); In the Pathfinder panel a group selected alone counts its nested shapes, so an expanded stroked object keeps the Shape Modes enabled (#1642, #1635); Object › Expand turns radial, grid and mirror repeats into groups of their instances (#1765, #1756); Select › Same matches type on its characters' fill and stroke (#1746, #1737); Fields commit what was typed when you click away or move to another field, as Enter does; Escape still discards it (#1787); Up and Down move through the command search results and Enter runs the highlighted one (#1798, #1757); Slider rails show on Medium Dark (#1808, #1662); On Windows the custom title bar's window stays truly borderless, so the pointer lines up with what is drawn on systems where the hidden caption offset it (#1791, #1758; awaiting the reporter's confirmation); Tool cursors are painted into the window on Wayland (#1652; not run on Linux, #1645 stays open until confirmed); Roughen with Smooth keeps turn-backs smooth instead of making cusps that draw as spikes (#1646, #1643); Scribble draws one zig-zag per region, with no lines across holes and notches; convex shapes are unchanged (#1651, #1648); correction to the 2026-10-13 rows: a guide's position in the Transform panel (#1659) is not established by the documentation found (help pages read again on 2026-10-14) and is kept as a convenience pending the owner's decision, with no parity claim; scores unchanged (estimated) | | 2026-10-14 | minor | Files, untrusted input and automation: EPS import: a stitched shading keeps its hard colour changes (#1654, #1653); WMF/EMF import refuses an oversized embedded PNG or JPEG from its header, before any pixels are decoded (#1776); PDF open no longer takes exponential time on a crafted name tree with shared nodes when it looks for the editing data (#1781); The size limits on a native file's embedded data also apply to files read from memory, not only streamed ones (#1783); Saving over a private file on Unix no longer leaves a readable temporary copy beside it while the file is written (#1778); An Export for Screens suffix can no longer place files outside the export folder (#1777); Package refuses a folder where it would overwrite the open document or one of its linked files (#1786); ruzstd 0.9.1 fixes a decompression bomb (RUSTSEC-2026-0344) reachable from EPS/`.ai` and Affinity import (#1807); File Info dates are stamped only after a save succeeds, so a failed save no longer changes them (#1774); Linked files that change during a drag are updated after the drag instead of being missed (#1773); Scatter graph data near the largest number is refused with a message instead of hanging the app (#1780); One very long Liquify stroke segment can no longer create an unbounded number of dabs (#1779); A drag whose last preview failed is no longer recorded as a committed command in the journal that actions and scripts read (#1775); Tests: text import fuzzing no longer searches installed fonts (#1785); Tests: render tests that add fonts no longer race the frame cache tests (#1795); `text.create`, `text.createInPath` and the MCP `add_text` tool take `justify`, so point type can be centred on or end at its point (#1752, #1748); MCP limits: 16 MiB lines, 64-message batches, 256-step `command_batch` with 8 MiB of kept results and 256 MiB control replies (#1764, fixes #1727); MCP requires the JSON-RPC 2.0 envelope and matches control replies by id, and an app refusal without an id reports the app's reason (#1768, fixes #1728); The MCP server reconnects to the app before a call when the connection was closed or holds stray bytes, after an app restart or an idle expiry (#1739); #1684 and #1797 wrote their own evidence; scores unchanged | | 2026-10-14 | minor | Image Trace, measured in `image-trace-quality.md`: snap Curves To Lines moves no end of a line by more than 1.6 px, so long lines a degree or two off stay as the image has them (#1647: Technical Drawing's largest distance 4.21 px to 1.46); flat Logo takes the tones a scan's uneven paper makes of one colour for one colour (#1657: on scans 470 paths and 21 px off to 8.5 paths and 0.09 px); the Image Trace row keeps 75% (estimated): the score was raised for the measured work on 2026-10-13 and these two are refinements of it | | 2026-10-14 | minor | Malformed JSON app replies drop the MCP connection without replay and report uncertain completion (#1761); upstream notification and id-less refusal behavior preserved; scores and estimates unchanged. | +| 2026-10-13 | minor | Pinned PngSuite HTTPS files verified by SHA-256 before publication; bounded downloads replace archive extraction (#1729). Scores and hours unchanged. | +| 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-13 | minor | Files: SVGs that move objects with a CSS transform (`translate(100px, 0)`, `rotate(45deg)`) or style type with the `font` shorthand import as drawn (#1565); SVG symbols whose shapes use percentage lengths import at the size and place they are drawn, the percentages being of the symbol's `viewBox` (#1596); Inkscape's flowed text (`flowRoot`) opens as area type in its rectangle, a paragraph per `flowPara`, instead of being dropped, and a region that can't be read is left out with a warning (#1569, fixes #1568); The warning for an unsupported SVG filter names the filter, its primitives and the object, and says the object is drawn without it (#1599, fixes #1598); The pages of an Inkscape multi-page SVG become artboards, named by their labels (#1603, fixes #1601); SVG export with CSS classes or style entities stays fast in documents with many distinct styles (#1567); PDF and `.ai` import: evenly letter-spaced or tightened type on a path keeps its length along the path instead of closing up to the font's advances (#1586), and type on a path whose stroke is written glyph by glyph comes in as one text object with its stroke, not one object per glyph (#1585); The ICC profiles embedded in JPEG, TIFF and PSD exports have every tag on a 4-byte boundary (ICC.1 §7.1.2), so strict readers accept them (#1589, fixes #1584); GIF, PNG-8 and other indexed exports find each pixel's palette colour through an index instead of a scan, with identical output (#1576); Saving to a network drive on macOS falls back to a plain fsync when the share refuses a full flush (#1625, part of #1620; not confirmed on a real share), and a save still succeeds when a folder refuses the temporary file, the rename over the document or copying permissions: the content is written elsewhere first and copied in, and a failed copy keeps the temporary file and names it (#1649, part of #1620); A document opened by a relative path finds its relatively linked images, and `vectorcraft-cli convert` lists what reading the input left out as `importWarnings` (#1606, fixes #1604); A zero-length subpath draws its round cap as a dot and its projecting cap as a square, so dots in imported `.ai` and SVG files show (#1619, fixes #1613); scores unchanged (estimated) | | 2026-10-13 | minor | Tools, UI, effects and type: Pathfinder's Trim, Merge and Crop remove the objects' strokes, and Merge joins same-fill objects whatever their strokes were, as the public Pathfinder page describes (#1572, fixes #1570); Offset Path with an inset larger than the shapes says the offset leaves nothing, instead of asking to select paths (#1591, fixes #1590); The Symbols panel's New Symbol opens Symbol Options to name the symbol first, and Alt-click skips it (#1566, fixes #1560); Tool flyouts are as wide as their longest translated name and shortcut need, so names no longer run into shortcuts in Spanish and other languages (#1650, fixes #1616); Smart Guides: a point drawn with the Pen or a shape tool, and an anchor dragged with Direct Selection, snaps to where paths and ruler guides cross, labelled "intersect" (#1658, part of #1602; a moved selection's own anchors don't); The Transform panel shows a selected ruler guide's X or Y, and typing a value moves it (#1659, part of #1597). This is not established by the documentation we could find: the public help pages on guides, rulers and the Transform, Control and Properties panels (read again on 2026-10-14) describe selecting and dragging unlocked guides and the panels' X and Y for a selection, not a guide's position in a panel, and the one public statement of it is a user's [community answer](https://community.adobe.com/questions-652/adding-a-new-custom-guide-785586), which is not documentation. It is kept as a convenience pending the owner's decision, with no parity claim and nothing counted towards a score; Select Behind (Ctrl/Cmd-click) tests each object once, so a deep stack of overlapping objects no longer slows it; same objects in the same order (#1571); Commands that take `ids` run on the objects given with nothing selected, over MCP, the control channel and the CLI too (#1577, fixes #1575); Zig Zag spaces its ridges evenly along each segment, so lines and curves get a regular wave in Corner and Smooth (#1558); Warp › Arc (the effect and Envelope › Make with Warp) bends in the object's real proportions: a circular arc with concentric edges and no fold-over at 100%, so Arc on art that isn't square draws differently from before (#1581, fixes #1578); `effect.apply` and `effect.setParams` give an error for a misspelt or wrongly typed effect parameter instead of running the effect with its default (#1623, #1592); Area type in a non-rectangular shape moves a word too wide for the top lines down to the first line that holds it instead of breaking it (#1617, fixes #1615); Stylistic sets (ss01–ss20), stylistic and titling alternates, proportional and lining figures, and superscript and subscript glyphs can be turned on as OpenType features of a character style (`text.setStyle {features}`), and the OpenType panel keeps them (#1611, fixes #1607); The Polish translator confirmed the Polish rows the maintainers had added after the catalog was written (#1614); scores unchanged (estimated) | | 2026-10-13 | minor | PDF.R1: editable PDF import uses the shared pinned reader; layer and overprint scans preserve state across content windows and reject known incomplete input. Synthetic streaming, decoder-limit and dependency-source regressions added; scores and hours unchanged. | @@ -2384,15 +2392,15 @@ Edit › Copy and Cut put the selected ruler guides on the clipboard, with the s | 2026-10-13 | minor | Tools and UI: Make Compound Path on a group (such as outlined type) keeps the paint of its backmost path instead of drawing nothing, and compound paths joined into one keep their holes (#1537); a round corner next to a curved side curves smoothly instead of sticking out of the shape and the Smooth tool smooths only the stretch of path under the drag (#1547, fixes #1512); Join merges the selected anchor points that lie on one another, as after Average, into one anchor (#1582, fixes #1533); Lock Guides also locks the objects made into guides: they can't be selected, moved or deleted (#1543); they are drawn in the guide colour and style of Preferences, and a dragged selection snaps to ruler guides with Smart Guides on (#1583, fixes #1534); Edit › Keyboard Shortcuts lists Edit › Duplicate and the other commands that are in no menu but run without parameters (panel-menu commands such as Break Link to Symbol), so they can be given shortcuts (#1549, #1580, fixes #1539). Correction: the combined Average and Join with its corner or smooth choice was listed as done in [ui-parity.md](ui-parity.md) and does not exist in the app (`path.join` and `path.average` are separate commands, and Join has no such choice); it is now listed as missing there and in [gap 1](gaps.md); scores and hours unchanged (estimated) | | 2026-10-13 | minor | UI and tools: sliders in Save for Web, the raster export options, Preferences and the opacity popup show their rail, filled up to the handle (#1506, fixes #1483); in Outline view a template layer stays in Preview, dimmed (#1505, fixes #1486); Direct Selection takes the selected object's anchor where anchors of overlapping objects coincide (#1509, fixes #1508); a tab stop in the Tabs panel drags from where the press landed (#1487, fixes #1458); type: commands and MCP `add_text` report area type that does not fit its frame (#1500, fixes #1471); graphs: `graph.setData`'s CSV keeps each value exactly (#1485); scores and hours unchanged (estimated) | | 2026-10-13 | minor | 3D and Materials: outlined words and other shapes of many small curves take Rotate, Extrude & Bevel and Inflate (#1481), Twist runs 0° to 360° (#1524) and twisted walls keep their faces (#1525), all described in [three-d.md](three-d.md); rendering: print-size images draw, export and crop (#1517); `vectorcraft-cli perf` on Windows reports load and memory (#1502; no budget re-run); localization: French catalog complete again with 423 strings added and 188 reworded (#1511), a Hungarian message in the informal address (#1474); scores and hours unchanged (estimated) | -| 2026-10-12 | minor | File interop: Save As `.ai` also writes the legacy version 7 form (#868), keeping layers, groups, compound paths, clipping groups and type as text objects, one artboard per file, with Transparent Images, Hidden Objects and Text options; `.ai` legacy type is read on open; not yet tested by users who hand files back, so scores and hours are unchanged (estimated) | | 2026-10-13 | minor | Image Trace, measured in `image-trace-quality.md`: the palette is made of the picture's own colours (#1504): 16 Colors on flat art 70 → 4.2 paths and 5.1 → 0.13 px off, paths in a colour the art lacks 5.9 → 0; photos are smoothed within their tones first and stacked layers go by lightness (#1516): High Fidelity Photo 3,653 → 121 paths with its colour error 2.30 → 1.75, 16 megapixels traced where 4 were refused. Score and hours unchanged (estimated) | | 2026-10-13 | minor | 3D and Materials: Inflate takes Twist, Taper and Cap ([three-d.md](three-d.md)); scores unchanged (3D ~25%) | -| 2026-10-12 | minor | Width Point Edit: Shift-double-click enables adjoining adjustment; plain double-click keeps it off; scores and hours unchanged | | 2026-10-13 | minor | Automation: a command run through MCP, the control channel or the CLI refuses a parameter key it doesn't take, naming it and the nearest accepted key (#1496, fixes #1442; 598 of 737 engine commands checked, 168 params descriptions corrected); plug-ins: a numeric parameter's default stays inside its declared range (#1463); stability: a window surface larger than 8192 px is configured up to the graphics adapter's limit instead of closing the app (#1495, part of #1459; not reproduced on a mixed-scale setup); text frames, wrap shapes and type paths beyond a billion points hold no text instead of hanging the layout (#1451); scores and hours unchanged (estimated) | | 2026-10-13 | minor | File formats: PDF and `.ai` import keep a stroke opaque and unmasked when the fill under it was faded or masked by its group, and a `.ai` read as its PDF part keeps its RGB numbers (#1434); SVG export writes knockout groups of opaque art without masks, and `convert` takes `--decimals` (#1435); grayscale TIFFs with an alpha channel or other extra channels place, relink and open (#1489, part of #1466; colourising 1-bit and grayscale images is still open); score and hours unchanged (estimated) | | 2026-10-13 | minor | Path operations and transforms: Split Into Grid has its row and column sizes, gutters, totals, Add Guides and Preview (#1488, fixes #1462); the Pathfinder panel works on a group selected alone (#1493, for #1450; one plain path alone is not settled by the documentation); Transform Objects in the Transform dialogs and Transform Each, and Reflect across an angle (#1494, fixes #1448 and #1460); Fit Headline in a frame one line tall (#1445, fixes #1444); value fields: selected text is readable (#1490, part of #1465) and a focused field follows its value (#1497, fixes #1457); the many-object edits of #1430 and #1464 are under Performance already; scores and hours unchanged (estimated) | -| 2026-10-12 | minor | Character styles retain explicit Auto leading, propagate resets and preserve local leading overrides; scores and hours unchanged | | 2026-10-13 | minor | Image Trace: Black and White, Grayscale and Color place edges by the blend of the pixels across them and fit outlines as Flat Logo does; a harness measures every preset (`image-trace-quality.md`): Default's outlines 0.31 → 0.09 px off on average, Black and White Logo's 0.27 → 0.06 with 15 → 0.3 kinks, gaps between abutting paths 64 → 22 px. Score and hours unchanged (estimated) | +| 2026-10-12 | minor | File interop: Save As `.ai` also writes the legacy version 7 form (#868), keeping layers, groups, compound paths, clipping groups and type as text objects, one artboard per file, with Transparent Images, Hidden Objects and Text options; `.ai` legacy type is read on open; not yet tested by users who hand files back, so scores and hours are unchanged (estimated) | +| 2026-10-12 | minor | Width Point Edit: Shift-double-click enables adjoining adjustment; plain double-click keeps it off; scores and hours unchanged | +| 2026-10-12 | minor | Character styles retain explicit Auto leading, propagate resets and preserve local leading overrides; scores and hours unchanged | | 2026-10-12 | minor | TIFF is available in Export for Screens and Asset Export with shared format settings; scores and hours unchanged | | 2026-10-12 | minor | 3D and Materials: Extrude & Bevel, Inflate and Rotate with Position presets ([three-d.md](three-d.md)); 3D ~8% → ~25%, 50–85 → 30–55 h (estimated); menu 353/375 wired (measured from the stubs removed) | | 2026-10-12 | minor | File formats: an Affinity text frame too short for its first line is made taller with a warning instead of showing nothing (#1423); synthetic-file test; score and hours unchanged (estimated) | @@ -2416,8 +2424,6 @@ Edit › Copy and Cut put the selected ruler guides on the clipboard, with the s | 2026-10-12 | minor | Localization: Vietnamese (`vi`) catalog added, complete under the tests; Localization 63% → 71% (measured), hours 60–100 → 55–90; full readiness unchanged (~62%) | | 2026-10-12 | minor | Localization: Korean (`ko`) catalog added, complete under the tests; Localization 54% → 63% (measured), hours 70–110 → 60–100; full readiness unchanged (~62%) | | 2026-10-12 | minor | Desktop PDF spooler shared through craft-print; exact physical sizes, composed copies and Windows 7 fallback retained. Scores and remaining gaps unchanged. | -| 2026-10-11 | minor | Image Trace: an image over 64 megapixels (up to 192) is traced scaled down to 64 megapixels with its paths at the image's own size, instead of being refused; score and hours unchanged (estimated) | -| 2026-10-11 | minor | Czech render-site follow-up: file-dialog filters, diagnostics, preset numbering, contributor tooltips/plurals, gradient grammar and contextual printer mark styles; estimates unchanged | | 2026-10-12 | minor | Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged | | 2026-10-12 | minor | Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged | | 2026-10-12 | minor | Path operations: Simplify preview/counts, valid component selection after refit/subdivision (#1272); slider/auto/advanced UI remains open; scores/hours unchanged | @@ -2431,17 +2437,19 @@ Edit › Copy and Cut put the selected ruler guides on the clipboard, with the s | 2026-10-12 | minor | Colour: Eyedropper reads visible locked and outside-isolation sources (#1251); scores and hours unchanged | | 2026-10-12 | minor | Selection: segment picks are distinct from anchor picks; Delete preserves neighboring geometry (#1278), egui/native checks; scores unchanged | | 2026-10-12 | minor | Colour: unpainted zero-width strokes regain 1 pt when painted (#1253); scores and hours unchanged | +| 2026-10-12 | minor | UI chrome: native macOS save sheets gain a Format menu (#866), with extension regression tests and actual save/cancel verification; score and hours unchanged (estimated) | +| 2026-10-12 | minor | Compound-path appearance and fill-rule edits through directly selected members (#1197) verified with rendered holes and Undo/Redo; scores and hours unchanged | +| 2026-10-12 | minor | Path operations: Divide accepts open cutting paths (#1307); filled-face appearance, holes and bounded fallback regression-tested; scores and hours unchanged | +| 2026-10-12 | minor | UI chrome: numeric focus clicks select on release while drags retain their press position (#1326, in part); keyboard focus, stepping and double-click regressions pass; scores and hours unchanged; short movements below the click threshold remain open | +| 2026-10-12 | minor | DXF reads control-point SPLINE closure (#1299 follow-up after #1320); open curves stay open and curves retained; rational/non-rational import and export versions regression-tested; scores/hours unchanged | +| 2026-10-11 | minor | Image Trace: an image over 64 megapixels (up to 192) is traced scaled down to 64 megapixels with its paths at the image's own size, instead of being refused; score and hours unchanged (estimated) | +| 2026-10-11 | minor | Czech render-site follow-up: file-dialog filters, diagnostics, preset numbering, contributor tooltips/plurals, gradient grammar and contextual printer mark styles; estimates unchanged | | 2026-10-11 | minor | The Graph Data worksheet (Import Data, Cell Style, Transpose, Switch x/y, Revert). Scores unchanged within rounding | | 2026-10-11 | minor | Graph label formatting kept through regeneration. Scores unchanged within rounding | | 2026-10-11 | minor | Graph Type's widths past 100%, First Row/Column in Front and Draw Filled Lines. Scores unchanged within rounding | | 2026-10-11 | minor | Graph Type's Add Drop Shadow and Add Legend Across Top. Scores unchanged within rounding | | 2026-10-11 | minor | Graph Data's Transpose and Switch x/y, `|` line breaks in graph labels, and the pie graph options (Legends in Wedges, Position, Sort). Scores unchanged within rounding | | 2026-10-11 | minor | Graph designs and marker designs (Object › Graph › Design… and Marker…): menu stubs 27 → 25; the graphs row's missing items are column designs and Image Trace depth. Scores unchanged within rounding | -| 2026-10-12 | minor | UI chrome: native macOS save sheets gain a Format menu (#866), with extension regression tests and actual save/cancel verification; score and hours unchanged (estimated) | -| 2026-10-12 | minor | Compound-path appearance and fill-rule edits through directly selected members (#1197) verified with rendered holes and Undo/Redo; scores and hours unchanged | -| 2026-10-12 | minor | Path operations: Divide accepts open cutting paths (#1307); filled-face appearance, holes and bounded fallback regression-tested; scores and hours unchanged | -| 2026-10-12 | minor | UI chrome: numeric focus clicks select on release while drags retain their press position (#1326, in part); keyboard focus, stepping and double-click regressions pass; scores and hours unchanged; short movements below the click threshold remain open | -| 2026-10-12 | minor | DXF reads control-point SPLINE closure (#1299 follow-up after #1320); open curves stay open and curves retained; rational/non-rational import and export versions regression-tested; scores/hours unchanged | | 2026-10-11 | minor | Type core: Middle Eastern digit types (Character panel menu › Digits: Default, Arabic, Hindi, Farsi; `{digits}`) leave the missing list; score and hours unchanged at this rounding (estimated) | | 2026-10-11 | minor | PDF raster-effect cap reductions are reported per original object with requested/achieved ppi; scores and hours unchanged | | 2026-10-11 | minor | Platforms: Preferences › Performance › Linux Display Server opens the window through XWayland, where file drops work (#1151); score unchanged (estimated) | @@ -2457,21 +2465,17 @@ Edit › Copy and Cut put the selected ruler guides on the clipboard, with the s | 2026-10-10 | major | Full re-measure against Illustrator 2026 30.x from code counts, open issues and public docs; added breadth/depth/ready-for-real-work numbers, generative-AI row, calibration from PR timestamps; merged the ROADMAP's parity estimate, honest assessment and "Shipped so far" into this file | | 2026-10-05 | major | (in ROADMAP.md) M3 and M4 rows re-derived after the paint and files milestones | | 2026-10-02 | major | (in ROADMAP.md) First 22-area weighted estimate | -| 2026-10-28 | minor | The center drag turns only the rays by one degree per document point of increasing pointer y and back at the same rate for decreasing y. This is an application rule independent of event frequency. Shift keeps the current turn. Space freezes rotation on every sample, including the first; the initial samples establish size until the gesture begins, after which Space translates the flare without changing its size or turn. Releasing a modifier resumes from the current pointer position without catching up skipped rotation. Scores and remaining estimates unchanged. | -| 2026-10-28 | minor | Drop Shadow Darkness subtracts the normalized amount from RGB source channels or adds it to CMYK/Gray black before profile conversion and blur; near-100% becomes gray black, with shadow opacity separate. SVG keeps live filters for uniform sRGB sources and renders shadows for other RGB profiles, source models and gradients. Gradient-stop adjustment is an application rule; spot/custom colors and composite-source parity remain open. Scores and estimates unchanged. | -| 2026-10-28 | minor | Blob Brush Shift locks after clear axial movement, keeps earlier samples fixed, and waits without an off-axis trail. Activation uses raw press displacement in logical screen pixels, with exact quarter-turn boundaries. Explicit application rule; scores and estimates unchanged. | -| 2026-10-28 | minor | Flare Tool Options opened by a canvas click previews its settings at that click. Reset refreshes the enabled preview while keeping the dialog open. Cancel discards preview art and tentative options; OK accepts one flare in one Undo step. Editing existing flare artwork remains outside this dialog scope. Scores and estimates unchanged. | > **Tab-drag status (2026-10-22):** updated from upstream, including the Turkish catalog and all three tab-drag translations requested in #3479; Selection-tool reset regression-checked and clipboard evidence scoped to internal state; drag lifecycle, insertion and regression contracts documented; behavior, scores and estimates unchanged. | Date | Change | Tab-drag history | |---|---|---| +| 2026-10-30 | minor | Gradient panel presets (30.0) done: the colour area no longer lists them; scores and estimates unchanged. | | 2026-10-22 | minor | Verify Selection-tool reset after tab activation, drop and cancellation; narrow clipboard evidence to the tested internal clipboard and correct documentation grammar; behavior, scores and estimates unchanged. | | 2026-10-22 | minor | Merge current main and complete the Turkish tab-drag labels and validation messages requested by the maintainer; all 24 catalogs covered; scores and estimates unchanged. | | 2026-10-22 | minor | Tab-drag follow-up: reconcile upstream progress updates and document drag lifecycle, insertion behavior and regression contracts; behavior, scores and estimates unchanged. | | 2026-10-22 | minor | Artwork dragged with Selection switches visible document tabs after a 400 ms hover, then copies editable artwork on canvas release; source and clipboard stay intact; scores and hours unchanged | -| 2026-10-30 | minor | Gradient panel presets (30.0) done: the colour area no longer lists them; scores and estimates unchanged. | 2026-10-28 Flare interaction: the center drag turns only the rays by one degree per document point of increasing pointer y and back at the same rate for decreasing y. This is an application rule independent of event frequency. Shift keeps the current turn; Space during either drag moves the flare without changing its size or turn, including the first drag sample. Releasing a modifier resumes from the current pointer position without catching up skipped rotation. Scores and remaining estimates unchanged. ## EPS Save evidence (2026-10-26) -Ordinary Save and Save As keep EPS as EPS, with the chosen page frame, transparent margins and the complete editable VectorCraft document. Existing import-loss confirmation and transparency warnings remain; the Save TIFF preview is capped at 1024 pixels, while Export retains 2048. See [file-format-parity.md](file-format-parity.md#eps-save-and-source-protection) for limits and regression evidence. This narrows G9 save-back workflow coverage; native Illustrator editing-data fidelity, parity scores and hour estimates are unchanged. Since 2026-11-12 the EPS Options of Save As show Use Artboards, unchecked by default: unchecked, the bounding box fits the visible art; checked, Save keeps the chosen page frame and margins described here. \ No newline at end of file +Ordinary Save and Save As keep EPS as EPS, with the chosen page frame, transparent margins and the complete editable VectorCraft document. Existing import-loss confirmation and transparency warnings remain; the Save TIFF preview is capped at 1024 pixels, while Export retains 2048. See [file-format-parity.md](file-format-parity.md#eps-save-and-source-protection) for limits and regression evidence. This narrows G9 save-back workflow coverage; native Illustrator editing-data fidelity, parity scores and hour estimates are unchanged. Since 2026-11-12 the EPS Options of Save As show Use Artboards, unchecked by default: unchecked, the bounding box fits the visible art; checked, Save keeps the chosen page frame and margins described here. diff --git a/docs/ui-parity.md b/docs/ui-parity.md index dbe69b7603..c54338341b 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -51,12 +51,9 @@ Artboard Options forwards the active Artboard tool's Scale Artwork with Artboard | Date | Change | Summary | |---|---|---| -| 2026-11-03 | minor | A double-click on a ruler adds a guide at the pointer (#5239): a vertical one on the top ruler, a horizontal one on the left ruler; 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. | | 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-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-10-30 | minor | Home screen: welcome, New / Open and Community are centred and the large Discord button uses the theme accent, part of #4684; scores and estimates unchanged (estimated) | | 2026-11-06 | minor | Snap to Grid: a straight segment dragged with Direct Selection lands its anchors on the grid, not the pressed point (#3835); scores and estimates unchanged | | 2026-11-06 | minor | Drop Shadow: a swatch beside Color follows the hex and a double-click opens the Color Picker over the dialog, whose OK fills Color (#5364); scores and estimates unchanged | | 2026-11-06 | minor | Drop Shadow, Outer Glow and Inner Glow: spinner arrows beside Opacity, the offsets, Blur and Darkness (#5367, #5368); scores and estimates unchanged | @@ -85,11 +82,14 @@ Artboard Options forwards the active Artboard tool's Scale Artwork with Artboard | 2026-11-04 | minor | Selection Tools add-on: Select by Artboards selects the objects overlapping, fully inside or fully outside several chosen artboards, from a dialog that lists, searches and multi-selects the artboards and fits one in the window on double-click (#5525, #5548, #5549, a VectorCraft addition); scores and estimates unchanged. | | 2026-11-04 | minor | File › Export › Export As PNG… exports the active artboard, or each artboard the Artboard tool has selected (one file each), instead of always the first (#5502); a choice the caller gives is kept; scores and estimates unchanged | | 2026-11-04 | minor | Document Tools add-on (a VectorCraft addition): Object › Plug-ins › Document Tools › Batch Trace Folder asks for a folder of images and a destination folder and traces each image into it (#4921); scores and estimates unchanged | +| 2026-11-03 | minor | A double-click on a ruler adds a guide at the pointer (#5239): a vertical one on the top ruler, a horizontal one on the left ruler; scores and estimates unchanged. | | 2026-11-03 | minor | The Action Options row cites the public Actions panel page and describes its controls. Scores and estimates remain unchanged. | | 2026-11-03 | minor | Enclosing Lasso provides whole-object enclosure selection as an add-on with no default shortcut or Basic toolbar slot. Scores and estimates remain unchanged. | | 2026-11-03 | minor | Transform previews valid typed values while numeric fields remain focused. Effect dialogs use the object, group, or layer targeted in Appearance. | | 2026-11-03 | minor | New gradients use CMYK default stops in CMYK documents, including Gradient tool creation. Scores and estimates unchanged. | | 2026-11-03 | minor | Help Bar has its own Window menu toggle and workspace visibility setting. 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-10-30 | minor | Home screen: welcome, New / Open and Community are centred and the large Discord button uses the theme accent, part of #4684; scores and estimates unchanged (estimated) | | 2026-10-26 | minor | Pencil Fidelity goes down to 0.01 pt (dialog, tool and engine; Smooth keeps 0.05) on a logarithmic slider across the Tool Options row, with hundredths in the field and key steps that fit the range (#3328); scores and estimates unchanged (estimated) | | 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. | @@ -365,6 +365,7 @@ next unit in the order a right-click on a ruler lists them, from Feet back to Po | Interaction | Status | Evidence | |---|---|---| +| Ordinary Save refused by filesystem permissions/read-only storage offers Save As | focused UI regressions passed; native acceptance pending | #6146; `io::tests_read_only_save`; same name/format suggested; unchanged format keeps options, changed format opens its Save options; Documents folder, UID-bound continuation; cancellation and automation isolation | | Selection artwork drag hovers a visible document tab to activate it, then drops editable artwork on its canvas | done | `tests_document_drag`, `edit.dropSelection`, lossless resource and one-step Undo regressions; overflow and cross-window drag remain separate | | A document tab dragged along the tab strip moves its document; a line marks where it goes, the current document and every document's view stay as they were, and a drag off the strip still floats it | done | #5663, [public source](https://helpx.adobe.com/illustrator/using/customizing-workspace.html); `document.move`; `tests::tab_reorder`, `chrome::tests` (drop index with floating and view tabs), `engine` `tests::document_move` | | A middle-click on a document tab or its × closes it as the × does; closing a modified document's last view asks to save first (VectorCraft addition, as in PhotoCraft) | done | #6506; `chrome::tests::a_middle_click_on_a_tab_closes_it_as_the_close_box_does`; not scored | @@ -522,41 +523,44 @@ before release (#6373). [Paint faces and edges](https://helpx.adobe.com/illustra |---|---|---| | 2026-11-12 | minor | Remove Background for a selected image in the Contextual Task Bar, Properties › Quick Actions and the Control bar, which runs at once with its defaults. Scores and estimates unchanged. | | 2026-11-12 | minor | Live Paint Bucket fills faces crossed in one drag (#6373). Scores and estimates unchanged. | +| 2026-11-12 | minor | Save refusal follow-up (#6146): changed format opens its Save options; unchanged format retains original options. Focused UI regressions passed, including the format-change case that failed before the fix; native panel acceptance pending. Scores and estimates unchanged. | | 2026-11-11 | minor | Ruler origin: the corner drag snaps as a drawn point does, onto the art's and the artboards' anchors, centres, edges and paths, and with Snap to Point along the ruler guides; scores and estimates unchanged | | 2026-11-11 | minor | Eyedropper copies effective character dimensions between transformed text objects (#6404); scores and estimates unchanged. | +| 2026-11-11 | minor | Ordinary Save to a location refusing writes offers Save As (#6146); cancellation preserves the original path and modified state; automation receives READ_ONLY_LOCATION. Scores and estimates unchanged. Validation pending. | | 2026-11-11 | minor | Outline view: a click on a path's outline selects it through an image or a gradient mesh in front, which Outline draws as a frame or mesh lines (#6437). | | 2026-11-11 | minor | A middle-click on a document tab (or its ×) closes it as the × does; closing a document's last view with unsaved changes asks to save first (#6506). VectorCraft addition; scores and estimates unchanged. | -| 2026-11-03 | minor | A double-click on a ruler adds a guide at the pointer (#5239): a vertical one on the top ruler, a horizontal one on the left ruler; scores and estimates unchanged. | -| 2026-11-10 | minor | Symbol libraries: Inventory Icons joins Basic Shapes, UI Frames and Map Markers as a fourth built-in library (Sword, Shield, Potion, Key, Gem; no default 9-slice). | | 2026-11-11 | minor | Files that record no units (PDF, EPS or `.ai` without ruler units, raster images, metafiles, Affinity files, importer plug-ins) open and revert in Preferences › Units › General instead of points; native, SVG and DXF files and `.ai`/EPS ruler units are kept (#5820). Scores and estimates unchanged. | +| 2026-11-11 | minor | Blob Brush: while painting, the preview of a round tip is the outline of the area the stroke adds, round at both ends, as the stroke it commits (#4343). Scores unchanged (estimated). | +| 2026-11-10 | minor | Symbol libraries: Inventory Icons joins Basic Shapes, UI Frames and Map Markers as a fourth built-in library (Sword, Shield, Potion, Key, Gem; no default 9-slice). | | 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-10 | minor | Vector Halftone Preview leaves the dots unobscured and keeps the selection box (#6369). Scores and estimates unchanged. | | 2026-11-09 | minor | Interim macOS canvas cursor delivery (#2292): existing artwork in aligned 1×/2× AppKit representations, visible system fallback and host-owned restoration. Headless routing, bitmap and ownership coverage; native timing, menus, focus/handback, mixed displays, Spotlight and full-screen Spaces remain unverified. 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 | 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-07 | minor | Pen and Curvature tool with Cmd/Ctrl held: Edit › Copy and Cut take the anchors and segments the lent Direct Selection picked, as with Direct Selection chosen, not the whole path; with the Pen's own last anchor selected Copy still takes the whole path (#6123); scores and estimates unchanged | | 2026-11-07 | minor | Live Corners: with Direct Selection, while every corner of a path is selected, a Shift-click on a corner widget picks that corner alone instead of deselecting it (#5188); adding a hidden corner's widget by Shift-click stays open (#1232); scores and 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 | Keyboard shortcuts: Cmd/Ctrl+Alt+. and Cmd/Ctrl+Alt+, (Stroke Weight Up and Down) step each selected stroke's weight, or the next object drawn's (#5797); scores and estimates unchanged | | 2026-11-06 | minor | Properties: Quick Actions has Repeat Options for one selected radial, grid or mirror repeat, opening Object › Repeat › Options with its values (#5181); scores and estimates unchanged | | 2026-11-06 | minor | Control bar: with type selected, a Paragraph: link (the Paragraph panel in a popover) and the Align left, Align center and Align right buttons follow the Character controls; the bar leaves them out from its right end when short of room, after X/Y/W/H and the Align buttons (#5372); scores and estimates unchanged | +| 2026-11-05 | minor | Document tabs are reordered by dragging them along the tab strip (`document.move`, #5663). | +| 2026-11-05 | minor | Compound Shapes add-on (beyond Illustrator): Object › Plug-ins › Compound Shapes lists Make, Release and Expand Compound Shape, and holds a setting, off by default, that makes the Pathfinder panel's Shape Mode buttons make a compound shape on a click and give the permanent result on Alt-click (the default is the reverse). Selected members of a compound shape always take the clicked mode. The setting is `window.pathfinder.compoundShapes`; no existing behavior changes while it is off. 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. | -| 2026-11-05 | minor | Document tabs are reordered by dragging them along the tab strip (`document.move`, #5663). | | 2026-11-04 | minor | Path Precision Tools add-on (a VectorCraft addition): Set Corners… rounds only the corners in an angle range (#5545); scores and estimates unchanged | | 2026-11-04 | minor | Path Precision Tools add-on (a VectorCraft addition): Set Corners… rounds all, the odd-numbered or the even-numbered anchors (#5546); scores and estimates unchanged | -| 2026-11-05 | minor | Compound Shapes add-on (beyond Illustrator): Object › Plug-ins › Compound Shapes lists Make, Release and Expand Compound Shape, and holds a setting, off by default, that makes the Pathfinder panel's Shape Mode buttons make a compound shape on a click and give the permanent result on Alt-click (the default is the reverse). Selected members of a compound shape always take the clicked mode. The setting is `window.pathfinder.compoundShapes`; no existing behavior changes while it is off. Scores and estimates unchanged. | | 2026-11-04 | minor | New Document: the units chosen in Preset Details belong to the chosen document, as the pane's options are listed "for the selected preset" ([public source](https://helpx.adobe.com/illustrator/desktop/add-and-import-files/start-a-new-file/create-and-save-custom-document-presets.html)); 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 (#2078); scores and estimates unchanged | -| 2026-11-01 | minor | Object › Image Trace › Batch Trace Folder asks for a source and a destination folder and traces each image into it (#4921); scores and estimates unchanged | -| 2026-11-01 | minor | Edit › Copy and Cut take the selected ruler guides, with the objects or alone; pasting puts them on the layer new art goes to, moved as the objects are (alone: where they were), Paste in Place onto the active artboard and Paste on All Artboards onto each (#3226); scores and estimates unchanged | | 2026-11-04 | minor | Area type: Align Top / Center / Bottom / Justify Vertically buttons in the Properties panel (Paragraph section) and the Control panel set its vertical alignment in the frame, as Area Type Options does; turned off for frames with rows or columns and threaded frames (#3759); scores and estimates unchanged | +| 2026-11-03 | minor | A double-click on a ruler adds a guide at the pointer (#5239): a vertical one on the top ruler, a horizontal one on the left ruler; scores and estimates unchanged. | | 2026-11-03 | minor | Enclosing Lasso selects fully enclosed objects with a freehand loop under Object › Plug-ins; scores and estimates unchanged. | | 2026-11-03 | minor | Type on a Path Options has a Preview checkbox (#5360, as public tutorials of the dialog show); scores and estimates unchanged | -| 2026-11-02 | minor | Font-family fields in Character, Properties and Control accept the wheel without opening the menu (#5253); scores and estimates unchanged. | | 2026-11-03 | minor | Symbol editing draws and drags 9-slice guides on the canvas (#5280). Guides outside the artwork remain unsupported. Scores and hours remain unchanged. | +| 2026-11-02 | minor | Font-family fields in Character, Properties and Control accept the wheel without opening the menu (#5253); scores and estimates unchanged. | +| 2026-11-01 | minor | Object › Image Trace › Batch Trace Folder asks for a source and a destination folder and traces each image into it (#4921); scores and estimates unchanged | +| 2026-11-01 | minor | Edit › Copy and Cut take the selected ruler guides, with the objects or alone; pasting puts them on the layer new art goes to, moved as the objects are (alone: where they were), Paste in Place onto the active artboard and Paste on All Artboards onto each (#3226); scores and estimates unchanged | | 2026-11-01 | minor | A double-click on a Repeat with the Selection tool enters isolation mode on it, where its source art (type included: a double-click there edits the text) is edited and every copy follows (#5054); scores and estimates unchanged | | 2026-11-01 | minor | Live Corners: while a corner widget is pressed or dragged with the Selection or Direct Selection tool, the Up and Down arrows step the corners' kind (round, inverted round, chamfer) with the radius, as one undo step (#2898); scores and estimates unchanged | | 2026-11-01 | minor | Keyboard Shortcuts: every Effect menu item (Distort & Transform › Transform…, Stylize › Drop Shadow…, …) is listed under its menu path and takes a shortcut of its own, which the menu shows and which opens that effect's dialog (or applies an effect without options) (#3296); scores and estimates unchanged | -| 2026-10-31 | minor | Units › East Asian Type acts (#394): Q and Ha (a quarter of a millimetre each) are units that preference offers; 4 Preferences options remain stored but unused; scores and estimates unchanged | | 2026-11-01 | minor | Edit › Copy and Cut take the selected ruler guides, with the objects or alone; pasting puts them on the layer new art goes to, moved as the objects are (alone: where they were), Paste in Place onto the active artboard and Paste on All Artboards onto each; a Copy of guides alone empties the system clipboard, so Paste takes the guides, not what another app copied before (#3226); scores and estimates unchanged | | 2026-11-01 | minor | Color Effects add-on (a VectorCraft addition): Find Nearest Swatch (#4923); scores and estimates unchanged | | 2026-11-01 | minor | Artboards panel: a row dragged above or below another reorders the artboards (#4687); scores and estimates unchanged | @@ -566,7 +570,6 @@ before release (#6373). [Paint faces and edges](https://helpx.adobe.com/illustra | 2026-11-01 | minor | Arrange Tools add-on (a VectorCraft addition): Randomize Opacity (#4925); scores and estimates unchanged | | 2026-11-01 | minor | Arrange Tools add-on (a VectorCraft addition): Sort Objects (#4917); scores and estimates unchanged | | 2026-11-01 | minor | Blob Brush and Eraser: the tip's outline (size, angle, roundness, at the zoom) follows the pointer before and while painting, around a small centre dot, updating at once with `[` / `]` and Tool Options; precise cursors (Caps Lock) show only the crosshair (#4977); scores and estimates unchanged (estimated) | -| 2026-11-11 | minor | Blob Brush: while painting, the preview of a round tip is the outline of the area the stroke adds, round at both ends, as the stroke it commits (#4343). Scores unchanged (estimated). | | 2026-11-01 | minor | Layers panel: an open blend lists its spine as a `<Path>` row below its key objects, and a click selects the blend so Direct Selection edits the spine (#4820); scores and estimates unchanged | | 2026-11-01 | minor | Pen with Cmd/Ctrl held: the lent Selection tool no longer scales the path being drawn by bounding-box handles the canvas hides while its end anchor is selected (a flat path did nothing under a resize pointer); a Cmd drag moves the path instead, and with Direct Selection moves anchors and segments, one undo step each (#4980); scores and estimates unchanged | | 2026-11-01 | minor | Dynamic symbols (#4938): Symbol Options › Symbol Type; per-instance appearance for objects of a dynamic symbol's art; Reset in the Properties panel and the Symbols panel menu; on-canvas Direct Selection of the parts remains | @@ -578,6 +581,7 @@ before release (#6373). [Paint faces and edges](https://helpx.adobe.com/illustra | 2026-11-01 | minor | Arrange Tools add-on (a VectorCraft addition): Group Overlapping Objects (#4928); scores and estimates unchanged | | 2026-11-01 | minor | Character panel › Language: the selected characters' language, from Document Setup's list, which gains Hindi, Marathi, Gujarati, Bengali, Punjabi, Tamil, Telugu, Kannada, Malayalam, Odia and Urdu (#4946, #4943); the Digits menu's numeral names keep their own translations; scores unchanged | | 2026-11-01 | minor | VectorCraft Options' Version is a menu of the versions VectorCraft writes, each named by the releases that read it, and an uncompressible version turns Use Compression off (#4931); a save refused before writing shows the Can't save alert with the reason (#4933); About › Contributors' table keeps its header row above the scrolling rows (#4944); the Gradient panel's freeform point swatch opens the point's popover (#4953); scores unchanged | +| 2026-10-31 | minor | Units › East Asian Type acts (#394): Q and Ha (a quarter of a millimetre each) are units that preference offers; 4 Preferences options remain stored but unused; scores and estimates unchanged | | 2026-10-31 | minor | A file opened from the file manager while VectorCraft runs opens as a new tab in that window, which comes to the front, on Windows and Linux (#4892); `--new-instance` opts out; scores unchanged | | 2026-10-31 | minor | Object › Path › Smooth: Auto-Smooth takes a shaky line to one smooth curve, and direct-selected anchors smooth only their section; scores unchanged | | 2026-10-31 | minor | New Document: More Settings has Save Preset beside the name, as Preset Details does, and saves its settings (#4893); scores and estimates unchanged | @@ -594,7 +598,6 @@ before release (#6373). [Paint faces and edges](https://helpx.adobe.com/illustra | 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 | @@ -620,17 +623,19 @@ before release (#6373). [Paint faces and edges](https://helpx.adobe.com/illustra | 2026-10-30 | minor | Gradient panel: Presets menu and Save/Delete Gradient Preset (30.0); scores and estimates unchanged | | 2026-10-29 | minor | 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 and estimates unchanged | | 2026-10-29 | minor | Object › Path › Smooth… with its slider and Auto-Smooth (#4115); scores and estimates unchanged | +| 2026-10-28 | minor | Blob Brush Shift locks after clear axial movement, keeps earlier samples fixed, and waits without an off-axis trail. Activation uses raw press displacement in logical screen pixels, with exact quarter-turn boundaries. Explicit application rule; scores and estimates unchanged. | +| 2026-10-28 | minor | Flare Tool Options opened by a canvas click previews its settings at that click. Reset refreshes the enabled preview while keeping the dialog open. Cancel discards preview art and tentative options; OK accepts one flare in one Undo step. Editing existing flare artwork remains outside this dialog scope. Scores and estimates unchanged. | | 2026-10-25 | minor | Selection tool: a double-click on a threaded port breaks the thread there (`text.thread.break`); scores and estimates unchanged | | 2026-10-25 | minor | 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); headless canvas and tool regressions; scores and estimates unchanged. | | 2026-10-25 | minor | Floating panels open in OS windows of their own on the desktop (inside the window on the web and on Wayland); saved layouts from earlier versions convert to the shared docking layout (#4135); scores and estimates unchanged (estimated) | | 2026-10-25 | minor | Object › Expand… is dimmed for a selection with appearance attributes, with a tooltip to use Object › Expand Appearance first, so a stroked line with an effect expands to a stroked line before its stroke is outlined (#3089). Scores and estimates unchanged. | -| 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-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-25 | minor | Transform Each opens on the values it was last confirmed with (Copy aside) and has a Copy button left of Cancel instead of a Copy checkbox (#1732). Scores and estimates unchanged. | -| 2026-10-24 | minor | Recolor Artwork › Assign: double-clicking a row's new color opens the Color Picker over the dialog; its OK sets the row's new color in the model it was picked in (a CMYK mix stays CMYK, RGB becomes CMYK in a CMYK document), Cancel leaves the row as it was. A Lab new color is kept, but a Lab color can't be typed yet: the picker's Lab fields work in RGB, as for fill and stroke (part of #4028; Lab entry, swatch libraries and the row menu remain). Scores and estimates unchanged. | | 2026-10-25 | minor | Pathfinder panel › Pathfinder Options… opens a dialog with Precision, Remove Redundant Points and Divide and Outline Will Remove Unpainted Artwork, which the panel's operations use (#3426); scores unchanged | | 2026-10-25 | minor | Artboards panel › Delete Empty Artboards deletes the artboards with no artwork in one undo step and makes one that is left active; scores unchanged | | 2026-10-25 | minor | Transform panel menu: Transform Object Only / Pattern Only / Both and Use Registration Point for Symbol act in the Transform panel and the Properties panel's Transform section; scores and estimates unchanged | +| 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-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-24 | minor | Recolor Artwork › Assign: double-clicking a row's new color opens the Color Picker over the dialog; its OK sets the row's new color in the model it was picked in (a CMYK mix stays CMYK, RGB becomes CMYK in a CMYK document), Cancel leaves the row as it was. A Lab new color is kept, but a Lab color can't be typed yet: the picker's Lab fields work in RGB, as for fill and stroke (part of #4028; Lab entry, swatch libraries and the row menu remain). Scores and estimates unchanged. | | 2026-10-24 | minor | Properties: Merge Live Paint shows only when the selection holds nothing Live Paint refuses (type, images and other objects that aren't paths), which `livePaint.merge` and Make now refuse too. Scores unchanged. | | 2026-10-24 | minor | G1: Properties has the additional contextual controls of the public Properties panel overview: Blend Options and Merge Live Paint in Quick Actions, the Width Profile list and an applied effect's Delete icon in Appearance, and the kind and radius of corners selected with Direct Selection. Scores unchanged. | | 2026-10-24 | minor | Docs catch-up: Relative rounding (#3091) marked done; the stale "Video rulers: missing" duplicate row removed (done, #2849). Scores and estimates unchanged. | @@ -644,13 +649,13 @@ before release (#6373). [Paint faces and edges](https://helpx.adobe.com/illustra | 2026-10-23 | minor | Pencil: Shift held at the press draws a straight line at 45° steps from the Constrain Angle (part of #3799). Scores and estimates unchanged. | | 2026-10-23 | minor | Fix #3705: in Draw Inside, art drawn or pasted inside the isolated object takes isolation mode into its clip group, so it can be clicked and isn't dimmed (it used to stay locked and dimmed beside the isolated path); undo or redo that removes the isolated object ends isolation mode; scores and estimates unchanged. | | 2026-10-23 | minor | Preferences › General › Constrain Angle: arrow keys, Shift drags and new rectangles, ellipses, grids, type, polygons, stars and size-dialog shapes follow it (#394). Scores and estimates unchanged. | -| 2026-10-22 | minor | Pen, Anchor Point and Direct Selection drags: the preview gets every pointer position, including where it re-enters the window and the move in the release's frame (#834); score unchanged (estimated) | | 2026-10-23 | minor | Guides: a path moved by a press on its stroke or beside its edge, and a straight segment Direct Selection drags, snap by the nearest point of the path, landing exactly on a ruler guide (#3701); scores and estimates unchanged. | | 2026-10-23 | minor | Fix #3705 in part: drawing modes apply to pastes (Draw Inside puts Paste, Paste in Place and Paste on All Artboards inside the object drawn into; Draw Behind puts Paste in Place and Paste on All Artboards behind the selection), and Shift+D goes from Draw Behind back to Draw Normal when Draw Inside can't draw into the selection instead of stopping at Draw Behind; scores and estimates unchanged. | | 2026-10-23 | minor | Artboard Options: Show Video Safe Areas (action-safe 3.5% and title-safe 5% in from each edge, EBU R 95, canvas only) and Video Ruler Pixel Aspect Ratio, per artboard; View › Rulers › Show Video Rulers counts the active artboard in those pixels (#2849); two rows done; scores and estimates unchanged. | | 2026-10-23 | minor | Artboard Options: Show Center Mark and Show Cross Hairs (`artboard.setProps`/`artboard.new` `showCenterMark`, `showCrossHairs`) draw a center cross and lines from the middle of each side through the center on the canvas only, in one undo step; video safe areas, video rulers and the pixel aspect ratio remain missing; scores and estimates unchanged. | | 2026-10-23 | minor | Simplify: Object › Path › Simplify opens its slider at the Auto-Simplify level for the selection, previewing that result, instead of at 1 pt; partial-path simplify remains missing; scores and estimates unchanged. | | 2026-10-23 | minor | Simplify of part of a path: with anchors or segments of a path direct-selected, Object › Path › Simplify (slider, Auto-Simplify and dialog) refits only each run of selected segments between its end anchors; the Simplify row is done (its stale duplicate removed); scores and estimates unchanged. | +| 2026-10-22 | minor | Pen, Anchor Point and Direct Selection drags: the preview gets every pointer position, including where it re-enters the window and the move in the release's frame (#834); score unchanged (estimated) | | 2026-10-22 | minor | Gradient panel: the Method menu, Classic or Perceptual (Illustrator 30.0); scores and estimates unchanged. | | 2026-10-22 | minor | Gradient panel: the Dither box (Illustrator 30.0); scores and estimates unchanged. | | 2026-10-22 | minor | Recolor Artwork Edit chips scroll within the available screen height (#3416); small palettes retain their tab height; scores and estimates unchanged. | @@ -658,27 +663,27 @@ before release (#6373). [Paint faces and edges](https://helpx.adobe.com/illustra | 2026-10-21 | minor | Workspaces keep the layout changes made in them when switching away and back, and across restarts; Reset <name> returns to the saved layout; score unchanged (estimated) | | 2026-10-21 | minor | Panels dragged by the mouse join a dock group as tabs, split it top/bottom or become icons in the icon column; dock tabs close from their context menu; score unchanged (estimated) | | 2026-10-21 | minor | A double-click on a dock tab collapses its group to its tabs, as floating groups already did, and expands it again; another of its tabs expands it (`window.panel.collapse`); scores unchanged (estimated) | -| 2026-10-20 | minor | Workspaces keep the layout changes made in them when switching away and back, and across restarts; Reset <name> returns to the saved layout; score unchanged (estimated) | | 2026-10-21 | minor | M3: highlighted-text Eyedropper sampling preserves ranges and rejects unsupported source/target transparency atomically; regression-tested; scores and estimates unchanged. | | 2026-10-21 | minor | Shape Builder Gap Detection shares Live Paint’s spatial gap finder and temporary merge bridges (#2526); Custom is capped at 72 pt in the existing options dialog. Scores and estimates unchanged. | | 2026-10-21 | minor | Tangent and straight-segment perpendicular snapping use saved Smart Guides Geometric Guides preferences and Control bar quick access (#2537); no View submenu. Scores and estimates unchanged. | | 2026-10-21 | minor | 3D: original Standard, Diffuse, Top Left and Right lighting presets; command access, effective-setting recognition and preview/history; scores and estimates unchanged | | 2026-10-21 | minor | G1: one selected clip group's Properties panel has Edit Clipping Path and Edit Contents at its top (public doc cited in target-app-parity.md). Scores unchanged. | -| 2026-10-19 | minor | Distribute Objects and Distribute Spacing space the selected ruler guides across the axis with the objects, as items with no size, and two or more guides selected alone (#1597 in part); with a key guide and a spacing, the guide stays put and the other items are spaced from it (key guide from #3165); the Distribute Spacing value is enabled for a key guide; scores unchanged | | 2026-10-21 | minor | Snap to Glyph marked done (#3124); scores unchanged | +| 2026-10-20 | minor | Workspaces keep the layout changes made in them when switching away and back, and across restarts; Reset <name> returns to the saved layout; score unchanged (estimated) | | 2026-10-20 | minor | The selection tools (Selection, Direct Selection, Group Selection) take a clipping path on its outline, within the selection tolerance, before the art it clips, so its segments can be dragged where the art covers the mask's edge; Select Behind lists the same, and the eyedropper and the tools that act on what is drawn are unchanged. Doc and tool regression tests. Scores unchanged. | | 2026-10-20 | minor | Polygon and Star exact-size placement remembers the chosen count for later drags and dialog openings; Cancel preserves the previous count; existing opening gestures retained; scores and hours unchanged. | | 2026-10-20 | minor | Live trace result bounds and selection highlights exclude the retained bitmap except in source-image views; scores and hours unchanged. | +| 2026-10-19 | minor | Distribute Objects and Distribute Spacing space the selected ruler guides across the axis with the objects, as items with no size, and two or more guides selected alone (#1597 in part); with a key guide and a spacing, the guide stays put and the other items are spaced from it (key guide from #3165); the Distribute Spacing value is enabled for a key guide; scores unchanged | | 2026-10-19 | minor | A ruler guide selected with objects can be the key object (#2143): the Selection tool's click on it, or Align to Key Object with one object; drawn thicker; Align lines the objects up on it and the guide stays put; the other way round, selected guides align to a key object, which one object selected with guides can be; scores unchanged | | 2026-10-19 | minor | Cache status-readout sample widths per UI, language, document unit and pixels per point; regressions check reuse across live frames and refresh after display-setting changes. Scores and estimates unchanged. | | 2026-10-19 | minor | Status messages and readouts use stable responsive allocations sized for localized labels and ordinary coordinates. Contributor original-file acceptance confirmed; scores and estimates unchanged. | | 2026-10-19 | minor | Cached canvas dimensions match the renderer limit before cache-key creation, view construction and worker submission. Regression coverage; scores and estimates unchanged. | | 2026-10-19 | minor | Cached canvas reprojection preserves scale during resize, rotation and DPI changes. Contributor original-file acceptance confirmed; scores and estimates unchanged. | | 2026-10-19 | minor | Color Picker layout (#2008): compact controls, centered header, stacked actions and aligned inputs; screen-reader channel values, current shared button names and styling, screen sampler and model-by-model Tab order retained. Narrow windows scroll vertically; scores and hours unchanged. | -| 2026-10-18 | minor | Mirror Repeat shows its axis when selected (#2877): drag the line or center handle to move it, drag an end handle to rotate it (Shift: 45°), Escape cancels and each drag is one undo step. Engine and headless canvas gesture regressions passed; native move, rotation and Escape screenshots reviewed. Formatting, Clippy, WASM and repository checks passed; the undo label reuses translated Repeat Options. (#2877) Scores and hours unchanged. | -| 2026-10-18 | minor | Bounding-box handles take priority where a selected Mirror Repeat axis crosses them: the Top handle scales the repeat, while the center still moves it and the ends still rotate it. A headless canvas regression confirms height changes while width stays fixed; scores and hours unchanged. | | 2026-10-19 | minor | Blend Options: Ease In / Ease Out and Color Ease In / Out sliders with live preview (#1124); the Blend panel row stays partial (no easing in the panel, no on-canvas handles) | | 2026-10-19 | minor | Smart guides and snapping: tangent and perpendicular snapping for lines drawn from an anchor and View › Snapping (#2530), both partial; scores and hours unchanged | +| 2026-10-18 | minor | Mirror Repeat shows its axis when selected (#2877): drag the line or center handle to move it, drag an end handle to rotate it (Shift: 45°), Escape cancels and each drag is one undo step. Engine and headless canvas gesture regressions passed; native move, rotation and Escape screenshots reviewed. Formatting, Clippy, WASM and repository checks passed; the undo label reuses translated Repeat Options. (#2877) Scores and hours unchanged. | +| 2026-10-18 | minor | Bounding-box handles take priority where a selected Mirror Repeat axis crosses them: the Top handle scales the repeat, while the center still moves it and the ends still rotate it. A headless canvas regression confirms height changes while width stays fixed; scores and hours unchanged. | | 2026-10-18 | minor | Status-bar pointer X/Y follows the active global/artboard ruler origin and General units; three painted-text regressions cover origins, mode/artboard/reset/unit changes and read-only state; scores and hours unchanged. | | 2026-10-17 | minor | Blend spine Anchor Point conversions, selection/stacking priority and Shift regression coverage (#2544); scores and hours unchanged. Windows native canvas checks and screenshots reviewed in PR #2643. | | 2026-10-17 | minor | Artboard Options has X and Y beside a reference point locator, counted from the ruler origin as the Transform panel's are; a typed X or Y moves the artboard's reference point there and a new Width or Height keeps it in place (#1876); scores unchanged (estimated) | @@ -693,29 +698,29 @@ before release (#6373). [Paint faces and edges](https://helpx.adobe.com/illustra | 2026-10-16 | minor | Batch exporting in Export for Screens (#1935): a file name pattern (document and item names, a counter, the date), folders per format, item or both, Layers and Selection tabs with thumbnails, EPS, AI and VectorCraft rows, a PDF row with printer's marks, and export presets (built-in Logo Package and Print-Ready PDF, saved ones); scores and hours unchanged (estimated) | | 2026-10-16 | minor | Anchor Point tool: a click on one handle end retracts that handle only; the anchor becomes a corner and keeps the other handle (#2267). Handle drags start past the same 2 px as anchor drags. Scores and hours unchanged. | | 2026-10-16 | minor | Smart Guides: the dragged end of a straight segment snaps to the segment's extension at any angle, with the guide from its other end (#2120); scores and hours unchanged | -| 2026-10-15 | minor | Cursors of their own for 36 more tool states: node-editing cursors as the public manual describes them, Anchor Point and Curvature, Free Transform's side-shear; the rest borrow a drawn cursor until they're drawn. Busy cursor listed as missing. Every cursor is listed in `crates/tools/src/cursors.toml` | | 2026-10-16 | minor | 3D lighting: one to ten directional lights, a selectable light list, Add Light and Delete Light, independent settings and artboard gestures; ambient applied once and smooth RGB vector export; scores and hour estimates unchanged | | 2026-10-16 | minor | 3D lighting: opaque RGB Color for the shared directional light, with neutral ambient, preview/reset controls and smooth vector Revolve shading; scores and hour estimates unchanged | +| 2026-10-16 | minor | Document window scroll bars (#2322): thumb drag, track paging and the wheel over a bar; hidden in Full Screen and Presentation Mode; scores unchanged | +| 2026-10-15 | minor | Cursors of their own for 36 more tool states: node-editing cursors as the public manual describes them, Anchor Point and Curvature, Free Transform's side-shear; the rest borrow a drawn cursor until they're drawn. Busy cursor listed as missing. Every cursor is listed in `crates/tools/src/cursors.toml` | +| 2026-10-15 | minor | Artboard tool: closing a clean active tab restores the displayed board before Properties Delete. Nine targeted checks passed; three original close-route captures reviewed; scores and hours unchanged. | | 2026-10-14 | minor | Effect > Warp: style, orientation and percent controls; focused checks passed and original captures reviewed; scores and hours unchanged | | 2026-10-14 | minor | G11: open-path anchor Delete leaves separate surviving runs; dedicated Remove Anchors remains connected. Keyboard and history regression coverage added; scores and hours unchanged. | | 2026-10-14 | minor | Curvature: the Curvature tool's curve: 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°), and the ends of an open path curve in, so uneven spacing neither overshoots nor loops; the rubber band, dragged, inserted and converted points use the same rule; the Pencil's fitting unchanged (#1236, in part); scores unchanged (estimated) | | 2026-10-14 | minor | Quick Image Trace presets reveal the adjustment panel with Advanced open; Properties reopens it. Sliders keep their preset row until release so the re-trace is committed. UI regressions added; scores and remaining estimates unchanged. | | 2026-10-14 | minor | The macOS save sheet's Format menu goes back to missing: #1855 withdrew it to restore saving (#866 reopened). Rows added for the X/Y/W/H of direct-selected anchors (#1839, fixes #1824), the toolbar and All Tools drawer (#1751) and the Actions panel selection (#1853, part of #1629); the snapping row notes Snap to Grid over Snap to Pixel (#1840). Scores unchanged (estimated) | | 2026-10-14 | minor | Existing-guide Escape cancellation: eight app-input regressions, the Direct Selection Escape control and current full CI passed; scores and hours unchanged. | -| 2026-10-15 | minor | Artboard tool: closing a clean active tab restores the displayed board before Properties Delete. Nine targeted checks passed; three original close-route captures reviewed; scores and hours unchanged. | | 2026-10-14 | minor | Artboard tab changes align the tool with the displayed board; same-tab multiselection and Move/Scale Artwork options preserved. Three tab regressions and historical full CI on `a0671777` passed; scores and hours unchanged. | | 2026-10-14 | minor | Width-point finite-arithmetic follow-up: atomicity, history and native round-trip regressions and current full CI passed; scores and hours unchanged. | | 2026-10-14 | minor | Align and Distribute act on the anchor points selected with Direct Selection: the points of one path align to each other, not to the artboard, and Distribute spaces them (#1641, fixes #1636; aligning to a key anchor is still missing); Delete removes every selected anchor of a closed path (it used to remove the wrong ones), removes objects selected whole along with the selected anchors of others, and keeps the remainder selected so a second Delete removes it (#1674); In isolation mode Direct Selection and Group Selection pick only inside the isolated object (#1666); Escape cancels a Direct Selection drag (#1803, #1743); Escape while typing ends the edit, keeps the type selected and switches to the Selection tool (#1802, #1686, #1656); A filled Pencil path keeps a stroke of None (#1804, part of #1679); With the Pen, a press that moves under 3 screen pixels is a click (a corner without handles), and a Precise Cursors command toggles the existing preference and can take a shortcut (#1663); The [ and ] keys reach the Blob Brush, the Eraser and the Symbolism tools in the app (#1734, part of #1661); In the Pathfinder panel a group selected alone counts its nested shapes, so an expanded stroked object keeps the Shape Modes enabled (#1642, #1635); Object › Expand turns radial, grid and mirror repeats into groups of their instances (#1765, #1756); Select › Same matches type on its characters' fill and stroke (#1746, #1737); Fields commit what was typed when you click away or move to another field, as Enter does; Escape still discards it (#1787); Up and Down move through the command search results and Enter runs the highlighted one (#1798, #1757); Slider rails show on Medium Dark (#1808, #1662); On Windows the custom title bar's window stays truly borderless, so the pointer lines up with what is drawn on systems where the hidden caption offset it (#1791, #1758; awaiting the reporter's confirmation); Tool cursors are painted into the window on Wayland (#1652; not run on Linux, #1645 stays open until confirmed); the guide-position row (#1659) is now "not scored": the documentation found doesn't establish it, and it is kept as a convenience pending the owner's decision; scores unchanged | | 2026-10-13 | minor | Pathfinder's Trim, Merge and Crop remove the objects' strokes, and Merge joins same-fill objects whatever their strokes were, as the public Pathfinder page describes (#1572, fixes #1570); Offset Path with an inset larger than the shapes says the offset leaves nothing, instead of asking to select paths (#1591, fixes #1590); The Symbols panel's New Symbol opens Symbol Options to name the symbol first, and Alt-click skips it (#1566, fixes #1560); Tool flyouts are as wide as their longest translated name and shortcut need, so names no longer run into shortcuts in Spanish and other languages (#1650, fixes #1616); Smart Guides: a point drawn with the Pen or a shape tool, and an anchor dragged with Direct Selection, snaps to where paths and ruler guides cross, labelled "intersect" (#1658, part of #1602; a moved selection's own anchors don't); The Transform panel shows a selected ruler guide's X or Y, and typing a value moves it (#1659, part of #1597). This is not established by the documentation we could find: the public help pages on guides, rulers and the Transform, Control and Properties panels (read again on 2026-10-14) describe selecting and dragging unlocked guides and the panels' X and Y for a selection, not a guide's position in a panel, and the one public statement of it is a user's [community answer](https://community.adobe.com/questions-652/adding-a-new-custom-guide-785586), which is not documentation. It is kept as a convenience pending the owner's decision, with no parity claim and nothing counted towards a score; Select Behind (Ctrl/Cmd-click) tests each object once, so a deep stack of overlapping objects no longer slows it; same objects in the same order (#1571); Commands that take `ids` run on the objects given with nothing selected, over MCP, the control channel and the CLI too (#1577, fixes #1575); scores unchanged (estimated) | | 2026-10-13 | minor | G1/M11: Artboard panel deletion, protected deletion keys and navigator/tool synchronization before and after deletion; history normalization preserves valid multiselection; history regressions and full workspace CI pass; scores and hours unchanged | -| 2026-10-16 | minor | Document window scroll bars (#2322): thumb drag, track paging and the wheel over a bar; hidden in Full Screen and Presentation Mode; scores unchanged | | 2026-10-13 | minor | Alt during a drag from either ruler swaps the new guide's orientation ([public shortcut reference](https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html)); tool and egui event tests cover preview, snapping, release coordinates, cancellation and one-step Undo/Redo; scores unchanged (estimated). | | 2026-10-13 | minor | G1: create width points through Width Point Edit with optional adjoining adjustment; command/pointer/history regressions and full CI passed; dialog layout reviewed; scores and hours unchanged | | 2026-10-13 | minor | Corner Radius: a link toggle behind the field in the Properties and Transform panels; off, a live rectangle shows a radius field for each corner (Top Left, Top Right, Bottom Right, Bottom Left), each setting its own corner (`singleCornerRadius` preference); split from #1594; score unchanged (estimated) | | 2026-10-13 | minor | Join merges the selected anchor points that lie on one another, as after Average, into one anchor (#1582, fixes #1533); correction: "Join with corner/smooth choice" was listed as done, but `path.join` and `path.average` are separate commands and Join has no such choice, so Average and Join in one step with that choice is its own row, missing; the Smooth tool smooths only the stretch of path under the drag and a round corner next to a curved side curves smoothly instead of sticking out of the shape (#1547, fixes #1512); Lock Guides also locks the objects made into guides: they can't be selected, moved or deleted (#1543); they are drawn in the guide colour and style of Preferences, and a dragged selection snaps to ruler guides with Smart Guides on (#1583, fixes #1534); Edit › Keyboard Shortcuts lists Edit › Duplicate and the other commands that are in no menu but run without parameters (panel-menu commands such as Break Link to Symbol), so they can be given shortcuts (#1549, #1580, fixes #1539); Make Compound Path on a group (such as outlined type) keeps the paint of its backmost path instead of drawing nothing, and compound paths joined into one keep their holes (#1537); scores unchanged (estimated) | | 2026-10-13 | minor | Sliders in Save for Web, the raster export options, Preferences and the opacity popup show their rail (#1506, fixes #1483); a template layer stays in Preview, dimmed, in Outline view (#1505, fixes #1486); Direct Selection takes the selected object's anchor where anchors of overlapping objects coincide (#1509, fixes #1508); a tab stop in the Tabs panel drags from where the press landed (#1487, fixes #1458); scores unchanged (estimated) | -| 2026-10-12 | minor | Width Point Edit: Shift-double-click enables adjoining adjustment; plain double-click keeps it off; scores and hours unchanged | | 2026-10-13 | minor | Split Into Grid has its row and column sizes, gutters, totals, Add Guides and Preview (#1488, fixes #1462); Transform Objects in the Transform dialogs and Transform Each, and Reflect across an angle (#1494, fixes #1448 and #1460); the Pathfinder panel works on a group selected alone and its greyed-out buttons say what they need (#1493, for #1450); selected text in value fields is white or black on the highlight (#1490, part of #1465); a focused field follows its value when a slider or preview changes it (#1497, fixes #1457); an Alt-drag copies once per drag (#1464); scores unchanged (estimated) | +| 2026-10-12 | minor | Width Point Edit: Shift-double-click enables adjoining adjustment; plain double-click keeps it off; scores and hours unchanged | | 2026-10-12 | minor | Transform dialogs (Move, Rotate, Scale, Reflect, Shear) preview on the canvas and step their angles and percentages with the arrow keys (#1428, fixes #1412); the transform tools, dialogs and Move act on anchors selected with Direct Selection (#1427, fixes #1408); the bounding box hides during a corner drag with the Selection tool (#1402, fixes #1229); a picture pastes when blank text sits beside it on the clipboard (#1404); scores unchanged (estimated) | | 2026-10-12 | minor | Dialogs opened from a menu take the keyboard focus every time they open (#1367); a first field that starts greyed out takes it once enabled (#1395); art flush with an artboard within a rounding error moves, copies and scales with it (#1375, from #1295); scores and hours unchanged | | 2026-10-12 | minor | Rectangular Grid and Polar Grid tool options (skew, frame, compound path, Fill Grid; #1377, in part): a skew slider under each divider count, kept as tool options and used by drags; the skew curve is our own; score unchanged (estimated) | @@ -745,8 +750,6 @@ before release (#6373). [Paint faces and edges](https://helpx.adobe.com/illustra | 2026-10-11 | minor | Edit Toolbar opens the All Tools drawer beside the toolbar instead of a centred dialog, dims the tools the toolbar holds, and edits the toolbar by drag and drop while it is open (#1018); score unchanged (estimated) | | 2026-10-11 | minor | Direct Selection: dragging a path selected whole by an anchor moves it whole; Alt-drag copies by an anchor too, toggles during the drag and snaps onto the original (#1104); score unchanged (estimated) | | 2026-10-10 | major | First checklist, built from the tool sources and tests against Illustrator 2026's public documentation | -| 2026-10-28 | minor | Blob Brush Shift locks after clear axial movement, keeps earlier samples fixed, and waits without an off-axis trail. Activation uses raw press displacement in logical screen pixels, with exact quarter-turn boundaries. Explicit application rule; scores and estimates unchanged. | -| 2026-10-28 | minor | Flare Tool Options opened by a canvas click previews its settings at that click. Reset refreshes the enabled preview while keeping the dialog open. Cancel discards preview art and tentative options; OK accepts one flare in one Undo step. Editing existing flare artwork remains outside this dialog scope. Scores and estimates unchanged. | > **Tab-drag status (2026-10-22):** updated from upstream, including the Turkish catalog and all three tab-drag translations requested in #3479; Selection-tool reset regression-checked and clipboard evidence scoped to internal state; drag lifecycle, insertion and regression contracts documented; behavior, scores and estimates unchanged.