mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-15 08:45:25 +00:00
Merge pull request #7683 from dotCooCoo/fix/hpgl-art-bounds-dynamic-symbols
Export: an HP-GL plot of the art's bounds leaves out images in dynamic symbols
This commit is contained in:
@@ -2,13 +2,13 @@
|
||||
//! (unknown keys are ignored, so one params object can carry the options of several formats).
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashSet;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde_json::Value;
|
||||
use vectorcraft_doc::{Artboard, Document, Node, NodeKind};
|
||||
use vectorcraft_doc::{Artboard, Document, Node, NodeKind, SymbolOverride};
|
||||
use vectorcraft_geom::Rect;
|
||||
use vectorcraft_render::AntiAlias;
|
||||
use vectorcraft_render::encode::jpeg::{self, JpegOptions};
|
||||
@@ -450,31 +450,85 @@ pub fn export_art_bounds(doc: &Document, format: &str) -> Option<Rect> {
|
||||
/// The bounds of the visible art of `doc` with the objects that `left_out` picks hidden
|
||||
/// ([`hidden_nodes`]) and the ones in the art of the symbols that instances show taken out
|
||||
/// ([`without_nodes`]: symbol art is measured with its hidden objects). An instance of a symbol
|
||||
/// whose art is all picked or hidden is hidden too. The art of a dynamic symbol is measured whole:
|
||||
/// an instance can paint its objects, and finds them by their places in the art.
|
||||
/// whose art is all picked or hidden is hidden too. The drawn instances of a dynamic symbol that
|
||||
/// make the same appearance changes to its art are measured on a copy of the symbol of their own,
|
||||
/// in which each object is judged with those changes ([`as_drawn`]); the objects taken out of
|
||||
/// a dynamic symbol's art leave empty groups in their places, by which instances find the objects
|
||||
/// they change.
|
||||
fn bounds_without(doc: &Document, left_out: &dyn Fn(&Node) -> bool) -> Option<Rect> {
|
||||
// The symbols instances show (the first of each name) that aren't dynamic.
|
||||
// The symbols instances show (the first of each name).
|
||||
let mut names = HashSet::new();
|
||||
let shown: Vec<(usize, &vectorcraft_doc::Symbol)> =
|
||||
doc.symbols.iter().enumerate().filter(|(_, s)| names.insert(s.name.as_str())).filter(|(_, s)| !s.dynamic).collect();
|
||||
let shown: Vec<usize> = doc.symbols.iter().enumerate().filter(|(_, s)| names.insert(s.name.as_str())).map(|(i, _)| i).collect();
|
||||
// The sets of appearance changes the drawn instances of each dynamic symbol make to its art;
|
||||
// `None` for a symbol with more than `MAX_ART_COPIES`, which is measured whole.
|
||||
let mut sets: HashMap<&str, Option<Vec<Vec<&SymbolOverride>>>> =
|
||||
shown.iter().filter_map(|i| doc.symbols.get(*i)).filter(|s| s.dynamic).map(|s| (s.name.as_str(), Some(Vec::new()))).collect();
|
||||
if !sets.is_empty() {
|
||||
let mut walked = HashSet::new();
|
||||
for l in &doc.layers {
|
||||
drawn_changes(doc, l, left_out, true, &mut walked, &mut sets);
|
||||
}
|
||||
}
|
||||
// Each set of changes gets a copy of its symbol, appended after the symbols, which the instances
|
||||
// that make those changes show instead.
|
||||
let variant = |symbol: &str, n: &Node| -> Option<String> {
|
||||
let k = sets.get(symbol)?.as_ref()?.iter().position(|c| *c == own_changes(n))?;
|
||||
Some(format!("{symbol}\u{0}{k}"))
|
||||
};
|
||||
let mut copies: Vec<(usize, &[&SymbolOverride])> = Vec::new();
|
||||
let remapped_doc = sets.values().flatten().any(|v| !v.is_empty()).then(|| {
|
||||
let mut d = doc.clone();
|
||||
for l in &mut d.layers {
|
||||
if let Some(n) = remapped(l, &variant) {
|
||||
*l = n;
|
||||
}
|
||||
}
|
||||
for s in &mut d.symbols {
|
||||
if let Some(n) = remapped(&s.art, &variant) {
|
||||
s.art = n;
|
||||
}
|
||||
}
|
||||
for i in &shown {
|
||||
let Some(s) = d.symbols.get(*i).cloned() else { continue };
|
||||
for (k, c) in sets.get(s.name.as_str()).into_iter().flatten().flatten().enumerate() {
|
||||
copies.push((d.symbols.len(), c.as_slice()));
|
||||
d.symbols.push(vectorcraft_doc::Symbol { name: format!("{}\u{0}{k}", s.name), ..s.clone() });
|
||||
}
|
||||
}
|
||||
d
|
||||
});
|
||||
let src = remapped_doc.as_ref().unwrap_or(doc);
|
||||
// The symbols to measure with the changes their instances make (`None` for a static symbol),
|
||||
// without the dynamic symbols measured whole.
|
||||
let entries: Vec<(usize, Option<&[&SymbolOverride]>)> = shown
|
||||
.iter()
|
||||
.filter_map(|i| src.symbols.get(*i).map(|s| (*i, s)))
|
||||
.filter(|(_, s)| !s.dynamic || sets.get(s.name.as_str()).is_some_and(Option::is_some))
|
||||
.map(|(i, s)| (i, s.dynamic.then_some(&[][..])))
|
||||
.chain(copies.iter().map(|(i, c)| (*i, Some(*c))))
|
||||
.collect();
|
||||
// The symbols whose art `left_out` leaves nothing of, those that only hold instances of such
|
||||
// symbols included.
|
||||
let mut empty: HashSet<&str> = HashSet::new();
|
||||
while let Some((_, s)) = shown.iter().find(|(_, s)| !empty.contains(s.name.as_str()) && all_left_out(&s.art, left_out, &empty)) {
|
||||
while let Some(s) = entries.iter().find_map(|(i, c)| {
|
||||
src.symbols.get(*i).filter(|s| !empty.contains(s.name.as_str()) && all_left_out(&s.art, left_out, &empty, *c, &mut Vec::new()))
|
||||
}) {
|
||||
empty.insert(&s.name);
|
||||
}
|
||||
let left_out =
|
||||
&|n: &Node| left_out(n) || matches!(&n.kind, NodeKind::SymbolInstance { symbol, .. } if empty.contains(symbol.as_str()) && !has_effects(n));
|
||||
let layers = hidden_nodes(&doc.layers, left_out);
|
||||
let symbols: Vec<(usize, Arc<Node>)> = shown
|
||||
let layers = hidden_nodes(&src.layers, left_out);
|
||||
let symbols: Vec<(usize, Arc<Node>)> = entries
|
||||
.iter()
|
||||
.filter(|(_, s)| !empty.contains(s.name.as_str()))
|
||||
.filter_map(|(i, s)| without_nodes(&s.art, left_out).map(|art| (*i, art)))
|
||||
.filter_map(|(i, c)| {
|
||||
let s = src.symbols.get(*i).filter(|s| !empty.contains(s.name.as_str()))?;
|
||||
without_nodes(&s.art, left_out, *c, &mut Vec::new()).map(|art| (*i, art))
|
||||
})
|
||||
.collect();
|
||||
if layers.is_none() && symbols.is_empty() {
|
||||
return vectorcraft_render::encode::art_bounds(doc);
|
||||
return vectorcraft_render::encode::art_bounds(src);
|
||||
}
|
||||
let mut d = doc.clone();
|
||||
let mut d = remapped_doc.unwrap_or_else(|| doc.clone());
|
||||
if let Some(layers) = layers {
|
||||
d.layers = layers;
|
||||
}
|
||||
@@ -486,22 +540,126 @@ fn bounds_without(doc: &Document, left_out: &dyn Fn(&Node) -> bool) -> Option<Re
|
||||
vectorcraft_render::encode::art_bounds(&d)
|
||||
}
|
||||
|
||||
/// The appearance changes instance `n` makes to its symbol's art, without those that change nothing.
|
||||
fn own_changes(n: &Node) -> Vec<&SymbolOverride> {
|
||||
n.extras().symbol_overrides.iter().filter(|o| !o.is_empty()).collect()
|
||||
}
|
||||
|
||||
/// `n` with each instance at any depth that `variant` gives another symbol (from the instance's
|
||||
/// symbol and the instance) made an instance of that symbol; `None` when none is.
|
||||
fn remapped(n: &Arc<Node>, variant: &dyn Fn(&str, &Node) -> Option<String>) -> Option<Arc<Node>> {
|
||||
if let NodeKind::SymbolInstance { symbol, .. } = &n.kind {
|
||||
let name = variant(symbol, n)?;
|
||||
let mut m = (**n).clone();
|
||||
if let NodeKind::SymbolInstance { symbol, .. } = &mut m.kind {
|
||||
*symbol = name;
|
||||
}
|
||||
return Some(Arc::new(m));
|
||||
}
|
||||
let children = n.children()?;
|
||||
let mut out: Option<Vec<Arc<Node>>> = None;
|
||||
for (i, c) in children.iter().enumerate() {
|
||||
if let Some(new) = remapped(c, variant)
|
||||
&& let Some(slot) = out.get_or_insert_with(|| children.clone()).get_mut(i)
|
||||
{
|
||||
*slot = new;
|
||||
}
|
||||
}
|
||||
out.map(|c| {
|
||||
let mut m = (**n).clone();
|
||||
if let Some(children) = m.children_mut() {
|
||||
*children = c;
|
||||
}
|
||||
Arc::new(m)
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether `n` is hidden, picked by `left_out`, an instance of one of the `empty` symbols, or a
|
||||
/// layer or a group holding only such objects (its clipping path among them when it clips). A
|
||||
/// Shaper group, or a layer, group or instance with effects, fills or strokes of its own, is none
|
||||
/// of these unless it is hidden or picked.
|
||||
fn all_left_out(n: &Node, left_out: &dyn Fn(&Node) -> bool, empty: &HashSet<&str>) -> bool {
|
||||
/// of these unless it is hidden or picked. In the art of a dynamic symbol (`changed`, the changes
|
||||
/// an instance makes to it; `n` is at place `at`), each object is judged with those changes
|
||||
/// ([`as_drawn`]).
|
||||
fn all_left_out(n: &Node, left_out: &dyn Fn(&Node) -> bool, empty: &HashSet<&str>, changed: Option<&[&SymbolOverride]>, at: &mut Vec<u32>) -> bool {
|
||||
let m = as_drawn(n, changed, at);
|
||||
!n.visible
|
||||
|| left_out(n)
|
||||
|| !has_effects(n)
|
||||
|| left_out(&m)
|
||||
|| !has_effects(&m)
|
||||
&& n.extras().shaper.is_none()
|
||||
&& match &n.kind {
|
||||
NodeKind::Layer { children, .. } | NodeKind::Group { children, .. } => children.iter().all(|c| all_left_out(c, left_out, empty)),
|
||||
NodeKind::Layer { children, .. } | NodeKind::Group { children, .. } => children.iter().enumerate().all(|(i, c)| {
|
||||
at.push(i as u32);
|
||||
let out = all_left_out(c, left_out, empty, changed, at);
|
||||
at.pop();
|
||||
out
|
||||
}),
|
||||
NodeKind::SymbolInstance { symbol, .. } => empty.contains(symbol.as_str()),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Add the sets of appearance changes that the instances in `n` and its subtree make to the art of
|
||||
/// the symbols `sets` names, each set once, and those of the instances in the art of the symbols the
|
||||
/// instances show (`walked`, the symbols already looked in). The instances that `left_out` picks or
|
||||
/// that are in an object it picks are left out, and with `hidden` (in a layer) the hidden ones and
|
||||
/// those in a hidden object. A symbol with more than [`MAX_ART_COPIES`] sets gets `None`.
|
||||
fn drawn_changes<'a>(
|
||||
doc: &'a Document,
|
||||
n: &'a Node,
|
||||
left_out: &dyn Fn(&Node) -> bool,
|
||||
hidden: bool,
|
||||
walked: &mut HashSet<&'a str>,
|
||||
sets: &mut HashMap<&'a str, Option<Vec<Vec<&'a SymbolOverride>>>>,
|
||||
) {
|
||||
if hidden && !n.visible || left_out(n) {
|
||||
return;
|
||||
}
|
||||
if let NodeKind::SymbolInstance { symbol, .. } = &n.kind {
|
||||
let changes = own_changes(n);
|
||||
if let Some(entry) = sets.get_mut(symbol.as_str())
|
||||
&& !changes.is_empty()
|
||||
{
|
||||
let full = match entry {
|
||||
Some(list) if !list.contains(&changes) && list.len() < MAX_ART_COPIES => {
|
||||
list.push(changes);
|
||||
false
|
||||
}
|
||||
Some(list) => !list.contains(&changes),
|
||||
None => false,
|
||||
};
|
||||
if full {
|
||||
*entry = None;
|
||||
}
|
||||
}
|
||||
if walked.insert(symbol.as_str())
|
||||
&& let Some(s) = doc.symbols.iter().find(|s| s.name == *symbol)
|
||||
{
|
||||
drawn_changes(doc, &s.art, left_out, false, walked, sets);
|
||||
}
|
||||
}
|
||||
for c in n.children().into_iter().flatten() {
|
||||
drawn_changes(doc, c, left_out, hidden, walked, sets);
|
||||
}
|
||||
}
|
||||
|
||||
/// The most copies of one dynamic symbol's art [`bounds_without`] measures, one for each set of
|
||||
/// appearance changes its instances make; a symbol whose instances make more sets is measured whole.
|
||||
const MAX_ART_COPIES: usize = 64;
|
||||
|
||||
/// `n`, the object at place `at` of a symbol's art, with the appearance changes `changed` that an
|
||||
/// instance makes to it applied in their order; `n` itself for the art of a symbol that is not
|
||||
/// dynamic (`changed` is `None`) and when no change names `at`.
|
||||
fn as_drawn<'a>(n: &'a Node, changed: Option<&[&SymbolOverride]>, at: &[u32]) -> Cow<'a, Node> {
|
||||
let here: Vec<SymbolOverride> =
|
||||
changed.into_iter().flatten().filter(|o| o.path == at).map(|o| SymbolOverride { path: Vec::new(), ..(*o).clone() }).collect();
|
||||
if here.is_empty() {
|
||||
return Cow::Borrowed(n);
|
||||
}
|
||||
let mut m = n.clone();
|
||||
vectorcraft_doc::symbol_overrides::apply(&mut m, &here);
|
||||
Cow::Owned(m)
|
||||
}
|
||||
|
||||
/// Whether `n` has a visible effect, fill or stroke of its own, which the objects it holds can
|
||||
/// take part in.
|
||||
fn has_effects(n: &Node) -> bool {
|
||||
@@ -511,25 +669,32 @@ fn has_effects(n: &Node) -> bool {
|
||||
/// `n` without the objects that `left_out` picks in it, when it is a layer or a group, and in the
|
||||
/// layers and groups in it at any depth; `None` when it picks none of them. A clipping layer or
|
||||
/// group keeps its clipping path, its first object, and a Shaper group and a layer or group with
|
||||
/// effects, fills or strokes of its own keep all they hold.
|
||||
fn without_nodes(n: &Arc<Node>, left_out: &dyn Fn(&Node) -> bool) -> Option<Arc<Node>> {
|
||||
/// effects, fills or strokes of its own keep all they hold. In the art of a dynamic symbol
|
||||
/// (`changed`, the changes an instance makes to it, as for [`all_left_out`]; `n` is at place
|
||||
/// `at`), each object is judged with those changes ([`as_drawn`]), and an object taken out leaves
|
||||
/// an empty group in its place.
|
||||
fn without_nodes(n: &Arc<Node>, left_out: &dyn Fn(&Node) -> bool, changed: Option<&[&SymbolOverride]>, at: &mut Vec<u32>) -> Option<Arc<Node>> {
|
||||
let (NodeKind::Layer { children, clip, .. } | NodeKind::Group { children, clip }) = &n.kind else { return None };
|
||||
if has_effects(n) || n.extras().shaper.is_some() {
|
||||
if has_effects(&as_drawn(n, changed, at)) || n.extras().shaper.is_some() {
|
||||
return None;
|
||||
}
|
||||
let mut changed = false;
|
||||
let mut taken = false;
|
||||
let kept: Vec<Arc<Node>> = children
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(i, c)| {
|
||||
if left_out(c) && !(*clip && i == 0) {
|
||||
changed = true;
|
||||
return None;
|
||||
}
|
||||
Some(without_nodes(c, left_out).inspect(|_| changed = true).unwrap_or_else(|| c.clone()))
|
||||
at.push(i as u32);
|
||||
let out = if left_out(&as_drawn(c, changed, at)) && !(*clip && i == 0) {
|
||||
taken = true;
|
||||
changed.map(|_| Arc::new(Node::group(c.id, Vec::new())))
|
||||
} else {
|
||||
Some(without_nodes(c, left_out, changed, at).inspect(|_| taken = true).unwrap_or_else(|| c.clone()))
|
||||
};
|
||||
at.pop();
|
||||
out
|
||||
})
|
||||
.collect();
|
||||
changed.then(|| {
|
||||
taken.then(|| {
|
||||
let mut new = (**n).clone();
|
||||
if let NodeKind::Layer { children, .. } | NodeKind::Group { children, .. } = &mut new.kind {
|
||||
*children = kept;
|
||||
|
||||
@@ -254,6 +254,90 @@ fn a_plot_of_the_arts_bounds_leaves_out_images_in_symbol_art() {
|
||||
}
|
||||
}
|
||||
|
||||
/// In the art of a dynamic symbol a placed image and a path that paints nothing are left out as
|
||||
/// well, until an instance paints the path, and a document whose only other visible art is an
|
||||
/// instance of a dynamic symbol of an image is refused.
|
||||
#[test]
|
||||
fn a_plot_of_the_arts_bounds_leaves_out_images_in_dynamic_symbol_art() {
|
||||
let mut s = two_boards();
|
||||
let plot = |s: &mut Session| s.execute("document.export", &json!({"format": "hpgl", "useArtboards": false})).map(|v| bytes(&v));
|
||||
let bounds = |s: &Session| super::export_art_bounds(&s.doc().unwrap().doc, "hpgl").unwrap();
|
||||
let same = |a: vectorcraft_geom::Rect, b: vectorcraft_geom::Rect| {
|
||||
[(a.x0, b.x0), (a.y0, b.y0), (a.x1, b.x1), (a.y1, b.y1)].iter().all(|(a, b)| (a - b).abs() < 1e-6)
|
||||
};
|
||||
let printing = s.doc().unwrap().doc.layers[0].id;
|
||||
s.execute("layer.new", &json!({})).unwrap();
|
||||
let mut png = vec![];
|
||||
image::DynamicImage::new_rgb8(4, 4).write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png).unwrap();
|
||||
let image = s.execute("file.place", &json!({"name": "far.png", "dataBase64": b64(&png), "at": [-600, 800]})).unwrap()["ids"][0].clone();
|
||||
let square = s.execute("shape.rectangle", &json!({"x": 200, "y": 200, "width": 20, "height": 20})).unwrap()["id"].clone();
|
||||
let bare = s.execute("shape.rectangle", &json!({"x": 500, "y": -400, "width": 20, "height": 20})).unwrap()["id"].clone();
|
||||
s.execute("paint.setStroke", &json!({"ids": [bare], "none": true})).unwrap();
|
||||
let before = bounds(&s);
|
||||
let inst = s.execute("symbol.new", &json!({"name": "Mixed", "ids": [image, square, bare], "type": "dynamic"})).unwrap()["id"].clone();
|
||||
let after = bounds(&s);
|
||||
assert!(same(before, after), "{before:?} {after:?}");
|
||||
|
||||
// A change of the path's blending mode leaves it painting nothing; once the instance paints the
|
||||
// path, the path is measured.
|
||||
let art = s.doc().unwrap().doc.symbols[0].art.clone();
|
||||
let x0 = |c: &std::sync::Arc<vectorcraft_doc::Node>| c.visual_bounds().map_or(f64::MIN, |b| b.x0);
|
||||
let at = (0..art.children().unwrap().len()).max_by(|a, b| x0(&art.children().unwrap()[*a]).total_cmp(&x0(&art.children().unwrap()[*b]))).unwrap();
|
||||
s.execute("symbol.instance.setAppearance", &json!({"id": inst, "path": [at], "blend": "multiply"})).unwrap();
|
||||
let blended = bounds(&s);
|
||||
assert!(same(before, blended), "{before:?} {blended:?}");
|
||||
s.execute("symbol.instance.setAppearance", &json!({"id": inst, "path": [at], "stroke": "#000000"})).unwrap();
|
||||
let painted = bounds(&s);
|
||||
assert!(painted.x1 > 500.0 && painted.y0 < -390.0, "{painted:?}");
|
||||
// A later change to the same object, as a file can hold, takes the paint off again.
|
||||
{
|
||||
let id = vectorcraft_doc::NodeId(inst.as_u64().unwrap());
|
||||
let d = std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc);
|
||||
let n = d.node_mut(id).unwrap();
|
||||
let mut overrides = n.extras().symbol_overrides.clone();
|
||||
overrides.push(vectorcraft_doc::SymbolOverride { path: vec![at as u32], opacity: Some(0.0), ..Default::default() });
|
||||
n.extras_mut().symbol_overrides = overrides.clone();
|
||||
let taken_off = bounds(&s);
|
||||
assert!(same(before, taken_off), "{before:?} {taken_off:?}");
|
||||
overrides.pop();
|
||||
let d = std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc);
|
||||
d.node_mut(id).unwrap().extras_mut().symbol_overrides = overrides;
|
||||
}
|
||||
// Another instance, far above, that doesn't paint the path is measured without it, whether or
|
||||
// not the first one paints it.
|
||||
let other = s.execute("symbol.place", &json!({"name": "Mixed", "x": 0, "y": -5000})).unwrap()["id"].clone();
|
||||
let with_paint = bounds(&s);
|
||||
s.execute("symbol.instance.reset", &json!({"ids": [inst]})).unwrap();
|
||||
let without_paint = bounds(&s);
|
||||
assert!((with_paint.y0 - without_paint.y0).abs() < 1e-6, "{with_paint:?} {without_paint:?}");
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
let undone = bounds(&s);
|
||||
assert!(same(undone, with_paint), "{undone:?} {with_paint:?}");
|
||||
s.execute("select.set", &json!({"ids": [other]})).unwrap();
|
||||
s.execute("edit.clear", &json!({})).unwrap();
|
||||
|
||||
s.execute("select.set", &json!({"ids": [inst]})).unwrap();
|
||||
s.execute("edit.clear", &json!({})).unwrap();
|
||||
s.execute("layer.setProps", &json!({"id": printing, "visible": false})).unwrap();
|
||||
let image = s.execute("file.place", &json!({"name": "far.png", "dataBase64": b64(&png), "at": [-600, 800]})).unwrap()["ids"][0].clone();
|
||||
s.execute("symbol.new", &json!({"name": "Image", "ids": [image], "type": "dynamic"})).unwrap();
|
||||
let e = plot(&mut s).unwrap_err().to_string();
|
||||
assert!(e.contains("no visible art"), "{e}");
|
||||
|
||||
// An instance that brings a group at 0% opacity back to full opacity still draws nothing when
|
||||
// the group holds only an image.
|
||||
let image = s.execute("file.place", &json!({"name": "far.png", "dataBase64": b64(&png), "at": [-600, 800]})).unwrap()["ids"][0].clone();
|
||||
let group = s.execute("object.group", &json!({"ids": [image]})).unwrap()["id"].clone();
|
||||
s.execute("transparency.set", &json!({"ids": [group], "opacity": 0})).unwrap();
|
||||
let inst = s.execute("symbol.new", &json!({"name": "Clear", "ids": [group], "type": "dynamic"})).unwrap()["id"].clone();
|
||||
let id = vectorcraft_doc::NodeId(inst.as_u64().unwrap());
|
||||
let d = std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc);
|
||||
d.node_mut(id).unwrap().extras_mut().symbol_overrides =
|
||||
[vec![], vec![0]].into_iter().map(|path| vectorcraft_doc::SymbolOverride { path, opacity: Some(1.0), ..Default::default() }).collect();
|
||||
let e = plot(&mut s).unwrap_err().to_string();
|
||||
assert!(e.contains("no visible art"), "{e}");
|
||||
}
|
||||
|
||||
/// 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]
|
||||
|
||||
+3
-2
@@ -1106,8 +1106,9 @@ way. `document.exportPdf {useArtboards: false}` writes that one page as well, an
|
||||
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.
|
||||
Such objects in the art of a symbol are left out of its instances too, and an instance whose symbol holds nothing
|
||||
else is left out. In the art of a dynamic symbol, an object that an instance's fill, stroke or opacity makes
|
||||
the plot draw is measured.
|
||||
A document whose only visible art is left out is refused, and Export As refuses an HP-GL plot of it before the save
|
||||
dialog. A PDF of that page, from either command, carries no Preserve Editing data; when Preserve Editing is on (as it
|
||||
is in VectorCraft Default), the export comes back with a warning that says so. A `.ai` export with
|
||||
|
||||
Reference in New Issue
Block a user