mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-11 02:19:56 -06:00
Paint: use the document color model for new gradient defaults
This commit is contained in:
+3
-1
@@ -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
|
||||
|
||||
@@ -312,10 +312,31 @@ pub(crate) fn apply_gradient_edit_in(
|
||||
bounds: Option<Rect>,
|
||||
inside: &dyn Fn(Point) -> bool,
|
||||
) -> std::result::Result<Paint, String> {
|
||||
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<vectorcraft_color::cms::Model>) -> 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<Rect>,
|
||||
inside: &dyn Fn(Point) -> bool,
|
||||
model: Option<vectorcraft_color::cms::Model>,
|
||||
) -> std::result::Result<Paint, String> {
|
||||
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<Value> {
|
||||
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<Value> {
|
||||
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<Value> {
|
||||
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<Rect>) -> Option<Paint> {
|
||||
fn vector_paint(cur: &Paint, e: &GeomEdit, to_doc: Affine, bounds: Option<Rect>, model: Option<vectorcraft_color::cms::Model>) -> Option<Paint> {
|
||||
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<Value> {
|
||||
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<Value> {
|
||||
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(())
|
||||
|
||||
@@ -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<vectorcraft_color::cms::Model> {
|
||||
pub(super) fn new_color_model(s: &Session, p: &Value) -> Option<vectorcraft_color::cms::Model> {
|
||||
let mode = s.active()?.doc.color_mode;
|
||||
(mode != vectorcraft_doc::ColorMode::Rgb && !bool_or(p, "keepModel", false)).then(|| mode.model())
|
||||
}
|
||||
|
||||
@@ -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) };
|
||||
|
||||
+4
-1
@@ -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. |
|
||||
|
||||
+4
-1
@@ -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. |
|
||||
|
||||
@@ -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. |
|
||||
|
||||
+4
-1
@@ -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. |
|
||||
|
||||
Reference in New Issue
Block a user