From 4e3ebb2810849f6c2ec798478d6f477b88b196fd Mon Sep 17 00:00:00 2001 From: Christian Lappin <36496308+christianlappin@users.noreply.github.com> Date: Wed, 11 Nov 2026 19:32:10 -0500 Subject: [PATCH 1/2] M3: preserve effective text dimensions in Eyedropper picks (#6404) --- crates/engine/src/cmd/appearance.rs | 114 ++++++++++-- crates/engine/src/tests/eyedropper.rs | 256 ++++++++++++++++++++++++++ docs/gaps.md | 4 +- docs/roadmap.md | 5 +- docs/target-app-parity.md | 11 +- docs/ui-parity.md | 4 +- 6 files changed, 368 insertions(+), 26 deletions(-) diff --git a/crates/engine/src/cmd/appearance.rs b/crates/engine/src/cmd/appearance.rs index 2dfeb497d..1f8d1d696 100644 --- a/crates/engine/src/cmd/appearance.rs +++ b/crates/engine/src/cmd/appearance.rs @@ -19,12 +19,12 @@ //! shape that a Convert to Shape or Outline Object effect on them gives them, below or above the //! image as its Contents row says; new ones go right below that row. -use std::sync::Arc; +use std::{collections::HashMap, sync::Arc}; use serde_json::{Value, json}; use vectorcraft_color::{BlendMode, Paint}; use vectorcraft_doc::appearance::SharedStack; -use vectorcraft_doc::{Appearance, AppearanceItem, CharStyle, FillLayer, Node, NodeKind, ParaStyle, StrokeLayer}; +use vectorcraft_doc::{Appearance, AppearanceItem, CharStyle, Document, FillLayer, Node, NodeKind, ParaStyle, StrokeLayer}; use vectorcraft_geom::Rect; use super::edit::{outermost, roots_of, selected_roots}; @@ -126,7 +126,7 @@ pub fn specs() -> Vec { "Eyedropper", [], None, - "{source: id, ids?, pickUp?, apply? (trees as eyedropper.setOptions, merged over the Eyedropper Options for this call), reverse?: bool, append?: bool, defaultsOnly?: bool} copy the attributes both picked up and applied (by default the whole appearance stack, opacity and blend mode, and from type to type the character and paragraph attributes) from `source` to ids (default: highlighted characters while editing type, after Select All in a frame of threaded text the text of the story's frames, leaving out `source`, otherwise the object selection; highlighted characters reject enabled non-neutral object transparency), the fill, stroke and weight also to the paint defaults; `defaultsOnly` updates only paint defaults and cannot be combined with ids, reverse or append; `reverse` (Alt-click) copies from the first selected object (or ids) onto `source` instead, or with nothing selected what the Eyedropper picked up last (a click without Alt); `append` (Shift+Alt-click) adds the source's fills, strokes and effects on top of each target's stack; placed gradients land at the same place relative to each target's bounds (defaults: fitted to new art); with an object of a dynamic symbol instance selected by Direct Selection and no ids, reverse, append or defaultsOnly, that object takes the source's fill and stroke (solid, none, gradient or pattern) for this instance only, as the Eyedropper Options allow, and a source that is not a path, compound path or type, or options that pick up neither color, are refused. On a path directly in a graph series, the graph also stores the paint, weight, opacity and blend mode of the topmost visible fill and stroke and the object's opacity and blend mode (exceptions: docs/mcp.md) → {ids}", + "{source: id, ids?, pickUp?, apply? (trees as eyedropper.setOptions, merged over the Eyedropper Options for this call), reverse?: bool, append?: bool, defaultsOnly?: bool} copy the attributes both picked up and applied (by default the whole appearance stack, opacity and blend mode, and from type to type the character and paragraph attributes using effective character dimensions while preserving target transforms) from `source` to ids (default: highlighted characters while editing type, after Select All in a frame of threaded text the text of the story's frames, leaving out `source`, otherwise the object selection; highlighted characters reject enabled non-neutral object transparency), the fill, stroke and weight also to the paint defaults; `defaultsOnly` updates only paint defaults and cannot be combined with ids, reverse or append; `reverse` (Alt-click) copies from the first selected object (or ids) onto `source` instead, or with nothing selected what the Eyedropper picked up last (a click without Alt); `append` (Shift+Alt-click) adds the source's fills, strokes and effects on top of each target's stack; placed gradients land at the same place relative to each target's bounds (defaults: fitted to new art); with an object of a dynamic symbol instance selected by Direct Selection and no ids, reverse, append or defaultsOnly, that object takes the source's fill and stroke (solid, none, gradient or pattern) for this instance only, as the Eyedropper Options allow, and a source that is not a path, compound path or type, or options that pick up neither color, are refused. On a path directly in a graph series, the graph also stores the paint, weight, opacity and blend mode of the topmost visible fill and stroke and the object's opacity and blend mode (exceptions: docs/mcp.md) → {ids}", has_doc, copy_from ), @@ -1198,7 +1198,7 @@ fn target_contents(s: &mut Session, p: &Value) -> Result { Ok(json!({ "ids": ids.iter().map(|i| i.0).collect::>() })) } -/// Sample enabled attributes, refusing object transparency that a text range cannot store. +/// Sample enabled attributes with transform-aware character dimensions and atomic validation. fn copy_from(s: &mut Session, p: &Value) -> Result { const C: &str = "appearance.copyFrom"; let clicked = id_param(p, "source").ok_or_else(|| bad(C, "missing `source` id"))?; @@ -1270,9 +1270,6 @@ fn copy_from(s: &mut Session, p: &Value) -> Result { None => s.eyedropper_sample.clone().ok_or_else(|| bad(C, "select the object to copy from"))?, }; let picked = Picked::of(&node); - if !reverse && !defaults_only { - s.eyedropper_sample = Some(node); - } // The highlighted characters' type objects and ranges: after Select All in threaded text, all // the text of every frame of the story, except a frame the attributes come from, in place of // the object selection. @@ -1285,6 +1282,15 @@ fn copy_from(s: &mut Session, p: &Value) -> Result { Some((id, start, end)) if targets.contains(&id) => vec![(id, Some((start, end)))], _ => vec![], }; + // Prepare every dimensional copy before changing defaults or the remembered sample. A bad + // source or target transform must refuse the entire pick, including a multi-frame range. + let characters = if attrs.character && !append { + let d = &s.doc()?.doc; + let ids = targets.iter().copied().filter(|_| ranges.is_empty()).chain(ranges.iter().map(|(id, _)| *id)); + copied_character_styles(&node, ids.filter_map(|id| d.node(id)))? + } else { + HashMap::new() + }; // Character styles cannot represent object transparency. Refuse before even changing // paint defaults; applying it to the object would also alter the unselected characters. if attrs.appearance.transparency { @@ -1298,11 +1304,27 @@ fn copy_from(s: &mut Session, p: &Value) -> Result { } } } - if !reverse { - picked.to_defaults(s, &attrs.appearance); - } + // The session's bounds check follows the selection. Check text targets reached through + // explicit IDs, a reverse pick, or a highlighted story as well. + let text_ids: Vec<_> = characters.keys().copied().collect(); + let check_text_bounds = |d: &Document| { + if !text_ids.is_empty() && !crate::sanity::objects_in_range(d, &text_ids) { + return Err(bad(C, "sampled text would exceed the canvas (coordinates out of range)")); + } + Ok(()) + }; + // Remember the pick only after the document edit has passed its own validation. + let finish = |s: &mut Session, ids: &[NodeId]| { + if !reverse { + if !defaults_only { + s.eyedropper_sample = Some(node); + } + picked.to_defaults(s, &attrs.appearance); + } + json!({ "ids": ids.iter().map(|i| i.0).collect::>() }) + }; if (targets.is_empty() && ranges.is_empty()) || attrs.is_empty() { - return Ok(json!({ "ids": [] })); + return Ok(finish(s, &[])); } if !ranges.is_empty() { let ids: Vec = ranges.iter().map(|(f, _)| *f).collect(); @@ -1312,24 +1334,26 @@ fn copy_from(s: &mut Session, p: &Value) -> Result { let Some(NodeKind::Text(t)) = d.node_mut(id).map(|n| &mut n.kind) else { return Err(EngineError::NoNode(id)) }; let (start, end) = range.unwrap_or((0, vectorcraft_text::edit::runs_len(&t.runs))); let placed = picked.placed(Some(t.local_bounds())); - vectorcraft_text::edit::style_range(&mut t.runs, start, end, |st| picked.run(st, &placed, &attrs)); + vectorcraft_text::edit::style_range(&mut t.runs, start, end, |st| Picked::run(st, &placed, &attrs, characters.get(&id))); if let Some((_, para)) = picked.text.as_ref().filter(|_| attrs.paragraph) { t.edit_paras(Some(t.paragraphs_in(start, end)), |p| *p = para.clone()); } super::typecmd::refresh_bounds(t); } + check_text_bounds(d)?; Ok(count_changed(d, &before, &ids)) })?; - return Ok(json!({ "ids": ids.iter().map(|i| i.0).collect::>() })); + return Ok(finish(s, &ids)); } // When every target comes out as it was, the document and its undo history stay as they are. let before = s.doc()?.doc.clone(); super::overprint::edit_counted(s, "Eyedropper", |d| { for id in &targets { if let Some(n) = d.node_mut(*id) { - picked.apply(n, &attrs, append); + picked.apply(n, &attrs, append, characters.get(id)); } } + check_text_bounds(d)?; // The paints the pick wrote, with a weight going with the paint of its stroke, as in `set_item`. `append` adds // only the colors of the visible rows the source has. let (fill, stroke) = (attrs.appearance.fill, attrs.appearance.stroke); @@ -1369,7 +1393,59 @@ fn copy_from(s: &mut Session, p: &Value) -> Result { let object = super::graph::capture_series_object_opacity(d, &objects); Ok(count_changed(d, &before, &targets) + usize::from(kept || painted || object)) })?; - Ok(json!({ "ids": targets.iter().map(|i| i.0).collect::>() })) + Ok(finish(s, &targets)) +} + +/// Read source typography in document space, then compensate for each target's transform. +/// Paint stays in its existing sampling path; character scale never changes object placement. +fn copied_character_styles<'a>(source: &Node, targets: impl Iterator) -> Result> { + const C: &str = "appearance.copyFrom"; + let mut styles = HashMap::new(); + let NodeKind::Text(source) = &source.kind else { return Ok(styles) }; + let Some(run) = source.runs.first() else { return Ok(styles) }; + let mut targets = targets + .filter_map(|n| match &n.kind { + NodeKind::Text(t) => Some((n.id, t)), + _ => None, + }) + .peekable(); + if targets.peek().is_none() { + return Ok(styles); + } + let effective = source.effective_char_style(&run.style).ok_or_else(|| bad(C, "source text transform is collapsed or unrepresentable"))?; + if !copied_dimensions_fit(&run.style, &effective) { + return Err(bad(C, "character dimensions cannot be represented with the source text transform")); + } + for (id, text) in targets { + let scale = text.style_scale().ok_or_else(|| bad(C, "target text transform is collapsed or unrepresentable"))?; + let mut local = effective.clone(); + local.size /= scale.points; + local.leading = local.leading.map(|v| v / scale.points); + local.baseline_shift /= scale.points; + local.tcy_up_down /= scale.points; + local.tcy_left_right /= scale.points; + local.h_scale /= scale.horizontal; + // As with dimensional type commands, reject overflow and underflow before layout. + if !copied_dimensions_fit(&effective, &local) { + return Err(bad(C, "character dimensions cannot be represented with the target text transform")); + } + styles.insert(id, local); + } + Ok(styles) +} + +/// Bound each converted type dimension and refuse loss to zero before layout reads it. +fn copied_dimensions_fit(from: &CharStyle, to: &CharStyle) -> bool { + [ + (from.size, to.size), + (from.leading.unwrap_or(0.0), to.leading.unwrap_or(0.0)), + (from.baseline_shift, to.baseline_shift), + (from.tcy_up_down, to.tcy_up_down), + (from.tcy_left_right, to.tcy_left_right), + (from.h_scale, to.h_scale), + ] + .iter() + .all(|(from, to)| to.is_finite() && to.abs() <= crate::MAX_COORD && (*from == 0.0 || *to != 0.0)) } /// What the Eyedropper picks up from an object. @@ -1422,8 +1498,8 @@ impl Picked { /// Copy character attributes and the enabled paint attributes through the same path for /// whole text objects and highlighted character ranges. - fn run(&self, st: &mut CharStyle, placed: &Appearance, a: &EyedropperAttrs) { - if let Some((cs, _)) = self.text.as_ref().filter(|_| a.character) { + fn run(st: &mut CharStyle, placed: &Appearance, a: &EyedropperAttrs, character: Option<&CharStyle>) { + if let Some(cs) = character { *st = CharStyle { fill: std::mem::take(&mut st.fill), stroke: std::mem::take(&mut st.stroke), @@ -1439,7 +1515,7 @@ impl Picked { /// Apply attributes `a` to `n`. `append` adds the fills and strokes (and effects) on top of its /// stack instead. Type takes the paints on its characters, and character and paragraph /// attributes from type. - fn apply(&self, n: &mut Node, a: &EyedropperAttrs, append: bool) { + fn apply(&self, n: &mut Node, a: &EyedropperAttrs, append: bool, character: Option<&CharStyle>) { let ap = &a.appearance; if ap.transparency { n.opacity = self.opacity; @@ -1456,7 +1532,7 @@ impl Picked { let placed = self.placed(Some(t.local_bounds())); let text = self.text.as_ref(); for r in &mut t.runs { - self.run(&mut r.style, &placed, a); + Self::run(&mut r.style, &placed, a, character); } if let Some((_, para)) = text.filter(|_| a.paragraph) { // The source's first paragraph's attributes, on every paragraph. diff --git a/crates/engine/src/tests/eyedropper.rs b/crates/engine/src/tests/eyedropper.rs index f8363fe93..5204995d6 100644 --- a/crates/engine/src/tests/eyedropper.rs +++ b/crates/engine/src/tests/eyedropper.rs @@ -215,6 +215,262 @@ fn type_picks_up_font_and_size() { assert_eq!((st.size, justify, st.fill.color().unwrap().to_hex()), (12.0, preserved_alignment, "#00ff00".into())); } +/// Sample document-space typography between text objects with different local transforms. +#[test] +fn scaled_type_pick_matches_effective_character_dimensions() { + for source_area in [false, true] { + for target_area in [false, true] { + let mut s = session(); + let create = |s: &mut Session, area: bool, x: f64| { + let mut p = json!({"x": x, "y": 100, "text": "Sample", "size": 12}); + if area { + p["area"] = json!({"width": 160, "height": 80}); + } + id_of(s.execute("text.create", &p).unwrap()) + }; + let src = create(&mut s, source_area, 100.0); + s.execute("object.transform", &json!({"matrix": [3, 0, 0, 2, 0, 0]})).unwrap(); + let dst = create(&mut s, target_area, 300.0); + s.execute("object.transform", &json!({"matrix": [0.5, 0, 0, 1.5, 0, 0]})).unwrap(); + let before = node(&s, dst); + let source = node(&s, src); + let history = undo_len(&s); + s.execute("appearance.copyFrom", &json!({"source": src.0})).unwrap(); + let after = node(&s, dst); + let NodeKind::Text(t) = &after.kind else { panic!("not text") }; + let NodeKind::Text(old) = &before.kind else { panic!("not text") }; + let effective = t.effective_char_style(&t.first_style()).unwrap(); + assert!((effective.size - 24.0).abs() < 1e-8, "sampled font size: {}", effective.size); + assert!((effective.h_scale - 150.0).abs() < 1e-8, "sampled horizontal scale: {}", effective.h_scale); + assert_eq!(t.xf, old.xf, "sampling keeps the target's transform"); + assert_eq!(node(&s, src), source); + assert_eq!(undo_len(&s), history + 1); + s.execute("edit.undo", &json!({})).unwrap(); + assert_eq!(node(&s, dst), before); + s.execute("edit.redo", &json!({})).unwrap(); + assert_eq!(node(&s, dst), after); + } + } +} + +/// A text object snapshot, including the local attributes and retained affine transform. +fn sampled_type(s: &Session, id: NodeId) -> vectorcraft_doc::TextObject { + let NodeKind::Text(t) = node(s, id).kind else { panic!("not text") }; + *t +} + +/// Compare the dimensional character attributes that text controls display in document units. +fn assert_sampled_dimensions(actual: &vectorcraft_doc::CharStyle, expected: &vectorcraft_doc::CharStyle) { + assert_eq!(actual.leading.is_some(), expected.leading.is_some()); + for (a, e) in [ + (actual.size, expected.size), + (actual.h_scale, expected.h_scale), + (actual.v_scale, expected.v_scale), + (actual.leading.unwrap_or(0.0), expected.leading.unwrap_or(0.0)), + (actual.baseline_shift, expected.baseline_shift), + (actual.tcy_up_down, expected.tcy_up_down), + (actual.tcy_left_right, expected.tcy_left_right), + ] { + assert!((a - e).abs() < 1e-8, "sampled dimension: {a} != {e}"); + } +} + +/// Rotation, shear, reflection and grouping stay on the objects while typography is copied. +#[test] +fn scaled_type_pick_preserves_affines_and_document_dimensions() { + for grouped in [false, true] { + let mut s = session(); + let src = id_of(s.execute("text.create", &json!({"x": 100, "y": 100, "text": "Source", "size": 12})).unwrap()); + s.execute("text.setStyle", &json!({"leading": 18})).unwrap(); + s.execute("text.setFormat", &json!({"hScale": 80, "vScale": 125, "baselineShift": 3, "tcyUpDown": 2, "tcyLeftRight": -1})).unwrap(); + if grouped { + s.execute("object.group", &json!({})).unwrap(); + } + s.execute("object.transform", &json!({"matrix": [0, -3, -2, 1, 0, 0]})).unwrap(); + let src_text = sampled_type(&s, src); + let expected = src_text.effective_char_style(&src_text.first_style()).unwrap(); + let dst = id_of(s.execute("text.create", &json!({"x": 300, "y": 100, "text": "Target", "size": 20})).unwrap()); + if grouped { + s.execute("object.group", &json!({})).unwrap(); + } + s.execute("object.transform", &json!({"matrix": [-1, 0.5, 1, 2, 0, 0]})).unwrap(); + let before = sampled_type(&s, dst); + s.execute("appearance.copyFrom", &json!({"source": src.0})).unwrap(); + let after = sampled_type(&s, dst); + assert_sampled_dimensions(&after.effective_char_style(&after.first_style()).unwrap(), &expected); + assert_eq!(after.xf, before.xf); + assert_eq!(sampled_type(&s, src), src_text); + let bytes = vectorcraft_format::save_file(&s.doc().unwrap().doc); + let reopened = vectorcraft_format::load(&bytes).unwrap(); + let NodeKind::Text(t) = &reopened.node(dst).unwrap().kind else { panic!("not text") }; + assert_sampled_dimensions(&t.effective_char_style(&t.first_style()).unwrap(), &expected); + } +} + +/// A transformed target compensates only the highlighted characters, retaining the others. +#[test] +fn scaled_type_pick_changes_only_the_highlighted_range() { + let mut s = session(); + let src = id_of(s.execute("text.create", &json!({"x": 100, "y": 100, "text": "Source", "size": 12})).unwrap()); + s.execute("object.transform", &json!({"matrix": [3, 0, 0, 2, 0, 0]})).unwrap(); + let source = sampled_type(&s, src); + let expected = source.effective_char_style(&source.first_style()).unwrap(); + let dst = id_of(s.execute("text.create", &json!({"x": 300, "y": 100, "text": "Sample", "size": 10})).unwrap()); + s.execute("object.transform", &json!({"matrix": [0.5, 0, 0, 3, 0, 0]})).unwrap(); + let before = sampled_type(&s, dst); + s.select_tool("type", ViewInfo::default()).unwrap(); + s.set_tool_option("textSelection", &json!({"id": dst.0, "start": 1, "end": 4})); + s.select_tool("eyedropper", ViewInfo::default()).unwrap(); + let history = undo_len(&s); + s.execute("appearance.copyFrom", &json!({"source": src.0})).unwrap(); + let after = sampled_type(&s, dst); + assert_eq!(after.runs.len(), 3); + assert_eq!((&*after.runs[0].text, &*after.runs[1].text, &*after.runs[2].text), ("S", "amp", "le")); + assert_eq!(after.runs[0].style, before.first_style()); + assert_eq!(after.runs[2].style, before.first_style()); + assert_sampled_dimensions(&after.effective_char_style(&after.runs[1].style).unwrap(), &expected); + assert_eq!(after.xf, before.xf); + assert_eq!(undo_len(&s), history + 1); + s.execute("edit.undo", &json!({})).unwrap(); + assert_eq!(sampled_type(&s, dst), before); + s.execute("edit.redo", &json!({})).unwrap(); + assert_eq!(sampled_type(&s, dst), after); +} + +/// Reject unusable source or target transforms before any part of a multi-object pick changes. +#[test] +fn scaled_type_pick_rejects_unrepresentable_dimensions_atomically() { + for (bad_source, factor) in [(true, 0.0), (false, 0.0), (false, 1e-306), (true, f64::from_bits(1))] { + let mut s = session(); + let size = if bad_source && factor > 0.0 { 0.1 } else { 24.0 }; + let src = id_of(s.execute("text.create", &json!({"x": 100, "y": 100, "text": "Source", "size": size, "color": "#00ff00"})).unwrap()); + if bad_source { + s.execute("object.transform", &json!({"matrix": [factor, 0, 0, factor, 0, 0]})).unwrap(); + } + let good = id_of(s.execute("text.create", &json!({"x": 300, "y": 100, "text": "Good", "size": 10})).unwrap()); + let other = id_of(s.execute("text.create", &json!({"x": 300, "y": 200, "text": "Other", "size": 12})).unwrap()); + if !bad_source { + s.execute("object.transform", &json!({"matrix": [factor, 0, 0, factor, 0, 0]})).unwrap(); + } + s.execute("select.set", &json!({"ids": [good.0, other.0]})).unwrap(); + let doc = s.doc().unwrap().doc.clone(); + let selection = s.doc().unwrap().selection.clone(); + let paint = s.paint.clone(); + let sample = s.eyedropper_sample.clone(); + let history = undo_len(&s); + let error = s.execute("appearance.copyFrom", &json!({"source": src.0})).unwrap_err(); + assert!(error.to_string().contains("transform"), "{error}"); + assert_eq!(s.doc().unwrap().doc, doc); + assert_eq!(s.doc().unwrap().selection, selection); + assert_eq!(s.paint, paint); + assert_eq!(s.eyedropper_sample, sample); + assert_eq!(undo_len(&s), history); + // A paint-only pick does not need a usable text transform. + s.execute("appearance.copyFrom", &json!({"source": src.0, "apply": {"character": false, "paragraph": false}})).unwrap(); + } +} + +/// A failed dimensional pick retains highlighted characters and the previously remembered sample. +#[test] +fn scaled_type_pick_rejection_preserves_the_highlighted_range_and_sample() { + let mut s = session(); + let saved = id_of(s.execute("text.create", &json!({"x": 100, "y": 100, "text": "Saved", "size": 18})).unwrap()); + s.execute("select.none", &json!({})).unwrap(); + s.execute("appearance.copyFrom", &json!({"source": saved.0})).unwrap(); + let src = id_of(s.execute("text.create", &json!({"x": 100, "y": 200, "text": "Source", "size": 24, "color": "#00ff00"})).unwrap()); + s.execute("object.transform", &json!({"matrix": [0, 0, 0, 0, 0, 0]})).unwrap(); + let dst = id_of(s.execute("text.create", &json!({"x": 300, "y": 100, "text": "Sample", "size": 10})).unwrap()); + s.select_tool("type", ViewInfo::default()).unwrap(); + s.set_tool_option("textSelection", &json!({"id": dst.0, "start": 1, "end": 4})); + s.select_tool("eyedropper", ViewInfo::default()).unwrap(); + let doc = s.doc().unwrap().doc.clone(); + let selection = s.doc().unwrap().selection.clone(); + let range = s.tool_options(); + let paint = s.paint.clone(); + let sample = s.eyedropper_sample.clone(); + let history = undo_len(&s); + assert!(s.execute("appearance.copyFrom", &json!({"source": src.0})).is_err()); + assert_eq!(s.doc().unwrap().doc, doc); + assert_eq!(s.doc().unwrap().selection, selection); + assert_eq!(s.tool_options(), range); + assert_eq!(s.paint, paint); + assert_eq!(s.eyedropper_sample, sample); + assert_eq!(undo_len(&s), history); + // Alt-click can still apply the last successful sample after the rejected pick. + s.execute("select.none", &json!({})).unwrap(); + s.execute("appearance.copyFrom", &json!({"source": dst.0, "reverse": true})).unwrap(); + assert_eq!(sampled_type(&s, dst).first_style().size, 18.0); +} + +/// A pick that would extend text beyond the canvas keeps the previous sample and defaults. +#[test] +fn scaled_type_pick_canvas_rejection_preserves_sample_and_defaults() { + for mode in ["selection", "ids", "reverse", "range"] { + let mut s = session(); + let saved = id_of(s.execute("text.create", &json!({"x": 40, "y": 40, "text": "Saved", "size": 18, "color": "#0000ff"})).unwrap()); + let src = id_of(s.execute("text.create", &json!({"x": 100, "y": 100, "text": "Source", "size": 96, "color": "#ff0000"})).unwrap()); + let dst = id_of(s.execute("text.create", &json!({"x": MAX_COORD - 100.0, "y": 200, "text": "WWWW", "size": 10})).unwrap()); + s.execute("appearance.copyFrom", &json!({"source": saved.0, "ids": []})).unwrap(); + let pick = match mode { + "ids" | "reverse" => { + s.execute("select.none", &json!({})).unwrap(); + if mode == "ids" { json!({"source": src.0, "ids": [dst.0]}) } else { json!({"source": dst.0, "ids": [src.0], "reverse": true}) } + } + "range" => { + s.select_tool("type", ViewInfo::default()).unwrap(); + s.set_tool_option("textSelection", &json!({"id": dst.0, "start": 0, "end": 3})); + s.select_tool("eyedropper", ViewInfo::default()).unwrap(); + json!({"source": src.0}) + } + _ => json!({"source": src.0}), + }; + let doc = s.doc().unwrap().doc.clone(); + let selection = s.doc().unwrap().selection.clone(); + let range = s.tool_options(); + let paint = s.paint.clone(); + let sample = s.eyedropper_sample.clone(); + let history = undo_len(&s); + + let error = s.execute("appearance.copyFrom", &pick).unwrap_err(); + assert!(error.to_string().contains("coordinates out of range"), "{mode}: {error}"); + assert_eq!(s.doc().unwrap().doc, doc, "{mode}"); + assert_eq!(s.doc().unwrap().selection, selection, "{mode}"); + assert_eq!(s.tool_options(), range, "{mode}"); + assert_eq!(s.paint, paint, "{mode}"); + assert_eq!(s.eyedropper_sample, sample, "{mode}"); + assert_eq!(undo_len(&s), history, "{mode}"); + } +} + +/// Disabled character copying and append preserve typography; reverse and cached picks convert it. +#[test] +fn scaled_type_pick_respects_options_and_reverse_sampling() { + let mut s = session(); + let src = id_of(s.execute("text.create", &json!({"x": 100, "y": 100, "text": "Source", "size": 12})).unwrap()); + s.execute("object.scale", &json!({"sx": 300, "sy": 200})).unwrap(); + let source = sampled_type(&s, src); + let expected = source.effective_char_style(&source.first_style()).unwrap(); + let dst = id_of(s.execute("text.create", &json!({"x": 300, "y": 100, "text": "Target", "size": 20})).unwrap()); + s.execute("object.scale", &json!({"sx": 50, "sy": 150})).unwrap(); + let before = sampled_type(&s, dst); + for options in [json!({"pickUp": {"character": false}}), json!({"apply": {"character": false}}), json!({"append": true})] { + let mut p = json!({"source": src.0, "ids": [dst.0]}); + p.as_object_mut().unwrap().extend(options.as_object().unwrap().clone()); + s.execute("appearance.copyFrom", &p).unwrap(); + assert_sampled_dimensions(&sampled_type(&s, dst).first_style(), &before.first_style()); + } + s.execute("select.set", &json!({"ids": [src.0]})).unwrap(); + s.execute("appearance.copyFrom", &json!({"source": dst.0, "reverse": true})).unwrap(); + let after = sampled_type(&s, dst); + assert_sampled_dimensions(&after.effective_char_style(&after.first_style()).unwrap(), &expected); + s.execute("edit.undo", &json!({})).unwrap(); + s.execute("select.none", &json!({})).unwrap(); + s.execute("appearance.copyFrom", &json!({"source": src.0})).unwrap(); + s.execute("appearance.copyFrom", &json!({"source": dst.0, "reverse": true})).unwrap(); + let cached = sampled_type(&s, dst); + assert_sampled_dimensions(&cached.effective_char_style(&cached.first_style()).unwrap(), &expected); +} + #[test] fn alt_click_applies_the_selection_to_the_clicked_object() { let mut s = session(); diff --git a/docs/gaps.md b/docs/gaps.md index da57837a6..a228181ab 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -1,6 +1,6 @@ # Gaps -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-11 · **Change:** minor (G5: built-in Energy Beams brush library; estimate 5–15 → 4–14 h; previously minor: G5: built-in Skill Icons symbol library; estimate 6–16 → 5–15 h; previously minor: ruler double-click adds a guide at the pointer, #5239; G5: built-in Status Effects graphic style library; estimate 7–17 → 6–16 h; MCP docs catch-up for UI Frames and Map Markers; previously minor: G5: built-in Inventory Icons symbol library; estimate 8–18 → 7–17 h; previously minor: G9: `.ai` save names a variable font instance's design-space setting, #6388; previously minor: G5: built-in Game Trails brush library; estimate 9–19 → 8–18 h; previously minor: Edit › Edit Custom Dictionary…: its dialog adds, deletes and changes the custom dictionary's words, #4936; previously minor: docs tidy-up, no content change: stray notes at the top moved into the revision table; scores and estimates unchanged; previously minor: G5: built-in Game HUD graphic style library; estimate 10–20 → 9–19 h; previously minor: G5: built-in Map Markers symbol library; estimate 11–21 → 10–20 h; previously minor: right-click on type: Spelling › Auto Spell Check, with its check mark, #4936; previously minor: right-click on a word Auto Spell Check underlined: its suggestions, Ignore All and Add to Dictionary, #4936; previously minor: G2: Effect Gallery thumbnails and in-dialog preview; estimate 1–2 h → 0; Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: PDF editable import preserves authored ActualText once, including empty/nested spans and authored ordering (#6165); cross-line/style layout remains approximate; scores and estimates unchanged; PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; G5: built-in UI Frames symbol library with default 9-slice; estimate 11–21 h; previously minor: G5: dynamic symbol part on-canvas takes opacity, blend, gradient and pattern; docs Overlap/auto-corners sync; estimate 12–22 h; previously minor: interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; previously minor: Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; previously minor: G5 docs: Art brush Overlap, Pattern brush auto corners and Global Edit Shift-click leave-out moved from Missing to Done (code already shipped); previously minor: custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: EPS import: embedded fonts (composite, Type 42, CID-keyed, Type 1, CFF FontSet) draw their glyphs; spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; G9: EPS gradients drawn as abutting strips import as one gradient, without seams; Tools stays beside the canvas with left-side panels; new gradients follow document color mode; Selection QoL: marquee modes and visible feedback, #5313; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; Indian-language hyphenation review and license allowlist, #4947; opening a slow file shows a progress window, part of #5930; Hindi catalog and contributor loanword preferences; agent wording pass complete; wider native proofreading welcome; an image zoomed in on is separated, colorized or proofed only where it shows (#4155); scores and estimates unchanged; a document's first frame renders on the render worker, so opening a heavy file no longer freezes the window; scores and estimates unchanged; large images in CMYK documents are shrunk before they are separated into ink planes (#4155); scores and estimates unchanged; `.ai` paths with a Calligraphic brush open brushed, with the 6D Art Pen's channels, and Windows reads the pen's tilt and barrel rotation, #372; scores and estimates unchanged; Compound Shapes add-on, beyond Illustrator; scores and estimates unchanged; Contour add-on, beyond Illustrator; scores and estimates unchanged; a slow first frame is waited for instead of restarting without the graphics adapter, and the menu bar no longer waits for the installed fonts' scan; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-11 · **Change:** minor (#6404: transform-aware Eyedropper character copying; scores and estimates unchanged; previously: G5: built-in Energy Beams brush library; estimate 5–15 → 4–14 h; previously minor: G5: built-in Skill Icons symbol library; estimate 6–16 → 5–15 h; previously minor: ruler double-click adds a guide at the pointer, #5239; G5: built-in Status Effects graphic style library; estimate 7–17 → 6–16 h; MCP docs catch-up for UI Frames and Map Markers; previously minor: G5: built-in Inventory Icons symbol library; estimate 8–18 → 7–17 h; previously minor: G9: `.ai` save names a variable font instance's design-space setting, #6388; previously minor: G5: built-in Game Trails brush library; estimate 9–19 → 8–18 h; previously minor: Edit › Edit Custom Dictionary…: its dialog adds, deletes and changes the custom dictionary's words, #4936; previously minor: docs tidy-up, no content change: stray notes at the top moved into the revision table; scores and estimates unchanged; previously minor: G5: built-in Game HUD graphic style library; estimate 10–20 → 9–19 h; previously minor: G5: built-in Map Markers symbol library; estimate 11–21 → 10–20 h; previously minor: right-click on type: Spelling › Auto Spell Check, with its check mark, #4936; previously minor: right-click on a word Auto Spell Check underlined: its suggestions, Ignore All and Add to Dictionary, #4936; previously minor: G2: Effect Gallery thumbnails and in-dialog preview; estimate 1–2 h → 0; Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: PDF editable import preserves authored ActualText once, including empty/nested spans and authored ordering (#6165); cross-line/style layout remains approximate; scores and estimates unchanged; PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; G5: built-in UI Frames symbol library with default 9-slice; estimate 11–21 h; previously minor: G5: dynamic symbol part on-canvas takes opacity, blend, gradient and pattern; docs Overlap/auto-corners sync; estimate 12–22 h; previously minor: interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; previously minor: Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; previously minor: G5 docs: Art brush Overlap, Pattern brush auto corners and Global Edit Shift-click leave-out moved from Missing to Done (code already shipped); previously minor: custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: EPS import: embedded fonts (composite, Type 42, CID-keyed, Type 1, CFF FontSet) draw their glyphs; spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; G9: EPS gradients drawn as abutting strips import as one gradient, without seams; Tools stays beside the canvas with left-side panels; new gradients follow document color mode; Selection QoL: marquee modes and visible feedback, #5313; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; Indian-language hyphenation review and license allowlist, #4947; opening a slow file shows a progress window, part of #5930; Hindi catalog and contributor loanword preferences; agent wording pass complete; wider native proofreading welcome; an image zoomed in on is separated, colorized or proofed only where it shows (#4155); scores and estimates unchanged; a document's first frame renders on the render worker, so opening a heavy file no longer freezes the window; scores and estimates unchanged; large images in CMYK documents are shrunk before they are separated into ink planes (#4155); scores and estimates unchanged; `.ai` paths with a Calligraphic brush open brushed, with the 6D Art Pen's channels, and Windows reads the pen's tilt and barrel rotation, #372; scores and estimates unchanged; Compound Shapes add-on, beyond Illustrator; scores and estimates unchanged; Contour add-on, beyond Illustrator; scores and estimates unchanged; a slow first frame is waited for instead of restarting without the graphics adapter, and the menu bar no longer waits for the installed fonts' scan; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x) ## Feature submission evidence @@ -166,6 +166,7 @@ macOS cursor delivery (#2292) is narrowed to a native host adapter using the exi - **Improved:** Eyedropper has an original pipette cursor at its sampling tip; Use Precise Cursors keeps the crosshair. Bitmap/DPI and preference checks cover the distinction (#1269). OS cursor display and Caps Lock remain unverified. +- **Done (#6404):** Eyedropper character copying compensates for source and target text transforms, including highlighted ranges and reverse sampling; invalid dimensions reject the pick before document, default-paint or sample changes. G1 scores and estimates unchanged. - **Done (#1251):** Eyedropper source reads are independent of locks and isolation, including Shift/Shift+Alt and gradient stops. Source and selection preservation, unchanged Alt reverse/selection restrictions, hidden art and Undo/Redo have regression coverage. This addresses the reported behavior, with no new full-parity measurement. - **Done (#1253):** adding paint to an unpainted zero-width stroke restores the existing 1 pt default, including new-art defaults and Swap Fill/Stroke. The real Eyedropper inheritance sequence, targeted appearance items, preserved widths and Undo/Redo have regression coverage. - **Done:** a busy cursor while a file opens off the UI thread or a labelled background save or export runs (`Cursor::Busy` in `crates/tools/src/cursors.toml`). Heavy synchronous effect previews remain open. @@ -855,6 +856,7 @@ trap, and the defaults (0.25 pt, 100%, 40%) are VectorCraft's own, the pages giv | Date | Change | Summary | |---|---|---| +| 2026-11-11 | minor | Eyedropper copies effective character dimensions between transformed text objects (#6404); scores and estimates unchanged. | | 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. | diff --git a/docs/roadmap.md b/docs/roadmap.md index 1b5574061..41066cbb4 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,6 +1,6 @@ # Roadmap: milestones and what's next -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-10 · **Change:** minor (M10/G5: Energy Beams brush library; MCP docs list Energy Beams; previously minor: M10/G5: Skill Icons symbol library; MCP docs list Skill Icons; previously minor: M10/G5: Status Effects graphic style library; MCP docs catch-up for symbol libraries; previously minor: M10/G5: Inventory Icons built-in symbol library; previously minor: Edit › Edit Custom Dictionary…: its dialog adds, deletes and changes the custom dictionary's words, #4936; previously minor: M10/G5: Game HUD graphic style library; previously minor: resizable icon-strip flyouts and bottom-aligned panel footers, #5713; previously minor: M10/G5: Map Markers built-in symbol library; previously minor: right-click on type: Spelling › Auto Spell Check, with its check mark, #4936; previously minor: right-click on a word Auto Spell Check underlined: its suggestions, Ignore All and Add to Dictionary, #4936; previously minor: G2: Effect Gallery thumbnails and in-dialog preview are in; Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: PDF editable import preserves authored ActualText once, including empty/nested spans and authored ordering (#6165); cross-line/style layout remains approximate; scores and estimates unchanged; PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: EPS import: embedded fonts (composite, Type 42, CID-keyed, Type 1, CFF FontSet) draw their glyphs; spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; M4/M7: `.ai`/EPS type keeps its tab stops, and right-aligned lines with tabs lay out as the file's page, #6147; M4/M7: `.ai`/EPS type from the text document lays its lines out closer to the file's page, within limits, #6137; numeric fields step on wheel hover without taking keyboard focus, #5949; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen; Selection QoL: marquee modes and visible feedback, #5313; SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; Indian-language hyphenation review and license allowlist, #4947; Hindi catalog and contributor loanword preferences; full wording review in progress; Effect dialogs: a colour swatch beside a hex colour opens the Color Picker, #5365; shape tools move the selection from its centre point and turn it from the zone outside a box corner, #5477; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-11 · **Change:** minor (#6404: transform-aware Eyedropper character copying; scores and estimates unchanged; previously: M10/G5: Energy Beams brush library; MCP docs list Energy Beams; previously minor: M10/G5: Skill Icons symbol library; MCP docs list Skill Icons; previously minor: M10/G5: Status Effects graphic style library; MCP docs catch-up for symbol libraries; previously minor: M10/G5: Inventory Icons built-in symbol library; previously minor: Edit › Edit Custom Dictionary…: its dialog adds, deletes and changes the custom dictionary's words, #4936; previously minor: M10/G5: Game HUD graphic style library; previously minor: resizable icon-strip flyouts and bottom-aligned panel footers, #5713; previously minor: M10/G5: Map Markers built-in symbol library; previously minor: right-click on type: Spelling › Auto Spell Check, with its check mark, #4936; previously minor: right-click on a word Auto Spell Check underlined: its suggestions, Ignore All and Add to Dictionary, #4936; previously minor: G2: Effect Gallery thumbnails and in-dialog preview are in; Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: PDF editable import preserves authored ActualText once, including empty/nested spans and authored ordering (#6165); cross-line/style layout remains approximate; scores and estimates unchanged; PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: EPS import: embedded fonts (composite, Type 42, CID-keyed, Type 1, CFF FontSet) draw their glyphs; spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; M4/M7: `.ai`/EPS type keeps its tab stops, and right-aligned lines with tabs lay out as the file's page, #6147; M4/M7: `.ai`/EPS type from the text document lays its lines out closer to the file's page, within limits, #6137; numeric fields step on wheel hover without taking keyboard focus, #5949; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen; Selection QoL: marquee modes and visible feedback, #5313; SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; Indian-language hyphenation review and license allowlist, #4947; Hindi catalog and contributor loanword preferences; full wording review in progress; Effect dialogs: a colour swatch beside a hex colour opens the Color Picker, #5365; shape tools move the selection from its centre point and turn it from the zone outside a box corner, #5477; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x) > > Vector Halftone preview update (2026-11-10): temporary dot selection marks are hidden, and the bounding box stays available (#6369). > @@ -142,7 +142,7 @@ native wording review, layout checks and a web Devanagari font remain. [Notes](l | M0 | Skeleton + vertical slice | ✅ done | — | | 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 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 | +| 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) | | 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 | @@ -190,6 +190,7 @@ separate. Scores and hours unchanged. [Behavior and command details](development | Date | Change | Summary | |---|---|---| +| 2026-11-11 | minor | Eyedropper copies effective character dimensions between transformed text objects (#6404); scores and estimates unchanged. | | 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 | diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 490dfb38d..45631e09c 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1,6 +1,6 @@ # Parity with Adobe Illustrator -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-11 · **Change:** minor (Envelopes: with Distort Appearance, type's strokes paint along the bent glyph outlines at their own weight, as users' `.ai` files draw them, which changes how VectorCraft's own envelopes of stroked type draw (#6644; scores and estimates unchanged); previously minor: G5: Energy Beams brush library; bundled content ~44% → ~46%; previously minor: G5: Skill Icons symbol library; bundled content ~42% → ~44%; previously minor: `.ai` and EPS import: an envelope saved in Edit Contents whose mesh lies away from its drawn art by a move opens live on that art (#6570; scores and estimates unchanged); previously minor: ruler double-click adds a guide at the pointer, #5239; G5: Status Effects graphic style library; bundled content ~40% → ~42%; MCP symbol libraries list UI Frames and Map Markers; previously minor: G5: Inventory Icons built-in symbol library; bundled content ~38% → ~40%; previously minor: `.ai` and EPS import: stroked type in an envelope maps from its glyphs grown by half its stroke (#3179; scores and estimates unchanged); previously minor: `.ai` and EPS import: warps of type open as mesh envelopes on their saved grid where VectorCraft's warp doesn't draw as their drawn art (#3179; scores and estimates unchanged); G5: Game Trails brush library; bundled content ~36% → ~38%; previously minor: Edit › Edit Custom Dictionary…: its dialog adds, deletes and changes the custom dictionary's words, #4936; previously minor: `.ai` and EPS import: envelopes of type open live where they draw as their drawn art and the layers with them draw as the page (#3179; scores and estimates unchanged); G5: Game HUD graphic style library; bundled content ~34% → ~36%; previously minor: Line Arrow arrowhead (#5421; scores and estimates unchanged); G5: Map Markers built-in symbol library; bundled content ~32% → ~34%; previously minor: right-click on type: Spelling › Auto Spell Check, with its check mark, #4936; previously minor: right-click on a word Auto Spell Check underlined: its suggestions, Ignore All and Add to Dictionary, #4936; previously minor: Canvas: a 3D effect changed on type or a group redraws where its solid was; Fit to Artwork Bounds and per-artboard SVG export build each solid once for all artboards; previously minor: Effect Gallery: a thumbnail for each filter and a preview area inside the dialog, on an original drawing; Raster effects 52% → 55%, hours 1–2 → none; headline numbers unchanged; Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: PDF editable import preserves authored ActualText once, including empty/nested spans and authored ordering (#6165); cross-line/style layout remains approximate; scores and estimates unchanged; PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; G5: UI Frames built-in symbol library with default 9-slice; bundled content ~32%; previously minor: G5: dynamic symbol part on-canvas opacity/blend/gradient/pattern; strokes remaining no longer lists Overlap/auto corners; symbols ~88%; previously minor: interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; previously minor: harakat paint fallback clarified; previously minor: current Guides feature-area rows consolidated using existing ruler evidence; scores and estimates unchanged; previously minor: Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; previously minor: G5 docs: Art brush Overlap and Pattern brush auto corners removed from strokes remaining; symbols remaining lists dynamic on-canvas beyond solid and guides outside the art; previously minor: custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: EPS import: embedded fonts (composite, Type 42, CID-keyed, Type 1, CFF FontSet) draw their glyphs; spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; `.ai`/EPS type keeps its tab stops, and right-aligned lines with tabs lay out as the file's page, #6147; `.ai`/EPS type from the text document lays its lines out closer to the file's page, within limits, #6137; `.ai` and EPS import: older Art brush objects read; numeric fields step on wheel hover without taking keyboard focus, and a panel wheel turn cannot seed a canvas zoom glide, #5949; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen; Tools stays beside the canvas with left-side panels; new gradients follow document color mode; Selection QoL: marquee modes and visible feedback, #5313; SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; the canvas keeps selection outlines and anchors between frames; Indian-language hyphenation review and license allowlist, #4947; Stroke gradients: an arrowhead aligned to the tip is painted to the corners of its base, #5964; Drop Shadow and glow dialogs: spinner arrows, #5367, #5368; Stroke Weight Up and Down shortcuts, #5797; Hindi catalog and contributor loanword preferences; full wording review in progress; Properties in Edit Artboards: a Transform section with the reference point above the Artboards section, #5219; the Control bar's Stroke: and Opacity: links scrub their fields, #4088; an image zoomed in on is separated, colorized or proofed only where it shows (#4155); a document's first frame renders on the render worker, so opening a heavy file no longer freezes the window, #4155; Control bar Paragraph section with type selected, #5372; translating unchanged type reuses its glyph and character-stroke outlines; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026; Units › East Asian Type acts, with Q and Ha units; scores unchanged) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-11 · **Change:** minor (#6404: transform-aware Eyedropper character copying; scores and estimates unchanged; previously: Envelopes: with Distort Appearance, type's strokes paint along the bent glyph outlines at their own weight, as users' `.ai` files draw them, which changes how VectorCraft's own envelopes of stroked type draw (#6644; scores and estimates unchanged); previously minor: G5: Energy Beams brush library; bundled content ~44% → ~46%; previously minor: G5: Skill Icons symbol library; bundled content ~42% → ~44%; previously minor: `.ai` and EPS import: an envelope saved in Edit Contents whose mesh lies away from its drawn art by a move opens live on that art (#6570; scores and estimates unchanged); previously minor: ruler double-click adds a guide at the pointer, #5239; G5: Status Effects graphic style library; bundled content ~40% → ~42%; MCP symbol libraries list UI Frames and Map Markers; previously minor: G5: Inventory Icons built-in symbol library; bundled content ~38% → ~40%; previously minor: `.ai` and EPS import: stroked type in an envelope maps from its glyphs grown by half its stroke (#3179; scores and estimates unchanged); previously minor: `.ai` and EPS import: warps of type open as mesh envelopes on their saved grid where VectorCraft's warp doesn't draw as their drawn art (#3179; scores and estimates unchanged); G5: Game Trails brush library; bundled content ~36% → ~38%; previously minor: Edit › Edit Custom Dictionary…: its dialog adds, deletes and changes the custom dictionary's words, #4936; previously minor: `.ai` and EPS import: envelopes of type open live where they draw as their drawn art and the layers with them draw as the page (#3179; scores and estimates unchanged); G5: Game HUD graphic style library; bundled content ~34% → ~36%; previously minor: Line Arrow arrowhead (#5421; scores and estimates unchanged); G5: Map Markers built-in symbol library; bundled content ~32% → ~34%; previously minor: right-click on type: Spelling › Auto Spell Check, with its check mark, #4936; previously minor: right-click on a word Auto Spell Check underlined: its suggestions, Ignore All and Add to Dictionary, #4936; previously minor: Canvas: a 3D effect changed on type or a group redraws where its solid was; Fit to Artwork Bounds and per-artboard SVG export build each solid once for all artboards; previously minor: Effect Gallery: a thumbnail for each filter and a preview area inside the dialog, on an original drawing; Raster effects 52% → 55%, hours 1–2 → none; headline numbers unchanged; Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: PDF editable import preserves authored ActualText once, including empty/nested spans and authored ordering (#6165); cross-line/style layout remains approximate; scores and estimates unchanged; PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; G5: UI Frames built-in symbol library with default 9-slice; bundled content ~32%; previously minor: G5: dynamic symbol part on-canvas opacity/blend/gradient/pattern; strokes remaining no longer lists Overlap/auto corners; symbols ~88%; previously minor: interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; previously minor: harakat paint fallback clarified; previously minor: current Guides feature-area rows consolidated using existing ruler evidence; scores and estimates unchanged; previously minor: Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; previously minor: G5 docs: Art brush Overlap and Pattern brush auto corners removed from strokes remaining; symbols remaining lists dynamic on-canvas beyond solid and guides outside the art; previously minor: custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: EPS import: embedded fonts (composite, Type 42, CID-keyed, Type 1, CFF FontSet) draw their glyphs; spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; `.ai`/EPS type keeps its tab stops, and right-aligned lines with tabs lay out as the file's page, #6147; `.ai`/EPS type from the text document lays its lines out closer to the file's page, within limits, #6137; `.ai` and EPS import: older Art brush objects read; numeric fields step on wheel hover without taking keyboard focus, and a panel wheel turn cannot seed a canvas zoom glide, #5949; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen; Tools stays beside the canvas with left-side panels; new gradients follow document color mode; Selection QoL: marquee modes and visible feedback, #5313; SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; the canvas keeps selection outlines and anchors between frames; Indian-language hyphenation review and license allowlist, #4947; Stroke gradients: an arrowhead aligned to the tip is painted to the corners of its base, #5964; Drop Shadow and glow dialogs: spinner arrows, #5367, #5368; Stroke Weight Up and Down shortcuts, #5797; Hindi catalog and contributor loanword preferences; full wording review in progress; Properties in Edit Artboards: a Transform section with the reference point above the Artboards section, #5219; the Control bar's Stroke: and Opacity: links scrub their fields, #4088; an image zoomed in on is separated, colorized or proofed only where it shows (#4155); a document's first frame renders on the render worker, so opening a heavy file no longer freezes the window, #4155; Control bar Paragraph section with type selected, #5372; translating unchanged type reuses its glyph and character-stroke outlines; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026; Units › East Asian Type acts, with Q and Ha units; scores unchanged) > > Vector Halftone preview update (2026-11-10): temporary dot selection marks are hidden, and the bounding box stays available (#6369). > @@ -217,7 +217,7 @@ dominate; 3D, graphs and automation are specialist). They sum to 105. Feature de | Selection, transform & align tools | 6 | 85% | Direct Selection segment click/Delete corrected (#1278); deletion splits an ordinary contour into selected sibling objects with bounds-fitted gradients kept in place (#3380, deletion only; `crates/engine/tests/it/delete_path_pieces.rs`); open-path anchor Delete keeps gaps; Select › Start Global Edit finds the paths, compound paths and symbol instances of the selected object's shape (Appearance, Size and artboard options) and gives an edit of one to all of them in proportion, as one undo step (#3521), and on a group the groups like it, an edit inside one reaching the same object in the others (#4939), and a Shift-click on a highlighted match leaves it out; the Align panel menu's Align to Glyph Bounds › Point Text and Area Text measure type by its glyphs' ink for Align and Distribute (#4231); remaining: Free Transform/Scale/Reflect along a rotated box, Global Edit of rotated copies, transform nuances (no-op anchor Align history regression-tested, #1180 follow-up) | 4–6 | estimated; Global Edit regression-tested (`crates/engine/src/tests/globaledit.rs`; Quick Action, menu label and Escape in `panels::properties::tests::start_global_edit_quick_action_and_escape`) | | Drawing tools (Pen, Curvature, Pencil, shapes, Shaper) | 7 | 85% | Shaper construction widget, Pencil/Pen nuances, Blob Brush options, Touch Type, Dimension tool; see [ui-parity.md](ui-parity.md) | 8–13 | estimated; Curvature point display regression-tested (#1236, in part); Polygon/Star exact-size count persistence regression-covered; Live Corners' Relative rounding done (#3091), score unchanged; Copy and Cut with the Pen or Curvature tool take the anchors a Cmd/Ctrl-lent Direct Selection picked (#6123), score and hours unchanged | | Path operations, Pathfinder, Shape Builder, Live Paint | 5 | 80% | Shape Builder on the reporter's file (#937: art past 4,096 segments is cut by lines again; unconfirmed on the file itself); gap detection shares Live Paint’s finder, with a 72 pt custom cap and the existing options; Live Paint Gap Options | 4–8 | estimated; 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 (lowered from 86%: open issues); compound attributes regression-tested (#1197); single-result source names survive SVG export/import (#1293), with [SVG ID semantics](https://www.w3.org/TR/SVG2/struct.html#IDAttribute) and original-shape tests; Simplify component selection and preview regression-tested (#1272); Simplify of a Direct Selection part of a path regression-tested (`tests/simplify_part.rs`); Divide open-cutting-path geometry and appearance regression-tested (#1307); the Shape Builder's edge-source grid is bounded in memory and files a segment once per cell (`planar.rs` test `rect_index_holds_a_bounded_number_of_references`); live compound shapes (Make, Alt-click, member Shape Modes, Release, Expand, saved and exported) regression-tested (#4522) | -| Colour, swatches, gradients, patterns, mesh, recolor | 7 | 92% | 30.0's gradient additions are done (dither, perceptual blending, presets); freeform gradients in SVG/PDF, mixed spot/process gradients in SVG, PDF gradients mixing a spot color with RGB or Lab stops, and dithered spot gradient fills and Perceptual spot gradients with RGB or Lab stops other than white or more than 4,096 stops on their curve, which PDF export writes as images in process colors | 4–7 | estimated; Recolor model selection is available through the engine and MCP (#2520), and the dialog's color sliders (HSB, RGB, CMYK, Lab, Tint for a spot color) edit the selected row's new color in its mode, and the Edit tab's Global Adjust sliders (Saturation, Brightness, Temperature, Luminosity, -100% to 100%) change every recolored color at once (lowered from 96%: target moved); unpainted zero-width stroke restoration fixed (#1253), score and hours unchanged; Eyedropper source reads regression-tested (#1251); screen-bounded Recolor Edit chip list (#3416), score and hours unchanged | +| Colour, swatches, gradients, patterns, mesh, recolor | 7 | 92% | Eyedropper copies effective character dimensions across text transforms (#6404); 30.0's gradient additions are done (dither, perceptual blending, presets); freeform gradients in SVG/PDF, mixed spot/process gradients in SVG, PDF gradients mixing a spot color with RGB or Lab stops, and dithered spot gradient fills and Perceptual spot gradients with RGB or Lab stops other than white or more than 4,096 stops on their curve, which PDF export writes as images in process colors| 4–7 | estimated; Recolor model selection is available through the engine and MCP (#2520), and the dialog's color sliders (HSB, RGB, CMYK, Lab, Tint for a spot color) edit the selected row's new color in its mode, and the Edit tab's Global Adjust sliders (Saturation, Brightness, Temperature, Luminosity, -100% to 100%) change every recolored color at once (lowered from 96%: target moved); unpainted zero-width stroke restoration fixed (#1253), score and hours unchanged; Eyedropper source reads regression-tested (#1251); screen-bounded Recolor Edit chip list (#3416), score and hours unchanged | | Strokes, brushes, width profiles | 5 | 88% | Shift-double-click adjoining adjustment is implemented; brush libraries are done (six generated libraries including Game Trails and Energy Beams, User Defined, Other Library…, Save Brushes); Art brush Overlap and Pattern brush auto corners are done; remaining: pen pressure on macOS, Stylus Wheel, more built-in brush libraries, tilt, bearing and rotation for Art, Pattern and Bristle brushes and on native Wayland and the web | 8–15 | estimated; width-point creation and adjoining adjustment regression-tested; dialog layout reviewed; evidence below; finite-arithmetic follow-up regressions and full CI passed (evidence below) | | Appearance, transparency, graphic styles, masks | 5 | 97% | raster effect reach in preview bounds | 1–2 | estimated; Transform typing, sliders, preview, and undo are covered in `crates/ui-egui/src/panels/tests_appearance.rs` | | 3D and Materials (and Turntable) | 4 | ~25% | live Revolve with automatic smooth vector shading and default-on continuous circular caps, Extrude & Bevel (twist, taper, caps, five bevel shapes), Inflate and Rotate with Position presets (colored Inflate uses smooth membrane lighting; white-light Inflate retains its existing shading; Extrude & Bevel and Rotate retain planar shading; one to ten colored lights, shared neutral ambient, painter visibility); materials, light presets, shadows, ray tracing, the rest of 3D Classic (Revolve, Rotate, Map Art), Turntable views, textures, map art and shading in 3D export | 30–55 | estimated (Extrude & Bevel, Inflate and Rotate added 2026-10-12) | @@ -349,7 +349,7 @@ Depth here is "does the basic use work", not the area's full score. | Shape tools (rectangle, ellipse, polygon, star, line) | 9 | 95% | all shape tools, Live Corners | | Selection, move, scale, rotate | 9 | 92% | bounding box, smart guides, transform tools | | Pen and Pencil, basic use | 7 | 88% | [ui-parity.md](ui-parity.md); a few Pencil and Curvature options missing | -| Fill, stroke, colour, swatches | 9 | 95% | M3 done; highlighted-text Eyedropper font/paint and source/target transparency refusal regression-tested; scores and hours unchanged | +| Fill, stroke, colour, swatches | 9 | 95% | M3 done; transform-aware Eyedropper character dimensions (#6404); highlighted-text Eyedropper font/paint and source/target transparency refusal regression-tested; scores and hours unchanged | | Gradients, basic | 4 | 90% | Gradient panel and on-canvas annotator | | Type: point and area text, font, size | 8 | 88% | font menu with previews; Hyphenation… and Justification… options per paragraph (#2310, #2755) | | Align, group, arrange | 6 | 95% | | @@ -1695,6 +1695,10 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur Evidence for the shared light Color: geometry tests in `tests_lighting`, effect paint tests, command/save/undo/expand and SVG/PDF checks in `three_d_light_color`, and headless picker/reset/preview tests in `tests_revolve`. Revolve keeps smooth colored vector ramps and continuous flat-lit caps. Inflate geometry tests check spherical lighting, shared vertex/edge continuity, concave cell coverage, puffed backs at perspective and legacy single-light/list equivalence; rendered command/export regressions cover smooth shading and editable expansion. Four legacy white-light PNGs match main byte for byte; [release measurements](3d-lighting-performance.md) record the colored solid-fill batch and the remaining source-gradient cost. +## Eyedropper character dimensions (2026-11-11) + +`appearance.copyFrom` reads the source's effective character dimensions and converts them into each target's local text space, preserving target placement, rotation, shear and reflection (#6404). Point and area text, grouped text, highlighted ranges, reverse and remembered sampling, attribute opt-outs, Undo/Redo and native-file round trips are regression-tested. Picks with unrepresentable dimensions or text that would extend beyond the canvas leave the document, paint defaults and remembered sample intact. Paint-only copying remains available. Tests: `crates/engine/src/tests/eyedropper.rs` (`scaled_type_pick_*`). Scores and estimates unchanged. + ## Highlighted-text Eyedropper (2026-10-21) Type/Eyedropper switches preserve highlighted characters; sampling changes only their enabled @@ -1998,6 +2002,7 @@ Edit › Copy and Cut put the selected ruler guides on the clipboard, with the s | Date | Change | Summary | |---|---|---| +| 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 | 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. | diff --git a/docs/ui-parity.md b/docs/ui-parity.md index 296826671..c9e043aa6 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -1,6 +1,6 @@ # UI and interaction parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-11 · **Change:** minor (Outline view: images and gradient meshes are picked on what Outline draws, so a click inside them reaches the path behind, #6437; previously minor: a middle-click on a document tab closes it, #6506; previously minor: Blob Brush: a round tip's preview is the outline of the area it adds, round at both ends, as the stroke it commits (#4343); scores and estimates unchanged; previously:; ruler double-click adds a guide at the pointer, #5239; G5: Inventory Icons built-in symbol library (four built-ins); previously minor: files that record no units open in Preferences › Units › General, #5820; previously minor: interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; numeric fields step on wheel hover without taking keyboard focus, #5949; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen; Selection QoL: marquee modes and visible feedback, #5313; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; Drop Shadow and glow dialogs: spinner arrows, #5367, #5368; Stroke Weight Up and Down shortcuts, #5797; Layers: Rename (`ui.rename`) opens the name field of the selected layer, group or object, #5324; Properties in Edit Artboards: Transform section above the Artboards section, #5219; scrubby labels: the Control bar's Stroke: and Opacity: links and the Properties panel's Stroke link scrub their fields, #4088; Properties: Quick Actions has Repeat Options for one selected repeat, #5181; Direct Selection: Shift-click picks several blend spine points, which drag together, #5285; Direct Selection: Cmd/Ctrl held lends the Selection tool and shows its bounding box, #3901; Join tool trims two crossing open paths and joins them at the crossing, #5844; Control bar: Paragraph: link and Align left/center/right while type is selected, #5372; shape tools: a press on the selection's centre point moves it and one in the rotate zone outside a box corner turns it, #5477; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; Units › East Asian Type acts, with Q and Ha units; scores unchanged) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-11 · **Change:** minor (#6404: transform-aware Eyedropper character copying; scores and estimates unchanged; previously: Outline view: images and gradient meshes are picked on what Outline draws, so a click inside them reaches the path behind, #6437; previously minor: a middle-click on a document tab closes it, #6506; previously minor: Blob Brush: a round tip's preview is the outline of the area it adds, round at both ends, as the stroke it commits (#4343); scores and estimates unchanged; previously:; ruler double-click adds a guide at the pointer, #5239; G5: Inventory Icons built-in symbol library (four built-ins); previously minor: files that record no units open in Preferences › Units › General, #5820; previously minor: interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; numeric fields step on wheel hover without taking keyboard focus, #5949; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen; Selection QoL: marquee modes and visible feedback, #5313; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; Drop Shadow and glow dialogs: spinner arrows, #5367, #5368; Stroke Weight Up and Down shortcuts, #5797; Layers: Rename (`ui.rename`) opens the name field of the selected layer, group or object, #5324; Properties in Edit Artboards: Transform section above the Artboards section, #5219; scrubby labels: the Control bar's Stroke: and Opacity: links and the Properties panel's Stroke link scrub their fields, #4088; Properties: Quick Actions has Repeat Options for one selected repeat, #5181; Direct Selection: Shift-click picks several blend spine points, which drag together, #5285; Direct Selection: Cmd/Ctrl held lends the Selection tool and shows its bounding box, #3901; Join tool trims two crossing open paths and joins them at the crossing, #5844; Control bar: Paragraph: link and Align left/center/right while type is selected, #5372; shape tools: a press on the selection's centre point moves it and one in the rotate zone outside a box corner turns it, #5477; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; Units › East Asian Type acts, with Q and Ha units; scores unchanged) > > Vector Halftone preview update (2026-11-10): temporary dot selection marks are hidden, and the bounding box stays available (#6369). > @@ -302,6 +302,7 @@ tools, never checked side by side) isn't captured row by row. | Space: Hand; Cmd+Space / Cmd+Alt+Space: zoom in/out; middle button pans | done | Space pressed after a tool takes a drag keeps the view still and shows that tool's cursor. Shape and anchor tools may use Space to move the geometry being drawn. Cmd+Space pressed mid-drag likewise keeps the active tool cursor. Space held before a press starts a view gesture, and its view cursor returns after release. `space_during_a_pencil_drag_keeps_the_pencil_cursor`, `cmd_space_mid_tool_drag_keeps_the_active_tool_cursor` | | Document window scroll bars along the right and bottom edges (Normal and Full Screen Mode with Menu Bar; none in Full Screen and Presentation Mode): a thumb as long as the view against the art and the artboards; dragging it scrolls, a click on the track pages, the wheel over a bar scrolls along it; in a rotated view they follow the screen's axes | partial | #2322. The span is the art and the artboards with half a window around them, not Illustrator's 227 in canvas: VectorCraft has no bounded drawing area (#2284). A track click pages rather than jumping to the click; the page size and the rotated-view behaviour are our own choice (the public documentation gives neither) | | Cmd/Ctrl: last-used selection tool; Cmd+Alt: Group Selection inside Direct Selection | done | Cmd+Alt lends Group Selection only while Direct Selection is active (Cmd borrow from the Pen keeps Direct Selection so Alt stays copy/subtract); Cmd alone lends Direct Selection the Selection tool: its bounding box shows while Cmd is held over objects selected whole and its handles scale and rotate them, and a Cmd drag moves the whole object while its direct-selected anchors stay selected (#3901; no box over a selection with anchors picked yet); engine pointer and canvas regressions | +| Eyedropper copies effective character dimensions between transformed text objects | done | Point/area and grouped text, highlighted ranges, reverse/remembered sampling, opt-outs and atomic invalid-transform rejection; `scaled_type_pick_*` (#6404) | | Eyedropper styles highlighted characters only and preserves ranges across Type switches | done | Font/paint, Shift color-only, Unicode, Undo/Redo and stale-range regression tests; enabled non-neutral source or target object transparency is refused atomically; disable Transparency to sample supported attributes | | Eyedropper reads visible sources while their layer/object is locked or outside isolation | done | Ordinary/Shift/Shift+Alt and gradient-stop reads preserve the source and selection; Alt reverse remains editable-only; hidden art stays excluded (`eyedropper_sources.rs`, #1251) | | Add a visible stroke after drawing with inherited None/zero-width paint | done | Paint and Swap Fill/Stroke restore the existing 1 pt default; positive widths and painted zero widths stay as set; Undo/Redo and targeted appearance items tested (`stroke_visibility.rs`, #1253) | @@ -511,6 +512,7 @@ separate. Scores and hours unchanged. [Behavior and command details](development | Date | Change | Summary | |---|---|---| +| 2026-11-11 | minor | Eyedropper copies effective character dimensions between transformed text objects (#6404); scores and estimates unchanged. | | 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. | From f6dbc82e6e3e62e72f7ddd4a49313e5e7c25cfe0 Mon Sep 17 00:00:00 2001 From: Christian Lappin <36496308+christianlappin@users.noreply.github.com> Date: Wed, 11 Nov 2026 21:00:24 -0500 Subject: [PATCH 2/2] M3: validate paragraph-only Eyedropper bounds (#6404) Paragraph justification can move point text past the canvas limit even when character copying is disabled. Check those targets before keeping the edit or remembered sample, and cover the explicit-target case with a regression. --- crates/engine/src/cmd/appearance.rs | 14 +++++++++---- crates/engine/src/tests/eyedropper.rs | 29 +++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/crates/engine/src/cmd/appearance.rs b/crates/engine/src/cmd/appearance.rs index 1f8d1d696..1f64e582a 100644 --- a/crates/engine/src/cmd/appearance.rs +++ b/crates/engine/src/cmd/appearance.rs @@ -1282,12 +1282,12 @@ fn copy_from(s: &mut Session, p: &Value) -> Result { Some((id, start, end)) if targets.contains(&id) => vec![(id, Some((start, end)))], _ => vec![], }; + let target_ids = || targets.iter().copied().filter(|_| ranges.is_empty()).chain(ranges.iter().map(|(id, _)| *id)); // Prepare every dimensional copy before changing defaults or the remembered sample. A bad // source or target transform must refuse the entire pick, including a multi-frame range. let characters = if attrs.character && !append { let d = &s.doc()?.doc; - let ids = targets.iter().copied().filter(|_| ranges.is_empty()).chain(ranges.iter().map(|(id, _)| *id)); - copied_character_styles(&node, ids.filter_map(|id| d.node(id)))? + copied_character_styles(&node, target_ids().filter_map(|id| d.node(id)))? } else { HashMap::new() }; @@ -1305,8 +1305,14 @@ fn copy_from(s: &mut Session, p: &Value) -> Result { } } // The session's bounds check follows the selection. Check text targets reached through - // explicit IDs, a reverse pick, or a highlighted story as well. - let text_ids: Vec<_> = characters.keys().copied().collect(); + // explicit IDs, a reverse pick, or a highlighted story as well. Paragraph changes can + // move point text even when character copying is disabled. + let text_ids: Vec<_> = if attrs.paragraph && !append && picked.text.is_some() { + let d = &s.doc()?.doc; + target_ids().filter(|id| d.node(*id).is_some_and(|n| matches!(n.kind, NodeKind::Text(_)))).collect() + } else { + characters.keys().copied().collect() + }; let check_text_bounds = |d: &Document| { if !text_ids.is_empty() && !crate::sanity::objects_in_range(d, &text_ids) { return Err(bad(C, "sampled text would exceed the canvas (coordinates out of range)")); diff --git a/crates/engine/src/tests/eyedropper.rs b/crates/engine/src/tests/eyedropper.rs index 5204995d6..b3a9012e0 100644 --- a/crates/engine/src/tests/eyedropper.rs +++ b/crates/engine/src/tests/eyedropper.rs @@ -442,6 +442,35 @@ fn scaled_type_pick_canvas_rejection_preserves_sample_and_defaults() { } } +/// Changing justification can move explicitly targeted point text outside the canvas even +/// when character copying is disabled. +#[test] +fn paragraph_only_pick_rejects_out_of_bounds_explicit_target() { + let mut s = session(); + let saved = id_of(s.execute("text.create", &json!({"x": 40, "y": 40, "text": "Saved", "color": "#0000ff"})).unwrap()); + let src = id_of(s.execute("text.create", &json!({"x": 100, "y": 100, "text": "Source", "color": "#ff0000"})).unwrap()); + s.execute("text.setStyle", &json!({"justify": "left"})).unwrap(); + let dst = id_of(s.execute("text.create", &json!({"x": 100, "y": 200, "text": "WWWW", "size": 16})).unwrap()); + s.execute("text.setStyle", &json!({"justify": "right"})).unwrap(); + s.execute("object.transform", &json!({"matrix": [1, 0, 0, 1, MAX_COORD - 101.0, 0]})).unwrap(); + s.execute("select.none", &json!({})).unwrap(); + s.execute("appearance.copyFrom", &json!({"source": saved.0, "ids": []})).unwrap(); + let doc = s.doc().unwrap().doc.clone(); + let selection = s.doc().unwrap().selection.clone(); + let paint = s.paint.clone(); + let sample = s.eyedropper_sample.clone(); + let history = undo_len(&s); + + let pick = json!({"source": src.0, "ids": [dst.0], "apply": {"character": false}}); + let error = s.execute("appearance.copyFrom", &pick).unwrap_err(); + assert!(error.to_string().contains("coordinates out of range"), "{error}"); + assert_eq!(s.doc().unwrap().doc, doc); + assert_eq!(s.doc().unwrap().selection, selection); + assert_eq!(s.paint, paint); + assert_eq!(s.eyedropper_sample, sample); + assert_eq!(undo_len(&s), history); +} + /// Disabled character copying and append preserve typography; reverse and cached picks convert it. #[test] fn scaled_type_pick_respects_options_and_reverse_sampling() {