From 951f689e8f14be259b61eb78c48cfeef40e243e8 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 14 Nov 2026 09:59:19 -0800 Subject: [PATCH] Export: an HP-GL plot of the art's bounds measures images by the art their effects add An image or a gradient mesh with a visible Crop Marks effect, or with a Vector Halftone effect that makes dots, is measured in a plot of the art's bounds. A halftone that makes no dots, such as one past the effect's cell limit, leaves the image out as before. --- crates/cad/src/hpgl/write.rs | 13 ++++++----- crates/cad/src/hpgl/write/tests.rs | 26 ++++++++++++++++++---- crates/engine/src/cmd/fileio/encode.rs | 4 ++-- crates/engine/src/cmd/fileio/tests_hpgl.rs | 23 +++++++++++++++++++ docs/mcp.md | 1 + 5 files changed, 56 insertions(+), 11 deletions(-) diff --git a/crates/cad/src/hpgl/write.rs b/crates/cad/src/hpgl/write.rs index 7d753e1801..95c8ecfb7a 100644 --- a/crates/cad/src/hpgl/write.rs +++ b/crates/cad/src/hpgl/write.rs @@ -1027,15 +1027,18 @@ fn overlaps(a: Rect, b: Rect) -> bool { /// Whether a plot draws nothing of `n` wherever it is, apart from what every export leaves out /// (hidden objects, guides and template layers): an object at 0% opacity, a non-printing layer, a /// placed image, a gradient mesh whose every point is at 0% opacity, or a path or compound path -/// without effects whose fills and strokes paint nothing. A clipping path still clips when the -/// plot draws nothing of it. -pub fn plots_nothing(n: &Node) -> bool { +/// without effects whose fills and strokes paint nothing. An image or a mesh in `doc` with a +/// visible Crop Marks effect, or with a Vector Halftone effect that makes dots, is not one of them. +/// A clipping path still clips when the plot draws nothing of it. +pub fn plots_nothing(doc: &Document, n: &Node) -> bool { let unpainted = || n.appearance.effects.is_empty() && top_stroke(n).is_none() && top_fill(n).is_none(); + let dots = || vectorcraft_effects::addons::halftone::halftone_art(doc, n).is_some_and(|g| g.children().is_some_and(|c| !c.is_empty())); + let adds_art = || vectorcraft_effects::has_crop_marks(n) || dots(); n.opacity <= 0.0 || match &n.kind { NodeKind::Layer { printable, .. } => !printable, - NodeKind::Image(_) => true, - NodeKind::Mesh(m) => m.points.iter().all(|p| p.opacity <= 0.0), + NodeKind::Image(_) => !adds_art(), + NodeKind::Mesh(m) => !adds_art() && m.points.iter().all(|p| p.opacity <= 0.0), NodeKind::Path { .. } | NodeKind::Compound { .. } => unpainted(), _ => false, } diff --git a/crates/cad/src/hpgl/write/tests.rs b/crates/cad/src/hpgl/write/tests.rs index c600a60eab..60f7cf5b3d 100644 --- a/crates/cad/src/hpgl/write/tests.rs +++ b/crates/cad/src/hpgl/write/tests.rs @@ -183,7 +183,7 @@ fn meshes_in_blends_are_outlines_and_clear_meshes_draw_nothing() { #[test] fn plots_nothing_picks_what_a_plot_draws_nothing_of() { type Make = fn(&mut Document) -> Node; - let cases: [(&str, Make, bool); 6] = [ + let cases: [(&str, Make, bool); 7] = [ ("stroked", |d| stroked(d, rect(10.0, 10.0, 40.0, 40.0), Color::BLACK), false), ("unpainted", |d| Node::path(d.alloc_id(), rect(10.0, 10.0, 40.0, 40.0), Appearance::basic(Paint::None, Paint::None, 1.0)), true), ( @@ -211,6 +211,24 @@ fn plots_nothing_picks_what_a_plot_draws_nothing_of() { }, true, ), + ( + "image with Crop Marks", + |d| { + let image = vectorcraft_doc::ImageObject { + key: "raster-1".into(), + width: 4, + height: 2, + xf: Affine::translate((10.0, 20.0)) * Affine::scale(5.0), + link: None, + placement: Default::default(), + pixels: None, + }; + let mut n = Node::new(d.alloc_id(), NodeKind::Image(Box::new(image))); + n.appearance.effects.push(vectorcraft_doc::Effect { id: "cropMarks".into(), params: serde_json::json!({}), visible: true }); + n + }, + false, + ), ("mesh", |d| Node::new(d.alloc_id(), NodeKind::Mesh(mesh_at(10.0))), false), ( "clear mesh", @@ -230,16 +248,16 @@ fn plots_nothing_picks_what_a_plot_draws_nothing_of() { vec![n] }); let Some(n) = held else { panic!("{name}") }; - assert_eq!(plots_nothing(&n), nothing, "{name}"); + assert_eq!(plots_nothing(&d, &n), nothing, "{name}"); assert_eq!(text(&plot(&d, &opts(&d))) == "IN;PU;SP0;", nothing, "{name}"); } let mut d = doc_with(|d| vec![stroked(d, rect(10.0, 10.0, 40.0, 40.0), Color::BLACK)]); let layer = d.layers[0].id; - assert!(!plots_nothing(&d.layers[0])); + assert!(!plots_nothing(&d, &d.layers[0])); if let Some(NodeKind::Layer { printable, .. }) = d.node_mut(layer).map(|n| &mut n.kind) { *printable = false; } - assert!(plots_nothing(&d.layers[0])); + assert!(plots_nothing(&d, &d.layers[0])); assert_eq!(text(&plot(&d, &opts(&d))), "IN;PU;SP0;"); } diff --git a/crates/engine/src/cmd/fileio/encode.rs b/crates/engine/src/cmd/fileio/encode.rs index 3461aca056..0bb18483cb 100644 --- a/crates/engine/src/cmd/fileio/encode.rs +++ b/crates/engine/src/cmd/fileio/encode.rs @@ -444,7 +444,7 @@ pub(super) fn art_page(cmd: &str, doc: &Document, p: &Value, left_out: impl Fn(& /// when there is none, which such an export refuses. pub fn export_art_bounds(doc: &Document, format: &str) -> Option { let doc = &*doc.without_edit_modes(); - bounds_without(doc, &|n| format == "hpgl" && vectorcraft_cad::hpgl::write::plots_nothing(n)) + bounds_without(doc, &|n| format == "hpgl" && vectorcraft_cad::hpgl::write::plots_nothing(doc, n)) } /// The bounds of the visible art of `doc` with the objects that `left_out` picks hidden @@ -770,7 +770,7 @@ fn encode_files(doc: &Document, f: &Format, p: &Value) -> Result { { return Ok(enc); } - let left_out = |n: &Node| f.id == "hpgl" && vectorcraft_cad::hpgl::write::plots_nothing(n); + let left_out = |n: &Node| f.id == "hpgl" && vectorcraft_cad::hpgl::write::plots_nothing(doc, n); if use_artboards == Some(false) && let Some((art, q)) = art_page(C, doc, p, left_out)? { diff --git a/crates/engine/src/cmd/fileio/tests_hpgl.rs b/crates/engine/src/cmd/fileio/tests_hpgl.rs index 5b7e3f0185..a7fe16690e 100644 --- a/crates/engine/src/cmd/fileio/tests_hpgl.rs +++ b/crates/engine/src/cmd/fileio/tests_hpgl.rs @@ -254,6 +254,29 @@ fn a_plot_of_the_arts_bounds_leaves_out_images_in_symbol_art() { } } +/// A placed image with a Vector Halftone effect is measured when the effect makes dots, and left +/// out when it would take more cells than the effect makes. +#[test] +fn a_plot_of_the_arts_bounds_measures_images_by_their_halftone_dots() { + let mut s = two_boards(); + let plot = |s: &mut Session| s.execute("document.export", &json!({"format": "hpgl", "useArtboards": false})).map(|v| bytes(&v)); + let printing = s.doc().unwrap().doc.layers[0].id; + s.execute("layer.new", &json!({})).unwrap(); + let mut png = vec![]; + image::DynamicImage::new_rgb8(100, 100).write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png).unwrap(); + s.execute("file.place", &json!({"name": "dots.png", "dataBase64": b64(&png), "at": [300, 300]})).unwrap(); + s.execute("layer.setProps", &json!({"id": printing, "visible": false})).unwrap(); + + // At 300 lines per inch a 100 pt image takes more cells than the effect makes. + s.execute("effect.apply", &json!({"effect": "addon.color.halftone", "params": {"frequency": 300}})).unwrap(); + let e = plot(&mut s).unwrap_err().to_string(); + assert!(e.contains("no visible art"), "{e}"); + + s.execute("effect.apply", &json!({"effect": "addon.color.halftone", "params": {"frequency": 20}})).unwrap(); + let out = String::from_utf8(plot(&mut s).unwrap()).unwrap(); + assert!(out.contains("PD"), "{out}"); +} + #[test] fn export_options_are_checked() { let mut s = two_boards(); diff --git a/docs/mcp.md b/docs/mcp.md index 5f9abe7988..19c0cba278 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -1089,6 +1089,7 @@ way. `document.exportPdf {useArtboards: false}` writes that one page as well, an (unless `includeNonPrinting` or `createLayers` keeps them; a `.ai` file keeps layers by default). An HP-GL plot of the art's bounds is measured without what the plot leaves out: non-printing layers, objects at 0% opacity, placed images, gradient meshes whose every point is at 0% opacity, and paths without effects whose fills and strokes paint nothing. +An image or a mesh with a Crop Marks effect, or with a Vector Halftone effect that makes dots, is measured. Such objects in the art of a symbol that is not dynamic are left out of its instances too, and an instance whose symbol holds nothing else is left out. A document whose only visible art is left out is refused, and Export As refuses an HP-GL plot of it before the save