mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Use Preview Bounds: a clip group is measured where its clipping path's stroke paints
The renderer clips a clip group by its clipping path's geometry before the path's geometry effects, and paints the path's strokes over the clipped art without clipping them, with those effects. With Offset Path on a stroked clipping path, the stroke paints along the offset shape while the group was measured by the path's own stroke. With Use Preview Bounds on, a clip group or layer whose clipping path has strokes with geometry effects is now measured as the clipping path's geometry and those strokes as drawn, in the Transform panel, the bounding box, Align and Distribute. This holds inside groups and for a clip group with a fill or stroke of its own. A clipping path without a stroke still measures as the region it clips to, and a hidden one adds no stroke.
This commit is contained in:
@@ -8,7 +8,9 @@
|
||||
//! their own, or with a fill or stroke of their own that has geometry effects or paints members
|
||||
//! with geometry effects, are given as their evaluated art ([`evaluate_container`]). Blends,
|
||||
//! envelopes and repeats whose objects have geometry effects are given as their evaluated art
|
||||
//! ([`expanded_live_group`]), with each of those objects given as its own art. Fills and
|
||||
//! ([`expanded_live_group`]), with each of those objects given as its own art. A clip group or
|
||||
//! layer whose clipping path has strokes with geometry effects is given as that path's geometry
|
||||
//! before its effects, which the renderer clips by, and those strokes as drawn. Fills and
|
||||
//! strokes that paint nothing (hidden, or painted None) are left out. [`bounds_of`],
|
||||
//! [`oriented_bounds`] and [`anchor_bounds`] measure the visual bounds of that art, strokes
|
||||
//! included, where [`Document::bounds_of`], [`Document::oriented_bounds`] and
|
||||
@@ -20,6 +22,7 @@ use std::cell::{Cell, RefCell};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use vectorcraft_doc::color::Paint;
|
||||
use vectorcraft_doc::{Appearance, AppearanceItem, Document, Node, NodeId, NodeKind, OrientedBox, Selection, SymbolBoxes};
|
||||
use vectorcraft_geom::{Affine, PathData, Rect, Shape, union_opt};
|
||||
|
||||
@@ -85,8 +88,9 @@ pub fn bounds_in(symbols: &SymbolBoxes, n: &Node, a: Affine) -> Option<Rect> {
|
||||
|
||||
/// `n` as plain art with the geometry effects in it applied, at every depth, symbol instances
|
||||
/// measured by `symbols`. Raster effects stay on the art. A group, layer or intertwine without a
|
||||
/// clip is rebuilt with the drawn art of each member that has one. `None` when no geometry effect
|
||||
/// in `n` changes what it draws.
|
||||
/// clip is rebuilt with the drawn art of each member that has one. A clip group or layer becomes a
|
||||
/// group of its clipping path without strokes and the drawn art of the path's strokes, when they
|
||||
/// have one. `None` when no geometry effect in `n` changes what it draws.
|
||||
pub fn drawn_art(symbols: &SymbolBoxes, n: &Node) -> Option<Node> {
|
||||
Drawn::new(symbols).drawn_art(n)
|
||||
}
|
||||
@@ -137,10 +141,11 @@ impl<'s, 'd> Drawn<'s, 'd> {
|
||||
|
||||
/// See [`drawn_art`].
|
||||
fn drawn_art(&self, n: &Node) -> Option<Node> {
|
||||
let Some(mut art) = self.own_art(n) else { return self.with_drawn_members(n) };
|
||||
let Some(mut art) = self.own_art(n) else { return self.clip_art(n).or_else(|| self.with_drawn_members(n)) };
|
||||
// The art's members can carry geometry effects of their own: type's fills and strokes, the
|
||||
// members of an evaluated group and the objects in a symbol's art.
|
||||
if let Some(m) = self.with_drawn_members(&art) {
|
||||
// members of an evaluated group, the clipping path of an evaluated clip group and the
|
||||
// objects in a symbol's art.
|
||||
if let Some(m) = self.clip_art(&art).or_else(|| self.with_drawn_members(&art)) {
|
||||
art = m;
|
||||
}
|
||||
Some(art)
|
||||
@@ -220,6 +225,33 @@ impl<'s, 'd> Drawn<'s, 'd> {
|
||||
}
|
||||
Some(m)
|
||||
}
|
||||
|
||||
/// Clip group or layer `n` as its clipping path's strokes draw it: a group of the clipping path
|
||||
/// without its strokes, measured as the region it clips to, and its strokes ([`Node::clip_paint`])
|
||||
/// as their [`Self::drawn_art`]. The renderer clips by the clipping path's geometry before its
|
||||
/// geometry effects ([`crate::clip_outline`]) and paints the path's fills inside that region and
|
||||
/// its strokes over the clipped art, unclipped. `None` for any other object, when the clipping
|
||||
/// path is hidden and when its strokes have no drawn art.
|
||||
fn clip_art(&self, n: &Node) -> Option<Node> {
|
||||
if !n.clips() {
|
||||
return None;
|
||||
}
|
||||
let clip = n.children()?.first().filter(|c| c.visible)?;
|
||||
let strokes = self.drawn_art(&clip.clip_paint().stroke?)?;
|
||||
Some(Node::group(n.id, vec![Arc::new(unstroked(clip)), Arc::new(strokes)]))
|
||||
}
|
||||
}
|
||||
|
||||
/// Clipping path `clip` without its strokes or its characters' strokes.
|
||||
fn unstroked(clip: &Node) -> Node {
|
||||
let mut n = clip.clone();
|
||||
n.appearance.items.retain(AppearanceItem::is_fill);
|
||||
if let NodeKind::Text(t) = &mut n.kind {
|
||||
for r in &mut t.runs {
|
||||
r.style.stroke = Paint::None;
|
||||
}
|
||||
}
|
||||
n
|
||||
}
|
||||
|
||||
/// Whether `n` itself has a geometry effect that [`Drawn::own_art`] applies, its own or one of its
|
||||
|
||||
@@ -171,6 +171,46 @@ fn a_groups_own_fill_is_measured_on_its_members_where_they_are() {
|
||||
assert!(near(s.transform_bounds(&[g]).unwrap(), Rect::new(100.0, 100.0, 300.0, 200.0), 1e-6), "{:?}", s.transform_bounds(&[g]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_clip_group_is_measured_where_its_clipping_paths_stroke_paints() {
|
||||
// A 200 pt square clipped by a 100 pt square at (100, 100) with Offset Path 20. The clipped art
|
||||
// is clipped by the square without the offset, so the group is measured by that square.
|
||||
let mut s = session();
|
||||
let content = NodeId(run(&mut s, "shape.rectangle", json!({"x": 50, "y": 50, "width": 200, "height": 200}))["id"].as_u64().unwrap());
|
||||
let path = NodeId(run(&mut s, "shape.rectangle", json!({"x": 100, "y": 100, "width": 100, "height": 100}))["id"].as_u64().unwrap());
|
||||
run(&mut s, "select.set", json!({"ids": [content.0, path.0]}));
|
||||
let clip = NodeId(run(&mut s, "object.clippingMask.make", json!({}))["id"].as_u64().unwrap());
|
||||
run(&mut s, "effect.apply", json!({"effect": "path.offsetPath", "params": {"offset": 20}, "ids": [path.0], "item": null}));
|
||||
preview(&mut s, true);
|
||||
let square = Rect::new(100.0, 100.0, 200.0, 200.0);
|
||||
assert!(near(s.transform_bounds(&[clip]).unwrap(), square, 1e-6), "{:?}", s.transform_bounds(&[clip]));
|
||||
// A 4 pt stroke on the clipping path paints along the offset shape, over the clipped art, from
|
||||
// 78 to 222.
|
||||
run(&mut s, "paint.setStroke", json!({"ids": [path.0], "color": "#000000"}));
|
||||
run(&mut s, "stroke.set", json!({"ids": [path.0], "weight": 4}));
|
||||
let stroked = Rect::new(78.0, 78.0, 222.0, 222.0);
|
||||
assert!(near(s.transform_bounds(&[clip]).unwrap(), stroked, 1e-6), "{:?}", s.transform_bounds(&[clip]));
|
||||
// A fill of the clip group's own keeps that box.
|
||||
run(&mut s, "appearance.addFill", json!({"ids": [clip.0]}));
|
||||
run(&mut s, "paint.setFill", json!({"ids": [clip.0], "item": 0, "color": "#000000"}));
|
||||
assert!(near(s.transform_bounds(&[clip]).unwrap(), stroked, 1e-6), "with a fill: {:?}", s.transform_bounds(&[clip]));
|
||||
// Turned 30°, the box keeps that size.
|
||||
run(&mut s, "select.set", json!({"ids": [clip.0]}));
|
||||
run(&mut s, "object.rotate", json!({"angle": 30}));
|
||||
let turned = s.transform_box(&[clip]).unwrap();
|
||||
assert!(
|
||||
(turned.angle - 30.0).abs() < 1e-9 && (turned.rect.width() - 144.0).abs() < 1e-6 && (turned.rect.height() - 144.0).abs() < 1e-6,
|
||||
"{turned:?}"
|
||||
);
|
||||
// In a group, the clip group is measured the same way.
|
||||
run(&mut s, "object.rotate", json!({"angle": -30}));
|
||||
let group = NodeId(run(&mut s, "object.group", json!({}))["id"].as_u64().unwrap());
|
||||
assert!(near(s.transform_bounds(&[group]).unwrap(), stroked, 1e-6), "{:?}", s.transform_bounds(&[group]));
|
||||
// Without Use Preview Bounds, the clipping path's geometry is measured.
|
||||
preview(&mut s, false);
|
||||
assert!(near(s.transform_bounds(&[clip]).unwrap(), square, 1e-6), "{:?}", s.transform_bounds(&[clip]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_fills_and_strokes_that_paint_are_measured() {
|
||||
let item = |s: &Session, id: NodeId, fill: bool| {
|
||||
|
||||
Reference in New Issue
Block a user