From 4db6e7b448d8d5dbfd5d0ab83bddc6968c43c068 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Tue, 3 Nov 2026 04:59:59 -0800 Subject: [PATCH] Paint: use the document color model for new gradient defaults --- ROADMAP.md | 4 ++- crates/engine/src/cmd/gradient.rs | 47 +++++++++++++++++++++-------- crates/engine/src/cmd/paint.rs | 2 +- crates/engine/src/tests/gradient.rs | 41 +++++++++++++++++++++++++ docs/gaps.md | 5 ++- docs/roadmap.md | 5 ++- docs/target-app-parity.md | 5 ++- docs/ui-parity.md | 5 ++- 8 files changed, 95 insertions(+), 19 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 7aa720205..6cf80d362 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,5 +1,7 @@ # VectorCraft Roadmap +- Paint: new gradients use the document's color model for default stops, including Gradient tool creation. Scores and estimates unchanged. + - PDF.R1: editable PDF import uses the shared reader, scans page content in windows, and rejects known incomplete input before returning a document. - PDF.W1: PDF export and print composition use the shared optional writer for configuration, metadata, font preparation and guarded finalization; drawing and output policy remain app-owned. @@ -9,7 +11,7 @@ **Stage: alpha** · next: beta, ~13 points (ready for real work ~62% → ~75%, and reliable `.ai` exchange) and ~170–270 h away -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-03 · **Change:** minor (Progress log row for the Type Tools add-on's Text Tweaker, #4924; headline numbers unchanged; previously: font-family fields accept the mouse wheel, #5253; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-03 · **Change:** minor (new gradients follow document color mode; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x) VectorCraft is a clean-room, open-source, pure-Rust reimplementation of the Adobe Illustrator workflow. It runs on macOS, Windows, Linux, FreeBSD and the web (WASM), and agents can drive all of it over MCP, the CLI and a JSON diff --git a/crates/engine/src/cmd/gradient.rs b/crates/engine/src/cmd/gradient.rs index b506c18f8..52daa9b74 100644 --- a/crates/engine/src/cmd/gradient.rs +++ b/crates/engine/src/cmd/gradient.rs @@ -312,10 +312,31 @@ pub(crate) fn apply_gradient_edit_in( bounds: Option, inside: &dyn Fn(Point) -> bool, ) -> std::result::Result { - let mut gp = match paint { + apply_gradient_edit_in_model(paint, p, bounds, inside, None) +} + +/// New implicit stops use the document's model; existing gradients retain their own colors. +fn gradient_for_edit(paint: &Paint, model: Option) -> GradientPaint { + match paint { Paint::Gradient(g) => (**g).clone(), - _ => GradientPaint::new(Gradient::default()), - }; + _ => { + let mut gradient = Gradient::default(); + for stop in &mut gradient.stops { + stop.color = super::paint::stored_color(model, stop.color); + } + GradientPaint::new(gradient) + } + } +} + +fn apply_gradient_edit_in_model( + paint: &Paint, + p: &Value, + bounds: Option, + inside: &dyn Fn(Point) -> bool, + model: Option, +) -> std::result::Result { + let mut gp = gradient_for_edit(paint, model); let mut switched = false; if let Some(k) = str_param(p, "kind") { let kind = parse_kind(k)?; @@ -414,13 +435,15 @@ fn edit_gradient(s: &mut Session, p: &Value) -> Result { return Err(bad(C, "`dither` applies to gradient fills (stroke: false)")); } super::paint::check_keep_model(p, C)?; + let model = super::paint::new_color_model(s, p); // New RGB stop colors take the document's model; those of the swatches the stops name don't. let stored = stop_colors(s, p, p); // Stops naming swatches take their colours from them. let p = &link_stops(&s.doc()?.doc, p).map_err(|e| bad(C, e))?; // Validate against the defaults first so bad params fail without touching the document. let default_paint = if stroke { s.paint.stroke.clone() } else { s.paint.fill.clone() }; - let new_default = apply_gradient_edit(&default_paint, &with_stored_stops(p, &default_paint, &stored), None).map_err(|e| bad(C, e))?; + let new_default = apply_gradient_edit_in_model(&default_paint, &with_stored_stops(p, &default_paint, &stored), None, &|_| true, model) + .map_err(|e| bad(C, e))?; let freeform = str_param(p, "kind").and_then(GradientKind::parse) == Some(GradientKind::Freeform); edit_items(s, &ids, item, C, "Gradient", !stroke, |n, index| { if index.is_none() @@ -429,7 +452,7 @@ fn edit_gradient(s: &mut Session, p: &Value) -> Result { let lb = t.local_bounds(); for r in &mut t.runs { let (cur, b) = run_paint_mut(r, stroke, lb); - *cur = apply_gradient_edit(cur, &with_stored_stops(p, cur, &stored), Some(b)).map_err(|e| bad(C, e))?; + *cur = apply_gradient_edit_in_model(cur, &with_stored_stops(p, cur, &stored), Some(b), &|_| true, model).map_err(|e| bad(C, e))?; } return Ok(()); } @@ -441,7 +464,7 @@ fn edit_gradient(s: &mut Session, p: &Value) -> Result { None => &|_| true, }; let old = n.appearance.paint_at(index, !stroke).unwrap_or(&Paint::None); - let np = apply_gradient_edit_in(old, &with_stored_stops(p, old, &stored), b, inside).map_err(|e| bad(C, e))?; + let np = apply_gradient_edit_in_model(old, &with_stored_stops(p, old, &stored), b, inside, model).map_err(|e| bad(C, e))?; n.appearance.set_paint_at(index, !stroke, np); if let Some(m) = mode && let Some(st) = n.appearance.stroke_at_mut(index) @@ -547,12 +570,9 @@ impl GeomEdit { /// `cur` with the vector, aspect and focal point of `e` (given in the document). `to_doc` maps /// the paint's space (text space for type runs) to the document and `bounds` (in that space) fits /// an unplaced gradient, so what the document shows carries over where `e` keeps it. -fn vector_paint(cur: &Paint, e: &GeomEdit, to_doc: Affine, bounds: Option) -> Option { +fn vector_paint(cur: &Paint, e: &GeomEdit, to_doc: Affine, bounds: Option, model: Option) -> Option { let from_doc = invert(to_doc)?; - let mut gp = match cur { - Paint::Gradient(g) => (**g).clone(), - _ => GradientPaint::new(Gradient::default()), - }; + let mut gp = gradient_for_edit(cur, model); let kind = gp.gradient.kind; let map = |mut g: GradientGeom, a: Affine| { if a != Affine::IDENTITY { @@ -597,6 +617,7 @@ fn set_gradient_geom(s: &mut Session, p: &Value) -> Result { if targets.is_empty() { return Err(EngineError::Other("nothing selected".into())); } + let model = super::paint::new_color_model(s, &Value::Null); edit_items(s, &targets, item, C, "Gradient", !stroke, |n, index| { if index.is_none() && let NodeKind::Text(t) = &mut n.kind @@ -607,14 +628,14 @@ fn set_gradient_geom(s: &mut Session, p: &Value) -> Result { run_stroke_weight(&mut r.style); } let (paint, b) = run_paint_mut(r, stroke, lb); - if let Some(np) = vector_paint(paint, &edit, xf, Some(b)) { + if let Some(np) = vector_paint(paint, &edit, xf, Some(b), model) { *paint = np; } } return Ok(()); } let b = item_paint_bounds(n, index, !stroke); - if let Some(np) = vector_paint(n.appearance.paint_at(index, !stroke).unwrap_or(&Paint::None), &edit, Affine::IDENTITY, b) { + if let Some(np) = vector_paint(n.appearance.paint_at(index, !stroke).unwrap_or(&Paint::None), &edit, Affine::IDENTITY, b, model) { n.appearance.set_paint_at(index, !stroke, np); } Ok(()) diff --git a/crates/engine/src/cmd/paint.rs b/crates/engine/src/cmd/paint.rs index 33c69589b..ec59e4e9e 100644 --- a/crates/engine/src/cmd/paint.rs +++ b/crates/engine/src/cmd/paint.rs @@ -303,7 +303,7 @@ pub(crate) fn check_keep_model(p: &Value, cmd: &str) -> Result<()> { /// The model RGB colours given in `p` are stored in: the document's in a CMYK document (`None` in /// an RGB one, or with `keepModel`). -fn new_color_model(s: &Session, p: &Value) -> Option { +pub(super) fn new_color_model(s: &Session, p: &Value) -> Option { let mode = s.active()?.doc.color_mode; (mode != vectorcraft_doc::ColorMode::Rgb && !bool_or(p, "keepModel", false)).then(|| mode.model()) } diff --git a/crates/engine/src/tests/gradient.rs b/crates/engine/src/tests/gradient.rs index c36addcbc..e6f9383fe 100644 --- a/crates/engine/src/tests/gradient.rs +++ b/crates/engine/src/tests/gradient.rs @@ -30,6 +30,47 @@ fn fill_gradient(s: &Session, id: NodeId) -> GradientPaint { } } +#[test] +fn new_gradient_defaults_follow_the_cmyk_document_mode() { + for kind in ["linear", "radial", "freeform"] { + let mut s = Session::new(); + s.execute("file.new", &json!({"width":200,"height":200,"colorMode":"cmyk"})).unwrap(); + let id = rect(&mut s, 10.0, 10.0, 100.0, 100.0); + s.execute("paint.editGradient", &json!({"kind":kind})).unwrap(); + let g = fill_gradient(&s, id); + assert!(g.gradient.stops.iter().all(|stop| matches!(stop.color, Color::Cmyk { .. })), "{kind}: {:?}", g.gradient.stops); + if let Some(freeform) = g.freeform { + assert!(freeform.points.iter().all(|point| matches!(point.color, Color::Cmyk { .. })), "freeform points must use the same color model"); + } + } +} + +#[test] +fn editing_an_explicit_rgb_gradient_in_cmyk_keeps_its_color_model() { + let mut s = Session::new(); + s.execute("file.new", &json!({"width":200,"height":200,"colorMode":"cmyk"})).unwrap(); + let id = rect(&mut s, 10.0, 10.0, 100.0, 100.0); + s.execute("paint.editGradient", &json!({"kind":"linear","keepModel":true})).unwrap(); + let before = fill_gradient(&s, id).gradient.stops; + assert!(before.iter().all(|stop| matches!(stop.color, Color::Rgb { .. }))); + s.execute("paint.editGradient", &json!({"angle":45})).unwrap(); + assert_eq!(fill_gradient(&s, id).gradient.stops, before); +} + +#[test] +fn drawing_a_new_gradient_vector_uses_cmyk_default_stops() { + for stroke in [false, true] { + let mut s = Session::new(); + s.execute("file.new", &json!({"width":200,"height":200,"colorMode":"cmyk"})).unwrap(); + let id = rect(&mut s, 10.0, 10.0, 100.0, 100.0); + s.execute("paint.setGradientGeom", &json!({"start":[10,10],"end":[110,10],"stroke":stroke})).unwrap(); + let appearance = &node(&s, id).appearance; + let paint = if stroke { appearance.stroke_paint() } else { appearance.fill_paint() }; + let Paint::Gradient(g) = paint else { panic!("not a gradient") }; + assert!(g.gradient.stops.iter().all(|stop| matches!(stop.color, Color::Cmyk { .. })), "stroke={stroke}: {:?}", g.gradient.stops); + } +} + /// A three-stop freeform gradient with a 0.3 midpoint, partial opacity and a placed vector. fn three_stops() -> GradientPaint { let stop = |offset, color, opacity, midpoint| GradientStop { opacity, midpoint, ..GradientStop::new(offset, color) }; diff --git a/docs/gaps.md b/docs/gaps.md index 783dc9711..87721de91 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -1,9 +1,11 @@ # Gaps -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-03 · **Change:** minor (G15: a compound path's assembled path is kept between frames; estimate unchanged) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-03 · **Change:** minor (new gradients follow document color mode; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x) ## Feature submission evidence +Creating a gradient from solid or empty paint uses the document's color model for its default stops, including drawing its vector with the Gradient tool. Explicit `keepModel: true` retains RGB defaults, and existing gradients keep their own colors. Scores and estimates are unchanged. + The closed font-family fields in Character, Properties and Control now accept the mouse wheel for selected text (#5253). Each effective notch applies one family and adds one undo step; the list follows saved font filters, ignores search text, and @@ -26,6 +28,7 @@ Artboard Options forwards the active Artboard tool's Scale Artwork with Artboard | Date | Change | Summary | |---|---|---| +| 2026-11-03 | minor | New gradients use CMYK default stops in CMYK documents, including Gradient tool creation. Scores and estimates unchanged. | | 2026-11-02 | minor | Font-family fields step selected text with the mouse wheel (#5253); no scored gap or estimate reassessed. | | 2026-10-24 | minor | G12: General › Corner Radius is the Rounded Rectangle tool's radius, a change while the tool is active reaches it, and the radius ↑/↓ set while drawing (or a Tool Options value) becomes the preference, within 0–1296 pt (#394, `crates/engine/src/tests/constrain_tools.rs`); no entry closed, estimate unchanged. | | 2026-10-22 | minor | Reconcile upstream documentation after review F-3; preserve every dated revision and the original scaleArt runtime evidence without repeating completed jobs. | diff --git a/docs/roadmap.md b/docs/roadmap.md index bf5f67279..2ea970a6a 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,9 +1,11 @@ # Roadmap: milestones and what's next -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-03 · **Change:** minor (font-family fields accept the mouse wheel, #5253; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-03 · **Change:** minor (new gradients follow document color mode; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x) ## Feature submission evidence +Creating a gradient from solid or empty paint uses the document's color model for its default stops, including drawing its vector with the Gradient tool. Explicit `keepModel: true` retains RGB defaults, and existing gradients keep their own colors. Scores and estimates are unchanged. + **Feature evidence last updated:** 2026-11-03. The shared assessment status line and revision history are current upstream; this dated section records the submission's feature evidence and revisions without rescoring upstream or claiming a new combined build or runtime measurement. @@ -19,6 +21,7 @@ Artboard Options forwards the active Artboard tool's Scale Artwork with Artboard | Date | Change | Summary | |---|---|---| +| 2026-11-03 | minor | New gradients use CMYK default stops in CMYK documents, including Gradient tool creation. Scores and estimates unchanged. | | 2026-10-30 | minor | M4: G9: `.ai`/EPS editing data: a blend takes the transparency (opacity, blend mode, knockout) in its state block after its record, so its steps knock each other out as the page draws them; a user's file comes in 0.6% from its page instead of 1.3%. Scores and estimates unchanged. | | 2026-10-30 | minor | M4: File formats: `.ai`/EPS editing-data fidelity from an audit of a user's files: All Caps and Small Caps (#4708); placed files at their box's corner, kept where they draw closer to the page (#4664, #4723, #4742); pages cropped to the art, with `UserUnit` (#4658, #4727); gradient mesh corners with handle weights of 0 and the state written after a mesh (#4738, #4744, #4748); type on a path spaced as the file lays it out (#4743); synthetic regressions in `crates/eps`; scores and estimates unchanged. | | 2026-10-30 | minor | Raster effects: bounded length fields in effect dialogs clamp on commit and reject invalid inputs visibly (#4638); estimates unchanged. | diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 9f54f0f89..7513ebe09 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1,9 +1,11 @@ # Parity with Adobe Illustrator -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-03 · **Change:** minor (Type Tools add-on: Text Tweaker, #4924; font-family fields accept the mouse wheel, #5253; Units › East Asian Type acts, with Q and Ha units; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-03 · **Change:** minor (new gradients follow document color mode; 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) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026) ## Feature submission evidence +Creating a gradient from solid or empty paint uses the document's color model for its default stops, including drawing its vector with the Gradient tool. Explicit `keepModel: true` retains RGB defaults, and existing gradients keep their own colors. Scores and estimates are unchanged. + **Feature evidence last updated:** 2026-11-03. The shared assessment status line and revision history are current upstream; this dated section records the submission's feature evidence and revisions without rescoring upstream or claiming a new combined build or runtime measurement. @@ -19,6 +21,7 @@ Artboard Options forwards the active Artboard tool's Scale Artwork with Artboard | Date | Change | Summary | |---|---|---| +| 2026-11-03 | minor | New gradients use CMYK default stops in CMYK documents, including Gradient tool creation. Scores and estimates unchanged. | | 2026-11-03 | minor | Type Tools add-on: Text Tweaker (a VectorCraft addition, #4924): evidence added; scores unchanged. | | 2026-11-02 | minor | Legacy `.ai` saves (version 7) write type with rotated characters as outlines that keep each character's rotation, with a warning unless `outlineText` is on (#5186); scores unchanged. | | 2026-10-31 | minor | Object › Path › Smooth smooths a path along its length before refitting it, so Auto-Smooth takes a shaky line to one smooth curve instead of leaving its jags, and with anchor points direct-selected it smooths only that section; scores unchanged. | diff --git a/docs/ui-parity.md b/docs/ui-parity.md index ae4eb5626..a0b422584 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -1,9 +1,11 @@ # UI and interaction parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-03 · **Change:** minor (font-family fields accept the mouse wheel, #5253; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; Units › East Asian Type acts, with Q and Ha units; scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-03 · **Change:** minor (new gradients follow document color mode; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; Units › East Asian Type acts, with Q and Ha units; scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x) ## Feature submission evidence +Creating a gradient from solid or empty paint uses the document's color model for its default stops, including drawing its vector with the Gradient tool. Explicit `keepModel: true` retains RGB defaults, and existing gradients keep their own colors. Scores and estimates are unchanged. + **Feature evidence last updated:** 2026-11-03. The shared assessment status line and revision history are current upstream; this dated section records the submission's feature evidence and revisions without rescoring upstream or claiming a new combined build or runtime measurement. @@ -19,6 +21,7 @@ Artboard Options forwards the active Artboard tool's Scale Artwork with Artboard | Date | Change | Summary | |---|---|---| +| 2026-11-03 | minor | New gradients use CMYK default stops in CMYK documents, including Gradient tool creation. Scores and estimates unchanged. | | 2026-10-26 | minor | Pencil Fidelity goes down to 0.01 pt (dialog, tool and engine; Smooth keeps 0.05) on a logarithmic slider across the Tool Options row, with hundredths in the field and key steps that fit the range (#3328); scores and estimates unchanged (estimated) | | 2026-10-22 | minor | Reconcile upstream documentation after review F-3; preserve every dated revision and the original scaleArt runtime evidence without repeating completed jobs. | | 2026-10-22 | minor | Reconcile upstream documentation after review F-2; preserve every dated revision and the original scaleArt runtime evidence without repeating completed jobs. |