mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-13 13:09:16 +00:00
Merge pull request #7036 from dotCooCoo/fix/pattern-cancel-dangling
Patterns: canceling a new pattern leaves no swatch or paint of it
This commit is contained in:
+159
-42
@@ -5,7 +5,7 @@
|
||||
//! tint (`Paint::Solid.swatch` / `.tint`, `GradientStop.swatch` / `.tint`); a tint swatch is a
|
||||
//! swatch whose own paint links to its base that way. [`Document::map_solid_paints`] rewrites
|
||||
//! those links (and colours) wherever art and swatches hold them, copying only the nodes that
|
||||
//! change.
|
||||
//! change; [`Document::map_paints`] replaces whole paints of any kind in the same places.
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::Arc;
|
||||
@@ -124,6 +124,52 @@ impl Document {
|
||||
/// ([`Paint::map_links`]). Only nodes holding a changed paint (and the paths to them) are
|
||||
/// copied. Returns the number of changed paints.
|
||||
pub fn map_solid_paints(&mut self, f: &mut LinkFn) -> usize {
|
||||
let mut n = self.map_art_paints(&mut relink(f));
|
||||
for gs in &mut self.graphic_styles {
|
||||
for it in &mut gs.appearance.items {
|
||||
n += usize::from(item_paint_mut(it).map_links(f));
|
||||
}
|
||||
}
|
||||
for w in self.swatches_iter_mut() {
|
||||
n += usize::from(w.paint.map_links(f));
|
||||
}
|
||||
let mut g = relink(f);
|
||||
for def in &mut self.char_styles {
|
||||
for key in ["fill", "stroke"] {
|
||||
n += usize::from(map_style_paint(&mut def.attrs, key, &mut g));
|
||||
}
|
||||
}
|
||||
n
|
||||
}
|
||||
|
||||
/// Visit every paint in the places [`Document::map_solid_paints`] visits (fills, strokes, text
|
||||
/// runs, image colorize paints, dynamic symbol instances' own paints and graphs' stored series and
|
||||
/// label paints in the art, opacity masks included, symbol definitions, graph designs, pattern tiles and the tile that pattern
|
||||
/// editing's Cancel puts back; graphic styles; the swatches; character styles' fill and
|
||||
/// stroke), whatever its kind. `f(paint)` gives the paint to put in its place, or `None` to keep
|
||||
/// it. Only nodes holding a changed paint (and the paths to them) are copied. Returns the number
|
||||
/// of changed paints.
|
||||
pub fn map_paints(&mut self, f: &mut PaintFn) -> usize {
|
||||
let mut n = self.map_art_paints(f);
|
||||
for gs in &mut self.graphic_styles {
|
||||
for it in &mut gs.appearance.items {
|
||||
n += usize::from(replace_paint(item_paint_mut(it), f));
|
||||
}
|
||||
}
|
||||
for w in self.swatches_iter_mut() {
|
||||
n += usize::from(replace_paint(&mut w.paint, f));
|
||||
}
|
||||
for def in &mut self.char_styles {
|
||||
for key in ["fill", "stroke"] {
|
||||
n += usize::from(map_style_paint(&mut def.attrs, key, f));
|
||||
}
|
||||
}
|
||||
n
|
||||
}
|
||||
|
||||
/// [`Document::map_paints`] limited to the node trees: the art, symbol definitions, graph
|
||||
/// designs, pattern tiles and the tile that pattern editing's Cancel puts back.
|
||||
fn map_art_paints(&mut self, f: &mut PaintFn) -> usize {
|
||||
let mut n = map_trees(&mut self.layers, f);
|
||||
for s in &mut self.symbols {
|
||||
n += map_tree(&mut s.art, f);
|
||||
@@ -134,19 +180,6 @@ impl Document {
|
||||
for p in self.patterns.iter_mut().chain(self.pattern_edit.iter_mut().filter_map(|pe| pe.original.as_mut())) {
|
||||
n += map_trees(&mut p.art, f);
|
||||
}
|
||||
for gs in &mut self.graphic_styles {
|
||||
for it in &mut gs.appearance.items {
|
||||
n += usize::from(item_paint_mut(it).map_links(f));
|
||||
}
|
||||
}
|
||||
for w in self.swatches_iter_mut() {
|
||||
n += usize::from(w.paint.map_links(f));
|
||||
}
|
||||
for def in &mut self.char_styles {
|
||||
for key in ["fill", "stroke"] {
|
||||
n += usize::from(map_style_paint(&mut def.attrs, key, f));
|
||||
}
|
||||
}
|
||||
n
|
||||
}
|
||||
|
||||
@@ -156,9 +189,10 @@ impl Document {
|
||||
let roots: Vec<NodeId> =
|
||||
ids.iter().copied().filter(|id| !self.ancestry(*id).is_some_and(|a| a[..a.len() - 1].iter().any(|p| ids.contains(p)))).collect();
|
||||
let mut n = 0;
|
||||
let mut g = relink(f);
|
||||
for id in roots {
|
||||
let Some(node) = self.node(id) else { continue };
|
||||
if let Some((new, count)) = map_node(node, f)
|
||||
if let Some((new, count)) = map_node(node, &mut g)
|
||||
&& let Some(slot) = self.node_mut(id)
|
||||
{
|
||||
*slot = new;
|
||||
@@ -176,7 +210,7 @@ impl Document {
|
||||
pub fn visit_paints(&self, f: &mut dyn FnMut(&Paint)) {
|
||||
let mut tree = |n: &Node| {
|
||||
walk_with_masks(n, &mut |m| {
|
||||
node_paints(m).chain(override_paints(m)).for_each(&mut *f);
|
||||
node_paints(m).chain(override_paints(m)).chain(graph_paints(m)).for_each(&mut *f);
|
||||
if let NodeKind::Mesh(mesh) = &m.kind {
|
||||
mesh.points.iter().for_each(|p| f(&Paint::solid(p.color)));
|
||||
}
|
||||
@@ -258,14 +292,39 @@ impl FreeSwatchNames {
|
||||
/// true when it changed them.
|
||||
pub type LinkFn<'a> = dyn FnMut(&mut Color, &mut Option<String>, &mut f32) -> bool + 'a;
|
||||
|
||||
/// Apply `f` to the paint a character style's attribute `key` holds, as [`Paint::map_links`]
|
||||
/// does, and store the result in the attribute. True when it changed.
|
||||
fn map_style_paint(attrs: &mut serde_json::Map<String, serde_json::Value>, key: &str, f: &mut LinkFn) -> bool {
|
||||
let Some(value) = attrs.get_mut(key) else { return false };
|
||||
let Ok(mut paint) = serde_json::from_value::<Paint>(value.clone()) else { return false };
|
||||
if !paint.map_links(f) {
|
||||
return false;
|
||||
/// What [`Document::map_paints`] applies to each paint: the paint to put in its place, or `None`
|
||||
/// to keep it.
|
||||
pub type PaintFn<'a> = dyn FnMut(&Paint) -> Option<Paint> + 'a;
|
||||
|
||||
/// `f` as a [`PaintFn`]: a solid or gradient paint with `f` applied ([`Paint::map_links`]) when
|
||||
/// `f` changed it, `None` for every other paint.
|
||||
fn relink<'a>(f: &'a mut LinkFn<'_>) -> impl FnMut(&Paint) -> Option<Paint> + 'a {
|
||||
move |p| match p {
|
||||
Paint::Solid { .. } | Paint::Gradient(_) => {
|
||||
let mut q = p.clone();
|
||||
q.map_links(f).then_some(q)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Put `f(p)` in place of `p` when it gives a paint. True when it did.
|
||||
fn replace_paint(p: &mut Paint, f: &mut PaintFn) -> bool {
|
||||
match f(p) {
|
||||
Some(q) => {
|
||||
*p = q;
|
||||
true
|
||||
}
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply `f` to the paint a character style's attribute `key` holds and store the result in the
|
||||
/// attribute. An attribute that is not a paint stays as it is. True when it changed.
|
||||
fn map_style_paint(attrs: &mut serde_json::Map<String, serde_json::Value>, key: &str, f: &mut PaintFn) -> bool {
|
||||
let Some(value) = attrs.get_mut(key) else { return false };
|
||||
let Ok(paint) = serde_json::from_value::<Paint>(value.clone()) else { return false };
|
||||
let Some(paint) = f(&paint) else { return false };
|
||||
match serde_json::to_value(&paint) {
|
||||
Ok(changed) => {
|
||||
*value = changed;
|
||||
@@ -323,6 +382,23 @@ pub fn override_paints(n: &Node) -> impl Iterator<Item = &Paint> {
|
||||
n.extras().symbol_overrides.iter().flat_map(|o| [o.fill.as_ref(), o.stroke.as_ref()]).flatten()
|
||||
}
|
||||
|
||||
/// The paints a graph keeps for its series and its labels ([`crate::graph::GraphSpec`]), which its
|
||||
/// marks and labels take each time it is drawn again and which [`node_paints`] leaves out.
|
||||
pub fn graph_paints(n: &Node) -> impl Iterator<Item = &Paint> {
|
||||
let g = n.extras().graph.as_deref();
|
||||
let series = g.into_iter().flat_map(|g| &g.series_paints).flat_map(|s| [s.fill.as_ref(), s.stroke.as_ref()]).flatten();
|
||||
let labels = g.into_iter().flat_map(|g| [&g.label_styles.values, &g.label_styles.categories, &g.label_styles.legend]).flatten();
|
||||
series.chain(labels.flat_map(|st| [&st.fill, &st.stroke]))
|
||||
}
|
||||
|
||||
/// Mutable [`graph_paints`] of graph `g`, in the same order.
|
||||
fn graph_paints_mut(g: &mut crate::graph::GraphSpec) -> impl Iterator<Item = &mut Paint> {
|
||||
let series = g.series_paints.iter_mut().flat_map(|s| [s.fill.as_mut(), s.stroke.as_mut()]).flatten();
|
||||
let l = &mut g.label_styles;
|
||||
let labels = [&mut l.values, &mut l.categories, &mut l.legend].into_iter().flatten();
|
||||
series.chain(labels.flat_map(|st| [&mut st.fill, &mut st.stroke]))
|
||||
}
|
||||
|
||||
/// Mutable [`node_paints`], in the same order.
|
||||
pub fn node_paints_mut(n: &mut Node) -> impl Iterator<Item = &mut Paint> {
|
||||
let runs = match &mut n.kind {
|
||||
@@ -333,20 +409,11 @@ pub fn node_paints_mut(n: &mut Node) -> impl Iterator<Item = &mut Paint> {
|
||||
n.appearance.items.iter_mut().map(item_paint_mut).chain(runs.iter_mut().flat_map(|r| [&mut r.style.fill, &mut r.style.stroke])).chain(colorize)
|
||||
}
|
||||
|
||||
/// `n` with `f` applied to its subtree's linkable colours, or `None` when nothing changed (then
|
||||
/// nothing was copied). Also returns the number of changed paints.
|
||||
fn map_node(n: &Node, f: &mut LinkFn) -> Option<(Node, usize)> {
|
||||
// Try each own solid or gradient paint on a scratch copy first, so unchanged nodes are never
|
||||
// cloned.
|
||||
let mut changes: Vec<(usize, Paint)> = vec![];
|
||||
for (i, p) in node_paints(n).enumerate() {
|
||||
if matches!(p, Paint::Solid { .. } | Paint::Gradient(_)) {
|
||||
let mut q = p.clone();
|
||||
if q.map_links(f) {
|
||||
changes.push((i, q));
|
||||
}
|
||||
}
|
||||
}
|
||||
/// `n` with `f` applied to its subtree's paints, or `None` when nothing changed (then nothing was
|
||||
/// copied). Also returns the number of changed paints.
|
||||
fn map_node(n: &Node, f: &mut PaintFn) -> Option<(Node, usize)> {
|
||||
// `f` gives each own paint's replacement first, so unchanged nodes are never cloned.
|
||||
let changes: Vec<(usize, Paint)> = node_paints(n).enumerate().filter_map(|(i, p)| f(p).map(|q| (i, q))).collect();
|
||||
let mut count = changes.len();
|
||||
let mut out: Option<Node> = None;
|
||||
if !changes.is_empty() {
|
||||
@@ -366,15 +433,29 @@ fn map_node(n: &Node, f: &mut LinkFn) -> Option<(Node, usize)> {
|
||||
let mut overrides = n.extras().symbol_overrides.clone();
|
||||
let mut k = 0;
|
||||
for p in overrides.iter_mut().flat_map(|o| [&mut o.fill, &mut o.stroke]).flatten() {
|
||||
if matches!(p, Paint::Solid { .. } | Paint::Gradient(_)) && p.map_links(f) {
|
||||
k += 1;
|
||||
}
|
||||
k += usize::from(replace_paint(p, f));
|
||||
}
|
||||
if k > 0 {
|
||||
out.get_or_insert_with(|| n.clone()).extras_mut().symbol_overrides = overrides;
|
||||
count += k;
|
||||
}
|
||||
}
|
||||
// A graph's series and label paints, which its marks and labels take when it is drawn again.
|
||||
let graph: Vec<(usize, Paint)> = graph_paints(n).enumerate().filter_map(|(i, p)| f(p).map(|q| (i, q))).collect();
|
||||
if !graph.is_empty() {
|
||||
count += graph.len();
|
||||
let m = out.get_or_insert_with(|| n.clone());
|
||||
if let Some(spec) = m.extras_if_any().and_then(|e| e.graph.as_deref_mut()) {
|
||||
let mut graph = graph.into_iter().peekable();
|
||||
for (i, p) in graph_paints_mut(spec).enumerate() {
|
||||
if graph.peek().is_some_and(|c| c.0 == i)
|
||||
&& let Some((_, q)) = graph.next()
|
||||
{
|
||||
*p = q;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(ch) = n.children() {
|
||||
for (i, c) in ch.iter().enumerate() {
|
||||
if let Some((new, k)) = map_node(c, f) {
|
||||
@@ -415,6 +496,13 @@ pub fn walk_with_masks<'a>(n: &'a Node, f: &mut dyn FnMut(&'a Node)) {
|
||||
/// brush: `f` applied to the linkable colors of `n` and its subtree (opacity masks included). Only
|
||||
/// the nodes holding a changed paint are copied. Returns the number of changed paints.
|
||||
pub fn map_node_solid_paints(n: &mut Node, f: &mut LinkFn) -> usize {
|
||||
map_node_paints(n, &mut relink(f))
|
||||
}
|
||||
|
||||
/// [`Document::map_paints`] for art a document keeps outside its trees, such as the art of a brush:
|
||||
/// `f` applied to every paint of `n` and its subtree (opacity masks included). Only the nodes
|
||||
/// holding a changed paint are copied. Returns the number of changed paints.
|
||||
pub fn map_node_paints(n: &mut Node, f: &mut PaintFn) -> usize {
|
||||
match map_node(n, f) {
|
||||
Some((new, count)) => {
|
||||
*n = new;
|
||||
@@ -424,7 +512,7 @@ pub fn map_node_solid_paints(n: &mut Node, f: &mut LinkFn) -> usize {
|
||||
}
|
||||
}
|
||||
|
||||
fn map_tree(n: &mut Arc<Node>, f: &mut LinkFn) -> usize {
|
||||
fn map_tree(n: &mut Arc<Node>, f: &mut PaintFn) -> usize {
|
||||
match map_node(n, f) {
|
||||
Some((new, count)) => {
|
||||
*n = Arc::new(new);
|
||||
@@ -434,7 +522,7 @@ fn map_tree(n: &mut Arc<Node>, f: &mut LinkFn) -> usize {
|
||||
}
|
||||
}
|
||||
|
||||
fn map_trees(v: &mut [Arc<Node>], f: &mut LinkFn) -> usize {
|
||||
fn map_trees(v: &mut [Arc<Node>], f: &mut PaintFn) -> usize {
|
||||
v.iter_mut().map(|n| map_tree(n, f)).sum()
|
||||
}
|
||||
|
||||
@@ -593,6 +681,35 @@ mod tests {
|
||||
assert_eq!(map_node_solid_paints(&mut loose, &mut to_blue), 0, "nothing left to change");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapping_reaches_the_series_and_label_paints_a_graph_keeps() {
|
||||
let (mut d, a, _) = doc();
|
||||
let red = linked(Color::rgb(1.0, 0.0, 0.0), "Brand");
|
||||
let spec = crate::graph::GraphSpec {
|
||||
series_paints: vec![crate::SeriesPaint { fill: Some(red.clone()), stroke: Some(red.clone()), ..Default::default() }],
|
||||
label_styles: crate::graph::LabelStyles {
|
||||
legend: Some(crate::CharStyle { fill: red.clone(), stroke: Paint::None, ..Default::default() }),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
d.node_mut(a).unwrap().extras_mut().graph = Some(Box::new(spec));
|
||||
let mut seen = 0;
|
||||
d.visit_paints(&mut |p| seen += usize::from(*p == red));
|
||||
assert_eq!(seen, 4, "the fill and the graph's series fill, series stroke and legend fill");
|
||||
let blue = Color::rgb(0.0, 0.0, 1.0);
|
||||
let mut to_blue = |c: &mut Color, s: &mut Option<String>, _: &mut f32| s.as_deref() == Some("Brand") && std::mem::replace(c, blue) != blue;
|
||||
assert_eq!(d.map_solid_paints(&mut to_blue), 4);
|
||||
let g = d.node(a).unwrap().extras().graph.clone().unwrap();
|
||||
assert_eq!(g.series_paints[0].fill, Some(linked(blue, "Brand")));
|
||||
assert_eq!(g.series_paints[0].stroke, Some(linked(blue, "Brand")));
|
||||
let legend = g.label_styles.legend.unwrap();
|
||||
assert_eq!((legend.fill, legend.stroke), (linked(blue, "Brand"), Paint::None));
|
||||
let mut cleared = |p: &Paint| (*p == linked(blue, "Brand")).then_some(Paint::None);
|
||||
assert_eq!(d.map_paints(&mut cleared), 4, "the fill and the graph's three");
|
||||
assert!(graph_paints(d.node(a).unwrap()).all(Paint::is_none));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapping_within_ids_visits_each_paint_once() {
|
||||
let (mut d, a, b) = doc();
|
||||
|
||||
@@ -397,7 +397,9 @@ pub(crate) fn commit_new_art_colors(s: &mut Session) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn record_new_art_colors(s: &mut Session, before: vectorcraft_tools::PaintDefaults) -> Result<()> {
|
||||
/// Record new-art paints `before` and the session's current ones with the step just made (or the
|
||||
/// interaction or undo group in progress), so Undo puts `before` back and Redo the current ones.
|
||||
pub(super) fn record_new_art_colors(s: &mut Session, before: vectorcraft_tools::PaintDefaults) -> Result<()> {
|
||||
let after = s.paint.clone();
|
||||
if before != after || s.batch_stash.is_some() || s.doc()?.undo_group.is_some() {
|
||||
let st = s.doc_mut()?;
|
||||
@@ -422,7 +424,9 @@ fn record_new_art_colors(s: &mut Session, before: vectorcraft_tools::PaintDefaul
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn new_art_colors_before(s: &Session) -> Result<vectorcraft_tools::PaintDefaults> {
|
||||
/// The new-art paints an undo step that starts now puts back: those at the start of the undo group
|
||||
/// or interaction in progress, else the session's current ones.
|
||||
pub(super) fn new_art_colors_before(s: &Session) -> Result<vectorcraft_tools::PaintDefaults> {
|
||||
let st = s.doc()?;
|
||||
Ok(st.undo_group.as_ref().map(|g| &g.paint).or_else(|| st.interaction.as_ref().map(|it| &it.paint)).cloned().unwrap_or_else(|| s.paint.clone()))
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ use std::sync::Arc;
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_color::Paint;
|
||||
use vectorcraft_doc::pattern::{self, Overlap, PATTERN_EDIT_LAYER, PatternDef, PatternEdit, RepeatKind, RepeatSpec, TileType, pattern_paint};
|
||||
use vectorcraft_doc::swatches::PaintFn;
|
||||
use vectorcraft_doc::{Document, Node, NodeId, NodeKind, Selection};
|
||||
use vectorcraft_geom::{Point, Rect};
|
||||
|
||||
@@ -54,7 +55,15 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
in_pattern_edit,
|
||||
pattern_done
|
||||
),
|
||||
cmd!("object.pattern.cancel", "Cancel", [], None, "{} leave pattern editing mode, discarding changes", in_pattern_edit, pattern_cancel),
|
||||
cmd!(
|
||||
"object.pattern.cancel",
|
||||
"Cancel",
|
||||
[],
|
||||
None,
|
||||
"{} leave pattern editing mode, discarding changes; a pattern made for this edit is removed with its swatches, and every paint of it, new art's fill and stroke included, becomes none",
|
||||
in_pattern_edit,
|
||||
pattern_cancel
|
||||
),
|
||||
cmd!(
|
||||
"object.pattern.saveCopy",
|
||||
"Save a Copy",
|
||||
@@ -78,7 +87,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Delete Pattern",
|
||||
[],
|
||||
None,
|
||||
"{name} delete a pattern swatch (objects painted with it lose that paint)",
|
||||
"{name} delete a pattern and its swatches; every paint of it, new art's fill and stroke included, becomes none → {unpainted: the number of document paints cleared}",
|
||||
has_doc,
|
||||
pattern_delete
|
||||
),
|
||||
@@ -403,26 +412,96 @@ pub(crate) fn finish_edit(d: &mut Document, sel: &mut Selection) -> Result<Strin
|
||||
Ok(pe.pattern)
|
||||
}
|
||||
|
||||
/// Leave pattern editing mode, discarding the changes (`object.pattern.cancel`). An existing
|
||||
/// pattern gets its definition back, its name included when Pattern Options renamed it while it was
|
||||
/// edited and no other pattern, swatch or color group has taken that name since. A new pattern is
|
||||
/// removed with its swatches, and every paint of it left in the document and in the fill, stroke
|
||||
/// and appearance new art takes becomes no paint ([`forget_pattern`]).
|
||||
fn pattern_cancel(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
s.edit("Cancel Pattern Editing", |d, sel| {
|
||||
let changed = s.edit("Cancel Pattern Editing", |d, sel| {
|
||||
let (pe, _) = leave_edit(d, sel)?;
|
||||
match pe.original {
|
||||
Some(orig) => {
|
||||
Some(mut orig) => {
|
||||
if orig.name != pe.pattern && (d.pattern(&orig.name).is_some() || swatch_name_clash(d, &pe.pattern, &orig.name)) {
|
||||
orig.name = pe.pattern.clone();
|
||||
}
|
||||
let back = (orig.name != pe.pattern).then(|| orig.name.clone());
|
||||
if let Some(def) = d.pattern_mut(&pe.pattern) {
|
||||
*def = orig;
|
||||
}
|
||||
if let Some(old) = &back {
|
||||
rename_pattern_uses(d, &pe.pattern, old);
|
||||
}
|
||||
Ok(back.map(|old| (pe.pattern, Some(old))))
|
||||
}
|
||||
None => {
|
||||
d.patterns.retain(|p| p.name != pe.pattern);
|
||||
d.swatches.retain(|sw| !matches!(&sw.paint, Paint::Pattern { pattern, .. } if *pattern == pe.pattern));
|
||||
forget_pattern(d, &pe.pattern);
|
||||
Ok(Some((pe.pattern, None)))
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
after_edit_mode(s, None)?;
|
||||
if let Some((name, to)) = changed {
|
||||
map_new_art_paints(s, &mut pattern_paints(&name, to.as_deref()))?;
|
||||
}
|
||||
ok()
|
||||
}
|
||||
|
||||
/// A [`PaintFn`] that puts a paint of pattern `to` in place of each paint of pattern `name`, with
|
||||
/// its placement kept, or no paint for `to` `None`.
|
||||
fn pattern_paints<'a>(name: &'a str, to: Option<&'a str>) -> impl FnMut(&Paint) -> Option<Paint> + 'a {
|
||||
move |p| match p {
|
||||
Paint::Pattern { pattern, xf } if pattern == name => Some(match to {
|
||||
Some(n) => Paint::Pattern { pattern: n.to_string(), xf: *xf },
|
||||
None => Paint::None,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove pattern `name` from `d`: its definition and every swatch, grouped or not, whose paint is
|
||||
/// the pattern. Every paint of the pattern left anywhere in the document (the art, opacity masks,
|
||||
/// type, symbol definitions, graph designs, graphs' stored series and label paints, other patterns'
|
||||
/// tiles, graphic and character styles and the art of brushes) becomes no paint. Returns the number of such paints.
|
||||
fn forget_pattern(d: &mut Document, name: &str) -> usize {
|
||||
d.patterns.retain(|p| p.name != name);
|
||||
let is_it = |sw: &vectorcraft_color::Swatch| matches!(&sw.paint, Paint::Pattern { pattern, .. } if pattern == name);
|
||||
d.swatches.retain(|sw| !is_it(sw));
|
||||
for g in &mut d.swatch_groups {
|
||||
g.swatches.retain(|sw| !is_it(sw));
|
||||
}
|
||||
super::swatch::map_all_paints(d, &mut pattern_paints(name, None))
|
||||
}
|
||||
|
||||
/// Apply `f` to the fill and stroke new art takes and to the paints of its template appearance. A
|
||||
/// change is recorded with the step just made, so Undo and Redo put these paints back with the
|
||||
/// document.
|
||||
fn map_new_art_paints(s: &mut Session, f: &mut PaintFn) -> Result<()> {
|
||||
let start = super::colormgmt::new_art_colors_before(s)?;
|
||||
let old = s.paint.clone();
|
||||
let pd = &mut s.paint;
|
||||
let template = pd.appearance.iter_mut().flat_map(|a| a.items.iter_mut()).map(|it| match it {
|
||||
vectorcraft_doc::AppearanceItem::Fill(f) => &mut f.paint,
|
||||
vectorcraft_doc::AppearanceItem::Stroke(s) => &mut s.paint,
|
||||
});
|
||||
for p in [&mut pd.fill, &mut pd.stroke].into_iter().chain(template) {
|
||||
if let Some(q) = f(p) {
|
||||
*p = q;
|
||||
}
|
||||
}
|
||||
if s.paint != old {
|
||||
super::colormgmt::record_new_art_colors(s, start)?;
|
||||
// The step takes the paints the command finally leaves, after the selection's appearance
|
||||
// is inherited.
|
||||
let st = s.doc()?;
|
||||
if st.interaction.is_none() {
|
||||
let uid = st.uid;
|
||||
s.batch_paint_docs.push(uid);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn pattern_save_copy(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let wanted = text_param(p, "name", "object.pattern.saveCopy")?.map(str::trim).filter(|n| !n.is_empty()).map(str::to_string);
|
||||
let name = s.edit("Save a Copy", |d, _| {
|
||||
@@ -524,6 +603,9 @@ fn pattern_options(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
if let Some(nn) = &new_name {
|
||||
map_new_art_paints(s, &mut pattern_paints(&name, Some(nn)))?;
|
||||
}
|
||||
Ok(json!({ "name": out_name }))
|
||||
}
|
||||
|
||||
@@ -552,47 +634,19 @@ fn swatch_name_clash(d: &Document, old: &str, new: &str) -> bool {
|
||||
}
|
||||
|
||||
/// Rename a pattern in swatches, paints and the edit record. Of the swatches named `old`, only the
|
||||
/// pattern's own take the new name.
|
||||
/// pattern's own take the new name. The paints are renamed wherever [`forget_pattern`] clears them.
|
||||
fn rename_pattern_uses(d: &mut Document, old: &str, new: &str) {
|
||||
let fix = |p: &mut Paint| {
|
||||
if let Paint::Pattern { pattern, .. } = p
|
||||
&& pattern == old
|
||||
{
|
||||
*pattern = new.to_string();
|
||||
}
|
||||
};
|
||||
for sw in d.swatches.iter_mut().chain(d.swatch_groups.iter_mut().flat_map(|g| g.swatches.iter_mut())) {
|
||||
for sw in d.swatches_iter_mut() {
|
||||
if sw.name == old && pattern_name(&sw.paint).is_some_and(|p| p == old) {
|
||||
sw.name = new.to_string();
|
||||
}
|
||||
fix(&mut sw.paint);
|
||||
}
|
||||
if let Some(pe) = &mut d.pattern_edit
|
||||
&& pe.pattern == old
|
||||
{
|
||||
pe.pattern = new.to_string();
|
||||
}
|
||||
let ids = nodes_using(d, Some(old));
|
||||
for id in ids {
|
||||
if let Some(n) = d.node_mut(id) {
|
||||
for it in n.appearance.items.iter_mut() {
|
||||
match it {
|
||||
vectorcraft_doc::AppearanceItem::Fill(f) => fix(&mut f.paint),
|
||||
vectorcraft_doc::AppearanceItem::Stroke(s) => fix(&mut s.paint),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn nodes_using(d: &Document, name: Option<&str>) -> Vec<NodeId> {
|
||||
let mut ids = vec![];
|
||||
d.walk(|n| {
|
||||
if pattern::uses_pattern(n, name) {
|
||||
ids.push(n.id);
|
||||
}
|
||||
});
|
||||
ids
|
||||
super::swatch::map_all_paints(d, &mut pattern_paints(old, Some(new)));
|
||||
}
|
||||
|
||||
fn pattern_delete(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
@@ -604,29 +658,8 @@ fn pattern_delete(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if editing(s).is_some_and(|e| e.pattern == name) {
|
||||
return Err(bad(C, "finish editing the pattern first"));
|
||||
}
|
||||
let n = s.edit("Delete Pattern", |d, _| {
|
||||
d.patterns.retain(|p| p.name != name);
|
||||
let is_it = |sw: &vectorcraft_color::Swatch| matches!(&sw.paint, Paint::Pattern { pattern, .. } if *pattern == name);
|
||||
d.swatches.retain(|sw| !is_it(sw));
|
||||
for g in &mut d.swatch_groups {
|
||||
g.swatches.retain(|sw| !is_it(sw));
|
||||
}
|
||||
let ids = nodes_using(d, Some(&name));
|
||||
for id in &ids {
|
||||
if let Some(n) = d.node_mut(*id) {
|
||||
for it in n.appearance.items.iter_mut() {
|
||||
let p = match it {
|
||||
vectorcraft_doc::AppearanceItem::Fill(f) => &mut f.paint,
|
||||
vectorcraft_doc::AppearanceItem::Stroke(s) => &mut s.paint,
|
||||
};
|
||||
if matches!(p, Paint::Pattern { pattern, .. } if *pattern == name) {
|
||||
*p = Paint::None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(ids.len())
|
||||
})?;
|
||||
let n = s.edit("Delete Pattern", |d, _| Ok(forget_pattern(d, &name)))?;
|
||||
map_new_art_paints(s, &mut pattern_paints(&name, None))?;
|
||||
Ok(json!({ "unpainted": n }))
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::collections::{HashMap, HashSet, VecDeque};
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_color::{Color, Gradient, GradientKind, GradientMethod, GradientStop, Paint, Swatch, SwatchGroup, tint_percent};
|
||||
use vectorcraft_doc::pattern::pattern_paint;
|
||||
use vectorcraft_doc::swatches::{LinkFn, color_name, map_node_solid_paints, node_colors};
|
||||
use vectorcraft_doc::swatches::{LinkFn, PaintFn, color_name, map_node_paints, map_node_solid_paints, node_colors};
|
||||
use vectorcraft_doc::{Document, NodeId};
|
||||
|
||||
use super::paint::paint_from;
|
||||
@@ -681,32 +681,16 @@ pub(super) fn map_default_paints(s: &mut Session, f: &mut LinkFn) {
|
||||
/// paint changed is merged into its entry ([`merge_stored`]), so fields the brush definition does
|
||||
/// not read are kept at every level.
|
||||
pub(super) fn map_swatch_links(d: &mut Document, f: &mut LinkFn) -> usize {
|
||||
use vectorcraft_brush::{Brush, BrushKind, Colorization};
|
||||
let mut n = d.map_solid_paints(f);
|
||||
// Each stored entry, its brush when it parses, the brush as written before the edit (only
|
||||
// when its paint changed), and whether its paint changed.
|
||||
let mut lib: Vec<(Value, Option<Brush>, Value, bool)> = match d.unknown.get(vectorcraft_brush::DOC_KEY) {
|
||||
Some(v) => v.as_array().into_iter().flatten().map(|e| (e.clone(), Brush::from_json(e.clone()).ok(), Value::Null, false)).collect(),
|
||||
None => {
|
||||
vectorcraft_brush::library(d).into_iter().map(|b| (serde_json::to_value(&b).unwrap_or_default(), Some(b), Value::Null, false)).collect()
|
||||
}
|
||||
};
|
||||
let mut in_brushes = 0;
|
||||
for (_, b, base, changed) in lib.iter_mut() {
|
||||
let Some(b) = b else { continue };
|
||||
let before_edit = serde_json::to_value(&*b).unwrap_or_default();
|
||||
use vectorcraft_brush::{BrushKind, Colorization};
|
||||
let n = d.map_solid_paints(f);
|
||||
n + edit_brush_library(d, &mut |b| {
|
||||
let key = match b.kind.colorization() {
|
||||
Colorization::HueShift { key } => Some(key),
|
||||
_ => None,
|
||||
};
|
||||
let before = key.map(|_| art_paint_colors(b.art()));
|
||||
let in_brush = b.art_mut().into_iter().map(|art| map_node_solid_paints(art, f)).sum::<usize>();
|
||||
*changed = in_brush > 0;
|
||||
if *changed {
|
||||
*base = before_edit;
|
||||
}
|
||||
in_brushes += in_brush;
|
||||
let (Some(key), Some(before)) = (key, before) else { continue };
|
||||
let in_brush = b.art_mut().into_iter().map(|art| map_node_solid_paints(art, &mut *f)).sum::<usize>();
|
||||
let (Some(key), Some(before)) = (key, before) else { return in_brush };
|
||||
// Colors are compared by hex value, as `art_colors` (Brush Options' key colors) lists them.
|
||||
let same = |a: &Color, b: &Color| a.to_hex() == b.to_hex();
|
||||
let after = art_paint_colors(b.art());
|
||||
@@ -719,10 +703,47 @@ pub(super) fn map_swatch_links(d: &mut Document, f: &mut LinkFn) -> usize {
|
||||
BrushKind::Art(a) => &mut a.colorization,
|
||||
BrushKind::Scatter(s) => &mut s.colorization,
|
||||
BrushKind::Pattern(p) => &mut p.colorization,
|
||||
BrushKind::Calligraphic(_) | BrushKind::Bristle(_) => continue,
|
||||
BrushKind::Calligraphic(_) | BrushKind::Bristle(_) => return in_brush,
|
||||
};
|
||||
*c = Colorization::HueShift { key };
|
||||
}
|
||||
in_brush
|
||||
})
|
||||
}
|
||||
|
||||
/// Apply a paint rewrite ([`Document::map_paints`]) to the document and to the art of the brushes
|
||||
/// in its library ([`edit_brush_library`]). Returns the number of changed paints.
|
||||
pub(super) fn map_all_paints(d: &mut Document, f: &mut PaintFn) -> usize {
|
||||
let n = d.map_paints(f);
|
||||
n + edit_brush_library(d, &mut |b| b.art_mut().into_iter().map(|art| map_node_paints(art, &mut *f)).sum())
|
||||
}
|
||||
|
||||
/// Apply `edit` to each brush of the document's library, the stored entries or, with none stored,
|
||||
/// the built-in library; `edit` returns how many paints of the brush's art it changed. The library
|
||||
/// is stored again only when a brush's paint changed. Stored entries are stored again as they were
|
||||
/// read, except that a brush whose paint changed is merged into its entry ([`merge_stored`]), so
|
||||
/// fields the brush definition does not read are kept at every level. Returns the number of
|
||||
/// changed paints.
|
||||
fn edit_brush_library(d: &mut Document, edit: &mut dyn FnMut(&mut vectorcraft_brush::Brush) -> usize) -> usize {
|
||||
use vectorcraft_brush::Brush;
|
||||
// Each stored entry, its brush when it parses, the brush as written before the edit (only
|
||||
// when its paint changed), and whether its paint changed.
|
||||
let mut lib: Vec<(Value, Option<Brush>, Value, bool)> = match d.unknown.get(vectorcraft_brush::DOC_KEY) {
|
||||
Some(v) => v.as_array().into_iter().flatten().map(|e| (e.clone(), Brush::from_json(e.clone()).ok(), Value::Null, false)).collect(),
|
||||
None => {
|
||||
vectorcraft_brush::library(d).into_iter().map(|b| (serde_json::to_value(&b).unwrap_or_default(), Some(b), Value::Null, false)).collect()
|
||||
}
|
||||
};
|
||||
let mut in_brushes = 0;
|
||||
for (_, b, base, changed) in lib.iter_mut() {
|
||||
let Some(b) = b else { continue };
|
||||
let before_edit = serde_json::to_value(&*b).unwrap_or_default();
|
||||
let in_brush = edit(b);
|
||||
*changed = in_brush > 0;
|
||||
if *changed {
|
||||
*base = before_edit;
|
||||
}
|
||||
in_brushes += in_brush;
|
||||
}
|
||||
if in_brushes > 0 {
|
||||
let lib = lib
|
||||
@@ -733,9 +754,8 @@ pub(super) fn map_swatch_links(d: &mut Document, f: &mut LinkFn) -> usize {
|
||||
})
|
||||
.collect();
|
||||
d.unknown.insert(vectorcraft_brush::DOC_KEY.into(), Value::Array(lib));
|
||||
n += in_brushes;
|
||||
}
|
||||
n
|
||||
in_brushes
|
||||
}
|
||||
|
||||
/// `new` (a definition as written after an edit) with the parts of `raw` (its stored entry) that
|
||||
|
||||
@@ -27,7 +27,8 @@ pub struct HistoryEntry {
|
||||
pub(crate) named: Option<NamedStep>,
|
||||
/// A number no other step takes ([`History::step`]).
|
||||
serial: u64,
|
||||
/// Original and converted new-art colors, only for steps that change document color mode.
|
||||
/// Original and changed new-art paints, only for steps that change them: a document color mode
|
||||
/// change, and a pattern Cancel, Delete or rename that clears or renames a pattern paint.
|
||||
pub(crate) color_mode_paints: Option<(vectorcraft_tools::PaintDefaults, vectorcraft_tools::PaintDefaults)>,
|
||||
/// Owned new-art snapshots included in both directional estimates.
|
||||
color_mode_bytes: usize,
|
||||
|
||||
@@ -217,9 +217,10 @@ pub struct Session {
|
||||
/// groups: for each step, the documents ([`DocState::uid`]) whose undo groups it belongs to, and
|
||||
/// the index of the document it left active when it changed the active document.
|
||||
pub(crate) batch_steps: Option<Vec<(Vec<u64>, Option<usize>)>>,
|
||||
/// After `command.batch` kept a color-mode step's new-art colors, until the top-level command
|
||||
/// has inherited the selection's appearance: the documents ([`DocState::uid`]) whose newest
|
||||
/// history step takes the colors the batch finally left.
|
||||
/// After `command.batch` kept a color-mode step's new-art colors, or a pattern command recorded
|
||||
/// the new-art paints it changed, until the top-level command has inherited the selection's
|
||||
/// appearance: the documents ([`DocState::uid`]) whose newest history step takes the colors the
|
||||
/// command finally left.
|
||||
pub(crate) batch_paint_docs: Vec<u64>,
|
||||
/// The composite fonts (lowercase names) that the open documents defined when the font
|
||||
/// database was last brought in step with them ([`cmd::compositefonts::sync`]).
|
||||
|
||||
@@ -290,7 +290,10 @@ impl Session {
|
||||
st.revision += 1;
|
||||
}
|
||||
st.doc = it.doc.clone();
|
||||
let restore_paint = cmd == "command.batch" || it.preview.as_ref().is_some_and(|(id, _)| id == "command.batch");
|
||||
// A batch, or a preview that changed the new-art paints (a pattern rename), started from
|
||||
// the paints the interaction began with.
|
||||
let restore_paint =
|
||||
cmd == "command.batch" || it.preview.as_ref().is_some_and(|(id, _)| id == "command.batch") || it.color_mode_paints.is_some();
|
||||
let paint = it.paint.clone();
|
||||
it.preview_paint = None;
|
||||
it.color_mode_paints = None;
|
||||
@@ -423,7 +426,7 @@ impl Session {
|
||||
if let Some(paint) = self
|
||||
.active()
|
||||
.and_then(|st| st.interaction.as_ref())
|
||||
.filter(|it| it.preview.as_ref().is_some_and(|(id, _)| id == "command.batch"))
|
||||
.filter(|it| it.preview.as_ref().is_some_and(|(id, _)| id == "command.batch") || it.color_mode_paints.is_some())
|
||||
.map(|it| it.paint.clone())
|
||||
{
|
||||
self.paint = paint;
|
||||
@@ -433,6 +436,9 @@ impl Session {
|
||||
st.undo_interaction();
|
||||
// Guides locked and slices locked or hidden during the interaction stay deselected.
|
||||
deselect_hidden(&menu, &st.doc, &mut st.selection);
|
||||
// With New Art Has Basic Appearance off, new art takes the appearance of the selection as
|
||||
// the document has it again, not as a preview left it.
|
||||
self.inherit_new_art();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_color::Paint;
|
||||
use vectorcraft_doc::pattern::{PATTERN_EDIT_LAYER, RepeatKind, TileType};
|
||||
use vectorcraft_doc::pattern::{PATTERN_EDIT_LAYER, RepeatKind, TileType, pattern_paint};
|
||||
use vectorcraft_doc::{Node, NodeKind};
|
||||
use vectorcraft_geom::{Point, Rect};
|
||||
use vectorcraft_tools::{Mods, PointerEvent, PointerKind};
|
||||
@@ -212,6 +212,186 @@ fn cancel_new_pattern_removes_it() {
|
||||
assert_eq!(doc(&s).layers.len(), 1);
|
||||
}
|
||||
|
||||
fn doc_mut(s: &mut Session) -> &mut vectorcraft_doc::Document {
|
||||
Arc::make_mut(&mut s.doc_mut().unwrap().doc)
|
||||
}
|
||||
|
||||
fn is_pattern(p: &Paint, name: &str) -> bool {
|
||||
matches!(p, Paint::Pattern { pattern, .. } if pattern == name)
|
||||
}
|
||||
|
||||
/// How many paints of pattern `name` the document holds: in the art and the definitions
|
||||
/// ([`vectorcraft_doc::Document::visit_paints`]), the swatches, the character styles and the
|
||||
/// stored brush library.
|
||||
fn pattern_uses(d: &vectorcraft_doc::Document, name: &str) -> usize {
|
||||
let mut n = 0;
|
||||
d.visit_paints(&mut |p| n += usize::from(is_pattern(p, name)));
|
||||
n += d.swatches_iter().filter(|w| is_pattern(&w.paint, name)).count();
|
||||
let styled = d.char_styles.iter().flat_map(|c| ["fill", "stroke"].map(|k| c.attrs.get(k).cloned())).flatten();
|
||||
n += styled.filter(|v| serde_json::from_value::<Paint>(v.clone()).is_ok_and(|p| is_pattern(&p, name))).count();
|
||||
let needle = format!("\"pattern\":\"{name}\"");
|
||||
n + d.unknown.get(vectorcraft_brush::DOC_KEY).map_or(0, |v| v.to_string().matches(&needle).count())
|
||||
}
|
||||
|
||||
/// Canceling a new pattern removes every swatch of it, a color group's included, and the fills and
|
||||
/// strokes of it on art outside the editing layer become no paint. The object's other paints, and
|
||||
/// a swatch that only shares the pattern's name, stay.
|
||||
#[test]
|
||||
fn cancel_new_pattern_forgets_group_swatch_and_outside_fills_and_strokes() {
|
||||
let mut s = session();
|
||||
let other = rect(&mut s, 100.0, 100.0, 50.0, 50.0);
|
||||
s.execute("paint.setStroke", &json!({"ids": [other.0], "color": "#00ff00"})).unwrap();
|
||||
let r = rect(&mut s, 0.0, 0.0, 10.0, 10.0);
|
||||
sel(&mut s, &[r]);
|
||||
s.execute("object.pattern.make", &json!({"name": "Tiles"})).unwrap();
|
||||
s.execute("paint.setFill", &json!({"ids": [other.0], "swatch": "Tiles"})).unwrap();
|
||||
let d = doc_mut(&mut s);
|
||||
let solid = Paint::solid(vectorcraft_color::Color::rgb(0.0, 0.0, 1.0));
|
||||
d.swatch_groups.push(vectorcraft_color::SwatchGroup {
|
||||
name: "Mine".into(),
|
||||
swatches: vec![
|
||||
vectorcraft_color::Swatch { name: "Grouped".into(), paint: pattern_paint("Tiles"), global: false, spot: false },
|
||||
vectorcraft_color::Swatch { name: "Tiles".into(), paint: solid.clone(), global: false, spot: false },
|
||||
],
|
||||
});
|
||||
assert!(pattern_uses(doc(&s), "Tiles") >= 3);
|
||||
s.execute("object.pattern.cancel", &json!({})).unwrap();
|
||||
let d = doc(&s);
|
||||
assert!(d.pattern("Tiles").is_none());
|
||||
assert_eq!(pattern_uses(d, "Tiles"), 0);
|
||||
assert!(d.swatch("Grouped").is_none(), "the group's pattern swatch goes");
|
||||
assert_eq!(d.swatch("Tiles").map(|w| &w.paint), Some(&solid), "a color swatch named like the pattern stays");
|
||||
let n = node(&s, other);
|
||||
assert!(n.appearance.fill_paint().is_none());
|
||||
assert!(matches!(n.appearance.stroke_paint(), Paint::Solid { .. }), "the stroke stays");
|
||||
}
|
||||
|
||||
/// Save a Copy keeps the copy as its own pattern when the new pattern is canceled, and the copy's
|
||||
/// tile art loses its paints of the canceled pattern.
|
||||
#[test]
|
||||
fn cancel_new_pattern_forgets_its_paints_in_a_saved_copy() {
|
||||
let mut s = session();
|
||||
let r = rect(&mut s, 0.0, 0.0, 10.0, 10.0);
|
||||
sel(&mut s, &[r]);
|
||||
s.execute("object.pattern.make", &json!({"name": "Tiles"})).unwrap();
|
||||
let layer = doc(&s).pattern_edit.as_ref().unwrap().layer;
|
||||
let tile = doc(&s).node(layer).unwrap().children().unwrap()[0].id;
|
||||
s.execute("paint.setFill", &json!({"ids": [tile.0], "swatch": "Tiles"})).unwrap();
|
||||
assert_eq!(s.execute("object.pattern.saveCopy", &json!({})).unwrap()["name"], "Tiles copy");
|
||||
s.execute("object.pattern.cancel", &json!({})).unwrap();
|
||||
let d = doc(&s);
|
||||
let copy = d.pattern("Tiles copy").expect("the copy stays");
|
||||
assert_eq!(copy.art.len(), 1);
|
||||
assert!(copy.art[0].appearance.fill_paint().is_none());
|
||||
assert_eq!(pattern_uses(d, "Tiles"), 0);
|
||||
}
|
||||
|
||||
/// Canceling a new pattern also clears its paints in type, opacity mask art, symbol definitions,
|
||||
/// graph designs, graphic and character styles and brush art, and in the fill, stroke and
|
||||
/// appearance new art takes, so new art does not take a paint of a pattern that is gone.
|
||||
#[test]
|
||||
fn cancel_new_pattern_forgets_hidden_paints_and_new_art_defaults() {
|
||||
let mut s = session();
|
||||
let masked = rect(&mut s, 100.0, 100.0, 50.0, 50.0);
|
||||
let m = rect(&mut s, 100.0, 100.0, 50.0, 50.0);
|
||||
s.execute("transparency.makeOpacityMask", &json!({"ids": [masked.0, m.0]})).unwrap();
|
||||
let t = id_of(&s.execute("text.create", &json!({"x": 20, "y": 300, "text": "Hi"})).unwrap());
|
||||
let band = rect(&mut s, 200.0, 200.0, 40.0, 10.0);
|
||||
let r = rect(&mut s, 0.0, 0.0, 10.0, 10.0);
|
||||
sel(&mut s, &[r]);
|
||||
s.execute("object.pattern.make", &json!({"name": "Tiles"})).unwrap();
|
||||
s.execute("paint.setFill", &json!({"ids": [band.0], "swatch": "Tiles"})).unwrap();
|
||||
s.execute("brush.new", &json!({"type": "art", "name": "Band", "ids": [band.0]})).unwrap();
|
||||
let p = pattern_paint("Tiles");
|
||||
let d = doc_mut(&mut s);
|
||||
let art = d.node(band).unwrap().clone();
|
||||
d.symbols.push(vectorcraft_doc::Symbol { name: "Sym".into(), art: Arc::new(art.clone()), dynamic: false });
|
||||
d.graph_designs.push(vectorcraft_doc::GraphDesign { name: "Bar".into(), art: Arc::new(art.clone()) });
|
||||
d.graphic_styles.push(vectorcraft_doc::GraphicStyle {
|
||||
name: "Style".into(),
|
||||
appearance: (*art.appearance).clone(),
|
||||
id: 0,
|
||||
opacity: 1.0,
|
||||
blend: Default::default(),
|
||||
isolate: false,
|
||||
knockout: Default::default(),
|
||||
unit_box: true,
|
||||
});
|
||||
let attrs = json!({"fill": p, "stroke": p, "size": 12});
|
||||
d.char_styles.push(vectorcraft_doc::TextStyleDef { name: "Char".into(), attrs: attrs.as_object().unwrap().clone() });
|
||||
if let Some(NodeKind::Text(text)) = d.node_mut(t).map(|n| &mut n.kind) {
|
||||
text.runs[0].style.fill = p.clone();
|
||||
}
|
||||
let mask = d.node_mut(masked).and_then(|n| n.extras_if_any()).and_then(|e| e.mask.as_deref_mut()).expect("the mask");
|
||||
Arc::make_mut(&mut mask.art).appearance = vectorcraft_doc::Appearance::basic(p.clone(), Paint::None, 0.0).into();
|
||||
s.paint.fill = p.clone();
|
||||
s.paint.appearance = Some(vectorcraft_doc::Appearance::basic(Paint::None, p.clone(), 1.0));
|
||||
assert!(pattern_uses(doc(&s), "Tiles") >= 10, "{}", pattern_uses(doc(&s), "Tiles"));
|
||||
s.execute("object.pattern.cancel", &json!({})).unwrap();
|
||||
assert_eq!(pattern_uses(doc(&s), "Tiles"), 0);
|
||||
assert!(doc(&s).char_styles[0].attrs["size"] == json!(12), "the style's other attributes stay");
|
||||
assert!(s.paint.fill.is_none());
|
||||
assert!(s.paint.appearance.as_ref().unwrap().items.iter().all(|it| match it {
|
||||
vectorcraft_doc::AppearanceItem::Fill(f) => !is_pattern(&f.paint, "Tiles"),
|
||||
vectorcraft_doc::AppearanceItem::Stroke(k) => !is_pattern(&k.paint, "Tiles"),
|
||||
}));
|
||||
let fresh = rect(&mut s, 300.0, 300.0, 20.0, 20.0);
|
||||
let n = node(&s, fresh);
|
||||
assert!(!is_pattern(&n.appearance.fill_paint(), "Tiles") && !is_pattern(&n.appearance.stroke_paint(), "Tiles"));
|
||||
}
|
||||
|
||||
/// Cancel is one undo step: Undo gives the pattern, its paints and the fill new art takes back, and
|
||||
/// Redo clears them again.
|
||||
#[test]
|
||||
fn cancel_new_pattern_is_one_undo_step() {
|
||||
let mut s = session();
|
||||
let other = rect(&mut s, 100.0, 100.0, 50.0, 50.0);
|
||||
let r = rect(&mut s, 0.0, 0.0, 10.0, 10.0);
|
||||
sel(&mut s, &[r]);
|
||||
s.execute("object.pattern.make", &json!({"name": "Tiles"})).unwrap();
|
||||
s.execute("paint.setFill", &json!({"ids": [other.0], "swatch": "Tiles"})).unwrap();
|
||||
s.paint.fill = pattern_paint("Tiles");
|
||||
let steps = s.doc().unwrap().history.undo.len();
|
||||
s.execute("object.pattern.cancel", &json!({})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), steps + 1);
|
||||
assert!(s.paint.fill.is_none() && doc(&s).pattern("Tiles").is_none());
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert!(is_pattern(&s.paint.fill, "Tiles"));
|
||||
assert!(doc(&s).pattern("Tiles").is_some() && doc(&s).pattern_edit.is_some());
|
||||
assert!(is_pattern(&node(&s, other).appearance.fill_paint(), "Tiles"));
|
||||
s.execute("edit.redo", &json!({})).unwrap();
|
||||
assert!(s.paint.fill.is_none() && doc(&s).pattern("Tiles").is_none());
|
||||
assert_eq!(pattern_uses(doc(&s), "Tiles"), 0);
|
||||
}
|
||||
|
||||
/// Canceling the edit of a pattern that Pattern Options renamed while it was edited gives the
|
||||
/// pattern its old name back, with its swatch and the paints of it, so nothing is left painted
|
||||
/// with a name no pattern has.
|
||||
#[test]
|
||||
fn cancel_after_renaming_an_edited_pattern_restores_its_name_and_paints() {
|
||||
let mut s = session();
|
||||
make_dots(&mut s);
|
||||
let big = rect(&mut s, 100.0, 100.0, 50.0, 50.0);
|
||||
s.execute("paint.setFill", &json!({"ids": [big.0], "swatch": "Dots"})).unwrap();
|
||||
s.execute("object.pattern.edit", &json!({"name": "Dots"})).unwrap();
|
||||
s.execute("pattern.options", &json!({"newName": "Spots"})).unwrap();
|
||||
assert!(is_pattern(&node(&s, big).appearance.fill_paint(), "Spots"));
|
||||
s.execute("object.pattern.cancel", &json!({})).unwrap();
|
||||
let d = doc(&s);
|
||||
assert!(d.pattern("Dots").is_some() && d.pattern("Spots").is_none());
|
||||
assert!(d.swatch("Dots").is_some_and(|w| is_pattern(&w.paint, "Dots")));
|
||||
assert!(is_pattern(&node(&s, big).appearance.fill_paint(), "Dots"));
|
||||
assert_eq!(pattern_uses(d, "Spots"), 0);
|
||||
// When Save a Copy has taken the old name since, the pattern keeps its new name and paints.
|
||||
s.execute("object.pattern.edit", &json!({"name": "Dots"})).unwrap();
|
||||
s.execute("pattern.options", &json!({"newName": "Spots"})).unwrap();
|
||||
s.execute("object.pattern.saveCopy", &json!({"name": "Dots"})).unwrap();
|
||||
s.execute("object.pattern.cancel", &json!({})).unwrap();
|
||||
let d = doc(&s);
|
||||
assert!(d.pattern("Dots").is_some() && d.pattern("Spots").is_some());
|
||||
assert!(is_pattern(&node(&s, big).appearance.fill_paint(), "Spots"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edit_then_cancel_restores_definition() {
|
||||
let mut s = session();
|
||||
@@ -381,6 +561,187 @@ fn delete_pattern_unpaints_objects() {
|
||||
assert!(s.execute("pattern.delete", &json!({"name": "Dots"})).is_err());
|
||||
}
|
||||
|
||||
/// Puts paints of pattern `name` where the layers do not reach: a symbol definition, another
|
||||
/// pattern's tile, a graphic style, a character style, a type run, a color group's swatch and the
|
||||
/// fill new art takes.
|
||||
fn paint_outside_the_layers(s: &mut Session, name: &str) {
|
||||
let t = id_of(&s.execute("text.create", &json!({"x": 20, "y": 300, "text": "Hi"})).unwrap());
|
||||
let p = pattern_paint(name);
|
||||
let d = doc_mut(s);
|
||||
let shape = vectorcraft_geom::shapes::rectangle(Rect::new(0.0, 0.0, 5.0, 5.0));
|
||||
let art = Node::path(d.alloc_id(), shape, vectorcraft_doc::Appearance::basic(p.clone(), Paint::None, 0.0));
|
||||
d.symbols.push(vectorcraft_doc::Symbol { name: "Sym".into(), art: Arc::new(art.clone()), dynamic: false });
|
||||
d.patterns.push(vectorcraft_doc::pattern::PatternDef::new("Other", vec![Arc::new(art.clone())]));
|
||||
d.graphic_styles.push(vectorcraft_doc::GraphicStyle {
|
||||
name: "Style".into(),
|
||||
appearance: (*art.appearance).clone(),
|
||||
id: 0,
|
||||
opacity: 1.0,
|
||||
blend: Default::default(),
|
||||
isolate: false,
|
||||
knockout: Default::default(),
|
||||
unit_box: true,
|
||||
});
|
||||
let attrs = json!({"fill": p});
|
||||
d.char_styles.push(vectorcraft_doc::TextStyleDef { name: "Char".into(), attrs: attrs.as_object().unwrap().clone() });
|
||||
if let Some(NodeKind::Text(text)) = d.node_mut(t).map(|n| &mut n.kind) {
|
||||
text.runs[0].style.fill = p.clone();
|
||||
}
|
||||
d.swatch_groups.push(vectorcraft_color::SwatchGroup {
|
||||
name: "Mine".into(),
|
||||
swatches: vec![vectorcraft_color::Swatch { name: "Grouped".into(), paint: p.clone(), global: false, spot: false }],
|
||||
});
|
||||
s.paint.fill = p;
|
||||
}
|
||||
|
||||
/// Delete Pattern clears the pattern's paints outside the layers too, and in the fill new art takes.
|
||||
#[test]
|
||||
fn delete_pattern_forgets_paints_outside_the_layers() {
|
||||
let mut s = session();
|
||||
make_dots(&mut s);
|
||||
paint_outside_the_layers(&mut s, "Dots");
|
||||
assert_eq!(pattern_uses(doc(&s), "Dots"), 7);
|
||||
let v = s.execute("pattern.delete", &json!({"name": "Dots"})).unwrap();
|
||||
assert_eq!(v["unpainted"], 5, "the symbol, the other tile, the graphic style, the character style and the type");
|
||||
assert_eq!(pattern_uses(doc(&s), "Dots"), 0);
|
||||
assert!(doc(&s).swatch("Grouped").is_none() && doc(&s).pattern("Other").is_some());
|
||||
assert!(s.paint.fill.is_none());
|
||||
}
|
||||
|
||||
/// A Pattern Options rename renames the pattern's paints outside the layers too, and in the fill
|
||||
/// new art takes.
|
||||
#[test]
|
||||
fn rename_pattern_reaches_paints_outside_the_layers() {
|
||||
let mut s = session();
|
||||
make_dots(&mut s);
|
||||
paint_outside_the_layers(&mut s, "Dots");
|
||||
s.execute("pattern.options", &json!({"name": "Dots", "newName": "Spots"})).unwrap();
|
||||
assert_eq!(pattern_uses(doc(&s), "Dots"), 0);
|
||||
assert_eq!(pattern_uses(doc(&s), "Spots"), 7);
|
||||
assert!(is_pattern(&s.paint.fill, "Spots"));
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert!(is_pattern(&s.paint.fill, "Dots"), "Undo gives the fill its old name back");
|
||||
}
|
||||
|
||||
/// A new graph whose stored series fill and stroke and value label fill and stroke are pattern
|
||||
/// `name`, as a later redraw would give them to its marks and labels.
|
||||
fn graph_painted_with(s: &mut Session, name: &str) -> NodeId {
|
||||
let v = s.execute("graph.create", &json!({"type": "column", "x": 200, "y": 200, "width": 60, "height": 40, "csv": ",2024\nQ1,3\nQ2,5"}));
|
||||
let id = id_of(&v.unwrap());
|
||||
let p = pattern_paint(name);
|
||||
let mut extras = doc_mut(s).node_mut(id).unwrap().extras_mut();
|
||||
let spec = extras.graph.as_mut().unwrap();
|
||||
spec.series_paints = vec![vectorcraft_doc::SeriesPaint { fill: Some(p.clone()), stroke: Some(p.clone()), ..Default::default() }];
|
||||
spec.label_styles.values = Some(vectorcraft_doc::CharStyle { fill: p.clone(), stroke: p, ..Default::default() });
|
||||
id
|
||||
}
|
||||
|
||||
/// The series and label paints a graph stores, in order: series fill and stroke, then the value
|
||||
/// labels' fill and stroke.
|
||||
fn stored_graph_paints(s: &Session, id: NodeId) -> Vec<Paint> {
|
||||
let n = node(s, id);
|
||||
let g = n.extras().graph.as_deref().unwrap();
|
||||
let series = g.series_paints.iter().flat_map(|p| [p.fill.clone(), p.stroke.clone()]).flatten();
|
||||
series.chain(g.label_styles.values.iter().flat_map(|st| [st.fill.clone(), st.stroke.clone()])).collect()
|
||||
}
|
||||
|
||||
/// Delete Pattern clears the series and label paints a graph stores, and a rename renames them, so
|
||||
/// a later `graph.setData` draws no paint of a missing pattern.
|
||||
#[test]
|
||||
fn delete_and_rename_reach_a_graphs_stored_paints() {
|
||||
let mut s = session();
|
||||
make_dots(&mut s);
|
||||
let g = graph_painted_with(&mut s, "Dots");
|
||||
s.execute("pattern.options", &json!({"name": "Dots", "newName": "Spots"})).unwrap();
|
||||
assert!(stored_graph_paints(&s, g).iter().all(|p| is_pattern(p, "Spots")), "the rename reaches the graph");
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert!(stored_graph_paints(&s, g).iter().all(|p| is_pattern(p, "Dots")));
|
||||
let v = s.execute("pattern.delete", &json!({"name": "Dots"})).unwrap();
|
||||
assert_eq!(v["unpainted"], 4, "the series fill and stroke and the value labels' fill and stroke");
|
||||
assert!(stored_graph_paints(&s, g).iter().all(Paint::is_none));
|
||||
s.execute("graph.setData", &json!({"id": g.0, "rows": [[2], [4]]})).unwrap();
|
||||
assert_eq!(pattern_uses(doc(&s), "Dots"), 0, "the redrawn graph holds no paint of the deleted pattern");
|
||||
}
|
||||
|
||||
/// Canceling a new pattern clears the series and label paints a graph stores.
|
||||
#[test]
|
||||
fn cancel_new_pattern_reaches_a_graphs_stored_paints() {
|
||||
let mut s = session();
|
||||
// The graph is made outside the editing layer, and its paints take the new pattern's name.
|
||||
let g = graph_painted_with(&mut s, "Tiles");
|
||||
let r = rect(&mut s, 0.0, 0.0, 10.0, 10.0);
|
||||
sel(&mut s, &[r]);
|
||||
s.execute("object.pattern.make", &json!({"name": "Tiles"})).unwrap();
|
||||
s.execute("object.pattern.cancel", &json!({})).unwrap();
|
||||
assert_eq!(stored_graph_paints(&s, g).len(), 4);
|
||||
assert!(stored_graph_paints(&s, g).iter().all(Paint::is_none));
|
||||
assert_eq!(pattern_uses(doc(&s), "Tiles"), 0);
|
||||
}
|
||||
|
||||
/// With New Art Has Basic Appearance off, Redo of Delete Pattern gives new art the paints the
|
||||
/// command left, the selected object's appearance taken after the pattern's fill was cleared.
|
||||
#[test]
|
||||
fn redo_of_delete_pattern_keeps_the_inherited_new_art_paints() {
|
||||
let mut s = session();
|
||||
let r = make_dots(&mut s);
|
||||
s.prefs.new_art_basic = false;
|
||||
sel(&mut s, &[r]);
|
||||
s.paint.fill = pattern_paint("Dots");
|
||||
s.execute("pattern.delete", &json!({"name": "Dots"})).unwrap();
|
||||
let after = s.paint.clone();
|
||||
assert!(!is_pattern(&after.fill, "Dots"));
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
s.execute("edit.redo", &json!({})).unwrap();
|
||||
assert_eq!(s.paint, after);
|
||||
}
|
||||
|
||||
/// A Pattern Options rename tried as a preview changes the fill new art takes only while the
|
||||
/// preview stands: another preview and Cancel start from the fill the interaction began with.
|
||||
/// Keeping the preview keeps the rename, and Undo takes it back.
|
||||
#[test]
|
||||
fn rename_previews_restore_the_fill_new_art_takes() {
|
||||
let mut s = session();
|
||||
make_dots(&mut s);
|
||||
s.paint.fill = pattern_paint("Dots");
|
||||
s.begin_interaction("Pattern Options").unwrap();
|
||||
s.preview("pattern.options", &json!({"name": "Dots", "newName": "Spots"})).unwrap();
|
||||
assert!(is_pattern(&s.paint.fill, "Spots"));
|
||||
s.preview("pattern.options", &json!({"name": "Dots", "newName": "Tiles"})).unwrap();
|
||||
assert!(is_pattern(&s.paint.fill, "Tiles"));
|
||||
s.cancel_interaction().unwrap();
|
||||
assert!(is_pattern(&s.paint.fill, "Dots") && doc(&s).pattern("Dots").is_some());
|
||||
s.begin_interaction("Pattern Options").unwrap();
|
||||
s.preview("pattern.options", &json!({"name": "Dots", "newName": "Spots"})).unwrap();
|
||||
s.commit_interaction().unwrap();
|
||||
assert!(is_pattern(&s.paint.fill, "Spots") && doc(&s).pattern("Spots").is_some());
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert!(is_pattern(&s.paint.fill, "Dots") && doc(&s).pattern("Dots").is_some());
|
||||
}
|
||||
|
||||
/// With New Art Has Basic Appearance off, a canceled rename preview leaves new art with the
|
||||
/// appearance of the selected object as the document has it again, so no paint of the previewed
|
||||
/// name is left.
|
||||
#[test]
|
||||
fn canceled_rename_preview_leaves_no_inherited_paint_of_the_new_name() {
|
||||
let mut s = session();
|
||||
make_dots(&mut s);
|
||||
let big = rect(&mut s, 100.0, 100.0, 50.0, 50.0);
|
||||
s.execute("paint.setFill", &json!({"ids": [big.0], "swatch": "Dots"})).unwrap();
|
||||
s.prefs.new_art_basic = false;
|
||||
sel(&mut s, &[big]);
|
||||
s.paint = vectorcraft_tools::PaintDefaults::default();
|
||||
s.begin_interaction("Pattern Options").unwrap();
|
||||
s.preview("pattern.options", &json!({"name": "Dots", "newName": "Spots"})).unwrap();
|
||||
s.cancel_interaction().unwrap();
|
||||
assert!(doc(&s).pattern("Dots").is_some() && doc(&s).pattern("Spots").is_none());
|
||||
let items = s.paint.appearance.iter().flat_map(|a| a.items.iter()).map(|it| match it {
|
||||
vectorcraft_doc::AppearanceItem::Fill(f) => f.paint.clone(),
|
||||
vectorcraft_doc::AppearanceItem::Stroke(k) => k.paint.clone(),
|
||||
});
|
||||
let paints: Vec<Paint> = [s.paint.fill.clone(), s.paint.stroke.clone()].into_iter().chain(items).collect();
|
||||
assert!(paints.iter().all(|p| !is_pattern(p, "Spots")), "{paints:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transform_patterns_only_moves_placement() {
|
||||
let mut s = session();
|
||||
|
||||
+11
@@ -4159,6 +4159,17 @@ the old name. A rename gives the new name to the pattern's swatches that had the
|
||||
old name keeps it. A blank name gives "New Pattern N" in `object.pattern.make` and "`<pattern>` copy" in
|
||||
`object.pattern.saveCopy`, and `pattern.options` keeps the name.
|
||||
|
||||
`object.pattern.cancel {}` leaves pattern editing mode and discards the changes. For a pattern that `object.pattern.make`
|
||||
made for the edit, it removes the pattern and every swatch of it, a color group's included. Every other paint of the
|
||||
pattern becomes no paint: on art outside the editing layer, in opacity mask art, type runs, symbol definitions, graph
|
||||
designs, the series and label paints a graph keeps for its next redraw, other patterns' tiles, graphic and character
|
||||
styles and brush art, and in the fill, stroke and appearance new art takes. A pattern that `object.pattern.saveCopy` saved during the edit stays, with those paints cleared in its tile.
|
||||
For an existing pattern, Cancel puts the definition back as it was when editing started; when `pattern.options` renamed
|
||||
it during the edit, the old name comes back with its swatches and paints, unless another pattern, swatch or color group
|
||||
has taken that name. `pattern.delete {name}` clears the paints of the pattern in the same places and returns
|
||||
`{unpainted}`, the number of document paints it cleared. A `pattern.options` rename renames the paints in the same
|
||||
places. Undo and Redo restore the fill, stroke and appearance new art takes with the document.
|
||||
|
||||
The pattern commands (`object.pattern.make`, `object.pattern.edit`, `object.pattern.saveCopy`, `pattern.options`,
|
||||
`pattern.delete`) and the Repeat commands (`object.repeat.radial`, `object.repeat.grid`, `object.repeat.mirror`,
|
||||
`object.repeat.options`) reject a param of the wrong type, such as `"instances": "8"`, `"edit": "false"` or
|
||||
|
||||
@@ -707,6 +707,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- **Control bar with the Artboard tool:** while the Artboard tool is in use, the Control bar shows the active artboard's controls in place of the object controls: Preset, Portrait and Landscape, New Artboard, Delete Artboard, the Name field, Artboard Options, Rearrange All Artboards, the Transform link, Move and Scale Artwork with Artboard and, while the bar has room, the reference point with X and Y, and W and H with the Constrain Width and Height Proportions link. Each runs the command its Properties or Transform panel counterpart runs (`artboard.setProps`, `artboard.new`, `artboard.delete`, `tool.options {tool: "artboard"}`), and the reference point is the one the Transform panel uses. In an 800-point window the controls up to the Transform link fit, Move and Scale Artwork with Artboard show as their boxes alone with their names in tooltips, and the link opens the Transform panel with the artboard's position and size. Pointer and key tests in `panels/artboards.rs` cover the controls, W about the reference point, the orientation and the name. Public reference: [Using multiple artboards](https://web.archive.org/web/20210925094818/https://helpx.adobe.com/illustrator/using/using-multiple-artboards.html) (archived copy).
|
||||
- **Names of new and renamed artboards, brushes and patterns:** `artboard.new` and `artboard.setProps` trim the white space around the name they are given, as the Artboards panel's rename does, and a blank name gives the artboard its default name, Artboard N for the Nth artboard. `brush.new`, `brush.duplicate`, `brush.options`, `object.pattern.make`, `object.pattern.saveCopy` and `pattern.options` trim the name as the Brush Options dialog and the Pattern Options panel do, and compare the trimmed name with the names there are; a blank name gives the name each command gives when there is none, and Brush Options and Pattern Options keep the name. Tests in `tests/panelcmds.rs`, `tests/brushsym.rs` and `tests/pattern.rs` cover each command. Public references: [Using multiple artboards](https://web.archive.org/web/20210925094818/https://helpx.adobe.com/illustrator/using/using-multiple-artboards.html), [Brushes](https://web.archive.org/web/20251018101754/https://helpx.adobe.com/illustrator/using/brushes.html) and [Create and edit patterns](https://web.archive.org/web/20211031180358/https://helpx.adobe.com/illustrator/using/create-edit-patterns.html) (archived copies).
|
||||
- **Pattern names and swatch names:** `pattern.options` refuses a new name that another pattern, a swatch or a color group has. It takes a name that only the pattern's own swatches have. The reference page does not say what happens when the new name is taken; the refusal follows VectorCraft's rule that swatches and color groups share one namespace (`Document::swatch_name_taken`), which `swatch.new` and `swatch.edit` also follow. A rename gives the new name only to the pattern's swatches that had the old one; any other swatch with the old name keeps it, and art linked to that swatch stays linked. `object.pattern.make` and `object.pattern.saveCopy` number a color group's name as they number a swatch's. Tests in `tests/pattern.rs` cover each command. Public reference: [Create and edit patterns](https://web.archive.org/web/20211031180358/https://helpx.adobe.com/illustrator/using/create-edit-patterns.html) (archived copy).
|
||||
- **Cancel, Delete and rename leave no paint of a missing pattern:** the help page lists Save a Copy, Done and Cancel for leaving pattern editing (https://helpx.adobe.com/illustrator/using/create-edit-patterns.html). Canceling a pattern made for the edit removes it with every swatch of it, a color group's included. Canceling a new pattern, `pattern.delete` and a `pattern.options` rename reach every paint of the pattern: art outside the editing layer, opacity mask art, type runs, symbol definitions, graph designs, the series and label paints a graph keeps for its next redraw, other patterns' tiles (a copy Save a Copy made included), graphic and character styles, brush art, and the fill, stroke and appearance new art takes. Canceling a new pattern and Delete make those paints no paint, a VectorCraft rule; a rename renames them. Canceling the edit of an existing pattern puts its definition back as it was when editing started; after a rename in Pattern Options, it also gives the pattern its old name back with its swatches and paints, unless another pattern, swatch or color group has taken that name. Undo and Redo restore new art's paints with the document. Tests in `tests/pattern.rs` cover each command.
|
||||
- **A clip group in Properties:** with one clip group selected, the panel has Edit Clipping Path and Edit Contents at its top, under the Clip Group heading, which select the clipping path or the clipped art for the selection tools to transform alone, as Object › Clipping Mask does (<https://www.adobe.com/learn/illustrator/web/crop-using-masks>: with a clip group selected, the tutorial uses Edit Clipping Path at the top of the Properties panel to select the mask for editing, and uses Edit Contents to select the masked art). Edit Contents shows only when the group clips something. Not checked against the reference app: whether Properties keeps the buttons while the clipping path or the contents is selected; the Control bar has no clip-group buttons here. Evidence: `panels::properties::tests::one_clip_group_has_edit_clipping_path_and_edit_contents_at_the_top`.
|
||||
- **Contextual controls in Properties:** the [Properties panel overview](https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/properties-panel-overview.html) lists controls that appear for particular selections and tools; Properties has them, each running the existing command. Quick Actions has Blend Options for one selected blend, with the Blend tool or any other, as the [blending page](https://helpx.adobe.com/illustrator/using/blending-objects.html) describes (#4817) (the Blend Options dialog, OK one undo step) and Merge Live Paint when a Live Paint group is selected with more paths and nothing Live Paint refuses (`livePaint.merge`). Appearance has the stroke's Variable Width Profile list, the Control bar's, with the Width tool or when the stroke has a variable width (`stroke.set {profile}`), and beside fx the Delete icon: with one effect on the one selected object (its own or a fill's or stroke's) its name and an icon that removes it (`effect.remove`), with several an icon that opens the Appearance panel, as the overview describes. With Direct Selection and corners of one path selected, Corner and Corner Radius set the kind and radius of those corners (`object.setLiveShape`); a live rectangle's and polygon's own rows already do. Each edit is one undo step. The overview's Variable Font icon was already in Character. Not checked against the reference app: whether Merge needs two Live Paint groups (the overview speaks of several selected groups; `livePaint.merge` takes a group and paths), and where exactly each control sits in its section. Evidence: `panels::properties::tests::{blend_options_is_a_quick_action_for_one_blend_with_the_blend_tool, merge_live_paint_is_a_quick_action_for_a_live_paint_group_and_more, the_width_profile_shows_for_a_variable_width_or_the_width_tool, direct_selected_corners_show_their_kind_and_radius, one_effect_has_a_delete_icon_in_appearance}`.
|
||||
- **Edit Artboards in Properties:** a Transform section comes first (#5219): the reference point the Transform panel and Control bar share, the active artboard's X and Y (of that point) and W and H with the Constrain Width and Height Proportions link, its preset and orientation, and Move and Scale Artwork with Artboard. The Artboards section follows, with the name field, Artboard Options and Fill. The active artboard's name is a field. Typing a new name and pressing Enter or clicking elsewhere renames that artboard in one undo step, as the Artboards panel's rename does (`artboard.setProps {index, name}`); pressing Escape or leaving the field empty keeps the old name. A click on the Artboard Options icon beside the field opens Artboard Options for the active artboard, as double-clicking the Artboard tool's button does (`tool.options {tool: "artboard"}`). Pointer and key tests in `panels/properties.rs` cover both. Public references: [Resize artboards](https://helpx.adobe.com/illustrator/desktop/create-manage-artboards/add-edit-artboards/resize-artboards.html) and [Create and add new artboards](https://helpx.adobe.com/illustrator/desktop/create-manage-artboards/add-edit-artboards/add-new-artboards.html).
|
||||
|
||||
Reference in New Issue
Block a user