From d45ebe465bfc074350c49337956e7aa6088e8ab4 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Thu, 5 Nov 2026 09:48:16 -0800 Subject: [PATCH] .ai save: the editing copy names the active artboard The editing copy of a .ai or .ait file that VectorCraft saves or exports now has `%_ /Int (CropAreaActive) ,` after the ArtboardArray in its recorded part, and marks that artboard as the selected one. The app records its view's active artboard before each command (DocState::active_artboard; an index past the last artboard is the last, as in the Artboards panel); without a view, the artboard the document opened on (Document::opened_artboard) is written, else the first. A .ai file read from its editing copy opens on that artboard. Before, the copy named no active artboard and always marked the first as selected. Docs: docs/mcp.md, docs/file-format-parity.md, the editingData option text. --- crates/doc/src/lib.rs | 4 +- crates/engine/src/cmd/fileio/export.rs | 6 +- crates/engine/src/cmd/fileio/native.rs | 2 +- crates/engine/src/cmd/fileio/save.rs | 16 +++++- .../engine/src/cmd/fileio/tests_aiimport.rs | 55 +++++++++++++++++++ crates/engine/src/doc_state.rs | 6 ++ crates/eps/src/ai_editing.rs | 11 ++-- crates/eps/src/ai_editing/tests.rs | 29 +++++++++- crates/ui-egui/src/app/commands.rs | 3 + crates/ui-egui/src/io.rs | 45 ++++++++++++++- crates/ui-egui/src/io/save.rs | 15 ++++- docs/file-format-parity.md | 4 +- docs/mcp.md | 5 ++ 13 files changed, 185 insertions(+), 16 deletions(-) diff --git a/crates/doc/src/lib.rs b/crates/doc/src/lib.rs index 988df4c51..6d069872d 100644 --- a/crates/doc/src/lib.rs +++ b/crates/doc/src/lib.rs @@ -326,8 +326,8 @@ pub struct Document { #[serde(skip)] pub retired_artboard_id: RetiredArtboardId, /// The id of the artboard an imported file names as its active one (a `.ai` file's - /// `CropAreaActive`): the document opens with that artboard active. `None`: the first. Never - /// saved. + /// `CropAreaActive`): the document opens with that artboard active. A `.ai` save or export + /// names it as the file's active artboard. `None`: the first. Native files do not keep it. #[serde(skip)] pub opened_artboard: Option, /// Some object is a dimension (the Dimension tool): edits refresh dimensions only while this diff --git a/crates/engine/src/cmd/fileio/export.rs b/crates/engine/src/cmd/fileio/export.rs index 852f2c735..77adaeac9 100644 --- a/crates/engine/src/cmd/fileio/export.rs +++ b/crates/engine/src/cmd/fileio/export.rs @@ -190,7 +190,11 @@ fn source<'a>(s: &'a mut Session, f: &Format, p: &Value, cmd: &str) -> Result<(C return Ok((Cow::Owned(doc), display_doc_title)); } let st = s.doc()?; - let doc = export_source(st, p)?; + let mut doc = export_source(st, p)?; + // A `.ai` export names the view's active artboard, as a `.ai` save does. + if let Some(id) = super::save::moved_active_artboard(st, &doc, f) { + doc.to_mut().opened_artboard = Some(id); + } let display_doc_title = super::has_own_title(&doc); Ok((super::titled_for_export(&doc, st.path.as_deref(), f.id).map_or(doc, Cow::Owned), display_doc_title)) } diff --git a/crates/engine/src/cmd/fileio/native.rs b/crates/engine/src/cmd/fileio/native.rs index 4d293fe69..770c11644 100644 --- a/crates/engine/src/cmd/fileio/native.rs +++ b/crates/engine/src/cmd/fileio/native.rs @@ -109,7 +109,7 @@ pub const AI_OPTIONS: &[FormatOption] = &[ name: "editingData", ty: "boolean", default: "true", - description: "also carry an editing copy of the artwork in the form other apps that open .ai files read: layers and sublayers, groups, compound paths, paths, fills and strokes, colors, linear and radial gradients, patterns on a grid of tiles, swatches, artboards, the transparency of objects and layers, objects' opacity masks, clipping groups and layers clipped by a path, embedded images with their pixels, and linked images as placed files with a preview, with type, other patterns, symbols, other clipping objects and live objects as plain filled and stroked paths, freeform gradients, gradients along or across a stroke and gradients of more than 256 stops as their average color, gradients that dither or mix perceptually as classic ones, and gradient stops linked to global process colors as their colors (written only with every artboard, as Preserve Editing is)", + description: "also carry an editing copy of the artwork in the form other apps that open .ai files read: layers and sublayers, groups, compound paths, paths, fills and strokes, colors, linear and radial gradients, patterns on a grid of tiles, swatches, artboards and which one is active, the transparency of objects and layers, objects' opacity masks, clipping groups and layers clipped by a path, embedded images with their pixels, and linked images as placed files with a preview, with type, other patterns, symbols, other clipping objects and live objects as plain filled and stroked paths, freeform gradients, gradients along or across a stroke and gradients of more than 256 stops as their average color, gradients that dither or mix perceptually as classic ones, and gradient stops linked to global process colors as their colors (written only with every artboard, as Preserve Editing is)", }, FormatOption { name: "aiVersion", diff --git a/crates/engine/src/cmd/fileio/save.rs b/crates/engine/src/cmd/fileio/save.rs index b594c51b8..55fcbaa08 100644 --- a/crates/engine/src/cmd/fileio/save.rs +++ b/crates/engine/src/cmd/fileio/save.rs @@ -447,22 +447,34 @@ fn is_svg(f: &Format) -> bool { } /// The document as written: native files carry the view to reopen at and the Layers panel's open -/// rows and transparency grid visibility. +/// rows and transparency grid visibility; a `.ai` file names the view's active artboard +/// ([`DocState::active_artboard`]) as its active one. fn doc_to_save(st: &DocState, f: &Format) -> Arc { if !is_native(f) { return st.doc.clone(); } let open = st.layers_open.saved(&st.doc); - if st.doc.last_view == st.view && st.doc.layers_open == open && st.doc.transparency_grid == st.transparency_grid { + let active = moved_active_artboard(st, &st.doc, f); + if st.doc.last_view == st.view && st.doc.layers_open == open && st.doc.transparency_grid == st.transparency_grid && active.is_none() { return st.doc.clone(); } let mut d = (*st.doc).clone(); + if active.is_some() { + d.opened_artboard = active; + } d.last_view = st.view.clone(); d.transparency_grid = st.transparency_grid; d.layers_open = open; Arc::new(d) } +/// The artboard a `.ai` or `.ait` file written from `st` (`doc`, the document it writes) names as +/// its active one, when that is not already the one `doc` opened on: the view's active artboard +/// ([`DocState::active_artboard`]), if `doc` has it. +pub(super) fn moved_active_artboard(st: &DocState, doc: &Document, f: &Format) -> Option { + st.active_artboard.filter(|id| matches!(f.id, "ai" | "ait") && doc.artboards.iter().any(|a| a.id == *id) && doc.opened_artboard != Some(*id)) +} + /// What a format loses against a native file (reported whenever a save writes it). fn fidelity_warning(f: &Format) -> Option { (!is_native(f)).then(|| { diff --git a/crates/engine/src/cmd/fileio/tests_aiimport.rs b/crates/engine/src/cmd/fileio/tests_aiimport.rs index 0aa1733d8..e9e24e5a1 100644 --- a/crates/engine/src/cmd/fileio/tests_aiimport.rs +++ b/crates/engine/src/cmd/fileio/tests_aiimport.rs @@ -148,6 +148,61 @@ fn an_ai_file_opens_with_its_active_artboard() { assert_eq!(d.opened_artboard, Some(d.artboards[1].id), "{r}"); } +/// A `.ai` save names the view's active artboard in its editing copy, else the one the document +/// opened on, else the first; the file read from that copy opens on it. +#[test] +fn an_ai_save_names_its_active_artboard() { + let mut s = Session::new(); + s.execute("file.new", &json!({"width": 60, "height": 40, "artboards": 3})).unwrap(); + s.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 20, "height": 20})).unwrap(); + let ids: Vec = s.doc().unwrap().doc.artboards.iter().map(|a| a.id).collect(); + let opts = LoadOptions { native: false, ..Default::default() }; + let active = |s: &mut Session, format: &str| { + let cmd = if format == "ait" { "file.saveAsTemplate" } else { "file.saveCopy" }; + let saved = s.execute(cmd, &json!({"format": format})).unwrap(); + let bytes = vectorcraft_format::base64_decode(saved["dataBase64"].as_str().unwrap()).unwrap(); + let l = load::load_with(&format!("a.{format}"), &bytes, &opts).unwrap(); + assert!(l.editing_data && !l.restored, "{:?}", l.warnings); + assert_eq!(l.doc.artboards.len(), 3); + l.doc.opened_artboard.and_then(|id| l.doc.artboards.iter().position(|a| a.id == id)).unwrap_or(0) + }; + assert_eq!(active(&mut s, "ai"), 0); + // The view's. + s.doc_mut().unwrap().active_artboard = Some(ids[2]); + assert_eq!((active(&mut s, "ai"), active(&mut s, "ait")), (2, 2)); + // The one the document opened on, when the view names none or an artboard it doesn't have. + Arc::make_mut(&mut s.doc_mut().unwrap().doc).opened_artboard = Some(ids[1]); + for view in [None, Some(99)] { + s.doc_mut().unwrap().active_artboard = view; + assert_eq!(active(&mut s, "ai"), 1, "{view:?}"); + } + // The view's first artboard over the one the document opened on. + s.doc_mut().unwrap().active_artboard = Some(ids[0]); + assert_eq!(active(&mut s, "ai"), 0); + assert_eq!(s.doc().unwrap().doc.opened_artboard, Some(ids[1]), "the document is unchanged"); +} + +/// A `.ai` export (`document.serialize`, `document.export`, with or without `selectedOnly`) names +/// the view's active artboard in its editing copy, as a save does. +#[test] +fn an_ai_export_names_its_active_artboard() { + let mut s = Session::new(); + s.execute("file.new", &json!({"width": 60, "height": 40, "artboards": 3})).unwrap(); + s.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 20, "height": 20})).unwrap(); + let ids: Vec = s.doc().unwrap().doc.artboards.iter().map(|a| a.id).collect(); + s.doc_mut().unwrap().active_artboard = Some(ids[2]); + let opts = LoadOptions { native: false, ..Default::default() }; + for (cmd, selected) in [("document.serialize", false), ("document.serialize", true), ("document.export", false)] { + let out = s.execute(cmd, &json!({"format": "ai", "selectedOnly": selected})).unwrap(); + let bytes = vectorcraft_format::base64_decode(out["dataBase64"].as_str().unwrap()).unwrap(); + let l = load::load_with("a.ai", &bytes, &opts).unwrap(); + assert!(l.editing_data && !l.restored, "{cmd}: {:?}", l.warnings); + let active = l.doc.opened_artboard.and_then(|id| l.doc.artboards.iter().position(|a| a.id == id)); + assert_eq!(active, Some(2), "{cmd} {selected}"); + } + assert_eq!(s.doc().unwrap().doc.opened_artboard, None, "the document is unchanged"); +} + #[test] fn an_ai_unread_group_keeps_its_layers_and_live_neighbor() { let live = format!("0 0 1 0 k\n{}f", ai::rect(10.0, 10.0, 14.0, 14.0)); diff --git a/crates/engine/src/doc_state.rs b/crates/engine/src/doc_state.rs index 3d1c4be89..5c3a9130d 100644 --- a/crates/engine/src/doc_state.rs +++ b/crates/engine/src/doc_state.rs @@ -76,6 +76,11 @@ pub struct DocState { /// The view saved into native files (`Document::last_view`); the UI keeps it current before a /// save and restores it when the document opens. pub view: Option, + /// The id of the view's active artboard, which the UI keeps current before each command it + /// runs: a `.ai` save or export names that artboard as the file's active one. `None`, or an + /// artboard the document no longer has: the artboard the document opened on + /// ([`vectorcraft_doc::Document::opened_artboard`]), else the first. + pub active_artboard: Option, /// Restored by Data Recovery: the title says "[Recovered]" and Save asks where to save it /// (suggesting `path`, the file it was copied from) instead of overwriting that file. pub recovered: bool, @@ -266,6 +271,7 @@ impl DocState { save_artboards: vec![], converted: false, view, + active_artboard: None, recovered: false, recovery: None, print_tiling: false, diff --git a/crates/eps/src/ai_editing.rs b/crates/eps/src/ai_editing.rs index 26293842c..b6ea72eea 100644 --- a/crates/eps/src/ai_editing.rs +++ b/crates/eps/src/ai_editing.rs @@ -431,10 +431,12 @@ fn ruler_units(u: Unit) -> u8 { /// bottom-right corners in art space (`PositionPoint1`, `PositionPoint2`), its top-left corner on /// the canvas (`RulerOrigin`, `origin` being art space's origin there), its pixel aspect ratio /// (`PAR`), `DisplayMark` 0 (as in the files users sent), whether it is the selected one (the -/// first) and its name; then the canvas's size. Every record holds all of these entries. The -/// artboards and the canvas size are written in the `/Recorded` part of the `/Document` -/// dictionary, followed by an empty `/NotRecorded` part. +/// active one) and its name; then the index of the active artboard (`CropAreaActive`: the one +/// [`Document::opened_artboard`] names, else the first) and the canvas's size. Every record holds +/// all of these entries. The artboards, the active artboard and the canvas size are written in the +/// `/Recorded` part of the `/Document` dictionary, followed by an empty `/NotRecorded` part. fn artboards(s: &mut String, doc: &Document, to_art: Affine, origin: (f64, f64)) { + let active = doc.opened_artboard.and_then(|id| doc.artboards.iter().position(|a| a.id == id)).unwrap_or(0); s.push_str("%AI9_BeginDocumentData\n%_/Document :\n%_/Dictionary :\n%_/Array :\n"); for (i, ab) in doc.artboards.iter().enumerate() { let r = to_art.transform_rect_bbox(ab.rect); @@ -449,10 +451,11 @@ fn artboards(s: &mut String, doc: &Document, to_art: Affine, origin: (f64, f64)) ps::push_num(s, if par.is_finite() && par > 0.0 { par } else { 1.0 }); // Display marks: 1 the center mark, 2 cross hairs, 4 video safe areas. let mark = u8::from(ab.show_center_mark) | u8::from(ab.show_cross_hairs) << 1 | u8::from(ab.show_video_safe_areas) << 2; - let _ = writeln!(s, " /Real (PAR) ,\n%_{mark} /Int (DisplayMark) ,\n%_{} /Bool (IsArtboardSelected) ,", u8::from(i == 0)); + let _ = writeln!(s, " /Real (PAR) ,\n%_{mark} /Int (DisplayMark) ,\n%_{} /Bool (IsArtboardSelected) ,", u8::from(i == active)); let _ = writeln!(s, "%_{} /UnicodeString (Name) ,\n%_; ,", ps::bytes(ab.name.as_bytes())); } s.push_str("%_; (ArtboardArray) ,\n"); + let _ = writeln!(s, "%_{active} /Int (CropAreaActive) ,"); let _ = writeln!(s, "%_{CANVAS_SIZE} /Int (AIDocumentCanvasSize) ,"); s.push_str("%_; /Recorded ,\n%_/Dictionary : /NotRecorded ,\n%_; /NotRecorded ,\n%_;\n%AI9_EndDocumentData\n"); } diff --git a/crates/eps/src/ai_editing/tests.rs b/crates/eps/src/ai_editing/tests.rs index 529c54a5f..c3262ac98 100644 --- a/crates/eps/src/ai_editing/tests.rs +++ b/crates/eps/src/ai_editing/tests.rs @@ -367,9 +367,9 @@ fn add_rect(doc: &mut Document, r: Rect) { doc.insert(Some(layer), usize::MAX, rect).unwrap(); } -/// The end of the document data: the artboards and the canvas size in the `/Recorded` part of the -/// `/Document` dictionary, then an empty `/NotRecorded` part. -const RECORDED: &str = "%_; (ArtboardArray) ,\n%_16383 /Int (AIDocumentCanvasSize) ,\n%_; /Recorded ,\n%_/Dictionary : /NotRecorded ,\n%_; /NotRecorded ,\n%_;\n%AI9_EndDocumentData\n"; +/// The end of the document data: the artboards, the active artboard (the first) and the canvas +/// size in the `/Recorded` part of the `/Document` dictionary, then an empty `/NotRecorded` part. +const RECORDED: &str = "%_; (ArtboardArray) ,\n%_0 /Int (CropAreaActive) ,\n%_16383 /Int (AIDocumentCanvasSize) ,\n%_; /Recorded ,\n%_/Dictionary : /NotRecorded ,\n%_; /NotRecorded ,\n%_;\n%AI9_EndDocumentData\n"; /// One letter-size artboard: the art 36 points in from its top-left corner, the artboard's record /// and the boxes, with the artboards in the `/Recorded` part of the `/Document` dictionary. @@ -984,6 +984,29 @@ fn artboard_display_marks_are_written() { assert!(ab.show_center_mark && !ab.show_cross_hairs && ab.show_video_safe_areas); } +/// The active artboard, the one `opened_artboard` names (else the first), is the selected one and +/// its index follows the artboards in the recorded part; the editing data reads back with that +/// artboard active. +#[test] +fn the_active_artboard_follows_the_artboards() { + let mut doc = Document::new(200.0, 100.0); + for (id, x0) in [(2, 250.0), (3, 500.0)] { + doc.artboards.push(Artboard { id, rect: Rect::new(x0, 0.0, x0 + 200.0, 100.0), ..doc.artboards[0].clone() }); + } + let first = doc.artboards[0].id; + // None, the first, the third, and an artboard the document doesn't have (the first). + for (opened, index) in [(None, 0), (Some(first), 0), (Some(3), 2), (Some(9), 0)] { + doc.opened_artboard = opened; + let p = program(&doc); + let entry = format!("%_; (ArtboardArray) ,\n%_{index} /Int (CropAreaActive) ,\n%_16383 /Int (AIDocumentCanvasSize) ,\n%_; /Recorded ,\n"); + assert!(p.contains(&entry), "{opened:?}: {p}"); + assert_eq!(p.matches("%_1 /Bool (IsArtboardSelected) ,\n").count(), 1, "{opened:?}"); + let back = read_back(&doc); + let active = back.opened_artboard.and_then(|id| back.artboards.iter().position(|a| a.id == id)).unwrap_or(0); + assert_eq!((back.artboards.len(), active), (3, index), "{opened:?}"); + } +} + /// A guide is its path followed by `(n) *` (closed) or `(N) *` (open), with no `(N) *` before /// it, and reads back as the same guide. #[test] diff --git a/crates/ui-egui/src/app/commands.rs b/crates/ui-egui/src/app/commands.rs index 0652e6d9a..a21411587 100644 --- a/crates/ui-egui/src/app/commands.rs +++ b/crates/ui-egui/src/app/commands.rs @@ -173,6 +173,9 @@ impl VectorcraftApp { return r; } } + // A `.ai` file a command writes (a save, an export, a batch's save) names the view's + // active artboard. + io::remember_active_artboard(self); // Native files carry the view they reopen at. if fileio::SaveMode::of(id).is_some() { io::remember_view(self); diff --git a/crates/ui-egui/src/io.rs b/crates/ui-egui/src/io.rs index 4818a0c15..d394d8e6c 100644 --- a/crates/ui-egui/src/io.rs +++ b/crates/ui-egui/src/io.rs @@ -28,7 +28,7 @@ pub use open::{ }; #[cfg(test)] pub(crate) use open::{open_file, opened_with_notes}; -pub use save::{ask_format_options, ask_pdf_options, remember_view, save, save_pdf}; +pub use save::{ask_format_options, ask_pdf_options, remember_active_artboard, remember_view, save, save_pdf}; pub(crate) use save::{export_path, is_web, report_saved, target_path, write_named, write_reporting, write_to}; #[cfg(test)] pub(crate) use save::{report_write_error, saved_status}; @@ -164,6 +164,49 @@ mod tests { assert!(!app.session.doc().unwrap().is_dirty()); } + /// A `.ai` file saved from the app names the view's active artboard as its active one, in + /// the editing copy other apps read. + #[test] + fn a_saved_ai_file_names_the_views_active_artboard() { + let (mut app, written) = app(); + app.run("file.new", json!({"width": 60, "height": 40, "artboards": 3})).unwrap(); + app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 20, "height": 20})).unwrap(); + app.sync_views(); + let opts = fileio::LoadOptions { native: false, ..Default::default() }; + for artboard in [2, 0] { + if let Some(v) = app.view_mut() { + v.artboard = artboard; + } + save(&mut app, SaveMode::Copy, &json!({"path": "Art.ai"}), false).unwrap(); + let bytes = written.borrow_mut().pop().map(|(_, b)| b).unwrap(); + let l = fileio::load_with("Art.ai", &bytes, &opts).unwrap(); + assert!(l.editing_data && !l.restored, "{:?}", l.warnings); + let active = l.doc.opened_artboard.and_then(|id| l.doc.artboards.iter().position(|a| a.id == id)).unwrap_or(0); + assert_eq!(active, artboard); + } + } + + /// A `.ai` file a command exports names the view's active artboard as its active one: an + /// index past the last artboard names the last, as the Artboards panel shows it. + #[test] + fn an_exported_ai_file_names_the_views_active_artboard() { + let (mut app, _) = app(); + app.run("file.new", json!({"width": 60, "height": 40, "artboards": 3})).unwrap(); + app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 20, "height": 20})).unwrap(); + app.sync_views(); + let opts = fileio::LoadOptions { native: false, ..Default::default() }; + for (artboard, active) in [(1, 1), (99, 2)] { + if let Some(v) = app.view_mut() { + v.artboard = artboard; + } + let out = app.run("document.serialize", json!({"format": "ai"})).unwrap(); + let bytes = vectorcraft_format::base64_decode(out["dataBase64"].as_str().unwrap()).unwrap(); + let l = fileio::load_with("Art.ai", &bytes, &opts).unwrap(); + assert!(l.editing_data && !l.restored, "{:?}", l.warnings); + assert_eq!(l.doc.opened_artboard.and_then(|id| l.doc.artboards.iter().position(|a| a.id == id)), Some(active), "{artboard}"); + } + } + fn bytes_of(app: &mut VectorcraftApp, cmd: &str, p: Value) -> Vec { let v = app.session.execute(cmd, &p).unwrap(); vectorcraft_format::base64_decode(v["dataBase64"].as_str().unwrap()).unwrap() diff --git a/crates/ui-egui/src/io/save.rs b/crates/ui-egui/src/io/save.rs index 3392e7e43..3d81aff54 100644 --- a/crates/ui-egui/src/io/save.rs +++ b/crates/ui-egui/src/io/save.rs @@ -171,14 +171,27 @@ pub(super) fn target_path_keeping( pick_path(app, &FilePick { name, folder, filters }, keeps) } -/// Keep the active document's saved view current before a save (native files reopen at it). +/// Keep the active document's saved view current before a save (native files reopen at it), and +/// its active artboard ([`remember_active_artboard`]). pub fn remember_view(app: &mut VectorcraftApp) { + remember_active_artboard(app); let Some(v) = app.view().copied().filter(|v| v.fitted) else { return }; if let Some(st) = app.session.active_mut() { st.view = Some(SavedView { name: String::new(), center: v.center, zoom: v.zoom, rotation: v.rotation }); } } +/// Keep the active document's active artboard current (a `.ai` file saved or exported names it +/// as its active one): the view's, an index past the last artboard being the last one as in the +/// Artboards panel; `None` without a view. +pub fn remember_active_artboard(app: &mut VectorcraftApp) { + let index = app.view().map(|v| v.artboard); + if let Some(st) = app.session.active_mut() { + let last = st.doc.artboards.len().saturating_sub(1); + st.active_artboard = index.and_then(|i| st.doc.artboards.get(i.min(last))).map(|a| a.id); + } +} + /// A path typed in a save panel: kept when its extension names one of the panel's file types (a /// save format, or a template format for Save as Template, `mode`). An extension that names /// another format (`card.png`, or `card.ait` in Save As) is replaced with `f`'s, and `f`'s is diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index 971dfa016..22c13bdbb 100644 --- a/docs/file-format-parity.md +++ b/docs/file-format-parity.md @@ -62,7 +62,9 @@ private editing data. VectorCraft: Editing Data for Other Apps) every page's `PieceInfo` also carries an editing copy of the art in `AIPrivateData` streams, in the form VectorCraft's `.ai` reader reads: layers and sublayers, groups, paths with their fills and strokes (a compound path as one path of all its subpaths), process, spot and global colors, swatches and color groups, guides, - hidden and locked objects, object names, every artboard with its name, the transparency of objects and layers + hidden and locked objects, object names, every artboard with its name, the active artboard (`CropAreaActive`: the + view's active artboard when the app saves or exports the file, else the one the document opened on, else the first; + the reader opens the file on it), the transparency of objects and layers (opacity, blending mode, isolation, knockout and knockout shape), objects' opacity masks, and clipping groups and clipping layers clipped by a path or compound path, linear and radial gradients on fills and strokes (with their stops' opacity, midpoints and spot colors, elliptical radials and focal points) and gradient swatches, and diff --git a/docs/mcp.md b/docs/mcp.md index ee359ccdf..0b418bbc7 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -4684,6 +4684,11 @@ or in `options` (`file.formatOptions {format}` lists them with their values): - `pdfCompatible`: native files (default false) also carry a PDF of every artboard (`pdf` in the file); `.ai` files (default true) with false write blank pages around the native document (smaller; other apps show empty pages). - `.ai` only: `compress` (default true) is the PDF option `compression.compressText`. +- The editing copy of a `.ai` or `.ait` file (`editingData`) names its active artboard (`CropAreaActive`): the active + artboard of the app's view when the app saves or exports the file, otherwise the artboard the document opened on (the + active artboard of the `.ai` file it was opened from), else the first. A `.ai` file read from its editing copy + opens on that artboard. VectorCraft opens a `.ai` file it saved from the native document the file carries, on the + first artboard. File → Save As asks for these in the save options dialog (VectorCraft Options) after the save panel, and Save a Copy does too for a `.ai` file; Save, Save a Copy to a native file and Save as Template reuse the options as last saved (or Use Compression).