diff --git a/crates/engine/src/tests/swatches.rs b/crates/engine/src/tests/swatches.rs index 8abaec348c..e59e9a90b1 100644 --- a/crates/engine/src/tests/swatches.rs +++ b/crates/engine/src/tests/swatches.rs @@ -1099,13 +1099,18 @@ fn fill_and_stroke(s: &Session, id: NodeId) -> (Paint, Paint) { /// in the batch, and one drawn after it, the same fill and stroke. Returns the batch's rectangle's /// fill and stroke. fn batch_matches_steps(steps: &[(&str, Value)]) -> (Paint, Paint) { + batch_matches_steps_from(brand_defaults_session, steps) +} + +/// [`batch_matches_steps`] with both sessions made by `setup`. +fn batch_matches_steps_from(setup: impl Fn() -> Session, steps: &[(&str, Value)]) -> (Paint, Paint) { let shape = ("shape.rectangle", json!({"x": 0, "y": 0, "width": 10, "height": 10})); - let mut one_by_one = brand_defaults_session(); + let mut one_by_one = setup(); for (id, p) in steps { run(&mut one_by_one, id, p.clone()); } let single = rect(&mut one_by_one); - let mut batched = brand_defaults_session(); + let mut batched = setup(); let commands: Vec = steps.iter().chain([&shape]).map(|(id, p)| json!({"command": id, "params": p})).collect(); let r = run(&mut batched, "command.batch", json!({ "commands": commands })); let in_batch = NodeId(r["results"][steps.len()]["id"].as_u64().unwrap()); @@ -1133,6 +1138,43 @@ fn swatch_steps_in_a_batch_change_the_new_art_paints_as_commands_do() { assert_eq!(batch_matches_steps(&[("swatch.editGroup", json!({"group": "G", "colors": ["#ababab"]}))]), (gray.clone(), gray)); } +#[test] +fn spot_colors_options_in_a_batch_change_the_new_art_paints_as_the_command_does() { + let _g = crate::tests_colormgmt::GLOBAL.lock().unwrap_or_else(|e| e.into_inner()); + let lab = Color::Lab { l: 55.0, a: 60.0, b: 40.0 }; + let cms = vectorcraft_color::cms::active(); + let cmyk = cms.convert(&lab, vectorcraft_color::cms::Model::Cmyk, cms.settings().intent); + assert!(matches!(cmyk, Color::Cmyk { .. })); + // The default fill and stroke for new art are linked to 40 % of the Lab spot swatch "Lab Ink". + let lab_ink_defaults = |use_lab: bool| { + let mut s = session(); + run(&mut s, "swatch.new", json!({"name": "Lab Ink", "color": {"l": 55, "a": 60, "b": 40}, "spot": true})); + run(&mut s, "swatch.spotOptions", json!({ "useLab": use_lab })); + run(&mut s, "select.set", json!({"ids": []})); + run(&mut s, "paint.setFill", json!({"swatch": "Lab Ink", "tint": 40})); + run(&mut s, "paint.setStroke", json!({"swatch": "Lab Ink", "tint": 40})); + s + }; + let ink = |color: Color| Paint::Solid { color: color.tinted(0.4), swatch: Some("Lab Ink".into()), tint: 0.4 }; + assert_eq!(lab_ink_defaults(true).paint.fill, ink(lab)); + let to_cmyk = [("swatch.spotOptions", json!({"useLab": false}))]; + assert_eq!(batch_matches_steps_from(|| lab_ink_defaults(true), &to_cmyk), (ink(cmyk), ink(cmyk))); + assert_eq!(lab_ink_defaults(false).paint.fill, ink(cmyk)); + let to_lab = [("swatch.spotOptions", json!({"useLab": true}))]; + assert_eq!(batch_matches_steps_from(|| lab_ink_defaults(false), &to_lab), (ink(lab), ink(lab))); +} + +#[test] +fn default_swatches_replace_in_a_batch_unlinks_the_new_art_paints_as_the_command_does() { + // Brand and Other are not default swatches, so replacing the swatches drops the links to them. + let blue = Paint::solid(Color::from_hex("#2a6fb0").unwrap()); + let replace = [("swatch.resetDefaults", json!({"replace": true}))]; + assert_eq!(batch_matches_steps(&replace), (blue.clone(), blue)); + // Without replace the swatches stay, and so do the links. + let link = linked("#2a6fb0", "Brand"); + assert_eq!(batch_matches_steps(&[("swatch.resetDefaults", json!({}))]), (link.clone(), link)); +} + #[test] fn undo_and_redo_of_a_swatch_edit_put_back_the_new_art_paints() { let edit = json!({"name": "Brand", "newName": "Signal", "color": "#00ff00"}); diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 7e36c7f318..df7e2a15c2 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1492,7 +1492,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - **CSS swatch libraries:** fractional linear and radial gradient stop positions are preserved, so nearby stops such as 12.5% and 12.890625% remain distinct instead of collapsing to 13%. Reference: [CSS Images 3 color stops](https://www.w3.org/TR/css-images-3/#color-stop-syntax). The original synthetic regression is `palette_io::tests::css_preserves_fractional_gradient_stop_positions` in `crates/color/src/palette_io.rs`. Scores and hours are unchanged. - **Swatches:** names follow the colour model and are unique; Swatch Options (process/spot, Global, Gray/RGB/HSB/CMYK/Web, live preview); editing a global or spot swatch recolours every linked fill, stroke and text run in one undo step; multi-select and delete with confirmation; New Swatch and New Color Group dialogs (a group from the selected artwork's colours); swatches in colour groups convert, separate and count like any other. The Swatches panel selects ranges and whole groups, has a find field, and drags swatches to reorder, regroup or paint art; Add Used Colors, Select All Unused, Merge, Ungroup and Sort by Kind; a CMYK document keeps applied colours in CMYK. Swatch libraries: nine generated colour libraries, seven gradient libraries (among them Organic Fades, soft perceptually mixed multi-stop fades that thin out to transparent, and Holographic, seven opaque linear foils of narrow pastel and pearl bands and seven radial glows that fade out to a clear edge) and three pattern libraries (Geometric Patterns; Grit Textures with eight seamless grain, speckle, stipple, scratch and fleck tiles; and Optical Effects with eight of VectorCraft's own seamless wave, ring, warped-check, stripe, chevron and lens tiles; their swatches bring their patterns) in a library panel, saved as `.vcswatches`, `.gpl`, swatch exchange (`.ase`) or CSS, and loaded from `.vcswatches`, `.gpl` and `.ase` files (`.ase` both ways: RGB, CMYK, Lab and Gray; global, spot and process; groups) and from color books (`.acb`, read only: the RGB, CMYK or Lab colors of a book such as a Pantone book a user owns, named with its prefix and suffix, spot unless it says process, #831); `swatch.library.copyToUser` copies a loaded library into the User Defined folder without replacing a file, and a file with the same extension and bytes as a User Defined library opens as that library. Tints of global and spot colours ("Name 40%") stay linked and separate as a percentage of their plate. Spot colours can be defined in Lab (Spot Colors options). A built-in [Registration] swatch prints on every plate. Editing, renaming, deleting or merging a global swatch also changes the colors linked to it in graph designs, the art of brushes and the tile that pattern editing's Cancel puts back, in the same undo step as the rest of the art; Default Swatches with replace unlinks them there too. - A swatch step in `command.batch` (Delete, Swatch Options, Merge, Edit Color Group) changes the fill and stroke for new art as the command run on its own does, so art drawn later in the batch or after it takes the same paint; a batch that fails, and a canceled batch preview, put back the paints it started with, and a canceled Swatch Options preview leaves them as they were. These swatch commands also change the fills and strokes of the appearance new art takes, and Undo and Redo of the command or the batch put the paints for new art back with the swatches. Evidence: `swatch_steps_in_a_batch_change_the_new_art_paints_as_commands_do`, `undo_and_redo_of_a_swatch_edit_put_back_the_new_art_paints`, `swatch_edits_reach_the_paints_of_the_new_art_template`, `a_failed_batch_puts_back_the_new_art_paints_a_swatch_step_changed` and `canceling_a_swatch_options_preview_keeps_the_new_art_paints` in `crates/engine/src/tests/swatches.rs`. + A swatch step in `command.batch` (Delete, Swatch Options, Merge, Edit Color Group) changes the fill and stroke for new art as the command run on its own does, so art drawn later in the batch or after it takes the same paint; a batch that fails, and a canceled batch preview, put back the paints it started with, and a canceled Swatch Options preview leaves them as they were. These swatch commands also change the fills and strokes of the appearance new art takes, and Undo and Redo of the command or the batch put the paints for new art back with the swatches. Spot Colors options (`swatch.spotOptions`) and Default Swatches with replace (`swatch.resetDefaults {replace: true}`) as `command.batch` steps also change the fill and stroke for new art as each command run on its own does: a fill or stroke linked to a Lab spot swatch keeps its link and tint and takes the Lab values or their CMYK equivalent, and one linked to a swatch the replace removes keeps its color and loses the link. Evidence: `swatch_steps_in_a_batch_change_the_new_art_paints_as_commands_do`, `spot_colors_options_in_a_batch_change_the_new_art_paints_as_the_command_does`, `default_swatches_replace_in_a_batch_unlinks_the_new_art_paints_as_the_command_does`, `undo_and_redo_of_a_swatch_edit_put_back_the_new_art_paints`, `swatch_edits_reach_the_paints_of_the_new_art_template`, `a_failed_batch_puts_back_the_new_art_paints_a_swatch_step_changed` and `canceling_a_swatch_options_preview_keeps_the_new_art_paints` in `crates/engine/src/tests/swatches.rs`. Add Used Colors (`swatch.addUsedColors`) and Select All Unused (`swatch.unused`) look colors, swatch names, links, tints and gradients up in hash sets instead of searching every swatch and color for each color or linked paint (New Color Group from artwork too), and New Swatch with several colors (`swatch.new`), New Color Group (`swatch.newGroup`) and Edit Color Group (`swatch.editGroup`) name their new swatches from one set of the names already taken. The new swatches keep their names, order and links, and Select All Unused lists the same swatches. Evidence: `add_used_colors_adds_each_new_color_once_with_free_names`, `add_used_colors_global_links_every_paint_of_a_new_color`, `unused_matches_links_tints_colors_gradients_and_patterns_exactly`, `new_swatches_from_colors_are_numbered_after_names_already_taken`, `new_color_group_numbers_its_colors_after_the_group_and_its_moved_swatches`, `new_color_group_from_artwork_lists_each_color_once_with_free_names` and `edit_color_group_numbers_extra_colors_after_the_names_it_keeps` in `crates/engine/src/tests/swatchcmds.rs`; `free_swatch_names_match_naming_each_swatch_as_it_is_added` and `free_swatch_names_match_new_swatch_names_as_swatches_are_added` in `crates/doc/src/swatches.rs`. - **Colorized images (#1466):** a fill color applied to a selected grayscale or 1-bit image (`paint.setFill`) colorizes it with black, white, a process color or a spot color, and refuses a process color when the image has an alpha channel ([Convert color modes](https://web.archive.org/web/20251220223850/https://helpx.adobe.com/illustrator/desktop/manage-colors/select-and-adjust-colors/convert-color-modes.html), "Colorize Grayscale or 1-bit images"); black and white are accepted there too. The canvas draws each pixel in the color as far as the pixel is dark, with white pixels transparent; PDF export writes the image in the color (a spot color as its ink, Registration in every ink) with the image as its soft mask; Separations Preview and printed separations show it on its color's plates; native files keep the color. SVG writes the fill color through a coverage mask (or a colorized PNG in Tiny 1.2); EPS writes binary coverage as an image mask in the color's ink, and a grayscale image as ink samples scaled by coverage, with white pixels left out at Level 3 and gray midtones written over white. The prompt for deleting several swatches and groups uses the interface language's plural form for the count. Evidence: `deleting_several_swatches_uses_each_count_form` in `crates/ui-egui/src/panels/swatches/tests.rs`.