diff --git a/crates/engine/src/cmd/addons/document.rs b/crates/engine/src/cmd/addons/document.rs index 654138a1dd..68681b50fa 100644 --- a/crates/engine/src/cmd/addons/document.rs +++ b/crates/engine/src/cmd/addons/document.rs @@ -132,8 +132,10 @@ fn export_all_open(s: &mut Session, p: &Value) -> Result { let active = s.active; for i in 0..s.docs.len() { // Each document in turn is the one Save a Copy writes; switching tabs this way leaves every - // document's tool and interaction alone, and the active one comes back below. + // document's tool and interaction alone, and the active one comes back below. Its colors + // are written through its own profiles. s.active = Some(i); + s.sync_front_profiles(); let Ok(st) = s.doc() else { continue }; let title = st.doc.title.clone(); let stem = fileio::file_stem(st.path.as_deref().unwrap_or(&title)); @@ -160,6 +162,7 @@ fn export_all_open(s: &mut Session, p: &Value) -> Result { } } s.active = active; + s.sync_front_profiles(); Ok(json!({ "files": files, "failed": failed, "format": format.id })) } diff --git a/crates/engine/src/cmd/colormgmt.rs b/crates/engine/src/cmd/colormgmt.rs index 16990fc2ea..6c75eae128 100644 --- a/crates/engine/src/cmd/colormgmt.rs +++ b/crates/engine/src/cmd/colormgmt.rs @@ -5,9 +5,11 @@ //! Colour settings and the proof view are process-wide (`vectorcraft_color::cms::active`, //! `vectorcraft_render::proof::view`). A document's assigned profiles are stored in //! `Document::color_profiles` (files before format v3: `Document.unknown["colorProfiles"]`) and -//! take the place of the working spaces in the active settings when assigned, and when the working -//! settings change while the document is in front ([`use_front_profiles`]); the working spaces stay -//! as Edit → Color Settings set them. +//! take the place of the working spaces in the active settings while the document is in front +//! ([`use_front_profiles`]): when they are assigned, when the working settings change, and when the +//! document comes to the front or its assignment comes back (an open, a switch, a close, a revert, +//! an undo or a rollback, [`Session::sync_front_profiles`]); the working spaces stay as Edit → Color +//! Settings set them. use std::collections::BTreeMap; use std::sync::Arc; @@ -181,7 +183,7 @@ fn touch_all(s: &mut Session) { /// Make the active settings the working settings with the profiles the front document is assigned /// in place of the working spaces (none, or no document open: the working spaces). The open /// documents redraw when the active settings change. -fn use_front_profiles(s: &mut Session) { +pub(crate) fn use_front_profiles(s: &mut Session) { let (rgb, cmyk) = s.active().map(|d| doc_profiles(&d.doc)).unwrap_or_default(); if cms::use_document_profiles(rgb.as_deref(), cmyk.as_deref()) { touch_all(s); diff --git a/crates/engine/src/cmd/fileio/batch.rs b/crates/engine/src/cmd/fileio/batch.rs index 18bc78b5d9..5bda9c06e2 100644 --- a/crates/engine/src/cmd/fileio/batch.rs +++ b/crates/engine/src/cmd/fileio/batch.rs @@ -41,6 +41,7 @@ pub(super) fn batch(s: &mut Session, p: &Value) -> Result { roll_back(s, start, &stash); s.notify_text_tool("batch.rollBack"); s.active = active; + s.sync_front_profiles(); (s.paint, s.fill_active, s.draw_mode, s.draw_inside, s.clipboard, s.untitled_counter, s.explicit_undo_group) = saved; s.pending_paint = None; return Err(e); diff --git a/crates/engine/src/cmd/links.rs b/crates/engine/src/cmd/links.rs index e3e72861b9..7df71e7422 100644 --- a/crates/engine/src/cmd/links.rs +++ b/crates/engine/src/cmd/links.rs @@ -1331,14 +1331,17 @@ pub(crate) fn refresh_placed(s: &mut Session, saved: u64, path: &str) { if targets.is_empty() { return; } - // Each document is updated as the active one, without switching tools. + // Each document is updated as the active one, through its own profiles, without switching + // tools. let back = s.active; for i in targets { s.active = Some(i); + s.sync_front_profiles(); // A file that can't be read leaves what its objects show: links.check reports it. let _ = update_where(s, None, Some(&path)); } s.active = back; + s.sync_front_profiles(); } // ---------- relative paths ---------- diff --git a/crates/engine/src/cmd/plugin_call.rs b/crates/engine/src/cmd/plugin_call.rs index f4b91edd9b..71fbcc8540 100644 --- a/crates/engine/src/cmd/plugin_call.rs +++ b/crates/engine/src/cmd/plugin_call.rs @@ -413,6 +413,7 @@ pub(crate) fn transaction(s: &mut Session, label: &str, f: impl FnOnce(&mut Sess st.interaction = None; } saved.restore(s); + s.sync_front_profiles(); Err(e) } } diff --git a/crates/engine/src/script/mod.rs b/crates/engine/src/script/mod.rs index 53f6e18f75..6a43b001d6 100644 --- a/crates/engine/src/script/mod.rs +++ b/crates/engine/src/script/mod.rs @@ -672,6 +672,7 @@ impl Savepoint { s.pending_paint = self.pending_paint; self.choices.put(s); s.journal.truncate(self.journal); + s.sync_front_profiles(); exact } } @@ -2199,6 +2200,9 @@ fn failure_text(report: &Value) -> String { fn run_slice(s: &mut Session, job: &mut Job) -> std::result::Result, String> { let depth = std::mem::replace(&mut s.depth, 0); job.world.swap(s); + // The run's front document draws and converts through its own profiles, and the interface's + // again after the slice. + s.sync_front_profiles(); s.script_in_run = true; let cell = job.host.session.clone(); std::mem::swap(s, &mut *cell.borrow_mut()); @@ -2221,6 +2225,7 @@ fn run_slice(s: &mut Session, job: &mut Job) -> std::result::Result self.refresh_document_text_bounds(index, |t| t.cached_bounds.is_none() && !sets_in_any(t, &names)), - None => self.sync_opened_composite_fonts(index), + None => { + self.sync_opened_composite_fonts(index); + self.sync_front_profiles(); + } } true } @@ -403,6 +417,7 @@ impl Session { } self.active = if self.docs.is_empty() { None } else { Some(index.min(self.docs.len() - 1)) }; self.sync_front_composite_fonts(); + self.sync_front_profiles(); true } diff --git a/crates/engine/src/session/edit.rs b/crates/engine/src/session/edit.rs index 3d0d9ba1c6..4d0df82644 100644 --- a/crates/engine/src/session/edit.rs +++ b/crates/engine/src/session/edit.rs @@ -311,6 +311,8 @@ impl Session { self.paint = paint.clone(); } self.pending_paint = None; + // The previous preview's Assign Profile is undone with it. + self.sync_front_profiles(); let rw = cmd::distortcmds::perspective_rewrite(self, cmd, params).or_else(|| cmd::bounds_rewrite(self, cmd, params)); let (cmd, params) = rw.as_ref().map_or((cmd, params), |(c, p)| (c.as_str(), p)); // A selected object's converted appearance must not replace the current paints while @@ -436,6 +438,8 @@ impl Session { st.undo_interaction(); // Guides locked and slices locked or hidden during the interaction stay deselected. deselect_hidden(&menu, &st.doc, &mut st.selection); + // A canceled preview's Assign Profile is undone with it. + self.sync_front_profiles(); // With New Art Has Basic Appearance off, new art takes the appearance of the selection as // the document has it again, not as a preview left it. self.inherit_new_art(); diff --git a/crates/engine/src/session/execute.rs b/crates/engine/src/session/execute.rs index 23079fe901..7509c7740c 100644 --- a/crates/engine/src/session/execute.rs +++ b/crates/engine/src/session/execute.rs @@ -168,6 +168,10 @@ impl Session { let (doc, journal, grouped) = (self.active().map(|d| d.uid), self.journal.len(), self.undo_group_docs()); let open = (self.depth == 0).then(|| self.docs.iter().map(|d| d.uid).collect::>()); let r = if self.depth == 0 { self.run_guarded(id, run) } else { self.run_nested(run) }; + // Colors are drawn and converted through the profiles of the document in front now, also + // after an undone or redone Assign Profile, a failed or panicked command's documents put + // back, and a batch step, before the next step converts through them. + self.sync_front_profiles(); if r.is_err() && self.depth == 0 { self.batch_paint_docs.clear(); // A failed or panicked command's documents are put back: type is drawn in their diff --git a/crates/engine/src/session/undo.rs b/crates/engine/src/session/undo.rs index d06934926c..3af4cafe5f 100644 --- a/crates/engine/src/session/undo.rs +++ b/crates/engine/src/session/undo.rs @@ -164,6 +164,10 @@ impl Session { if self.depth == 0 { self.trim_histories(); } + // A canceled group's Assign Profile is undone with it. + if cancel { + self.sync_front_profiles(); + } } /// When journal entry `n` is a `command.batch` with `steps` noted in canceled undo groups and @@ -280,5 +284,7 @@ impl Session { g.journal.retain(|&(n, _)| n < scope.journal); g.steps.retain(|&(n, _)| n < scope.journal); } + // A canceled scope's Assign Profile is undone with it. + self.sync_front_profiles(); } } diff --git a/crates/engine/tests/color_settings.rs b/crates/engine/tests/color_settings.rs index 6c7e90a188..fe56377d4e 100644 --- a/crates/engine/tests/color_settings.rs +++ b/crates/engine/tests/color_settings.rs @@ -180,6 +180,106 @@ fn a_batch_converts_through_the_profile_it_assigns() { assert_eq!(converted, fill(&device, b)); } +/// The profiles a document is assigned are in use while it is in front: a new or opened document +/// assigned none draws through the working spaces, switching back to the assigned document puts its +/// profile in use again, and closing a document puts the profiles of the document left in front, or +/// with none open the working spaces, in use. +#[test] +fn assigned_profiles_follow_the_front_document() { + let _g = settings(); + let dir = fresh_dir("front-profiles"); + let _reset = ColorSettingsReset { profile: None, paths: vec![dir.clone()] }; + std::fs::create_dir_all(&dir).unwrap(); + let tagged = dir.join("tagged.vectorcraft"); + let mut s = session(); + let working = cms::working_settings(); + assert_ne!(working.cmyk, cms::DEVICE_CMYK); + exec(&mut s, "edit.assignProfile", json!({"cmyk": cms::DEVICE_CMYK})); + exec(&mut s, "file.saveAs", json!({"path": tagged.to_str().unwrap()})); + exec(&mut s, "file.new", json!({"width": 100, "height": 100})); + assert_eq!(cms::active_settings(), working, "a new document draws through the working spaces"); + exec(&mut s, "document.activate", json!({"index": 0})); + assert_eq!(cms::active_settings().cmyk, cms::DEVICE_CMYK, "back in front, the document draws through its profile"); + assert!(s.set_active(1)); + assert_eq!(cms::active_settings(), working, "a tab switch to the untagged document"); + exec(&mut s, "file.close", json!({})); + assert_eq!(cms::active_settings().cmyk, cms::DEVICE_CMYK, "the document left in front draws through its profile"); + exec(&mut s, "file.close", json!({})); + assert_eq!(cms::active_settings(), working, "no document open: the working spaces"); + exec(&mut s, "document.open", json!({"path": tagged.to_str().unwrap()})); + assert_eq!(cms::active_settings().cmyk, cms::DEVICE_CMYK, "an opened document draws through its saved profile"); +} + +/// Undo and Redo of Assign Profile, and File › Revert to a file assigned no profile, put the +/// profiles the document has then in use. +#[test] +fn undo_redo_and_revert_put_the_document_profiles_in_use() { + let _g = settings(); + let dir = fresh_dir("undo-profiles"); + let _reset = ColorSettingsReset { profile: None, paths: vec![dir.clone()] }; + std::fs::create_dir_all(&dir).unwrap(); + let untagged = dir.join("untagged.vectorcraft"); + let mut s = session(); + let working = cms::working_settings(); + exec(&mut s, "file.saveAs", json!({"path": untagged.to_str().unwrap()})); + exec(&mut s, "edit.assignProfile", json!({"cmyk": cms::DEVICE_CMYK})); + assert_eq!(cms::active_settings().cmyk, cms::DEVICE_CMYK); + exec(&mut s, "edit.undo", json!({})); + assert_eq!(doc_profiles(&s.doc().unwrap().doc), (None, None)); + assert_eq!(cms::active_settings(), working, "undone: the working spaces"); + exec(&mut s, "edit.redo", json!({})); + assert_eq!(doc_profiles(&s.doc().unwrap().doc), (None, Some(cms::DEVICE_CMYK.to_string()))); + assert_eq!(cms::active_settings().cmyk, cms::DEVICE_CMYK, "redone: the assigned profile"); + exec(&mut s, "file.revert", json!({})); + assert_eq!(doc_profiles(&s.doc().unwrap().doc), (None, None)); + assert_eq!(cms::active_settings(), working, "reverted: the working spaces"); +} + +/// A batch that assigns a profile and then fails puts the document back without it, and the +/// working spaces back in use. +#[test] +fn a_failed_batch_puts_the_profiles_in_use_back() { + let _g = settings(); + let _reset = ColorSettingsReset { profile: None, paths: vec![] }; + let mut s = session(); + let working = cms::working_settings(); + let steps = json!([ + {"command": "edit.assignProfile", "params": {"cmyk": cms::DEVICE_CMYK}}, + {"command": "document.activate", "params": {"index": 99}}, + ]); + assert!(s.execute("command.batch", &json!({"commands": steps})).is_err()); + assert_eq!(doc_profiles(&s.doc().unwrap().doc), (None, None)); + assert_eq!(cms::active_settings(), working); +} + +/// A batch step that switches documents makes the next step convert through the profiles of the +/// document it switched to, not those of the document the batch began in. +#[test] +fn a_batch_converts_the_document_it_switches_to_through_its_profiles() { + let _g = settings(); + let _reset = ColorSettingsReset { profile: None, paths: vec![] }; + let blue = |s: &mut Session| { + let id = rect(s, 10.0, 10.0, 50.0, 50.0); + exec(s, "paint.setFill", json!({"ids": [id.0], "color": "#3366cc"})); + id + }; + let fill = |s: &Session, id| s.doc().unwrap().doc.node(id).unwrap().appearance.fill_paint().color(); + let mut working = session(); + let w = blue(&mut working); + exec(&mut working, "file.documentColorMode", json!({"mode": "cmyk"})); + let mut s = session(); + let b = blue(&mut s); + exec(&mut s, "file.new", json!({"width": 100, "height": 100})); + exec(&mut s, "edit.assignProfile", json!({"cmyk": cms::DEVICE_CMYK})); + let steps = json!([ + {"command": "document.activate", "params": {"index": 0}}, + {"command": "file.documentColorMode", "params": {"mode": "cmyk"}}, + ]); + exec(&mut s, "command.batch", json!({"commands": steps})); + assert_eq!(s.active_index(), Some(0)); + assert_eq!(fill(&s, b), fill(&working, w), "converted through the working CMYK space"); +} + /// View › Proof Setup › Working CMYK simulates the working CMYK space of Color Settings in a /// document assigned another CMYK profile, in Proof Colors and in Separations Preview, and a change /// of the working CMYK space shows in the next proof while the document keeps its profile. diff --git a/docs/mcp.md b/docs/mcp.md index 41e28969f4..c4f5a9a575 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -1434,8 +1434,10 @@ preferences (the folder the desktop app uses, so both see the same ones; `VECTOR start, after the profiles above are registered (#6335). Saved settings that name a profile that is no longer there, or a damaged file, leave the defaults (logged), and the next change writes over the file. `edit.assignProfile` tags the document only: colors show and convert through the profiles it assigns, and the - working spaces and the file stay as they are. The `workingCmyk` target of `view.proofSetup` simulates the working - CMYK space in such a document too. + working spaces and the file stay as they are. The profiles in use are those of the document in front, after + `file.new`, `document.open`, `document.activate`, `file.close`, `file.revert`, `edit.undo`, `edit.redo` and a + failed `command.batch` too; with none assigned, or no document open, the working spaces are. The `workingCmyk` + target of `view.proofSetup` simulates the working CMYK space in such a document too. Not remembered: the document, anything else. Nothing is read or written when `VECTORCRAFT_NO_PREFS` is set, and a server confined with `--automation-read-root` or diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index d9783b0216..11f0b4fa1c 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -703,7 +703,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - **Document Setup modal handoff:** Document Setup → Edit Artboards applies valid settings, closes the dialog and activates the Artboard tool. Pointer-click tests cover successful handoff, invalid settings, OK, Cancel and Undo in `dialogs/document_setup.rs`. - **Document Setup › Color:** a Color tab shows the document color mode (RGB Color or CMYK Color), a Convert to CMYK or Convert to RGB box for a mode change (cleared: the colors keep their numbers), and the RGB and CMYK profiles assigned to the document, with a working-space entry for none. OK runs `edit.assignProfile` and then `file.documentColorMode` with the dialog's other changes in one `command.batch`, and the document's changes are one undo step. An unchanged Color tab adds no color commands, and Cancel, or OK with nothing changed, records no undo step. A Color tab field with a value of the wrong kind is an error, and the dialog stays open. The tab is VectorCraft's own layout; File › Document Color Mode and Edit › Assign Profile… stay as they are. Tests in `dialogs/document_setup.rs`. -- **Assign Profile and the working spaces:** Edit › Assign Profile… tags the document and leaves the working spaces of Edit › Color Settings… as they are, in Color Settings, the Separations panel and the settings saved for the next start. Colors show and convert through the profiles assigned; a change in Color Settings keeps the profiles the document in front is assigned in use, and removing an assigned profile in use puts the working space back in its place. View › Proof Setup › Working CMYK simulates the working CMYK space of Color Settings in Proof Colors and Separations Preview, also in a document assigned another CMYK profile. Public references: [Changing the color profile for a document and About color working spaces](https://help.adobe.com/archive/en_US/illustrator/cs3/illustrator_cs3_help.pdf#page=150) and [Soft-proof presets](https://help.adobe.com/archive/en_US/illustrator/cs3/illustrator_cs3_help.pdf#page=134). Tests in `engine/tests/color_settings.rs`. +- **Assign Profile and the working spaces:** Edit › Assign Profile… tags the document and leaves the working spaces of Edit › Color Settings… as they are, in Color Settings, the Separations panel and the settings saved for the next start. Colors show and convert through the profiles assigned; a change in Color Settings keeps the profiles the document in front is assigned in use, and removing an assigned profile in use puts the working space back in its place. The profiles in use follow the document in front: a new, opened or reverted document, a switch to another tab, a close, Undo and Redo of Assign Profile, a failed batch, each step of a batch, Export All Open and the update of placed documents put that document's profiles in use, and the working spaces where it is assigned none or no document is open. View › Proof Setup › Working CMYK simulates the working CMYK space of Color Settings in Proof Colors and Separations Preview, also in a document assigned another CMYK profile. Public references: [Changing the color profile for a document and About color working spaces](https://help.adobe.com/archive/en_US/illustrator/cs3/illustrator_cs3_help.pdf#page=150) and [Soft-proof presets](https://help.adobe.com/archive/en_US/illustrator/cs3/illustrator_cs3_help.pdf#page=134). Tests in `engine/tests/color_settings.rs`. - **New Document's More Settings and Save Preset:** More Settings opens a pop-up over New Document with only Number of Artboards, the artboard layout, Change to Right-to-Left Layout, Spacing and Columns; Done, Escape or a click outside closes it and keeps the values. Save Preset turns the top of Preset Details into a Save Document Preset heading with the preset's name, Cancel and Save, and hides the document's name, Close and Create until the preset is saved or canceled; nothing in that mode creates a document. Save Preset's default name is the first "Preset N" no saved preset has. VectorCraft's own layout. Harness tests in `dialogs/new_document.rs` cover both. - **New Document profiles:** each built-in category's presets start documents from that category's profile, a native document holding the starting swatches, graphic styles, brushes, symbols and other assets, the ruler and Document Setup options and the print settings ([Create a custom new document profile](https://helpx.adobe.com/illustrator/desktop/add-and-import-files/start-a-new-file/create-a-custom-new-document-profile.html)). New Document's Manage Profiles pop-up opens a profile to edit and resets one to the shipped profile (`file.newProfiles`, `file.newProfiles.edit`, `file.newProfiles.reset`; `file.new {profile}`). The profiles live in VectorCraft's own `New Document Profiles` folder next to the preferences; a profile that cannot be read or is larger than 64 MiB leaves the defaults and is reported. Tests: `tests::newdoc::an_edited_profile_starts_the_documents_of_its_category`, `the_profile_follows_the_preset_access_and_reset`, `a_damaged_or_oversized_profile_leaves_the_defaults_and_is_reported`, `profile_names_are_checked_and_editing_needs_the_folder`, `dialogs::new_document::tests::manage_profiles_edits_and_resets_the_profiles`. - **Control bar with the Artboard tool:** while the Artboard tool is in use, the Control bar shows the active artboard's controls in place of the object controls: Preset, Portrait and Landscape, New Artboard, Delete Artboard, the Name field, Artboard Options, Rearrange All Artboards, the Transform link, Move and Scale Artwork with Artboard and, while the bar has room, the reference point with X and Y, and W and H with the Constrain Width and Height Proportions link. Each runs the command its Properties or Transform panel counterpart runs (`artboard.setProps`, `artboard.new`, `artboard.delete`, `tool.options {tool: "artboard"}`), and the reference point is the one the Transform panel uses. In an 800-point window the controls up to the Transform link fit, Move and Scale Artwork with Artboard show as their boxes alone with their names in tooltips, and the link opens the Transform panel with the artboard's position and size. Pointer and key tests in `panels/artboards.rs` cover the controls, W about the reference point, the orientation and the name. Public reference: [Using multiple artboards](https://web.archive.org/web/20210925094818/https://helpx.adobe.com/illustrator/using/using-multiple-artboards.html) (archived copy).