mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Use Preview Bounds: objects are measured with their geometry effects
With Use Preview Bounds on, the bounding box, the Transform panel, Align, Distribute, Distribute Spacing and object.setBounds measured each object without its geometry effects. A square with Offset Path 10 measured as the square, and type with Outline Object measured by its layout box instead of its glyphs. The new vectorcraft_effects::preview module measures objects as they are drawn. A path is measured by its effected shape, with the geometry effects of each fill and stroke that paints. Type, images, live objects and symbol instances are measured by their reshaped art. A group or layer with geometry or Pathfinder effects, or with a fill or stroke of its own that has geometry effects or paints members with geometry effects, is measured by its evaluated art. A blend, envelope or repeat whose objects have geometry effects is measured by its evaluated art, with those objects measured after their effects. Raster effects are not measured. In one object, the art of at most 4,096 symbol instances is measured with its geometry effects, and the other instances are measured without them. With Use Preview Bounds off, the measures are unchanged. object.setBounds sizes objects with geometry effects by measuring them after each scale it tries, so type with Outline Object set to 100 pt wide gets a glyph box 100 pt wide. Selection::anchors_box is split out of Selection::anchor_bounds. The doc comment of VectorcraftApp::selection_bounds said that it includes effects; it now says that it leaves them out.
This commit is contained in:
@@ -148,7 +148,11 @@ impl Selection {
|
||||
if self.anchors.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let mut acc = doc.bounds_of(&self.whole(), visual);
|
||||
vectorcraft_geom::union_opt(doc.bounds_of(&self.whole(), visual), self.anchors_box(doc))
|
||||
}
|
||||
/// The box around the anchors selected with Direct Selection. `None` without any.
|
||||
pub fn anchors_box(&self, doc: &Document) -> Option<vectorcraft_geom::Rect> {
|
||||
let mut acc = None;
|
||||
for (id, set) in &self.anchors {
|
||||
let Some(NodeKind::Path { path, .. }) = doc.node(*id).map(|n| &n.kind) else { continue };
|
||||
for a in set.iter().filter_map(|&(si, ai)| path.subpaths.get(si)?.anchors.get(ai)) {
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
//! art painting its members (its raster effects then apply to the composite).
|
||||
//! - [`clip_outline`] is the region a clip group clips to, shared by the renderer and the SVG and
|
||||
//! PDF writers.
|
||||
//! - [`preview`] measures objects as they are drawn, with their geometry effects (Use Preview
|
||||
//! Bounds).
|
||||
//! - **Plug-in effects** (`plugin.<plug-in id>`, Effect › Plug-ins) are geometry effects run by an
|
||||
//! installed WebAssembly plug-in (`vectorcraft-plugins`); [`effect_info`] describes them like
|
||||
//! built-in ones, and an effect whose plug-in isn't installed leaves the geometry as it is.
|
||||
@@ -41,6 +43,7 @@ mod lighting;
|
||||
mod live;
|
||||
mod marks;
|
||||
pub mod pixel;
|
||||
pub mod preview;
|
||||
mod raster;
|
||||
mod reshape;
|
||||
mod revolve;
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
//! Use Preview Bounds: objects measured as they are drawn, with their geometry effects.
|
||||
//!
|
||||
//! [`drawn_art`] gives an object whose geometry effects change what it draws as plain art. A path
|
||||
//! or compound path is given as its effected geometry, and a fill or stroke with geometry effects
|
||||
//! of its own paints its own geometry. Type, images, live objects and symbol instances are given as
|
||||
//! their reshaped art ([`reshape`]), and a symbol instance is also given as its reshaped art when
|
||||
//! its symbol's art has geometry effects. Groups and layers with geometry or Pathfinder effects of
|
||||
//! 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
|
||||
//! 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
|
||||
//! [`Selection::anchor_bounds`] measure the objects themselves. Raster effects (shadows, glows,
|
||||
//! blurs) are not measured. In one object, the art of at most 4,096 symbol instances is measured
|
||||
//! with its geometry effects, and the other instances in it are measured without them.
|
||||
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use vectorcraft_doc::{Appearance, AppearanceItem, Document, Node, NodeId, NodeKind, OrientedBox, Selection, SymbolBoxes};
|
||||
use vectorcraft_geom::{Affine, PathData, Rect, Shape, union_opt};
|
||||
|
||||
use crate::group::has_own_paint;
|
||||
use crate::reshape::{geometry_effects, leaf_geometry};
|
||||
use crate::{
|
||||
GeomContext, apply_geometry_with, evaluate_container, expanded_live_group, has_geometry, has_item_geometry, has_pathfinder, needs_outline,
|
||||
outline_art, paints, reshape,
|
||||
};
|
||||
|
||||
/// Most symbol instances in one object whose art [`Drawn`] measures with its geometry effects:
|
||||
/// past it, the other instances in the object are measured without geometry effects.
|
||||
const MAX_INSTANCES: usize = 4096;
|
||||
|
||||
/// The bounds of `ids`: with `preview`, their visual bounds as drawn ([`visual_bounds`]), else
|
||||
/// their geometric bounds ([`Document::bounds_of`]).
|
||||
pub fn bounds_of(doc: &Document, ids: &[NodeId], preview: bool) -> Option<Rect> {
|
||||
if !preview {
|
||||
return doc.bounds_of(ids, false);
|
||||
}
|
||||
let symbols = doc.symbol_boxes();
|
||||
let drawn = Drawn::new(&symbols);
|
||||
ids.iter().filter_map(|id| doc.node(*id)).fold(None, |acc, n| union_opt(acc, drawn.next().visual_bounds(n)))
|
||||
}
|
||||
|
||||
/// The bounding box of `ids` square to their shared angle ([`Document::bbox_angle`]), measured as
|
||||
/// [`bounds_of`] measures them: [`Document::oriented_bounds`] with `preview` false.
|
||||
pub fn oriented_bounds(doc: &Document, ids: &[NodeId], preview: bool) -> Option<OrientedBox> {
|
||||
if !preview {
|
||||
return doc.oriented_bounds(ids, false);
|
||||
}
|
||||
let angle = doc.bbox_angle(ids);
|
||||
if angle == 0.0 {
|
||||
return bounds_of(doc, ids, true).map(OrientedBox::aligned);
|
||||
}
|
||||
let to_local = OrientedBox { angle, rect: Rect::ZERO }.to_doc().inverse();
|
||||
let symbols = doc.symbol_boxes();
|
||||
let drawn = Drawn::new(&symbols);
|
||||
let rect = ids.iter().filter_map(|id| doc.node(*id)).fold(None, |acc, n| union_opt(acc, drawn.next().bounds_in(n, to_local)))?;
|
||||
Some(OrientedBox { angle, rect })
|
||||
}
|
||||
|
||||
/// With anchors selected with Direct Selection, the box around those anchors and the objects
|
||||
/// selected whole, measured by [`bounds_of`]. `None` without any ([`Selection::anchor_bounds`]).
|
||||
pub fn anchor_bounds(selection: &Selection, doc: &Document, preview: bool) -> Option<Rect> {
|
||||
if selection.anchors.is_empty() {
|
||||
return None;
|
||||
}
|
||||
union_opt(bounds_of(doc, &selection.whole(), preview), selection.anchors_box(doc))
|
||||
}
|
||||
|
||||
/// The visual bounds of `n` as drawn: those of its [`drawn_art`], else
|
||||
/// [`Node::visual_bounds_with`].
|
||||
pub fn visual_bounds(symbols: &SymbolBoxes, n: &Node) -> Option<Rect> {
|
||||
Drawn::new(symbols).visual_bounds(n)
|
||||
}
|
||||
|
||||
/// [`visual_bounds`] of `n` as `a` would place it, without changing it ([`Node::bounds_in`]):
|
||||
/// the art is drawn where `n` is and then placed by `a`.
|
||||
pub fn bounds_in(symbols: &SymbolBoxes, n: &Node, a: Affine) -> Option<Rect> {
|
||||
Drawn::new(symbols).bounds_in(n, a)
|
||||
}
|
||||
|
||||
/// `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.
|
||||
pub fn drawn_art(symbols: &SymbolBoxes, n: &Node) -> Option<Node> {
|
||||
Drawn::new(symbols).drawn_art(n)
|
||||
}
|
||||
|
||||
/// Whether [`drawn_art`] gives `n` art of its own, or would give one to an object in it. A symbol
|
||||
/// instance in `n` counts when its symbol's art has a geometry effect.
|
||||
pub fn draws_geometry(symbols: &SymbolBoxes, n: &Node) -> bool {
|
||||
Drawn::new(symbols).draws_geometry(n)
|
||||
}
|
||||
|
||||
/// One measurement: whether the art of each symbol checked so far draws geometry, by symbol name,
|
||||
/// and how many more instances' art it measures with its geometry effects in the current object ([`MAX_INSTANCES`]).
|
||||
struct Drawn<'s, 'd> {
|
||||
symbols: &'s SymbolBoxes<'d>,
|
||||
draws: RefCell<HashMap<String, bool>>,
|
||||
left: Cell<usize>,
|
||||
}
|
||||
|
||||
impl<'s, 'd> Drawn<'s, 'd> {
|
||||
fn new(symbols: &'s SymbolBoxes<'d>) -> Self {
|
||||
Drawn { symbols, draws: RefCell::default(), left: Cell::new(MAX_INSTANCES) }
|
||||
}
|
||||
|
||||
/// Sets the count of instances left back to [`MAX_INSTANCES`], for the next object.
|
||||
fn next(&self) -> &Self {
|
||||
self.left.set(MAX_INSTANCES);
|
||||
self
|
||||
}
|
||||
|
||||
/// See [`visual_bounds`].
|
||||
fn visual_bounds(&self, n: &Node) -> Option<Rect> {
|
||||
match self.drawn_art(n) {
|
||||
Some(art) => art.visual_bounds_with(self.symbols),
|
||||
None => n.visual_bounds_with(self.symbols),
|
||||
}
|
||||
}
|
||||
|
||||
/// See [`bounds_in`].
|
||||
fn bounds_in(&self, n: &Node, a: Affine) -> Option<Rect> {
|
||||
if a == Affine::IDENTITY {
|
||||
return self.visual_bounds(n);
|
||||
}
|
||||
match self.drawn_art(n) {
|
||||
Some(art) => art.bounds_in(self.symbols, a, true),
|
||||
None => n.bounds_in(self.symbols, a, true),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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) };
|
||||
// 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) {
|
||||
art = m;
|
||||
}
|
||||
Some(art)
|
||||
}
|
||||
|
||||
/// See [`draws_geometry`].
|
||||
fn draws_geometry(&self, n: &Node) -> bool {
|
||||
let mut found = false;
|
||||
n.walk(&mut |c| {
|
||||
found = found
|
||||
|| match &c.kind {
|
||||
NodeKind::SymbolInstance { symbol, .. } => !geometry_effects(c).is_empty() || self.symbol_draws(symbol),
|
||||
_ => reshapes(c),
|
||||
};
|
||||
});
|
||||
found
|
||||
}
|
||||
|
||||
/// Whether the art of the symbol named `name` has effects and [`Self::draws_geometry`], worked
|
||||
/// out once for each name.
|
||||
fn symbol_draws(&self, name: &str) -> bool {
|
||||
if let Some(d) = self.draws.borrow().get(name) {
|
||||
return *d;
|
||||
}
|
||||
let d = self.symbols.has_effects(name) && self.symbols.measure_art(name, |art| self.draws_geometry(art)).unwrap_or(false);
|
||||
self.draws.borrow_mut().insert(name.to_string(), d);
|
||||
d
|
||||
}
|
||||
|
||||
/// The art the geometry effects of `n` itself give it, or that a symbol instance gets from the
|
||||
/// geometry effects in its symbol's art. A group or layer with fills or strokes of its own that
|
||||
/// paint, around members with geometry effects, is evaluated too: its fills and strokes paint
|
||||
/// the members' geometry before their effects. A blend, envelope or repeat whose objects have
|
||||
/// geometry effects is evaluated as the renderer evaluates it ([`expanded_live_group`]), and
|
||||
/// the objects in its evaluated art keep their geometry effects. `None` when it has none, and
|
||||
/// for a symbol instance past [`MAX_INSTANCES`].
|
||||
fn own_art(&self, n: &Node) -> Option<Node> {
|
||||
let inner = || n.children().is_some_and(|ch| ch.iter().any(|c| self.draws_geometry(c)));
|
||||
let painted = || has_own_paint(n) && inner();
|
||||
match &n.kind {
|
||||
NodeKind::Group { .. } | NodeKind::Layer { .. } if reshapes(n) || painted() => evaluate_container(n),
|
||||
NodeKind::Path { .. } | NodeKind::Compound { .. } if reshapes(n) => path_art(n),
|
||||
NodeKind::SymbolInstance { symbol, .. } => {
|
||||
if geometry_effects(n).is_empty() && !self.symbol_draws(symbol) {
|
||||
return None;
|
||||
}
|
||||
let left = self.left.get().checked_sub(1)?;
|
||||
self.left.set(left);
|
||||
let art = self.symbols.measure_art(symbol, Arc::clone)?;
|
||||
// An instance in perspective is projected by `reshape`.
|
||||
reshape(n, Some(&*art)).or_else(|| outline_art(n, Some(&*art)))
|
||||
}
|
||||
_ if reshapes(n) => reshape(n, None),
|
||||
NodeKind::Blend { .. } | NodeKind::Envelope { .. } | NodeKind::Repeat(_) if inner() => {
|
||||
Some(expanded_live_group(Some(self.symbols.document()), n))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// `n`, a group, layer or intertwine without a clip, with each member that has a
|
||||
/// [`Self::drawn_art`] replaced by it. `None` for any other object and when no member has one.
|
||||
fn with_drawn_members(&self, n: &Node) -> Option<Node> {
|
||||
if n.clips() || !matches!(n.kind, NodeKind::Group { .. } | NodeKind::Layer { .. } | NodeKind::Intertwine { .. }) {
|
||||
return None;
|
||||
}
|
||||
let drawn: Vec<(usize, Node)> = n.children()?.iter().enumerate().filter_map(|(i, c)| Some((i, self.drawn_art(c)?))).collect();
|
||||
if drawn.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let mut m = n.clone();
|
||||
let children = m.children_mut()?;
|
||||
for (i, c) in drawn {
|
||||
if let Some(slot) = children.get_mut(i) {
|
||||
*slot = Arc::new(c);
|
||||
}
|
||||
}
|
||||
Some(m)
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether `n` itself has a geometry effect that [`Drawn::own_art`] applies, its own or one of its
|
||||
/// fill's or stroke's (a group or layer: also a Pathfinder effect; a symbol instance: only its own).
|
||||
fn reshapes(n: &Node) -> bool {
|
||||
match &n.kind {
|
||||
NodeKind::Group { .. } | NodeKind::Layer { .. } => {
|
||||
has_pathfinder(n) || !geometry_effects(n).is_empty() || n.appearance.items.iter().any(|i| paints(i) && has_geometry(i.effects()))
|
||||
}
|
||||
NodeKind::Path { guide: false, .. } | NodeKind::Compound { .. } => {
|
||||
has_geometry(&n.appearance.effects) || n.appearance.items.iter().any(|i| paints(i) && has_geometry(i.effects()))
|
||||
}
|
||||
_ => needs_outline(n) && (!geometry_effects(n).is_empty() || has_item_geometry(n)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Path or compound path `n` as a group of paths: its geometry after its own geometry effects
|
||||
/// painted by its fills and strokes without geometry effects, and that geometry after the
|
||||
/// geometry effects of each other fill or stroke that paints, painted by that fill or stroke, as
|
||||
/// the renderer draws them. The first path is left out when only fills or strokes with geometry
|
||||
/// effects paint. The paths keep `n`'s id and fill rule and carry no effects.
|
||||
fn path_art(n: &Node) -> Option<Node> {
|
||||
let (path, rule) = leaf_geometry(n)?;
|
||||
let ctx = GeomContext::of(n);
|
||||
let g = apply_geometry_with(&n.appearance.effects, &path, path.to_bezpath().bounding_box(), &ctx);
|
||||
let piece = |geometry: PathData, items: Vec<AppearanceItem>| {
|
||||
let mut ap: Appearance = (*n.appearance).clone();
|
||||
ap.items = items;
|
||||
ap.effects.clear();
|
||||
for i in &mut ap.items {
|
||||
i.effects_mut().clear();
|
||||
}
|
||||
let mut p = Node::path(n.id, geometry, ap);
|
||||
if let NodeKind::Path { rule: r, .. } = &mut p.kind {
|
||||
*r = rule;
|
||||
}
|
||||
Arc::new(p)
|
||||
};
|
||||
let (own, plain): (Vec<&AppearanceItem>, Vec<&AppearanceItem>) = n.appearance.items.iter().partition(|i| paints(i) && has_geometry(i.effects()));
|
||||
let mut pieces = vec![];
|
||||
if own.is_empty() || plain.iter().any(|i| paints(i)) {
|
||||
pieces.push(piece(g.clone(), plain.into_iter().cloned().collect()));
|
||||
}
|
||||
let bounds = g.to_bezpath().bounding_box();
|
||||
for item in own {
|
||||
let geometry = apply_geometry_with(item.effects(), &g, bounds, &ctx.item(item));
|
||||
pieces.push(piece(geometry, vec![item.clone()]));
|
||||
}
|
||||
Some(Node::group(n.id, pieces))
|
||||
}
|
||||
+111
-18
@@ -10,6 +10,7 @@ use vectorcraft_doc::corners::set_corners;
|
||||
use vectorcraft_doc::{Appearance, Document, Knockout, LiveCorners, LiveShape, Node, NodeId, NodeKind, OrientedBox, Scaling};
|
||||
use vectorcraft_geom::shapes::CornerKind;
|
||||
use vectorcraft_geom::{Affine, FillRule, PathData, Point, Rect, Vec2};
|
||||
use vectorcraft_render::effects;
|
||||
|
||||
use super::edit::{duplicate_in, outermost, selected_roots, selected_roots_to_transform};
|
||||
use super::opacitymask::percent;
|
||||
@@ -302,7 +303,7 @@ fn origin_of(s: &Session, p: &Value, ids: &[NodeId]) -> Result<Point> {
|
||||
// Direct-selected anchors transform alone (see [`apply_transform`]), about their own centre.
|
||||
let st = s.doc()?;
|
||||
if !bool_or(p, "copy", false)
|
||||
&& let Some(b) = st.selection.anchor_bounds(&st.doc, s.prefs.use_preview_bounds)
|
||||
&& let Some(b) = effects::preview::anchor_bounds(&st.selection, &st.doc, s.prefs.use_preview_bounds)
|
||||
{
|
||||
return Ok(b.center());
|
||||
}
|
||||
@@ -313,10 +314,16 @@ fn origin_of(s: &Session, p: &Value, ids: &[NodeId]) -> Result<Point> {
|
||||
/// its `strokes` and `corners` params, else the preferences. The values join the command's journal
|
||||
/// entry, so replaying it scales the same way whatever the preferences are then.
|
||||
pub(crate) fn scaling(s: &mut Session, p: &Value) -> Scaling {
|
||||
let sc = scaling_options(s, p);
|
||||
s.note_journal("strokes", json!(sc.strokes));
|
||||
s.note_journal("corners", json!(!sc.keep_corners));
|
||||
sc
|
||||
}
|
||||
|
||||
/// [`scaling`] without noting the values in the journal.
|
||||
fn scaling_options(s: &Session, p: &Value) -> Scaling {
|
||||
let strokes = bool_or(p, "strokes", s.prefs.scale_strokes);
|
||||
let corners = bool_or(p, "corners", s.prefs.scale_corners);
|
||||
s.note_journal("strokes", json!(strokes));
|
||||
s.note_journal("corners", json!(corners));
|
||||
Scaling {
|
||||
strokes,
|
||||
effects: strokes.then_some(vectorcraft_render::effects::scale_effect),
|
||||
@@ -1227,14 +1234,15 @@ fn set_props(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
impl Session {
|
||||
/// The bounds of `ids` the Transform panel, the bounding box, Align and Distribute measure:
|
||||
/// visual bounds (strokes included) with Use Preview Bounds on, else geometric bounds.
|
||||
/// with Use Preview Bounds on, visual bounds as drawn (strokes and geometry effects included,
|
||||
/// see [`effects::preview`]), else geometric bounds.
|
||||
pub fn transform_bounds(&self, ids: &[NodeId]) -> Option<Rect> {
|
||||
self.doc().ok()?.doc.bounds_of(ids, self.prefs.use_preview_bounds)
|
||||
effects::preview::bounds_of(&self.doc().ok()?.doc, ids, self.prefs.use_preview_bounds)
|
||||
}
|
||||
/// [`Session::transform_bounds`] square to the objects' own angle (their rotated bounding
|
||||
/// box, see [`Document::oriented_bounds`]).
|
||||
pub fn transform_box(&self, ids: &[NodeId]) -> Option<OrientedBox> {
|
||||
self.doc().ok()?.doc.oriented_bounds(ids, self.prefs.use_preview_bounds)
|
||||
effects::preview::oriented_bounds(&self.doc().ok()?.doc, ids, self.prefs.use_preview_bounds)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1270,9 +1278,9 @@ fn spacing(p: &Value, cmd: &str) -> Result<Option<f64>> {
|
||||
.transpose()
|
||||
}
|
||||
|
||||
/// `ids` with their bounds (`preview`: visual bounds).
|
||||
/// `ids` with their bounds (`preview`: visual bounds as drawn, see [`effects::preview::bounds_of`]).
|
||||
fn items_bounds(d: &Document, ids: &[NodeId], preview: bool) -> Vec<(NodeId, Rect)> {
|
||||
ids.iter().filter_map(|id| Some((*id, d.bounds_of(&[*id], preview)?))).collect()
|
||||
ids.iter().filter_map(|id| Some((*id, effects::preview::bounds_of(d, &[*id], preview)?))).collect()
|
||||
}
|
||||
|
||||
/// Move each object of `moves` by its offset and each ruler guide of `guides` (index, position)
|
||||
@@ -1336,7 +1344,7 @@ fn reference_rect(s: &Session, p: &Value, ids: &[NodeId], preview: bool) -> Resu
|
||||
let st = s.doc()?;
|
||||
match align_to(s, p) {
|
||||
Some("artboard") => {
|
||||
let b = st.doc.bounds_of(ids, preview).unwrap_or_default();
|
||||
let b = effects::preview::bounds_of(&st.doc, ids, preview).unwrap_or_default();
|
||||
// Align to Artboard uses artboard `artboard` (the app passes the active one). Without
|
||||
// it, or with an index the document doesn't have, it uses the artboard under the center
|
||||
// of the selection, or the first one when no artboard is there.
|
||||
@@ -1346,9 +1354,9 @@ fn reference_rect(s: &Session, p: &Value, ids: &[NodeId], preview: bool) -> Resu
|
||||
}
|
||||
Some("key") => {
|
||||
let k = st.selection.key.ok_or_else(|| EngineError::Other("no key object".into()))?;
|
||||
st.doc.bounds_of(&[k], preview).ok_or(EngineError::NoNode(k))
|
||||
effects::preview::bounds_of(&st.doc, &[k], preview).ok_or(EngineError::NoNode(k))
|
||||
}
|
||||
_ => st.doc.bounds_of(ids, preview).ok_or_else(|| EngineError::Other("nothing to align".into())),
|
||||
_ => effects::preview::bounds_of(&st.doc, ids, preview).ok_or_else(|| EngineError::Other("nothing to align".into())),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1721,7 +1729,7 @@ fn set_bounds(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let st = s.doc()?;
|
||||
if transforms_objects(p)
|
||||
&& !bool_or(p, "copy", false)
|
||||
&& let Some(b) = st.selection.anchor_bounds(&st.doc, s.prefs.use_preview_bounds)
|
||||
&& let Some(b) = effects::preview::anchor_bounds(&st.selection, &st.doc, s.prefs.use_preview_bounds)
|
||||
{
|
||||
return set_anchor_bounds(s, p, ids, b);
|
||||
}
|
||||
@@ -1730,12 +1738,12 @@ fn set_bounds(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
// the math below runs in its frame.
|
||||
let frame = d.oriented_bounds(&ids, false).ok_or_else(|| EngineError::Other("no bounds".into()))?;
|
||||
let g = frame.rect;
|
||||
// With Use Preview Bounds the values measure the visual box, whose margins around the
|
||||
// With Use Preview Bounds the values measure the visual box as drawn, whose margins around the
|
||||
// geometric box (left, top, right, bottom) scale with the strokes or stay. The margins of
|
||||
// `vs`, the visual box without the strokes in symbols' art, scale; those strokes keep their
|
||||
// weight when an instance scales, so the rest of the margins stay.
|
||||
let (v, vs) = if s.prefs.use_preview_bounds {
|
||||
(d.oriented_bounds(&ids, true).map_or(g, |b| b.rect), scaling_box(d, &ids, &frame).unwrap_or(g))
|
||||
(effects::preview::oriented_bounds(d, &ids, true).map_or(g, |b| b.rect), scaling_box(d, &ids, &frame).unwrap_or(g))
|
||||
} else {
|
||||
(g, g)
|
||||
};
|
||||
@@ -1784,16 +1792,101 @@ fn set_bounds(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
// The scales and the point above can miss the size or the reference point when the margins of
|
||||
// the objects differ, the members of groups and the objects in symbols' art included, and with
|
||||
// strokes that scale, when the sizes are out of proportion. The objects are then measured one
|
||||
// by one ([`Spans::measure`]), and the scales solved for.
|
||||
let solved = if s.prefs.use_preview_bounds && w >= 0.0 && h >= 0.0 {
|
||||
Spans::measure(d, &ids, &frame, strokes).refit(rp, refi, [w, h], (sx, sy, new_rp))
|
||||
} else {
|
||||
// by one ([`Spans::measure`]), and the scales solved for. Objects with geometry effects are
|
||||
// measured as drawn after each scale tried instead ([`refit_drawn`]).
|
||||
let symbols = d.symbol_boxes();
|
||||
let drawn = || ids.iter().filter_map(|id| d.node(*id)).any(|n| effects::preview::draws_geometry(&symbols, n));
|
||||
let solved = if !(s.prefs.use_preview_bounds && w >= 0.0 && h >= 0.0) {
|
||||
None
|
||||
} else if transforms_objects(p) && !bool_or(p, "typeAreas", false) && drawn() {
|
||||
let options = scaling_options(s, p);
|
||||
let to_local = frame.to_doc().inverse();
|
||||
let measure = |sx: f64, sy: f64| {
|
||||
let xf = frame.conjugate(about(rp, Affine::scale_non_uniform(sx, sy)));
|
||||
let sc = if Scaling::factor(xf).is_some() { options } else { Scaling::default() };
|
||||
ids.iter().filter_map(|id| d.node(*id)).fold(None, |acc, n| {
|
||||
let mut n = n.clone();
|
||||
n.transform(xf, sc);
|
||||
vectorcraft_geom::union_opt(acc, effects::preview::bounds_in(&symbols, &n, to_local))
|
||||
})
|
||||
};
|
||||
// The scales above keep the effects' reach as a margin that stays, and give 0 for a size
|
||||
// smaller than that reach. Strokes and effects that scale shrink the reach with the art, so
|
||||
// the search then starts from the size wanted over the size as drawn.
|
||||
let seed = |s: f64, size: f64, now: f64| if s <= 0.0 && size > 0.0 && now > 1e-9 { size / now } else { s };
|
||||
refit_drawn(measure, refi, [w, h], (seed(sx, w, v.width()), seed(sy, h, v.height())), [g.width() > 1e-9, g.height() > 1e-9])
|
||||
} else {
|
||||
Spans::measure(d, &ids, &frame, strokes).refit(rp, refi, [w, h], (sx, sy, new_rp))
|
||||
};
|
||||
let (scale, new_rp) = solved.map_or((scale, new_rp), |(sx, sy, at)| (about(rp, Affine::scale_non_uniform(sx, sy)), at));
|
||||
apply_transform(s, "Transform", ids, frame.conjugate(Affine::translate(to - new_rp) * scale), p, false)
|
||||
}
|
||||
|
||||
/// The scales along x and y that give the box `measure` returns for them the size `size`, and point
|
||||
/// `refi` of that box: the point that goes to the reference point's place. The search starts at
|
||||
/// `start`. Each step measures how the width and the height change with each scale that `free`
|
||||
/// marks true, by measuring again with that scale moved by 1e-4 of it (at least 1e-6), and moves
|
||||
/// those scales by the least-squares solution of that linear change, never below 0. The width can
|
||||
/// change with the y scale and the height with the x scale (a geometry effect can turn the art). A
|
||||
/// scale that `free` marks false stays as it is. The search stops when both sizes are within 1e-9
|
||||
/// of the sizes wanted (of the largest of 1 and those sizes), when no scale moves, when `measure`
|
||||
/// finds no box, or after 16 steps, and returns the scales that came nearest. `None` when `measure`
|
||||
/// finds no box for `start`.
|
||||
fn refit_drawn(
|
||||
measure: impl Fn(f64, f64) -> Option<Rect>,
|
||||
refi: usize,
|
||||
size: [f64; 2],
|
||||
start: (f64, f64),
|
||||
free: [bool; 2],
|
||||
) -> Option<(f64, f64, Point)> {
|
||||
let [w, h] = size;
|
||||
let tolerance = 1e-9 * w.max(h).max(1.0);
|
||||
let miss = |b: Rect| (b.width() - w).abs() + (b.height() - h).abs();
|
||||
let (mut sx, mut sy) = start;
|
||||
let mut b = measure(sx, sy)?;
|
||||
let mut best = (sx, sy, b);
|
||||
for _ in 0..16 {
|
||||
let f = [b.width() - w, b.height() - h];
|
||||
if f[0].abs() <= tolerance && f[1].abs() <= tolerance {
|
||||
break;
|
||||
}
|
||||
// How the width and the height change with the x scale (`a`) and the y scale (`c`).
|
||||
let change = |x: bool| -> Option<[f64; 2]> {
|
||||
let (on, s) = if x { (free[0], sx) } else { (free[1], sy) };
|
||||
if !on {
|
||||
return Some([0.0, 0.0]);
|
||||
}
|
||||
let ds = (s.abs() * 1e-4).max(1e-6);
|
||||
let m = if x { measure(sx + ds, sy) } else { measure(sx, sy + ds) }?;
|
||||
Some([(m.width() - b.width()) / ds, (m.height() - b.height()) / ds])
|
||||
};
|
||||
let (Some(a), Some(c)) = (change(true), change(false)) else { break };
|
||||
let dot = |u: [f64; 2], v: [f64; 2]| u[0] * v[0] + u[1] * v[1];
|
||||
let (aa, ac, cc, af, cf) = (dot(a, a), dot(a, c), dot(c, c), dot(a, f), dot(c, f));
|
||||
let det = aa * cc - ac * ac;
|
||||
let (dx, dy) = if det > 1e-12 * aa * cc && det > 0.0 {
|
||||
((ac * cf - cc * af) / det, (ac * af - aa * cf) / det)
|
||||
} else if aa >= cc && aa > 0.0 {
|
||||
(-af / aa, 0.0)
|
||||
} else if cc > 0.0 {
|
||||
(0.0, -cf / cc)
|
||||
} else {
|
||||
break;
|
||||
};
|
||||
let (nx, ny) = ((sx + dx).max(0.0), (sy + dy).max(0.0));
|
||||
if !(nx.is_finite() && ny.is_finite()) || (nx == sx && ny == sy) {
|
||||
break;
|
||||
}
|
||||
let Some(next) = measure(nx, ny) else { break };
|
||||
(sx, sy, b) = (nx, ny, next);
|
||||
if miss(b) < miss(best.2) {
|
||||
best = (sx, sy, b);
|
||||
}
|
||||
}
|
||||
let (sx, sy, b) = best;
|
||||
Some((sx, sy, vectorcraft_geom::reference_point(b, refi)))
|
||||
}
|
||||
|
||||
/// The `width` and `height` `object.setBounds` asks for a box now `b`: the one not given stays, or
|
||||
/// with `proportional` follows the one given.
|
||||
fn wanted_size(p: &Value, b: Rect) -> (f64, f64) {
|
||||
|
||||
@@ -48,19 +48,17 @@ fn outline_object_is_applied_in_one_undo_step_and_takes_no_parameters() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn type_with_outline_object_stays_editable_type_and_keeps_its_bounds() {
|
||||
fn type_with_outline_object_stays_editable_type_and_keeps_its_geometric_bounds() {
|
||||
let mut s = session();
|
||||
let id = text(&mut s);
|
||||
let before = [false, true].map(|preview| {
|
||||
s.prefs.use_preview_bounds = preview;
|
||||
s.transform_bounds(&[id])
|
||||
});
|
||||
let before = s.transform_bounds(&[id]);
|
||||
apply(&mut s, id);
|
||||
let after = [false, true].map(|preview| {
|
||||
s.prefs.use_preview_bounds = preview;
|
||||
s.transform_bounds(&[id])
|
||||
});
|
||||
assert_eq!(after, before);
|
||||
assert_eq!(s.transform_bounds(&[id]), before);
|
||||
// With Use Preview Bounds on, the type is measured by its glyph outlines.
|
||||
s.prefs.use_preview_bounds = true;
|
||||
let glyphs = vectorcraft_render::effects::reshape(&node(&s, id), None).unwrap().geometric_bounds();
|
||||
assert_eq!(s.transform_bounds(&[id]), glyphs);
|
||||
s.prefs.use_preview_bounds = false;
|
||||
// A native save keeps the effect.
|
||||
let back = vectorcraft_format::load(&vectorcraft_format::save(&s.doc().unwrap().doc, false)).unwrap();
|
||||
let ids: Vec<String> = back.node(id).unwrap().appearance.effects.iter().map(|e| e.id.clone()).collect();
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
//! Use Preview Bounds: the Transform panel values, the bounding box, Align and Distribute measure
|
||||
//! visual bounds (strokes included) when it is on.
|
||||
//! visual bounds (strokes and geometry effects included) when it is on.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::NodeId;
|
||||
use vectorcraft_geom::Rect;
|
||||
use vectorcraft_color::{Color, Paint};
|
||||
use vectorcraft_doc::{Appearance, Document, Effect, Node, NodeId, NodeKind, Symbol};
|
||||
use vectorcraft_geom::{Affine, Rect, shapes};
|
||||
use vectorcraft_tools::{PointerEvent, PointerKind};
|
||||
|
||||
use super::*;
|
||||
@@ -75,6 +78,335 @@ fn distribute_spacing_measures_preview_bounds() {
|
||||
assert_eq!(geometric(&s, b).x0 - geometric(&s, a).x1, 10.0);
|
||||
}
|
||||
|
||||
/// A 100 pt square at (`x`, 100) without a stroke, with Offset Path 10.
|
||||
fn offset_square(s: &mut Session, x: f64) -> NodeId {
|
||||
let id = NodeId(run(s, "shape.rectangle", json!({"x": x, "y": 100, "width": 100, "height": 100}))["id"].as_u64().unwrap());
|
||||
run(s, "paint.setStroke", json!({"none": true}));
|
||||
run(s, "effect.apply", json!({"effect": "path.offsetPath", "params": {"offset": 10}, "ids": [id.0], "item": null}));
|
||||
id
|
||||
}
|
||||
|
||||
/// Point type `text` (48 pt) at (`x`, 100) with Outline Object.
|
||||
fn outlined_type(s: &mut Session, x: f64, text: &str) -> NodeId {
|
||||
let id = NodeId(run(s, "text.create", json!({"x": x, "y": 100, "text": text, "size": 48}))["id"].as_u64().unwrap());
|
||||
run(s, "effect.apply", json!({"effect": "path.outlineObject", "ids": [id.0], "item": null}));
|
||||
id
|
||||
}
|
||||
|
||||
/// The box of the glyph outlines that type `id` with Outline Object is drawn as.
|
||||
fn glyphs(s: &Session, id: NodeId) -> Rect {
|
||||
let n = s.doc().unwrap().doc.node(id).unwrap().clone();
|
||||
vectorcraft_render::effects::reshape(&n, None).unwrap().geometric_bounds().unwrap()
|
||||
}
|
||||
|
||||
fn near(a: Rect, b: Rect, tolerance: f64) -> bool {
|
||||
[(a.x0, b.x0), (a.y0, b.y0), (a.x1, b.x1), (a.y1, b.y1)].iter().all(|(x, y)| (x - y).abs() <= tolerance)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_square_with_offset_path_is_measured_by_the_offset_shape() {
|
||||
let mut s = session();
|
||||
let a = offset_square(&mut s, 100.0);
|
||||
preview(&mut s, true);
|
||||
assert!(near(s.transform_bounds(&[a]).unwrap(), Rect::new(90.0, 90.0, 210.0, 210.0), 1e-6), "{:?}", s.transform_bounds(&[a]));
|
||||
// Align left with an unstroked rectangle at x 20: the offset shape's left edge lines up with it.
|
||||
rect(&mut s, 20.0, 50.0, 0.0);
|
||||
run(&mut s, "select.all", json!({}));
|
||||
run(&mut s, "object.align", json!({"horizontal": "left"}));
|
||||
assert!((geometric(&s, a).x0 - 30.0).abs() < 1e-6, "{:?}", geometric(&s, a));
|
||||
// Distribute Spacing 0: the offset shapes touch, so the squares are 20 pt apart.
|
||||
let mut s = session();
|
||||
let (a, b) = (offset_square(&mut s, 0.0), offset_square(&mut s, 300.0));
|
||||
preview(&mut s, true);
|
||||
run(&mut s, "select.set", json!({"ids": [a.0, b.0]}));
|
||||
run(&mut s, "object.distributeSpacing", json!({"axis": "horizontal", "spacing": 0}));
|
||||
assert!((geometric(&s, b).x0 - geometric(&s, a).x1 - 20.0).abs() < 1e-6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_stroke_with_offset_path_of_its_own_is_measured_where_it_paints() {
|
||||
// A 100 pt square with a 10 pt stroke whose own Offset Path 10 moves it 10 pt out: the stroke
|
||||
// paints from 15 pt outside the square to 5 pt outside it.
|
||||
let mut s = session();
|
||||
let id = NodeId(run(&mut s, "shape.rectangle", json!({"x": 100, "y": 100, "width": 100, "height": 100}))["id"].as_u64().unwrap());
|
||||
run(&mut s, "stroke.set", json!({"weight": 10}));
|
||||
let items = s.doc().unwrap().doc.node(id).unwrap().appearance.items.clone();
|
||||
let stroke = items.iter().position(|i| matches!(i, vectorcraft_doc::AppearanceItem::Stroke(_))).unwrap();
|
||||
run(&mut s, "effect.apply", json!({"effect": "path.offsetPath", "params": {"offset": 10}, "ids": [id.0], "item": stroke}));
|
||||
preview(&mut s, true);
|
||||
assert!(near(s.transform_bounds(&[id]).unwrap(), Rect::new(85.0, 85.0, 215.0, 215.0), 1e-6), "{:?}", s.transform_bounds(&[id]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_groups_own_stroke_with_offset_path_is_measured_where_it_paints() {
|
||||
// A group of an unstroked 100 pt square, with a 10 pt stroke of its own whose Offset Path 10
|
||||
// moves it 10 pt out: the stroke paints from 15 pt outside the square to 5 pt outside it.
|
||||
let mut s = session();
|
||||
run(&mut s, "shape.rectangle", json!({"x": 100, "y": 100, "width": 100, "height": 100}));
|
||||
run(&mut s, "paint.setStroke", json!({"none": true}));
|
||||
let g = NodeId(run(&mut s, "object.group", json!({}))["id"].as_u64().unwrap());
|
||||
run(&mut s, "appearance.addStroke", json!({"ids": [g.0]}));
|
||||
run(&mut s, "paint.setStroke", json!({"ids": [g.0], "item": 0, "color": "#000000"}));
|
||||
run(&mut s, "stroke.set", json!({"ids": [g.0], "item": 0, "weight": 10}));
|
||||
run(&mut s, "effect.apply", json!({"effect": "path.offsetPath", "params": {"offset": 10}, "ids": [g.0], "item": 0}));
|
||||
preview(&mut s, true);
|
||||
assert!(near(s.transform_bounds(&[g]).unwrap(), Rect::new(85.0, 85.0, 215.0, 215.0), 1e-6), "{:?}", s.transform_bounds(&[g]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_groups_own_fill_is_measured_on_its_members_where_they_are() {
|
||||
// A group with a fill of its own around a 100 pt square whose fill a Transform effect moves
|
||||
// 100 pt right: the group's fill paints the square where it is, and the square's own fill
|
||||
// paints 100 pt to the right of it.
|
||||
let mut s = session();
|
||||
let id = NodeId(run(&mut s, "shape.rectangle", json!({"x": 100, "y": 100, "width": 100, "height": 100}))["id"].as_u64().unwrap());
|
||||
run(&mut s, "paint.setStroke", json!({"none": true}));
|
||||
let items = s.doc().unwrap().doc.node(id).unwrap().appearance.items.clone();
|
||||
let fill = items.iter().position(|i| i.is_fill()).unwrap();
|
||||
run(&mut s, "effect.apply", json!({"effect": "distort.transform", "params": {"moveH": 100}, "ids": [id.0], "item": fill}));
|
||||
let g = NodeId(run(&mut s, "object.group", json!({}))["id"].as_u64().unwrap());
|
||||
run(&mut s, "appearance.addFill", json!({"ids": [g.0]}));
|
||||
run(&mut s, "paint.setFill", json!({"ids": [g.0], "item": 0, "color": "#000000"}));
|
||||
preview(&mut s, true);
|
||||
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 only_fills_and_strokes_that_paint_are_measured() {
|
||||
let item = |s: &Session, id: NodeId, fill: bool| {
|
||||
let items = s.doc().unwrap().doc.node(id).unwrap().appearance.items.clone();
|
||||
items.iter().position(|i| i.is_fill() == fill).unwrap()
|
||||
};
|
||||
// A 100 pt square without a stroke whose fill a Transform effect moves 100 pt right is measured
|
||||
// where the fill paints, not where the path is.
|
||||
let mut s = session();
|
||||
let id = NodeId(run(&mut s, "shape.rectangle", json!({"x": 100, "y": 100, "width": 100, "height": 100}))["id"].as_u64().unwrap());
|
||||
run(&mut s, "paint.setStroke", json!({"none": true}));
|
||||
let fill = item(&s, id, true);
|
||||
run(&mut s, "effect.apply", json!({"effect": "distort.transform", "params": {"moveH": 100}, "ids": [id.0], "item": fill}));
|
||||
preview(&mut s, true);
|
||||
assert!(near(s.transform_bounds(&[id]).unwrap(), Rect::new(200.0, 100.0, 300.0, 200.0), 1e-6), "{:?}", s.transform_bounds(&[id]));
|
||||
// A hidden 10 pt stroke with Offset Path 10 of its own adds nothing to the box of the square,
|
||||
// which Offset Path 5 of the object's own takes 5 pt out.
|
||||
let mut s = session();
|
||||
let id = NodeId(run(&mut s, "shape.rectangle", json!({"x": 100, "y": 100, "width": 100, "height": 100}))["id"].as_u64().unwrap());
|
||||
run(&mut s, "stroke.set", json!({"weight": 10}));
|
||||
let stroke = item(&s, id, false);
|
||||
run(&mut s, "effect.apply", json!({"effect": "path.offsetPath", "params": {"offset": 10}, "ids": [id.0], "item": stroke}));
|
||||
run(&mut s, "appearance.setItem", json!({"index": stroke, "visible": false, "ids": [id.0]}));
|
||||
preview(&mut s, true);
|
||||
assert!(near(s.transform_bounds(&[id]).unwrap(), Rect::new(100.0, 100.0, 200.0, 200.0), 1e-6), "{:?}", s.transform_bounds(&[id]));
|
||||
run(&mut s, "effect.apply", json!({"effect": "path.offsetPath", "params": {"offset": 5}, "ids": [id.0], "item": null}));
|
||||
assert!(near(s.transform_bounds(&[id]).unwrap(), Rect::new(95.0, 95.0, 205.0, 205.0), 1e-6), "{:?}", s.transform_bounds(&[id]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn type_with_outline_object_aligns_by_its_glyphs() {
|
||||
// "ace" has no ascender or descender; "Tg" has a capital and a descender. Aligned top, their
|
||||
// glyph tops line up; aligned bottom, the bottom of the g lines up with the bottom of "ace".
|
||||
let edge = |vertical: &str| {
|
||||
let mut s = session();
|
||||
let a = outlined_type(&mut s, 20.0, "ace");
|
||||
let b = outlined_type(&mut s, 200.0, "Tg");
|
||||
preview(&mut s, true);
|
||||
run(&mut s, "select.set", json!({"ids": [a.0, b.0]}));
|
||||
run(&mut s, "object.align", json!({"vertical": vertical}));
|
||||
let (a, b) = (glyphs(&s, a), glyphs(&s, b));
|
||||
if vertical == "top" { (a.y0, b.y0) } else { (a.y1, b.y1) }
|
||||
};
|
||||
for vertical in ["top", "bottom"] {
|
||||
let (a, b) = edge(vertical);
|
||||
assert!((a - b).abs() < 1e-6, "{vertical}: {a} and {b}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn type_with_outline_object_measures_as_its_outlines() {
|
||||
let mut s = session();
|
||||
let id = outlined_type(&mut s, 40.0, "Outline");
|
||||
preview(&mut s, true);
|
||||
let measured = s.transform_bounds(&[id]).unwrap();
|
||||
assert!(near(measured, glyphs(&s, id), 1e-9), "{measured:?}");
|
||||
// The type's Create Outlines copy has the same visual box.
|
||||
run(&mut s, "select.set", json!({"ids": [id.0]}));
|
||||
let outlines: Vec<NodeId> =
|
||||
run(&mut s, "type.createOutlines", json!({}))["ids"].as_array().unwrap().iter().map(|v| NodeId(v.as_u64().unwrap())).collect();
|
||||
let copy = s.doc().unwrap().doc.bounds_of(&outlines, true).unwrap();
|
||||
assert!(near(measured, copy, 0.01), "{measured:?} {copy:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_bounds_sizes_the_glyphs_of_type_with_outline_object() {
|
||||
let mut s = session();
|
||||
let id = outlined_type(&mut s, 40.0, "Outline");
|
||||
preview(&mut s, true);
|
||||
let before = glyphs(&s, id);
|
||||
run(&mut s, "select.set", json!({"ids": [id.0]}));
|
||||
run(&mut s, "object.setBounds", json!({"width": 100, "reference": 0}));
|
||||
let after = glyphs(&s, id);
|
||||
assert!((after.width() - 100.0).abs() < 1e-6, "{after:?}");
|
||||
assert!((after.x0 - before.x0).abs() < 1e-6 && (after.y0 - before.y0).abs() < 1e-6, "{before:?} {after:?}");
|
||||
assert!(near(s.transform_bounds(&[id]).unwrap(), after, 1e-9));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_bounds_sizes_the_offset_shape_of_a_square_with_offset_path() {
|
||||
// The offset shape is 120 pt wide. Set to 220 wide from its top-left corner, it is 220 wide and
|
||||
// stays 120 high, with Scale Strokes & Effects off (the offset stays 10 pt) and on (the offset
|
||||
// scales with the square).
|
||||
for strokes in [false, true] {
|
||||
let mut s = session();
|
||||
let id = offset_square(&mut s, 100.0);
|
||||
preview(&mut s, true);
|
||||
run(&mut s, "select.set", json!({"ids": [id.0]}));
|
||||
run(&mut s, "object.setBounds", json!({"width": 220, "reference": 0, "strokes": strokes}));
|
||||
let v = s.transform_bounds(&[id]).unwrap();
|
||||
assert!(near(v, Rect::new(90.0, 90.0, 310.0, 210.0), 1e-6), "strokes {strokes}: {v:?}");
|
||||
}
|
||||
// Set to 10 x 10, smaller than the 20 pt the offset adds, with Scale Strokes & Effects on: the
|
||||
// offset shrinks with the square, and the offset shape is 10 x 10.
|
||||
let mut s = session();
|
||||
let id = offset_square(&mut s, 100.0);
|
||||
preview(&mut s, true);
|
||||
run(&mut s, "select.set", json!({"ids": [id.0]}));
|
||||
run(&mut s, "object.setBounds", json!({"width": 10, "height": 10, "reference": 0, "strokes": true}));
|
||||
let v = s.transform_bounds(&[id]).unwrap();
|
||||
assert!(near(v, Rect::new(90.0, 90.0, 100.0, 100.0), 1e-6), "{v:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_bounds_sizes_art_that_a_transform_effect_turns() {
|
||||
// A 100 x 50 rectangle that a Transform effect turns 90° is drawn 50 wide and 100 high. Set to
|
||||
// 100 x 100, it is drawn 100 x 100: its width stays 100 and its height doubles.
|
||||
let mut s = session();
|
||||
let id = rect(&mut s, 100.0, 100.0, 0.0);
|
||||
run(&mut s, "effect.apply", json!({"effect": "distort.transform", "params": {"rotate": 90}, "ids": [id.0], "item": null}));
|
||||
preview(&mut s, true);
|
||||
run(&mut s, "select.set", json!({"ids": [id.0]}));
|
||||
run(&mut s, "object.setBounds", json!({"width": 100, "height": 100}));
|
||||
let v = s.transform_bounds(&[id]).unwrap();
|
||||
assert!((v.width() - 100.0).abs() < 1e-6 && (v.height() - 100.0).abs() < 1e-6, "{v:?}");
|
||||
let g = geometric(&s, id);
|
||||
assert!((g.width() - 100.0).abs() < 1e-6 && (g.height() - 100.0).abs() < 1e-6, "{g:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_symbol_instance_is_measured_with_the_geometry_effects_in_its_art() {
|
||||
let mut s = session();
|
||||
offset_square(&mut s, 100.0);
|
||||
let id = NodeId(run(&mut s, "symbol.new", json!({"name": "Offset"}))["id"].as_u64().unwrap());
|
||||
preview(&mut s, true);
|
||||
let v = s.transform_bounds(&[id]).unwrap();
|
||||
assert!(near(v, Rect::new(90.0, 90.0, 210.0, 210.0), 1e-6), "{v:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blends_envelopes_and_repeats_are_measured_with_the_geometry_effects_of_their_objects() {
|
||||
// A grid repeat of a 100 pt square without a stroke, with Offset Path 10: each copy is drawn
|
||||
// 10 pt out from its square.
|
||||
let mut s = session();
|
||||
let a = offset_square(&mut s, 100.0);
|
||||
run(&mut s, "select.set", json!({"ids": [a.0]}));
|
||||
let id = NodeId(run(&mut s, "object.repeat.grid", json!({}))["id"].as_u64().unwrap());
|
||||
preview(&mut s, true);
|
||||
let (g, v) = (geometric(&s, id), s.transform_bounds(&[id]).unwrap());
|
||||
assert!(g.width() > 200.0 && near(v, g.inflate(10.0, 10.0), 1e-6), "{v:?} {g:?}");
|
||||
// A blend between two such squares, 100 pt apart.
|
||||
let mut s = session();
|
||||
let (a, b) = (offset_square(&mut s, 0.0), offset_square(&mut s, 200.0));
|
||||
run(&mut s, "select.set", json!({"ids": [a.0, b.0]}));
|
||||
let id = NodeId(run(&mut s, "object.blend.make", json!({"steps": 1}))["id"].as_u64().unwrap());
|
||||
preview(&mut s, true);
|
||||
let v = s.transform_bounds(&[id]).unwrap();
|
||||
assert!(near(v, Rect::new(-10.0, 90.0, 310.0, 210.0), 1e-6), "{v:?}");
|
||||
// An envelope around one, with a warp that leaves it as it is.
|
||||
let mut s = session();
|
||||
let a = offset_square(&mut s, 100.0);
|
||||
run(&mut s, "select.set", json!({"ids": [a.0]}));
|
||||
let id = NodeId(run(&mut s, "object.envelope.makeWithWarp", json!({"style": "arc", "bend": 0}))["id"].as_u64().unwrap());
|
||||
preview(&mut s, true);
|
||||
let v = s.transform_bounds(&[id]).unwrap();
|
||||
assert!(near(v, Rect::new(90.0, 90.0, 210.0, 210.0), 1e-6), "{v:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_object_has_the_art_of_at_most_4096_symbol_instances_drawn() {
|
||||
// Symbol S0 is a filled 10 pt square with Offset Path 1, drawn from -1 to 11. Each of S1 to S12
|
||||
// holds two instances of the symbol before it, the second 20 × 2^(k - 1) pt to the right, so an
|
||||
// instance of S12 shows 4,096 squares in a row, 20 pt apart, through 8,191 instances. The art of
|
||||
// the first 4,096 is drawn: the instance itself and the 4,095 in the left half. The squares in
|
||||
// the right half are measured without the offset.
|
||||
let mut d = Document::new(400.0, 400.0);
|
||||
let mut filled = Appearance::basic(Paint::solid(Color::BLACK), Paint::None, 0.0);
|
||||
filled.effects.push(Effect { id: "path.offsetPath".into(), params: json!({"offset": 1}), visible: true });
|
||||
let square = Node::path(d.alloc_id(), shapes::rectangle(Rect::new(0.0, 0.0, 10.0, 10.0)), filled);
|
||||
d.symbols.push(Symbol { name: "S0".into(), art: Arc::new(square) });
|
||||
for k in 1..=12 {
|
||||
let mut instance =
|
||||
|dx: f64| Arc::new(Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: format!("S{}", k - 1), xf: Affine::translate((dx, 0.0)) }));
|
||||
let pair = vec![instance(0.0), instance(20.0 * f64::from(1 << (k - 1)))];
|
||||
let art = Node::group(d.alloc_id(), pair);
|
||||
d.symbols.push(Symbol { name: format!("S{k}"), art: Arc::new(art) });
|
||||
}
|
||||
let instance = Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "S12".into(), xf: Affine::IDENTITY });
|
||||
let (layer, id) = (d.layers[0].id, instance.id);
|
||||
d.insert(Some(layer), 0, instance).unwrap();
|
||||
let mut s = Session::new();
|
||||
s.add_document(d, None);
|
||||
preview(&mut s, true);
|
||||
let v = s.transform_bounds(&[id]).unwrap();
|
||||
assert!(near(v, Rect::new(-1.0, -1.0, 81910.0, 11.0), 1e-6), "{v:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_turned_type_objects_box_follows_its_glyphs() {
|
||||
let mut s = session();
|
||||
let id = outlined_type(&mut s, 40.0, "Outline");
|
||||
preview(&mut s, true);
|
||||
let before = s.transform_box(&[id]).unwrap();
|
||||
assert!(near(before.rect, glyphs(&s, id), 1e-9), "{before:?}");
|
||||
run(&mut s, "select.set", json!({"ids": [id.0]}));
|
||||
run(&mut s, "object.rotate", json!({"angle": 30}));
|
||||
let after = s.transform_box(&[id]).unwrap();
|
||||
assert!((after.angle - 30.0).abs() < 1e-9, "{after:?}");
|
||||
assert!(
|
||||
(after.rect.width() - before.rect.width()).abs() < 1e-6 && (after.rect.height() - before.rect.height()).abs() < 1e-6,
|
||||
"{before:?} {after:?}"
|
||||
);
|
||||
assert!(after.center().distance(before.center()) < 1e-6, "{before:?} {after:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn objects_selected_whole_with_anchors_turn_about_the_box_of_their_offset_shape() {
|
||||
// A square with Offset Path 10 (drawn from 90 to 210) selected whole with the top-left anchor
|
||||
// of an unstroked square at (300, 100); a half turn about the center of the box around both.
|
||||
let center_after = |on: bool| {
|
||||
let mut s = session();
|
||||
let a = offset_square(&mut s, 100.0);
|
||||
let b = NodeId(run(&mut s, "shape.rectangle", json!({"x": 300, "y": 100, "width": 50, "height": 50}))["id"].as_u64().unwrap());
|
||||
run(&mut s, "paint.setStroke", json!({"none": true}));
|
||||
preview(&mut s, on);
|
||||
run(&mut s, "select.set", json!({"ids": [a.0]}));
|
||||
run(&mut s, "select.anchors", json!({"id": b.0, "anchors": [[0, 0]], "mode": "add"}));
|
||||
run(&mut s, "object.rotate", json!({"angle": 180}));
|
||||
geometric(&s, a).center()
|
||||
};
|
||||
// On, the box runs from (90, 90) to (300, 210); off, from (100, 100) to (300, 200).
|
||||
assert!(center_after(true).distance(vectorcraft_geom::Point::new(240.0, 150.0)) < 1e-6, "{:?}", center_after(true));
|
||||
assert!(center_after(false).distance(vectorcraft_geom::Point::new(250.0, 150.0)) < 1e-6, "{:?}", center_after(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn without_preview_bounds_geometry_effects_are_not_measured() {
|
||||
let mut s = session();
|
||||
let ids = [offset_square(&mut s, 100.0), outlined_type(&mut s, 40.0, "Outline")];
|
||||
let d = &s.doc().unwrap().doc;
|
||||
for id in ids {
|
||||
assert_eq!(s.transform_bounds(&[id]), d.bounds_of(&[id], false));
|
||||
assert_eq!(s.transform_box(&[id]), d.oriented_bounds(&[id], false));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_w_field_sets_the_visual_width() {
|
||||
let mut s = session();
|
||||
@@ -357,3 +689,21 @@ fn the_bounding_box_handles_sit_on_the_preview_bounds() {
|
||||
assert!(drag(true).width() > 150.0, "a scale from the visual edge");
|
||||
assert_eq!(drag(false).width(), 100.0, "off, the point is no handle");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_bounding_box_handles_sit_on_the_offset_shape() {
|
||||
// Drag from the right edge of the offset shape (10 pt outside the square) 100 pt to the right.
|
||||
let drag = |on: bool| {
|
||||
let mut s = session();
|
||||
let id = offset_square(&mut s, 100.0);
|
||||
preview(&mut s, on);
|
||||
let v = ViewInfo::default();
|
||||
s.select_tool("selection", v).unwrap();
|
||||
for (k, x) in [(PointerKind::Down, 210.0), (PointerKind::Drag, 310.0), (PointerKind::Up, 310.0)] {
|
||||
s.pointer(&PointerEvent::new(k, x, 150.0), v).unwrap();
|
||||
}
|
||||
geometric(&s, id)
|
||||
};
|
||||
assert!(drag(true).width() > 150.0, "a scale from the edge of the offset shape");
|
||||
assert_eq!(drag(false).width(), 100.0, "off, the point is no handle");
|
||||
}
|
||||
|
||||
@@ -259,16 +259,16 @@ pub fn matrix_json(a: Affine) -> Value {
|
||||
json!([c[0], c[1], c[2], c[3], c[4], c[5]])
|
||||
}
|
||||
|
||||
/// Selection bounds used for the bounding box: visual bounds with Use Preview Bounds, else
|
||||
/// geometric bounds.
|
||||
/// Selection bounds used for the bounding box: visual bounds as drawn with Use Preview Bounds
|
||||
/// ([`vectorcraft_effects::preview`]), else geometric bounds.
|
||||
pub fn selection_bounds(cx: &ToolContext) -> Option<Rect> {
|
||||
cx.doc.bounds_of(&cx.selection.objects, cx.preview_bounds)
|
||||
vectorcraft_effects::preview::bounds_of(cx.doc, &cx.selection.objects, cx.preview_bounds)
|
||||
}
|
||||
|
||||
/// [`selection_bounds`] square to the selection's own angle: the bounding box the Selection tool
|
||||
/// shows and drags (rotated after a rotation).
|
||||
pub fn selection_box(cx: &ToolContext) -> Option<OrientedBox> {
|
||||
cx.doc.oriented_bounds(&cx.selection.objects, cx.preview_bounds)
|
||||
vectorcraft_effects::preview::oriented_bounds(cx.doc, &cx.selection.objects, cx.preview_bounds)
|
||||
}
|
||||
|
||||
/// The objects of `ids` that lie entirely inside marquee `r`, edges included, measured by their
|
||||
@@ -334,7 +334,7 @@ impl SelectionTool {
|
||||
if !cx.smart_guides {
|
||||
return None;
|
||||
}
|
||||
let c = cx.doc.bounds_of(&[id], cx.preview_bounds)?.center();
|
||||
let c = vectorcraft_effects::preview::bounds_of(cx.doc, &[id], cx.preview_bounds)?.center();
|
||||
(c.distance(p) <= reach).then_some((c, "center"))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -113,7 +113,9 @@ impl TransformTool {
|
||||
return Some(*p);
|
||||
}
|
||||
// Anchors selected with Direct Selection transform alone, about their own centre.
|
||||
cx.selection.anchor_bounds(cx.doc, cx.preview_bounds).map(|b| b.center()).or_else(|| selection_box(cx).map(|b| b.center()))
|
||||
vectorcraft_effects::preview::anchor_bounds(cx.selection, cx.doc, cx.preview_bounds)
|
||||
.map(|b| b.center())
|
||||
.or_else(|| selection_box(cx).map(|b| b.center()))
|
||||
}
|
||||
|
||||
/// Transform for dragging from `start` to `p` about `o`; returns the matrix and a readout.
|
||||
|
||||
@@ -1157,7 +1157,7 @@ impl VectorcraftApp {
|
||||
{
|
||||
return b;
|
||||
}
|
||||
let b = match st.selection.anchor_bounds(&st.doc, self.session.prefs.use_preview_bounds) {
|
||||
let b = match vectorcraft_effects::preview::anchor_bounds(&st.selection, &st.doc, self.session.prefs.use_preview_bounds) {
|
||||
Some(r) => Some(vectorcraft_doc::OrientedBox::aligned(r)),
|
||||
None => self.session.transform_box(&st.selection.objects),
|
||||
};
|
||||
@@ -1165,9 +1165,9 @@ impl VectorcraftApp {
|
||||
b
|
||||
}
|
||||
|
||||
/// The selection's visual bounds (stroke and effects included), square to the page: measured
|
||||
/// once per revision, as the canvas reads it every frame (a traced photo selects a group of
|
||||
/// hundreds of thousands of paths).
|
||||
/// The selection's visual bounds (strokes included, effects left out), square to the page:
|
||||
/// measured once per revision, as the canvas reads it every frame (a traced photo selects a
|
||||
/// group of hundreds of thousands of paths).
|
||||
pub fn selection_bounds(&mut self) -> Option<vectorcraft_geom::Rect> {
|
||||
let st = self.session.active()?;
|
||||
let key = (st.uid, st.revision);
|
||||
|
||||
@@ -450,6 +450,21 @@ mod tests {
|
||||
assert!(texts(&mut app, crate::panels::align::menu).iter().any(|t| t == "✓ Use Preview Bounds"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn use_preview_bounds_shows_the_size_after_geometry_effects() {
|
||||
// A 100 × 50 rectangle without a stroke, with Offset Path 10: 120 × 70 as drawn.
|
||||
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 300, "height": 300})).unwrap();
|
||||
app.run("shape.rectangle", json!({"x": 50, "y": 50, "width": 100, "height": 50})).unwrap();
|
||||
app.run("paint.setStroke", json!({"none": true})).unwrap();
|
||||
app.run("effect.apply", json!({"effect": "path.offsetPath", "params": {"offset": 10}, "item": null})).unwrap();
|
||||
let shown = texts(&mut app, show);
|
||||
assert!(shown.iter().any(|t| t == "100 pt") && shown.iter().any(|t| t == "50 pt"), "{shown:?}");
|
||||
set_pref(&mut app, "usePreviewBounds", true);
|
||||
let shown = texts(&mut app, show);
|
||||
assert!(shown.iter().any(|t| t == "120 pt") && shown.iter().any(|t| t == "70 pt"), "{shown:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rotation_field_shows_the_angle_the_box_keeps() {
|
||||
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default());
|
||||
|
||||
@@ -23,7 +23,7 @@ Part of [target-app-parity.md](target-app-parity.md); open work is in [gaps.md](
|
||||
| Convert to Shape | Rectangle, Rounded Rectangle, Ellipse | 3/3 | — |
|
||||
| Crop Marks | Crop Marks | 1/1 | — |
|
||||
| Distort & Transform | Free Distort, Pucker & Bloat, Roughen, Transform, Tweak, Twist, Zig Zag | 7/7 | Free Distort's dialog has a handle on each corner of the object's bounds, moved by dragging or with the arrow keys; Transform's Transform Objects/Patterns and Scale Strokes & Effects options, invisible Reflect checkboxes (#885); Zig Zag's ridges are evenly spaced along each segment (#1558), and Smooth points make a continuous wave whose peaks and valleys have handles along the path (#2343); Roughen with Smooth keeps turn-backs smooth instead of cusps that draw as spikes (#1646) |
|
||||
| Path | Offset Path, Outline Object, Outline Stroke | 3/3 | With Use Preview Bounds on, Align, Distribute and the Transform panel still measure type that carries Outline Object by its layout box (preview bounds leave out effects) |
|
||||
| Path | Offset Path, Outline Object, Outline Stroke | 3/3 | — |
|
||||
| Pathfinder | Add, Intersect, Exclude, Subtract, Minus Back, Divide, Trim, Merge, Crop, Outline, Hard Mix, Soft Mix, Trap | 10/13 | Hard Mix, Soft Mix, Trap |
|
||||
| Rasterize | Rasterize… (as a live effect) | 1/1 | — |
|
||||
| Stylize | Drop Shadow, Feather, Inner Glow, Outer Glow, Round Corners, Scribble | 6/6 | Scribble Options' Settings presets |
|
||||
|
||||
+12
@@ -2331,6 +2331,18 @@ stroke (see Stroke bounds and clicks); off, they measure the paths. The three Al
|
||||
`bounds: "preview"|"geometric"` for one call. `object.setBounds` then sets the visual box: with Scale Strokes &
|
||||
Effects off a 100 pt wide rectangle with a 10 pt stroke set to `width: 220` gets a 210 pt path.
|
||||
|
||||
With Use Preview Bounds on, these measures take in each object as it is drawn after its geometry effects. A path is
|
||||
measured by its effected shape, and a fill or stroke with geometry effects of its own is measured by the shape it
|
||||
paints. Type, images, live objects and symbol instances are measured by their reshaped art, and a symbol instance is
|
||||
also measured by its reshaped art when its symbol's art has geometry effects. A group or layer with geometry or
|
||||
Pathfinder effects, or with a fill or stroke of its own that has geometry effects or paints members with geometry
|
||||
effects, is measured by its evaluated art. A blend, envelope or repeat whose objects have geometry effects is
|
||||
measured by its evaluated art, with those objects measured after their effects. A 100 pt square without a stroke,
|
||||
with Offset Path 10, measures 120 pt, and type with Outline Object is measured by its glyph outlines. Raster effects such as shadows and glows are not
|
||||
measured. In one object, the art of at most 4,096 symbol instances is measured with its geometry effects, and the
|
||||
other instances are measured without them. `object.setBounds` with `width: 100` gives that type a glyph box 100 pt
|
||||
wide.
|
||||
|
||||
```json
|
||||
{"name":"run_command","arguments":{"command":"object.align","params":{"horizontal":"left","bounds":"preview"}}}
|
||||
```
|
||||
|
||||
@@ -640,6 +640,9 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- Extrude & Bevel, Inflate and Rotate (Effect › 3D and Materials): live effects in the `three-d` crate with depth, twist, taper, solid or hollow caps, five original bevel shapes (width, height, repeat, spacing, inside, both sides), Inflate's volume, both sides, twist, taper and cap, Rotate's turned fills and strokes, Position presets, the rotation and light gizmos, SVG/PDF vector output and Expand Appearance; one 3D effect per object; covered by geometry, effect, canvas-versus-export, command and dialog tests ([three-d.md](three-d.md)).
|
||||
- Initial live Revolve (#605, #846): a portable geometry and shading crate (`three-d`), editable path and compound profiles, compact options with artboard rotation and light gizmos, SVG/PDF vector output and Expand Appearance that can trim covered faces. Open ends have continuous caps by default, shading is smooth with one to ten colored lights, and intersecting surfaces' visibility is approximate ([revolve.md](revolve.md)).
|
||||
- Live effects with previewing dialogs: Distort & Transform, Path, Convert to Shape, 15 Warp styles, Round Corners, Scribble, Effect → Pathfinder (all 10 operations, live on groups; several loose objects are grouped first, in one undo step), Color Adjustments (Brightness/Contrast, Curves, Levels, Hue/Saturation, Shift to Color, Temperature/Tint, on vectors, live type and embedded images), and raster drop shadow, glows and feather. SVG and PDF export keep live effects (geometry baked; SVG raster effects as filters). Zig Zag spaces its ridges evenly along each segment, so lines and curves get a regular wave in Corner and Smooth (#1558). Warp › Arc (the effect and Envelope › Make with Warp) bends in the object's real proportions: a circular arc with concentric edges and no fold-over at 100%, so Arc on art that isn't square draws differently from before (#1581, fixes #1578). `effect.apply` and `effect.setParams` give an error for a misspelt or wrongly typed effect parameter instead of running the effect with its default (#1623, #1592). Roughen with Smooth keeps turn-backs smooth instead of making cusps that draw as spikes (#1646, #1643). Scribble draws one zig-zag per region, with no lines across holes and notches; convex shapes are unchanged (#1651, #1648). On the canvas, in Expand Appearance and in exports, Effect → Pathfinder uses the glyph outlines of type in the group, and results painted like the type keep its opacity and blend mode.
|
||||
- Effect › Path › Outline Object has no options. Type that carries it is drawn, exported (PDF and SVG as paths, with no font) and expanded as its glyph outlines while it stays editable, and the effects after it reshape those outlines. Paths and images draw as before. With Use Preview Bounds on, type is measured by its glyph outlines. Public reference: [Summary of effects](https://web.archive.org/web/20210901011839/https://helpx.adobe.com/illustrator/using/summary-effects.html) (archived copy).
|
||||
- With Use Preview Bounds on, the bounding box, the Transform panel, Align, Distribute, Distribute Spacing and `object.setBounds` measure each object after its geometry effects. A path is measured by its effected shape, and a fill or stroke with geometry effects of its own is measured by the shape it paints. Type, images, live objects and symbol instances are measured by their reshaped art, and a symbol instance is also measured by its reshaped art when its symbol's art has geometry effects. A group or layer with geometry or Pathfinder effects, or with a fill or stroke of its own that has geometry effects or paints members with geometry effects, is measured by its evaluated art. A blend, envelope or repeat whose objects have geometry effects is measured by its evaluated art, with those objects measured after their effects. A 100 pt square without a stroke, with Offset Path 10, measures 120 pt, and type with Outline Object aligns and sizes by its glyphs. Raster effects are not measured. Public reference: [Create text](https://web.archive.org/web/20220127090642/https://helpx.adobe.com/illustrator/using/creating-text.html) (archived copy).
|
||||
- Initial live Revolve (#605, #846): a portable geometry and shading crate (`three-d`), editable path and compound profiles, compact options with artboard rotation and light gizmos, SVG/PDF vector output and Expand Appearance that can trim covered faces. Cap is on by default with continuous curved vector rims; shading uses vector gradients automatically, with sharp profile corners, uniform caps and round seam joins. One light and approximate visibility remain ([revolve.md](revolve.md)).
|
||||
- Effect › Path › Outline Object has no options. Type that carries it is drawn, exported (PDF and SVG as paths, with no font) and expanded as its glyph outlines while it stays editable, and the effects after it reshape those outlines. Paths and images draw as before. With Use Preview Bounds on, type is still measured by its layout box. Public reference: [Summary of effects](https://web.archive.org/web/20210901011839/https://helpx.adobe.com/illustrator/using/summary-effects.html) (archived copy).
|
||||
- Initial live Revolve (#605, #846): a portable geometry and shading crate (`three-d`), editable path and compound profiles, compact options with artboard rotation and light gizmos, SVG/PDF vector output and Expand Appearance that can trim covered faces. Cap is on by default with continuous curved vector rims; shading uses vector gradients automatically, with sharp profile corners, uniform caps and round seam joins. The Lights list, Add Light and Delete Light manage one to ten independent colored lights; ambient is shared and visibility remains approximate ([revolve.md](revolve.md)).
|
||||
- Live effects with previewing dialogs: Distort & Transform, Path, Convert to Shape, 15 Warp styles, Round Corners, Scribble, Effect → Pathfinder (all 10 operations, live on groups; several loose objects are grouped first, in one undo step), Color Adjustments (Brightness/Contrast, Curves, Levels, Hue/Saturation, Shift to Color, Temperature/Tint, on vectors, live type and embedded images), and raster drop shadow, glows and feather. SVG and PDF export keep live effects (geometry baked; SVG raster effects as filters). Zig Zag spaces its ridges evenly along each segment, so lines and curves get a regular wave in Corner and Smooth (#1558). Warp › Arc (the effect and Envelope › Make with Warp) bends in the object's real proportions: a circular arc with concentric edges and no fold-over at 100%, so Arc on art that isn't square draws differently from before (#1581, fixes #1578). `effect.apply` and `effect.setParams` give an error for a misspelt or wrongly typed effect parameter instead of running the effect with its default (#1623, #1592). Roughen with Smooth keeps turn-backs smooth instead of making cusps that draw as spikes (#1646, #1643). Scribble draws one zig-zag per region, with no lines across holes and notches; convex shapes are unchanged (#1651, #1648).
|
||||
|
||||
Reference in New Issue
Block a user