diff --git a/crates/engine/src/cmd/fileio/save.rs b/crates/engine/src/cmd/fileio/save.rs index 166eda8b9..170b55f88 100644 --- a/crates/engine/src/cmd/fileio/save.rs +++ b/crates/engine/src/cmd/fileio/save.rs @@ -385,22 +385,27 @@ fn separate_boards(cmd: &str, f: &Format, doc: &Document, p: &Value) -> Result Document { let mut d = doc.clone(); let Some(a) = doc.artboards.get(i) else { return d }; - let r = a.rect; d.artboards = vec![a.clone()]; - // It opens fitted to its artboard, not at the master file's view. + keep_art_on(&mut d, a.rect); + d +} + +/// Reduce `d` to the objects touching `r`, in their layers (resources are kept), opening +/// fitted to its artboard rather than at the master file's view. +pub(super) fn keep_art_on(d: &mut Document, r: vectorcraft_geom::Rect) { d.last_view = None; d.layers_open = None; for layer in &mut d.layers { keep_on(layer, r); } - d } -/// Keep in layer `n` (and its sublayers) only the objects touching `r`. +/// Keep in layer `n` (and its sublayers) only the objects touching `r`, and those without bounds. fn keep_on(n: &mut Arc, r: vectorcraft_geom::Rect) { let touches = |b: vectorcraft_geom::Rect| b.x0 <= r.x1 && b.x1 >= r.x0 && b.y0 <= r.y1 && b.y1 >= r.y0; if let Some(children) = Arc::make_mut(n).children_mut() { - children.retain(|c| c.is_layer() || c.visual_bounds().is_some_and(touches)); + // An object whose bounds can't be measured (type in a missing font) stays: dropping it loses art. + children.retain(|c| c.is_layer() || c.visual_bounds().is_none_or(touches)); for c in children.iter_mut().filter(|c| c.is_layer()) { keep_on(c, r); } diff --git a/crates/engine/src/cmd/fileio/screens.rs b/crates/engine/src/cmd/fileio/screens.rs index 9c33d7e24..1d9c0303c 100644 --- a/crates/engine/src/cmd/fileio/screens.rs +++ b/crates/engine/src/cmd/fileio/screens.rs @@ -784,8 +784,21 @@ fn encode_and_write(export: ScreenExport, writer: &mut dyn FnMut(&str, &[u8]) -> write(all, "pdf", &options)? } else if r.whole_document() && item.artboard.is_some() { let b = item.artboard.unwrap_or(0); - let d = board_docs.entry(b).or_insert_with(|| Arc::new(single_artboard(&item.doc, item.region, &item.name))).clone(); - write(&d, r.format.id, &r.options_for(item.region, None)?)? + // That artboard alone, as Save's separateArtboards writes it: its art, not the others' (#3868). + let d = board_docs + .entry(b) + .or_insert_with(|| { + let mut d = single_artboard(&item.doc, item.region, &item.name); + super::save::keep_art_on(&mut d, item.region); + Arc::new(d) + }) + .clone(); + let mut options = r.options_for(item.region, None)?; + // An EPS page is the artboard, not its art's bounds: an artboard without art still writes. + if r.format.id == "eps" { + options["useArtboards"] = json!(true); + } + write(&d, r.format.id, &options)? } else { write(&item.doc, r.format.id, &r.options_for(item.region, item.artboard)?)? }; diff --git a/crates/engine/src/cmd/fileio/tests_batch.rs b/crates/engine/src/cmd/fileio/tests_batch.rs index 5535d8ed6..b889eb2d4 100644 --- a/crates/engine/src/cmd/fileio/tests_batch.rs +++ b/crates/engine/src/cmd/fileio/tests_batch.rs @@ -134,17 +134,25 @@ fn layers_and_the_selection_are_exported_by_name() { assert!(s.execute("document.exportForScreens", &json!({"selection": "one"})).is_err(), "nothing selected"); } -/// Document formats write each artboard as a document of its own: one artboard, its art. +/// Document formats write each artboard as a document of its own: one artboard, its art and +/// not the other artboards' (#3868). #[test] fn document_formats_write_one_artboard_each() { let mut s = session(2); + let second = s.doc().unwrap().doc.artboards[1].rect; + s.execute("shape.ellipse", &json!({"x": second.x0 + 5.0, "y": 5, "width": 10, "height": 10})).unwrap(); let r = export(&mut s, json!({"formats": [{"format": "vectorcraft"}, {"format": "ai"}, {"format": "eps"}]})); let n = names(&r); assert_eq!(n, ["Artboard-1.vectorcraft", "Artboard-1.ai", "Artboard-1.eps", "Artboard-2.vectorcraft", "Artboard-2.ai", "Artboard-2.eps"]); - let native = &files(&r)[0].1; - let mut t = Session::new(); - t.execute("document.open", &json!({"name": "a.vectorcraft", "dataBase64": vectorcraft_format::base64_encode(native)})).unwrap(); - assert_eq!(t.doc().unwrap().doc.artboards.len(), 1, "that artboard alone"); + let all = files(&r); + for (name, bytes) in &all { + let mut t = Session::new(); + t.execute("document.open", &json!({"name": name, "dataBase64": vectorcraft_format::base64_encode(bytes)})).unwrap(); + let d = &t.doc().unwrap().doc; + assert_eq!(d.artboards.len(), 1, "{name}: that artboard alone"); + let objects: usize = d.layers.iter().filter_map(|l| l.children()).map(|c| c.len()).sum(); + assert_eq!(objects, 1, "{name}: only its own artboard's art"); + } } #[test]