mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-29 20:27:05 -06:00
fix(doc): look into containers for objects beyond the canvas (#3575)
A layer's bounds leave out its hidden objects and a clipping container's are its clipping path's, so objects beyond the canvas inside them were kept. Layers and groups are now looked into object by object. A clipping path beyond the canvas goes with the objects it clips, so no other object becomes the clip; a top-level clipping layer is emptied and stops clipping. A removed artboard's ruler origin goes and its id is retired, as deleting it does.
This commit is contained in:
+109
-11
@@ -62,20 +62,45 @@ fn beyond(n: &Node) -> bool {
|
||||
n.geometric_bounds().is_some_and(|b| !rect_in_range(b))
|
||||
}
|
||||
|
||||
/// Remove the children of `node` that reach beyond the canvas: a layer or group that isn't
|
||||
/// clipping keeps those of its own children within it, anything else goes whole. Only a node
|
||||
/// with such a child is copied. Recursion follows the nesting, which [`Document::limit_depth`]
|
||||
/// bounds.
|
||||
/// Whether `n` is a layer or group, and if so whether it clips: the containers whose objects are
|
||||
/// looked at one by one. Anything else is one object, measured whole.
|
||||
fn clipping(n: &Node) -> Option<bool> {
|
||||
match n.kind {
|
||||
NodeKind::Layer { clip, .. } | NodeKind::Group { clip, .. } => Some(clip),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Does `n`, or any object in it, reach beyond the canvas? A container is looked into object by
|
||||
/// object: its own bounds leave out a layer's hidden objects and, when it clips, all but its
|
||||
/// clipping path.
|
||||
fn reaches_beyond(n: &Node) -> bool {
|
||||
match clipping(n) {
|
||||
None => beyond(n),
|
||||
Some(_) => n.children().is_some_and(|c| c.iter().any(|c| reaches_beyond(c))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Does `n` clip by a path that reaches beyond the canvas? Then the path and the objects it clips
|
||||
/// go together, so no other object becomes the clipping path.
|
||||
fn clips_beyond(n: &Node) -> bool {
|
||||
clipping(n) == Some(true) && n.children().and_then(|c| c.first()).is_some_and(|c| reaches_beyond(c))
|
||||
}
|
||||
|
||||
/// Remove the objects in `node` that reach beyond the canvas: a layer or group keeps those of its
|
||||
/// objects within it, unless its clipping path reaches beyond (see [`clips_beyond`]); anything
|
||||
/// else goes whole. Only containers holding such an object are copied. Recursion follows the
|
||||
/// nesting, which [`Document::limit_depth`] bounds.
|
||||
fn drop_children_beyond(node: &mut Arc<Node>, dropped: &mut usize) {
|
||||
if !node.children().is_some_and(|c| c.iter().any(|c| beyond(c))) {
|
||||
if !node.children().is_some_and(|c| c.iter().any(|c| reaches_beyond(c))) {
|
||||
return;
|
||||
}
|
||||
let Some(children) = Arc::make_mut(node).children_mut() else { return };
|
||||
children.retain_mut(|c| {
|
||||
if !beyond(c) {
|
||||
if !reaches_beyond(c) {
|
||||
return true;
|
||||
}
|
||||
if matches!(c.kind, NodeKind::Layer { clip: false, .. } | NodeKind::Group { clip: false, .. }) {
|
||||
if clipping(c).is_some() && !clips_beyond(c) {
|
||||
drop_children_beyond(c, dropped);
|
||||
return true;
|
||||
}
|
||||
@@ -86,16 +111,31 @@ fn drop_children_beyond(node: &mut Arc<Node>, dropped: &mut usize) {
|
||||
|
||||
impl Document {
|
||||
/// Remove what a read file has beyond the canvas ([`MAX_COORD`], or not finite): objects
|
||||
/// (see [`BeyondCanvas::objects`]), artboards, and guides with the artboard guides of those
|
||||
/// artboards, and put a ruler origin beyond it back at the origin. Run after
|
||||
/// [`Document::limit_depth`]. A document whose artboards were all removed has none left.
|
||||
/// (see [`BeyondCanvas::objects`]; a top-level layer clipping by a path beyond it is emptied
|
||||
/// and stops clipping), artboards with their ruler origins and artboard guides (their ids
|
||||
/// are retired, as deleting them does), and guides, and put a ruler origin beyond it back at
|
||||
/// the origin. Run after [`Document::limit_depth`]. A document whose artboards were all
|
||||
/// removed has none left.
|
||||
pub fn drop_beyond_canvas(&mut self) -> BeyondCanvas {
|
||||
let mut gone = BeyondCanvas::default();
|
||||
for layer in &mut self.layers {
|
||||
drop_children_beyond(layer, &mut gone.objects);
|
||||
if clips_beyond(layer) {
|
||||
let layer = Arc::make_mut(layer);
|
||||
if let NodeKind::Layer { children, clip, .. } = &mut layer.kind {
|
||||
gone.objects += children.len();
|
||||
children.clear();
|
||||
*clip = false;
|
||||
}
|
||||
} else {
|
||||
drop_children_beyond(layer, &mut gone.objects);
|
||||
}
|
||||
}
|
||||
let removed: Vec<u32> = self.artboards.iter().filter(|a| !rect_in_range(a.rect)).map(|a| a.id).collect();
|
||||
self.artboards.retain(|a| rect_in_range(a.rect));
|
||||
for id in &removed {
|
||||
self.artboard_ruler_origins.remove(id);
|
||||
self.retire_artboard_id(*id);
|
||||
}
|
||||
gone.artboards = removed.len();
|
||||
let guides = self.guides.len();
|
||||
self.guides.retain(|g| in_range(g.pos) && g.artboard.is_none_or(|a| !removed.contains(&a)));
|
||||
@@ -182,4 +222,62 @@ mod tests {
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/// A sublayer like the document's first layer, holding `children`.
|
||||
fn sublayer(d: &Document, id: u64, children: Vec<Arc<Node>>, clip: bool) -> Arc<Node> {
|
||||
let mut l = (*d.layers[0]).clone();
|
||||
l.id = NodeId(id);
|
||||
if let NodeKind::Layer { children: c, clip: k, .. } = &mut l.kind {
|
||||
*c = children;
|
||||
*k = clip;
|
||||
}
|
||||
Arc::new(l)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn objects_hidden_or_clipped_inside_a_container_are_looked_at_one_by_one() {
|
||||
// A layer's bounds leave out its hidden objects, and a clip group's are its clipping path's.
|
||||
let mut hidden = (*rect(41, 1e17)).clone();
|
||||
hidden.visible = false;
|
||||
let probe = Document::new(1.0, 1.0);
|
||||
let sub = sublayer(&probe, 40, vec![Arc::new(hidden), rect(42, 0.0)], false);
|
||||
let clip = Arc::new(Node::new(NodeId(30), NodeKind::Group { children: vec![rect(31, 0.0), rect(32, 1e17), rect(33, 5.0)], clip: true }));
|
||||
let mut d = doc(vec![sub, clip]);
|
||||
let layer = d.layers[0].id.0;
|
||||
assert_eq!(d.drop_beyond_canvas().objects, 2);
|
||||
assert_eq!(ids(&d), [layer, 40, 42, 30, 31, 33], "the far hidden object and the far clipped one go; the clipping path stays first");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_top_level_layer_clipping_by_a_path_beyond_the_canvas_is_emptied() {
|
||||
let mut d = doc(vec![rect(10, 1e17), rect(11, 0.0), rect(12, 5.0)]);
|
||||
if let NodeKind::Layer { clip, .. } = &mut Arc::make_mut(&mut d.layers[0]).kind {
|
||||
*clip = true;
|
||||
}
|
||||
assert_eq!(d.drop_beyond_canvas().objects, 3, "the clipping path and what it clipped go together");
|
||||
assert!(
|
||||
matches!(&d.layers[0].kind, NodeKind::Layer { children, clip: false, .. } if children.is_empty()),
|
||||
"no other object becomes the clipping path"
|
||||
);
|
||||
// A clipping sublayer goes whole, as a clip group does.
|
||||
let mut d = doc(vec![]);
|
||||
let sub = sublayer(&d, 40, vec![rect(41, 1e17), rect(42, 0.0)], true);
|
||||
let mut d2 = doc(vec![sub, rect(10, 0.0)]);
|
||||
let layer = d2.layers[0].id.0;
|
||||
assert_eq!(d2.drop_beyond_canvas().objects, 1);
|
||||
assert_eq!(ids(&d2), [layer, 10]);
|
||||
assert_eq!(d.drop_beyond_canvas(), BeyondCanvas::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_removed_artboards_ruler_origin_goes_and_its_id_is_not_given_again() {
|
||||
let mut d = doc(vec![]);
|
||||
let kept = d.artboards[0].id;
|
||||
d.artboards.push(Artboard { id: 9, rect: Rect::new(0.0, 1e17, 100.0, 1e17 + 100.0), ..d.artboards[0].clone() });
|
||||
d.artboard_ruler_origins.insert(kept, vectorcraft_geom::Vec2::new(1.0, 2.0));
|
||||
d.artboard_ruler_origins.insert(9, vectorcraft_geom::Vec2::new(3.0, 4.0));
|
||||
assert_eq!(d.drop_beyond_canvas().artboards, 1);
|
||||
assert_eq!(d.artboard_ruler_origins.keys().copied().collect::<Vec<_>>(), [kept]);
|
||||
assert!(d.next_artboard_id() > 9, "a new artboard doesn't take the removed one's id");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user