mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-11 18:54:55 +00:00
Merge pull request #6559 from dotCooCoo/fix/dimension-expand-keeps-live
Dimension tool: a dimension selected with its art stays live through Expand and Outline Stroke
This commit is contained in:
@@ -272,9 +272,9 @@ impl EndedLink {
|
||||
}
|
||||
|
||||
/// 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 otherwise leave the dimension unlinked, and then
|
||||
/// [`restore_unchanged`].
|
||||
/// measured. A command that replaces art calls this before it changes anything and
|
||||
/// [`restore_unchanged`] after, so that a dimension whose own art the command changed stays a
|
||||
/// plain group.
|
||||
pub(crate) fn end_links(doc: &mut Document, roots: &[NodeId]) -> Vec<EndedLink> {
|
||||
let mut ended = Vec::new();
|
||||
for id in reached(doc, roots) {
|
||||
@@ -287,18 +287,156 @@ pub(crate) fn end_links(doc: &mut Document, roots: &[NodeId]) -> Vec<EndedLink>
|
||||
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>) {
|
||||
/// The dimensions in `ended` whose own art is as it was are live again. One whose measured art the
|
||||
/// command changed from `before` so that the art no longer fits its link stays where it was, no
|
||||
/// longer linked, as [`refresh`] leaves a dimension whose art was deleted.
|
||||
pub(crate) fn restore_unchanged(before: &Document, 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));
|
||||
if !doc.node(e.id).and_then(Node::children).is_some_and(|art| *art == e.art) {
|
||||
continue;
|
||||
}
|
||||
let recipe = if e.source_kept(before, doc) { e.recipe } else { unlinked(&e.recipe) };
|
||||
if let Some(n) = doc.node_mut(e.id) {
|
||||
n.extras_mut().dimension = Some(Box::new(recipe));
|
||||
doc.dimensions = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `old` where it was, no longer linked. A move it had on its own and not yet drawn is kept when
|
||||
/// the moved dimension is valid.
|
||||
fn unlinked(old: &Dimension) -> Dimension {
|
||||
let mut unlinked = old.clone();
|
||||
unlinked.source = None;
|
||||
unlinked.moved = None;
|
||||
if let Some(v) = old.moved {
|
||||
let mut shifted = unlinked.clone();
|
||||
(shifted.a, shifted.b, shifted.vertex) = (shifted.a + v, shifted.b + v, shifted.vertex + v);
|
||||
if shifted.validate().is_ok() {
|
||||
unlinked = shifted;
|
||||
}
|
||||
}
|
||||
unlinked
|
||||
}
|
||||
|
||||
/// The whole dimensions under `roots` that are selected together with art they measure (art under
|
||||
/// `roots`), each one a root or held by groups without fills, strokes or effects of their own.
|
||||
/// Object › Expand, Object › Expand Appearance and Object › Path › Outline Stroke leave their
|
||||
/// lines and label as they are. A group with an appearance of its own changes everything it holds
|
||||
/// when it expands, its dimensions included.
|
||||
pub(crate) fn with_their_art(doc: &Document, roots: &[NodeId]) -> Vec<NodeId> {
|
||||
if !doc.dimensions {
|
||||
return Vec::new();
|
||||
}
|
||||
let mut found = Vec::new();
|
||||
for root in roots {
|
||||
let Some(n) = doc.node(*root) else { continue };
|
||||
n.walk(&mut |c| {
|
||||
if let Some(source) = c.extras().dimension.as_ref().and_then(|d| d.source.as_ref()) {
|
||||
found.push((*root, c.id, source.nodes()));
|
||||
}
|
||||
});
|
||||
}
|
||||
let under_roots = |id: NodeId| doc.ancestry(id).is_some_and(|a| a.iter().any(|a| roots.contains(a)));
|
||||
found
|
||||
.into_iter()
|
||||
.filter(|(root, id, _)| {
|
||||
let Some(ancestry) = doc.ancestry(*id) else { return false };
|
||||
let from = ancestry.iter().position(|a| a == root).unwrap_or(ancestry.len());
|
||||
ancestry.get(from..ancestry.len().saturating_sub(1)).unwrap_or_default().iter().all(|a| doc.node(*a).is_some_and(plain_group))
|
||||
})
|
||||
.filter(|(_, _, measured)| measured.iter().any(|m| under_roots(*m)))
|
||||
.map(|(_, id, _)| id)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// A group or layer without fills, strokes or effects of its own.
|
||||
fn plain_group(n: &Node) -> bool {
|
||||
matches!(n.kind, NodeKind::Group { .. } | NodeKind::Layer { .. }) && n.appearance.items.is_empty() && n.appearance.effects.is_empty()
|
||||
}
|
||||
|
||||
/// A dimension [`lift`] took out of the document, with its place, the export assets that list it or
|
||||
/// its parts (with their places in the asset list) and its parts' variable bindings.
|
||||
pub(crate) struct Lifted {
|
||||
parent: Option<NodeId>,
|
||||
index: usize,
|
||||
node: Arc<Node>,
|
||||
assets: Vec<(usize, vectorcraft_doc::assets::ExportAsset)>,
|
||||
bindings: Vec<(NodeId, std::collections::BTreeSet<String>)>,
|
||||
}
|
||||
|
||||
impl Lifted {
|
||||
pub(crate) fn id(&self) -> NodeId {
|
||||
self.node.id
|
||||
}
|
||||
}
|
||||
|
||||
/// Takes the [`with_their_art`] dimensions under `roots` out of `doc`, so a command that replaces
|
||||
/// art does not reach them; [`put_back`] puts them back.
|
||||
pub(crate) fn lift(doc: &mut Document, roots: &[NodeId]) -> Vec<Lifted> {
|
||||
let mut lifted = Vec::new();
|
||||
for id in with_their_art(doc, roots) {
|
||||
let Some((parent, index, _)) = doc.position(id) else { continue };
|
||||
let mut parts = std::collections::BTreeSet::new();
|
||||
if let Some(n) = doc.node(id) {
|
||||
n.walk(&mut |c| {
|
||||
parts.insert(c.id);
|
||||
});
|
||||
}
|
||||
let assets = doc.assets.iter().enumerate().filter(|(_, a)| a.nodes.iter().any(|n| parts.contains(n))).map(|(i, a)| (i, a.clone())).collect();
|
||||
let bindings = doc.variables.bindings.iter().filter(|(n, _)| parts.contains(n)).map(|(n, b)| (*n, b.clone())).collect();
|
||||
if let Ok(node) = doc.remove(id) {
|
||||
lifted.push(Lifted { parent, index, node, assets, bindings });
|
||||
}
|
||||
}
|
||||
lifted
|
||||
}
|
||||
|
||||
/// Each dimension [`lift`] took out, as it was, back in its place in `doc`, with its parts back in
|
||||
/// the export assets that listed them and their variable bindings back: edits made while it was out
|
||||
/// let go of them. A dimension whose group is gone goes on top of the first layer.
|
||||
pub(crate) fn put_back(doc: &mut Document, lifted: Vec<Lifted>) {
|
||||
for l in lifted.into_iter().rev() {
|
||||
let parent = if doc.children(l.parent).is_some() { l.parent } else { doc.layers.first().map(|layer| layer.id) };
|
||||
let Ok(ch) = doc.children_mut(parent) else { continue };
|
||||
let i = if parent == l.parent { l.index.min(ch.len()) } else { ch.len() };
|
||||
ch.insert(i, l.node);
|
||||
doc.dimensions = true;
|
||||
for (at, saved) in l.assets {
|
||||
let there: Vec<NodeId> = saved.nodes.iter().copied().filter(|n| doc.node(*n).is_some()).collect();
|
||||
if let Some(a) = doc.assets.iter_mut().find(|a| a.id == saved.id) {
|
||||
for (k, n) in there.iter().enumerate() {
|
||||
if !a.nodes.contains(n) {
|
||||
let k = k.min(a.nodes.len());
|
||||
a.nodes.insert(k, *n);
|
||||
}
|
||||
}
|
||||
} else if !there.is_empty() {
|
||||
let at = at.min(doc.assets.len());
|
||||
doc.assets.insert(at, vectorcraft_doc::assets::ExportAsset { nodes: there, ..saved });
|
||||
}
|
||||
}
|
||||
for (n, b) in l.bindings {
|
||||
doc.variables.bindings.entry(n).or_insert(b);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `doc` without the [`with_their_art`] dimensions under `roots`, to see what a command that leaves
|
||||
/// them alone would change: `doc` itself when there are none.
|
||||
pub(crate) fn without_kept<'a>(doc: &'a Document, roots: &[NodeId]) -> std::borrow::Cow<'a, Document> {
|
||||
let ids = with_their_art(doc, roots);
|
||||
if ids.is_empty() {
|
||||
return std::borrow::Cow::Borrowed(doc);
|
||||
}
|
||||
let mut view = doc.clone();
|
||||
for id in ids {
|
||||
// A dimension already gone (inside another one removed) needs nothing more.
|
||||
let _ = view.remove(id);
|
||||
}
|
||||
std::borrow::Cow::Owned(view)
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
@@ -320,17 +458,7 @@ pub(crate) fn refresh(before: &Document, doc: &mut Document) -> Result<bool> {
|
||||
// The art it measured was deleted, or rebuilt with other anchors, or no longer fits
|
||||
// (an ellipse that was a circle): it stays where it was, no longer linked, in this
|
||||
// same undo step, rather than attaching to a different edge.
|
||||
let mut unlinked = old.clone();
|
||||
unlinked.source = None;
|
||||
unlinked.moved = None;
|
||||
if let Some(v) = moved {
|
||||
let mut shifted = unlinked.clone();
|
||||
(shifted.a, shifted.b, shifted.vertex) = (shifted.a + v, shifted.b + v, shifted.vertex + v);
|
||||
if shifted.validate().is_ok() {
|
||||
unlinked = shifted;
|
||||
}
|
||||
}
|
||||
if replace(doc, id, unlinked).is_err()
|
||||
if replace(doc, id, unlinked(&old)).is_err()
|
||||
&& let Some(n) = doc.node_mut(id)
|
||||
{
|
||||
// Its recipe cannot be drawn again: its art stays as it is, as a plain group.
|
||||
|
||||
@@ -87,7 +87,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. A dimension whose art it changes, or one selected with the art it measures when that art is replaced, becomes a plain group of its art. Hidden fills, strokes and effects are dropped. In Safe Mode, an object with a visible plug-in effect (in it, or in a symbol it places) is an error and nothing changes. 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 becomes a plain group of its art, except one selected with art it measures, directly or in groups without fills, strokes or effects of their own: its lines and label stay as they are, and if the new art no longer fits its link it stays where it was, no longer linked. Hidden fills, strokes and effects are dropped. In Safe Mode, an object with a visible plug-in effect (in it, or in a symbol it places) is an error and nothing changes. 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
|
||||
),
|
||||
@@ -674,11 +674,19 @@ fn load_texture(params: &mut Value, cmd: &str) -> Result<()> {
|
||||
/// fills, strokes or effects of its own or such members, other objects with effects, or type with
|
||||
/// fills or strokes of its own?
|
||||
pub(crate) fn expandable(n: &Node) -> bool {
|
||||
expandable_except(n, &[])
|
||||
}
|
||||
|
||||
/// [`expandable`], leaving out the objects `skip` lists and what they hold.
|
||||
pub(crate) fn expandable_except(n: &Node, skip: &[NodeId]) -> bool {
|
||||
if skip.contains(&n.id) {
|
||||
return false;
|
||||
}
|
||||
match &n.kind {
|
||||
NodeKind::Path { clipping: true, .. } | NodeKind::Path { guide: true, .. } => false,
|
||||
NodeKind::Path { .. } | NodeKind::Compound { .. } => !n.appearance.is_basic() || vectorcraft_brush::has_brush(n),
|
||||
NodeKind::Group { children, .. } | NodeKind::Layer { children, .. } => {
|
||||
!n.appearance.items.is_empty() || !n.appearance.effects.is_empty() || children.iter().any(|c| expandable(c))
|
||||
!n.appearance.items.is_empty() || !n.appearance.effects.is_empty() || children.iter().any(|c| expandable_except(c, skip))
|
||||
}
|
||||
NodeKind::Text(_) => !n.appearance.items.is_empty() || !n.appearance.effects.is_empty(),
|
||||
NodeKind::Image(_) => effects::has_item_geometry(n) || !n.appearance.effects.is_empty(),
|
||||
@@ -688,9 +696,11 @@ pub(crate) fn expandable(n: &Node) -> bool {
|
||||
|
||||
fn can_expand(s: &Session) -> std::result::Result<(), String> {
|
||||
has_doc(s)?;
|
||||
let d = &s.doc().map_err(|e| e.to_string())?.doc;
|
||||
let ids = appearance_targets(s, &Value::Null).map_err(|e| e.to_string())?;
|
||||
if ids.iter().filter_map(|id| d.node(*id)).any(expandable) {
|
||||
let d = &s.doc().map_err(|e| e.to_string())?.doc;
|
||||
// A dimension selected with the art it measures is left as it is.
|
||||
let kept = super::dimension::with_their_art(d, &ids);
|
||||
if ids.iter().filter_map(|id| d.node(*id)).any(|n| expandable_except(n, &kept)) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err("select (or target) objects whose appearance isn't basic".into())
|
||||
@@ -975,7 +985,8 @@ fn expand_appearance(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
// In Safe Mode a plug-in effect draws nothing: expanding it would drop the effect and keep the
|
||||
// geometry it hasn't changed.
|
||||
if vectorcraft_plugins::safe_mode::active() {
|
||||
let d = &s.doc()?.doc;
|
||||
// A dimension selected with the art it measures is left as it is.
|
||||
let d = &*super::dimension::without_kept(&s.doc()?.doc, &roots);
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
if roots.iter().filter_map(|id| d.node(*id)).any(|n| holds_plugin_effect(d, n, false, &mut seen)) {
|
||||
return Err(EngineError::Other("Expand Appearance: a plug-in effect in the selection can't be expanded in Safe Mode".into()));
|
||||
@@ -985,20 +996,24 @@ fn expand_appearance(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
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);
|
||||
// A dimension selected with the art it measures keeps its own lines and label: it is out
|
||||
// of the document while the art expands.
|
||||
let lifted = super::dimension::lift(d, &roots);
|
||||
let kept = !lifted.is_empty();
|
||||
let 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);
|
||||
super::dimension::put_back(d, lifted);
|
||||
// A dimension whose source was replaced stays where it was, no longer linked.
|
||||
super::dimension::restore_unchanged(&before, 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);
|
||||
if out.is_empty() {
|
||||
if out.is_empty() || (kept && d.layers == before.layers) {
|
||||
return Err(EngineError::Other("Expand Appearance: the selection has a basic appearance".into()));
|
||||
}
|
||||
super::paint::check_colorized_kept(colorized_ok, d).map_err(|e| bad("Expand Appearance", e))?;
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
//! 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. A dimension whose art
|
||||
//! Expand changes becomes a plain group of that art.
|
||||
//! Expand changes becomes a plain group of that art, except one selected with art it measures,
|
||||
//! which Expand leaves as it is.
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
use std::sync::Arc;
|
||||
@@ -43,7 +44,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; a dimension whose art an option changes, or one selected with the art it measures when that art is replaced, becomes a plain group of its art → {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), 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; a dimension whose art an option changes becomes a plain group of its art, except one selected with art it measures, directly or in groups without fills, strokes or effects of their own: its lines and label stay as they are, and if the new art no longer fits its link it stays where it was, no longer linked → {ids}",
|
||||
has_selection,
|
||||
expand
|
||||
),
|
||||
@@ -55,7 +56,8 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"{} what each Expand option would change in the selection → {object, fill, stroke, gradient, appearance, symbolArt?: {fill, stroke, gradient}} (true: something to expand; gradient: a gradient fill that Fill expands, which Expand Gradient To applies to; appearance: appearance attributes Expand Appearance expands first, for which Object › Expand… is dimmed in the menus; symbolArt, when the selection holds symbol instances that Object expands: true when only their art holds it, which Fill and Stroke reach only with Object on)",
|
||||
has_selection,
|
||||
|s, _| {
|
||||
let e = Expandable::of(&s.doc()?.doc, &selected_roots(s)?);
|
||||
let roots = selected_roots(s)?;
|
||||
let e = Expandable::of(&super::dimension::without_kept(&s.doc()?.doc, &roots), &roots);
|
||||
let mut info =
|
||||
json!({"object": e.object(), "fill": e.fill, "stroke": e.stroke, "gradient": e.gradient, "appearance": has_appearance(s)});
|
||||
if e.symbols {
|
||||
@@ -74,7 +76,9 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
/// (#3089); `object.expand` itself still bakes them.
|
||||
pub fn has_appearance(s: &Session) -> bool {
|
||||
let (Ok(st), Ok(roots)) = (s.doc(), selected_roots(s)) else { return false };
|
||||
roots.iter().filter_map(|r| st.doc.node(*r)).any(super::effectcmd::expandable)
|
||||
// A dimension selected with the art it measures is left as it is.
|
||||
let kept = super::dimension::with_their_art(&st.doc, &roots);
|
||||
roots.iter().filter_map(|r| st.doc.node(*r)).any(|n| super::effectcmd::expandable_except(n, &kept))
|
||||
}
|
||||
|
||||
/// Every placed document in `roots` of `d` (at any depth) replaced by an editable copy of the art it
|
||||
@@ -241,9 +245,18 @@ fn expand_steps(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let stroke = switch_or(p, "stroke", true, C)?;
|
||||
let to = GradientTo::parse(p)?;
|
||||
let from = s.doc()?.history.undo.len();
|
||||
let mut e = Expandable::of(&s.doc()?.doc, &selected_roots(s)?);
|
||||
// Before anything expands: outlining the art a dimension measures would leave the dimension unlinked.
|
||||
let before = s.doc()?.doc.clone();
|
||||
let selected = s.doc()?.selection.objects.clone();
|
||||
// A dimension selected with the art it measures keeps its own lines and label: it is out of the
|
||||
// document while the art expands, so no step below reaches it.
|
||||
let roots = selected_roots(s)?;
|
||||
let mut lifted = vec![];
|
||||
if !super::dimension::with_their_art(&before, &roots).is_empty() {
|
||||
lifted = s.edit("Expand", |d, _| Ok(super::dimension::lift(d, &roots)))?;
|
||||
}
|
||||
let kept: Vec<NodeId> = lifted.iter().map(super::dimension::Lifted::id).collect();
|
||||
let mut e = Expandable::of(&s.doc()?.doc, &selected_roots(s)?);
|
||||
let mut ended = vec![];
|
||||
end_dimension_links(s, &mut ended)?;
|
||||
let rev0 = s.doc()?.revision;
|
||||
@@ -339,14 +352,25 @@ 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));
|
||||
if !ended.is_empty() || !lifted.is_empty() {
|
||||
s.edit("Expand", |d, sel| {
|
||||
if !lifted.is_empty() {
|
||||
super::dimension::put_back(d, lifted);
|
||||
// The dimensions put back follow what Expand made of the art they measure: a
|
||||
// dimension whose art no longer fits its link stays where it was, no longer linked.
|
||||
super::dimension::refresh(&before, d)?;
|
||||
let mut ids = sel.objects.clone();
|
||||
let again: Vec<NodeId> = kept.iter().copied().filter(|id| selected.contains(id) && !ids.contains(id)).collect();
|
||||
ids.extend(again);
|
||||
sel.set(ids);
|
||||
}
|
||||
// A dimension whose source Expand replaced stays where it was, no longer linked.
|
||||
super::dimension::restore_unchanged(&before, d, ended);
|
||||
Ok(())
|
||||
})?;
|
||||
if !kept.is_empty() && s.doc()?.doc.layers == before.layers {
|
||||
return Err(EngineError::Other("Expand: nothing to expand".into()));
|
||||
}
|
||||
}
|
||||
squash(s, from, "Expand");
|
||||
Ok(json!({ "ids": s.doc()?.selection.objects.iter().map(|i| i.0).collect::<Vec<_>>() }))
|
||||
|
||||
@@ -135,7 +135,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Outline Stroke",
|
||||
["Object", "Path"],
|
||||
None,
|
||||
"{} 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}",
|
||||
"{} replace stroked paths with filled outlines; a dimension whose lines are outlined becomes a plain group of its art, except one selected with art it measures, directly or in groups without fills, strokes or effects of their own: its lines stay as they are, and if the outlined art no longer fits its link it stays where it was, no longer linked → {ids}",
|
||||
has_selection,
|
||||
outline_stroke
|
||||
),
|
||||
@@ -1074,7 +1074,11 @@ 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);
|
||||
// A dimension selected with the art it measures keeps its own lines: it is out of the
|
||||
// document while the art is outlined.
|
||||
let lifted = super::dimension::lift(d, &roots);
|
||||
let kept = !lifted.is_empty();
|
||||
let ended = super::dimension::end_links(d, &roots);
|
||||
let brushes = vectorcraft_brush::library(d);
|
||||
let mut new_sel = vec![];
|
||||
let mut changed = 0;
|
||||
@@ -1083,10 +1087,10 @@ 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 {
|
||||
super::dimension::put_back(d, lifted);
|
||||
// A dimension whose source was outlined stays where it was, no longer linked.
|
||||
super::dimension::restore_unchanged(&before, d, ended);
|
||||
if changed == 0 || (kept && d.layers == before.layers) {
|
||||
return Err(EngineError::Other("Outline Stroke: no stroked paths selected".into()));
|
||||
}
|
||||
sel.set(new_sel.iter().copied());
|
||||
|
||||
@@ -596,6 +596,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 before = (!super::dimension::reached(d, &roots).is_empty()).then(|| d.clone());
|
||||
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![];
|
||||
@@ -691,7 +692,9 @@ fn create_outlines(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
new_sel.push(gid);
|
||||
}
|
||||
}
|
||||
super::dimension::restore_unchanged(d, ended);
|
||||
if let Some(before) = &before {
|
||||
super::dimension::restore_unchanged(before, 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