mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-12 19:22:48 +00:00
Dimension tool: Expand keeps a dimension as plain art after it moves
Object > Expand left a dimension's recipe on its group, so the next move redrew the live dimension and discarded the expanded art. Expand now ends the live link of a dimension whose art it changes, in the same undo step. Moving or scaling the group leaves its art as it is, the art no longer follows what it measured, and undo gives the live dimension back. Expand Appearance, Create Outlines and Outline Stroke had the same fault and now do the same, also when they reach only a part of a dimension. A dimension whose art a command leaves as it was stays live. A dimension selected with the art it measures becomes a plain group when Expand, Expand Appearance or Outline Stroke replaces only that art.
This commit is contained in:
@@ -195,6 +195,64 @@ fn replace(doc: &mut Document, id: NodeId, dim: Dimension) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A dimension whose live link [`end_links`] ended, as it was before.
|
||||
pub(crate) struct EndedLink {
|
||||
id: NodeId,
|
||||
recipe: Dimension,
|
||||
art: Vec<Arc<Node>>,
|
||||
}
|
||||
|
||||
/// The dimensions under `roots` or holding one of them. A dimension can be listed more than once.
|
||||
pub(crate) fn reached(doc: &Document, roots: &[NodeId]) -> Vec<NodeId> {
|
||||
let mut ids = Vec::new();
|
||||
for n in roots.iter().filter_map(|r| doc.node(*r)) {
|
||||
n.walk(&mut |c| {
|
||||
if c.extras().dimension.is_some() {
|
||||
ids.push(c.id);
|
||||
}
|
||||
});
|
||||
if doc.dimensions {
|
||||
let holders = doc.ancestry(n.id).unwrap_or_default();
|
||||
ids.extend(holders.into_iter().filter(|id| doc.node(*id).is_some_and(|h| h.extras().dimension.is_some())));
|
||||
}
|
||||
}
|
||||
ids
|
||||
}
|
||||
|
||||
impl EndedLink {
|
||||
/// Is the art this dimension measures, changed from `before` to `after`, still art it can link to?
|
||||
pub(crate) fn source_kept(&self, before: &Document, after: &Document) -> bool {
|
||||
!self.recipe.source.as_ref().is_some_and(|s| s.topology_changed(before, after)) && self.recipe.clone().resolve(after).is_ok()
|
||||
}
|
||||
}
|
||||
|
||||
/// The [`reached`] dimensions become plain groups, whose art does not redraw or follow what it
|
||||
/// measured. A command that replaces art calls this before it changes anything, since replacing
|
||||
/// the art a dimension measures would remove the dimension, and then [`restore_unchanged`].
|
||||
pub(crate) fn end_links(doc: &mut Document, roots: &[NodeId]) -> Vec<EndedLink> {
|
||||
let mut ended = Vec::new();
|
||||
for id in reached(doc, roots) {
|
||||
let Some(n) = doc.node_mut(id) else { continue };
|
||||
let art = n.children().cloned().unwrap_or_default();
|
||||
if let Some(recipe) = n.extras_mut().dimension.take() {
|
||||
ended.push(EndedLink { id, recipe: *recipe, art });
|
||||
}
|
||||
}
|
||||
ended
|
||||
}
|
||||
|
||||
/// The dimensions in `ended` whose art is as it was are live again.
|
||||
pub(crate) fn restore_unchanged(doc: &mut Document, ended: Vec<EndedLink>) {
|
||||
for e in ended {
|
||||
if doc.node(e.id).and_then(Node::children).is_some_and(|art| *art == e.art)
|
||||
&& let Some(n) = doc.node_mut(e.id)
|
||||
{
|
||||
n.extras_mut().dimension = Some(Box::new(e.recipe));
|
||||
doc.dimensions = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Called inside every edit, so dimensions and their undo history stay in step with the art they
|
||||
/// measure. Returns at once in documents without dimensions.
|
||||
pub(crate) fn refresh(before: &Document, doc: &mut Document) -> Result<bool> {
|
||||
|
||||
@@ -71,7 +71,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Expand Appearance",
|
||||
["Object"],
|
||||
None,
|
||||
"{ids?|id?: [..], target?} turn each object's appearance into objects: every fill and stroke becomes an object of its own (a stroke stays a stroke, with its weight, dashes, arrowheads and width profile, on its own copy of the path, which object.expand or object.path.outlineStroke outline; brushed strokes become their brush art) grouped in paint order under the object's id and transparency, geometry effects are baked, raster effects become an embedded image (shadows and outer glows below the art; blur, feather and inner glow replace it; an object whose only raster effect is its own Rasterize becomes the image object.rasterize makes with the effect's options), type with effects or fills of its own is outlined; Crop Marks become a group of the object and its marks; Vector Halftone (addon.color.halftone) becomes a group of its dot paths, one per ink; a group's or layer's own fills and strokes become objects among its members, which are expanded too. An outlined frame of threaded text leaves its thread with the text it shows, and a frame kept as type in a group stays in its thread. Hidden fills, strokes and effects are dropped. Enabled when a selected or targeted object's appearance isn't basic (`ids` then pick which objects) → {ids, warning? (an image in the art couldn't be decoded, as object.rasterize says)}",
|
||||
"{ids?|id?: [..], target?} turn each object's appearance into objects: every fill and stroke becomes an object of its own (a stroke stays a stroke, with its weight, dashes, arrowheads and width profile, on its own copy of the path, which object.expand or object.path.outlineStroke outline; brushed strokes become their brush art) grouped in paint order under the object's id and transparency, geometry effects are baked, raster effects become an embedded image (shadows and outer glows below the art; blur, feather and inner glow replace it; an object whose only raster effect is its own Rasterize becomes the image object.rasterize makes with the effect's options), type with effects or fills of its own is outlined; Crop Marks become a group of the object and its marks; Vector Halftone (addon.color.halftone) becomes a group of its dot paths, one per ink; a group's or layer's own fills and strokes become objects among its members, which are expanded too. An outlined frame of threaded text leaves its thread with the text it shows, and a frame kept as type in a group stays in its thread. A dimension whose art it changes, or whose measured art it replaces, becomes a plain group of its art. Hidden fills, strokes and effects are dropped. Enabled when a selected or targeted object's appearance isn't basic (`ids` then pick which objects) → {ids, warning? (an image in the art couldn't be decoded, as object.rasterize says)}",
|
||||
can_expand,
|
||||
expand_appearance
|
||||
),
|
||||
@@ -828,12 +828,17 @@ fn expand_appearance(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let failed = vectorcraft_render::decode_failures();
|
||||
let colorized_ok = super::paint::check_colorized(&s.doc()?.doc).is_ok();
|
||||
let ids = s.edit("Expand Appearance", |d, _| {
|
||||
let before = d.clone();
|
||||
let mut ended = super::dimension::end_links(d, &roots);
|
||||
let brushes = vectorcraft_brush::library(d);
|
||||
let mut out = vec![];
|
||||
let threaded = super::threads::frames(d);
|
||||
for id in &roots {
|
||||
expand_node(d, *id, &brushes, &mut out);
|
||||
}
|
||||
// A dimension whose source was replaced stays plain: its link no longer fits that art.
|
||||
ended.retain(|e| e.source_kept(&before, d));
|
||||
super::dimension::restore_unchanged(d, ended);
|
||||
// Outlined frames of threaded text leave their threads with the text they show; type that
|
||||
// stays type under a new id takes its frame's place.
|
||||
super::threads::leave_threads(d, &threaded);
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
//! as Break Link to Symbol places it. A linear or radial gradient fill becomes a
|
||||
//! gradient mesh, or solid strips / concentric ellipses, inside a clip group shaped like the object; a freeform
|
||||
//! gradient becomes a mesh shaped like the object. A pattern fill becomes the tiles it paints inside a clip
|
||||
//! group shaped like the object. Create Gradient Mesh shares the sampling.
|
||||
//! group shaped like the object. Create Gradient Mesh shares the sampling. A dimension whose art
|
||||
//! Expand changes becomes a plain group of that art.
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
use std::sync::Arc;
|
||||
@@ -19,6 +20,7 @@ use vectorcraft_doc::pattern::{PatternDef, place_art};
|
||||
use vectorcraft_doc::{Appearance, Document, Node, NodeId, NodeKind, Scaling};
|
||||
use vectorcraft_geom::{Affine, PathData, Point, Rect, SubPath, Vec2, shapes};
|
||||
|
||||
use super::dimension::EndedLink;
|
||||
use super::edit::selected_roots;
|
||||
use super::menucmds::{run_raw, squash};
|
||||
use super::object::as_clipping_path;
|
||||
@@ -41,7 +43,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Expand…",
|
||||
["Object"],
|
||||
None,
|
||||
"{object?: true (symbol instances without effects (their fills' and strokes' included) and not in perspective → their art, as Break Link to Symbol places it (the instances that art holds stay instances), type → outlines, live shapes → paths, effects baked, blends → groups of their steps, repeats → groups of their instances, Intertwine groups → groups of their objects and of the parts drawn over the others (clip groups), envelopes → their distorted content, placed documents → editable copies of their art, as placing the file without link gives, its symbols, patterns, swatches and images joining the document), fill?: true (gradient fills → `gradient` art, pattern fills → their tiles in a clip group shaped like the object), stroke?: true (strokes → filled outlines, the stroked paths that baking effects leaves included), gradient?: \"objects\" (default: `steps` solid strips, concentric ellipses for radial gradients) | \"mesh\" (a gradient mesh); both in a clip group shaped like the object (freeform gradients always become a mesh shaped like it), steps?: 1..1000 (255)} one undo step → {ids}",
|
||||
"{object?: true (symbol instances without effects (their fills' and strokes' included) and not in perspective → their art, as Break Link to Symbol places it (the instances that art holds stay instances), type → outlines, live shapes → paths, effects baked, blends → groups of their steps, repeats → groups of their instances, Intertwine groups → groups of their objects and of the parts drawn over the others (clip groups), envelopes → their distorted content, placed documents → editable copies of their art, as placing the file without link gives, its symbols, patterns, swatches and images joining the document), any option that changes a dimension's art makes the dimension a plain group of that art, fill?: true (gradient fills → `gradient` art, pattern fills → their tiles in a clip group shaped like the object), stroke?: true (strokes → filled outlines, the stroked paths that baking effects leaves included), gradient?: \"objects\" (default: `steps` solid strips, concentric ellipses for radial gradients) | \"mesh\" (a gradient mesh); both in a clip group shaped like the object (freeform gradients always become a mesh shaped like it), steps?: 1..1000 (255)} one undo step → {ids}",
|
||||
has_selection,
|
||||
expand
|
||||
),
|
||||
@@ -239,8 +241,12 @@ fn expand_steps(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let stroke = bool_or(p, "stroke", true);
|
||||
let to = GradientTo::parse(p)?;
|
||||
let from = s.doc()?.history.undo.len();
|
||||
let rev0 = s.doc()?.revision;
|
||||
let mut e = Expandable::of(&s.doc()?.doc, &selected_roots(s)?);
|
||||
// Before anything expands: outlining the art a dimension measures would remove the dimension.
|
||||
let before = s.doc()?.doc.clone();
|
||||
let mut ended = vec![];
|
||||
end_dimension_links(s, &mut ended)?;
|
||||
let rev0 = s.doc()?.revision;
|
||||
// Placed documents first: their art (type, strokes, gradients) expands below.
|
||||
if object && e.placed {
|
||||
let roots = selected_roots(s)?;
|
||||
@@ -286,6 +292,8 @@ fn expand_steps(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let roots = selected_roots(s)?;
|
||||
s.edit("Expand", |d, _| super::patterncmds::expand_repeats(d, &roots).map(|_| ()))?;
|
||||
}
|
||||
// The dimensions in the art those steps made.
|
||||
end_dimension_links(s, &mut ended)?;
|
||||
if object && e.effects {
|
||||
// Effects that Expand Appearance can't expand are left as they are. When it refuses to
|
||||
// leave a process color on a grayscale image with transparency, Expand fails as a whole.
|
||||
@@ -331,10 +339,28 @@ fn expand_steps(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if s.doc()?.revision == rev0 {
|
||||
return Err(EngineError::Other("Expand: nothing to expand".into()));
|
||||
}
|
||||
if !ended.is_empty() {
|
||||
// A dimension whose source Expand replaced stays plain: its link no longer fits that art.
|
||||
let after = s.doc()?.doc.clone();
|
||||
ended.retain(|e| e.source_kept(&before, &after));
|
||||
s.edit("Expand", |d, _| {
|
||||
super::dimension::restore_unchanged(d, std::mem::take(&mut ended));
|
||||
Ok(())
|
||||
})?;
|
||||
}
|
||||
squash(s, from, "Expand");
|
||||
Ok(json!({ "ids": s.doc()?.selection.objects.iter().map(|i| i.0).collect::<Vec<_>>() }))
|
||||
}
|
||||
|
||||
/// Ends the live link of each dimension under or holding the selection, adding it to `ended`.
|
||||
fn end_dimension_links(s: &mut Session, ended: &mut Vec<EndedLink>) -> Result<()> {
|
||||
let roots = selected_roots(s)?;
|
||||
if !super::dimension::reached(&s.doc()?.doc, &roots).is_empty() {
|
||||
ended.extend(s.edit("Expand", |d, _| Ok(super::dimension::end_links(d, &roots)))?);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn drop_live(n: &mut Node) {
|
||||
if let NodeKind::Path { live, .. } = &mut n.kind {
|
||||
*live = None;
|
||||
|
||||
@@ -134,7 +134,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Outline Stroke",
|
||||
["Object", "Path"],
|
||||
None,
|
||||
"{} replace stroked paths with filled outlines → {ids}",
|
||||
"{} replace stroked paths with filled outlines; a dimension whose lines are outlined, or selected with the art it measures when that art is outlined, becomes a plain group of its art → {ids}",
|
||||
has_selection,
|
||||
outline_stroke
|
||||
),
|
||||
@@ -980,6 +980,8 @@ pub(crate) fn replace_leaves(d: &mut Document, root: NodeId, mut f: impl FnMut(&
|
||||
fn outline_stroke(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
let roots = selected_roots(s)?;
|
||||
let ids = s.edit("Outline Stroke", |d, sel| {
|
||||
let before = d.clone();
|
||||
let mut ended = super::dimension::end_links(d, &roots);
|
||||
let brushes = vectorcraft_brush::library(d);
|
||||
let mut new_sel = vec![];
|
||||
let mut changed = 0;
|
||||
@@ -988,6 +990,9 @@ fn outline_stroke(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
new_sel.push(id);
|
||||
changed += n;
|
||||
}
|
||||
// A dimension whose source was outlined stays plain: its link no longer fits that art.
|
||||
ended.retain(|e| e.source_kept(&before, d));
|
||||
super::dimension::restore_unchanged(d, ended);
|
||||
if changed == 0 {
|
||||
return Err(EngineError::Other("Outline Stroke: no stroked paths selected".into()));
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Create Outlines",
|
||||
["Type"],
|
||||
Some("Cmd+Shift+O"),
|
||||
"{} convert the selected text to groups of compound paths (one per glyph); a frame of threaded text leaves its thread with the text it shows → {ids}",
|
||||
"{} convert the selected text to groups of compound paths (one per glyph); a frame of threaded text leaves its thread with the text it shows; a dimension whose label is outlined becomes a plain group of its art → {ids}",
|
||||
has_selection,
|
||||
create_outlines
|
||||
),
|
||||
@@ -554,6 +554,7 @@ fn create_outlines(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
if texts.is_empty() {
|
||||
return Err(EngineError::Other("Create Outlines: select text objects".into()));
|
||||
}
|
||||
let ended = super::dimension::end_links(d, &roots);
|
||||
let mut new_sel: Vec<NodeId> = roots.iter().copied().filter(|r| !texts.contains(r)).collect();
|
||||
let mut out = vec![];
|
||||
let threaded = super::threads::frames(d);
|
||||
@@ -648,6 +649,7 @@ fn create_outlines(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
new_sel.push(gid);
|
||||
}
|
||||
}
|
||||
super::dimension::restore_unchanged(d, ended);
|
||||
// Outlined frames of threaded text leave their threads with the text they show.
|
||||
super::threads::leave_threads(d, &threaded);
|
||||
sel.set(new_sel);
|
||||
|
||||
Reference in New Issue
Block a user