mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-14 04:11:43 +00:00
Merge pull request #6632 from dotCooCoo/feat/ruler-guides-as-objects
Guides: ruler guides are document objects on their layers
This commit is contained in:
@@ -486,7 +486,12 @@ fn info(args: &[String]) -> Result<(), String> {
|
||||
let base = h.call("engine.execute", json!({"command": "file.info", "params": {}}))?;
|
||||
let doc = h.session.doc().map_err(|e| e.to_string())?.doc.clone();
|
||||
let mut kinds: std::collections::BTreeMap<&'static str, usize> = Default::default();
|
||||
doc.walk(|n| *kinds.entry(n.kind_label()).or_default() += 1);
|
||||
// Ruler guides are not counted among the objects.
|
||||
doc.walk(|n| {
|
||||
if !n.is_ruler_guide() {
|
||||
*kinds.entry(n.kind_label()).or_default() += 1;
|
||||
}
|
||||
});
|
||||
let fonts = h.call("engine.execute", json!({"command": "text.fonts", "params": {}})).unwrap_or(Value::Null);
|
||||
let artboards: Vec<Value> =
|
||||
doc.artboards.iter().map(|a| json!({"name": a.name, "rect": [a.rect.x0, a.rect.y0, a.rect.width(), a.rect.height()]})).collect();
|
||||
|
||||
@@ -583,7 +583,8 @@ impl<'a> Scene<'a> {
|
||||
}
|
||||
|
||||
fn node(&mut self, n: &Node, force: bool) {
|
||||
if let NodeKind::Layer { template: true, .. } = n.kind {
|
||||
// Ruler guides draw on screen only: no entity and no loss warning.
|
||||
if n.is_ruler_guide() || n.is_template() {
|
||||
return;
|
||||
}
|
||||
// Symbols that hold symbols are written in full for each instance: depth alone doesn't
|
||||
@@ -676,6 +677,8 @@ impl<'a> Scene<'a> {
|
||||
self.node(c, false);
|
||||
}
|
||||
}
|
||||
// Ruler guides draw on screen only.
|
||||
NodeKind::RulerGuide { .. } => {}
|
||||
}
|
||||
self.opacity = opacity;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::{Arc, Deserialize, LayerColor, Node, NodeId, NodeKind, Rect, Serialize, SymbolBoxes, skip};
|
||||
use super::{Arc, Deserialize, LayerColor, Node, NodeId, Rect, Serialize, SymbolBoxes, skip};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Artboard {
|
||||
@@ -204,12 +204,7 @@ impl LockedArtboards<'_> {
|
||||
/// [`LockedArtboards::holds`] for new art too: is `n` on a locked artboard, whether or not it
|
||||
/// is new art?
|
||||
pub fn overlaps(&self, n: &Node) -> bool {
|
||||
if self.rects.is_empty()
|
||||
|| !n.visible
|
||||
|| n.is_layer()
|
||||
|| matches!(n.kind, NodeKind::Path { guide: true, .. })
|
||||
|| self.pattern_art.contains(&n.id)
|
||||
{
|
||||
if self.rects.is_empty() || !n.visible || n.is_layer() || n.is_guide() || self.pattern_art.contains(&n.id) {
|
||||
return false;
|
||||
}
|
||||
if let Some(held) = self.held.borrow().get(&n.id) {
|
||||
|
||||
@@ -123,6 +123,24 @@ fn drop_children_beyond(node: &mut Arc<Node>, dropped: &mut usize, layers: &mut
|
||||
});
|
||||
}
|
||||
|
||||
/// Remove the ruler guides of layer `layer` and of its sublayers that `out` picks, adding how many
|
||||
/// to `dropped`. Only layers holding one are copied.
|
||||
fn drop_guides(layer: &mut Arc<Node>, out: &impl Fn(&Node) -> bool, dropped: &mut usize) {
|
||||
fn holds(n: &Node, out: &impl Fn(&Node) -> bool) -> bool {
|
||||
n.is_layer() && n.children().is_some_and(|ch| ch.iter().any(|c| out(c) || holds(c, out)))
|
||||
}
|
||||
if !holds(layer, out) {
|
||||
return;
|
||||
}
|
||||
let Some(children) = Arc::make_mut(layer).children_mut() else { return };
|
||||
let before = children.len();
|
||||
children.retain(|c| !out(c));
|
||||
*dropped += before - children.len();
|
||||
for c in children.iter_mut().filter(|c| c.is_layer()) {
|
||||
drop_guides(c, out, dropped);
|
||||
}
|
||||
}
|
||||
|
||||
impl Document {
|
||||
/// Remove what a read file has beyond the canvas ([`MAX_COORD`], or not finite): objects
|
||||
/// (see [`BeyondCanvas::objects`]; a top-level layer clipping by a path beyond it is emptied
|
||||
@@ -141,8 +159,9 @@ impl Document {
|
||||
for c in children.iter() {
|
||||
layers_in(c, &mut layers);
|
||||
}
|
||||
gone.objects += children.len();
|
||||
children.clear();
|
||||
// The layer's own ruler guides stay; only its art goes.
|
||||
gone.objects += children.iter().filter(|c| !c.is_ruler_guide()).count();
|
||||
children.retain(|c| c.is_ruler_guide());
|
||||
*clip = false;
|
||||
}
|
||||
} else {
|
||||
@@ -161,9 +180,18 @@ impl Document {
|
||||
self.retire_artboard_id(*id);
|
||||
}
|
||||
gone.artboards = removed.len();
|
||||
let guides = self.guides.len();
|
||||
self.guides.retain(|g| in_range(g.pos) && g.artboard.is_none_or(|a| !removed.contains(&a)) && g.layer.is_none_or(|l| !layers.contains(&l)));
|
||||
gone.guides = guides - self.guides.len();
|
||||
// The saved list, when the guides are not nodes yet ([`Document::guides_into_nodes`]).
|
||||
let guides = self.saved_guides.len();
|
||||
self.saved_guides
|
||||
.retain(|g| in_range(g.pos) && g.artboard.is_none_or(|a| !removed.contains(&a)) && g.layer.is_none_or(|l| !layers.contains(&l)));
|
||||
gone.guides = guides - self.saved_guides.len();
|
||||
let out = |n: &Node| match n.kind {
|
||||
NodeKind::RulerGuide { pos, artboard, .. } => !in_range(pos) || artboard.is_some_and(|a| removed.contains(&a)),
|
||||
_ => false,
|
||||
};
|
||||
for layer in &mut self.layers {
|
||||
drop_guides(layer, &out, &mut gone.guides);
|
||||
}
|
||||
if !(in_range(self.ruler_origin.x) && in_range(self.ruler_origin.y)) {
|
||||
self.ruler_origin = Point::ZERO;
|
||||
gone.ruler_origin = true;
|
||||
@@ -232,12 +260,18 @@ mod tests {
|
||||
let far = Artboard { id: 9, rect: Rect::new(0.0, 1e17, 100.0, 1e17 + 100.0), ..d.artboards[0].clone() };
|
||||
d.artboards.push(far);
|
||||
let guide = |pos: f64, artboard| Guide { artboard, ..Guide::new(false, pos) };
|
||||
d.guides = vec![guide(50.0, None), guide(1e17, None), guide(f64::INFINITY, None), guide(20.0, Some(9))];
|
||||
d.saved_guides = vec![guide(50.0, None), guide(1e17, None), guide(f64::INFINITY, None), guide(20.0, Some(9))];
|
||||
d.ruler_origin = Point::new(1e17, 0.0);
|
||||
// The same guides as nodes, as a document has them once read.
|
||||
let mut nodes = d.clone();
|
||||
assert_eq!(nodes.guides_into_nodes(), 4);
|
||||
let gone = d.drop_beyond_canvas();
|
||||
assert_eq!(gone, BeyondCanvas { objects: 0, artboards: 1, guides: 3, ruler_origin: true });
|
||||
assert_eq!(d.artboards.len(), 1);
|
||||
assert_eq!(d.guides, [guide(50.0, None)], "the far artboard's own guide goes with it");
|
||||
assert_eq!(d.saved_guides, [guide(50.0, None)], "the far artboard's own guide goes with it");
|
||||
assert_eq!(nodes.drop_beyond_canvas(), gone);
|
||||
let left: Vec<(bool, f64, Option<u32>)> = nodes.ruler_guides().iter().map(|g| (g.vertical, g.pos, g.artboard)).collect();
|
||||
assert_eq!(left, [(false, 50.0, None)]);
|
||||
assert_eq!(d.ruler_origin, Point::ZERO);
|
||||
assert_eq!(
|
||||
gone.note().unwrap(),
|
||||
@@ -310,7 +344,8 @@ mod tests {
|
||||
let probe = Document::new(1.0, 1.0);
|
||||
let sub = sublayer(&probe, 40, vec![rect(41, 1e17), rect(42, 0.0)], true);
|
||||
let mut d = doc(vec![rect(10, 0.0), rect(11, 1e17), sub]);
|
||||
d.guides = vec![Guide { layer: Some(NodeId(40)), ..Guide::new(false, 5.0) }, Guide { layer: Some(d.layers[0].id), ..Guide::new(true, 5.0) }];
|
||||
d.saved_guides =
|
||||
vec![Guide { layer: Some(NodeId(40)), ..Guide::new(false, 5.0) }, Guide { layer: Some(d.layers[0].id), ..Guide::new(true, 5.0) }];
|
||||
d.saved_selections = vec![crate::records::SavedSelection { name: "s".into(), objects: vec![NodeId(10), NodeId(11)] }].into();
|
||||
d.assets = vec![
|
||||
crate::assets::ExportAsset { id: 1, name: "far".into(), nodes: vec![NodeId(11)], format_3d: None },
|
||||
@@ -320,7 +355,7 @@ mod tests {
|
||||
d.variables.bindings.insert(NodeId(10), ["w".to_string()].into());
|
||||
let gone = d.drop_beyond_canvas();
|
||||
assert_eq!((gone.objects, gone.guides), (2, 1), "the far rectangle, the clipping sublayer and its layer guide");
|
||||
assert_eq!(d.guides.len(), 1, "the guide of a layer that stays, stays");
|
||||
assert_eq!(d.saved_guides.len(), 1, "the guide of a layer that stays, stays");
|
||||
assert_eq!(d.saved_selections[0].objects, [NodeId(10)]);
|
||||
assert_eq!(d.assets.iter().map(|a| (a.id, a.nodes.clone())).collect::<Vec<_>>(), [(2, vec![NodeId(10)])]);
|
||||
assert_eq!(d.variables.bindings.keys().copied().collect::<Vec<_>>(), [NodeId(10)]);
|
||||
|
||||
@@ -132,7 +132,7 @@ fn limit(nodes: &mut Vec<Arc<Node>>, depth: usize) -> bool {
|
||||
out.push(a);
|
||||
continue;
|
||||
}
|
||||
let clip = matches!(n.kind, NodeKind::Group { clip: true, .. } | NodeKind::Layer { clip: true, .. });
|
||||
let clip = n.clips();
|
||||
let (visible, locked) = (n.visible, n.locked);
|
||||
let mut held = n.children_mut().map(std::mem::take).unwrap_or_default();
|
||||
if clip && !held.is_empty() {
|
||||
@@ -148,6 +148,13 @@ fn limit(nodes: &mut Vec<Arc<Node>>, depth: usize) -> bool {
|
||||
// What it held takes its place, and is looked at in its turn.
|
||||
left.extend(held.into_iter().rev());
|
||||
}
|
||||
// The ruler guides of a sublayer merged into its parent layer go above the parent's art, with
|
||||
// the parent's own.
|
||||
if out.iter().any(|c| c.is_ruler_guide()) {
|
||||
let (guides, art): (Vec<_>, Vec<_>) = out.into_iter().partition(|c| c.is_ruler_guide());
|
||||
out = art;
|
||||
out.extend(guides);
|
||||
}
|
||||
*nodes = out;
|
||||
true
|
||||
}
|
||||
|
||||
+31
-10
@@ -10,7 +10,7 @@ use super::{
|
||||
|
||||
impl Document {
|
||||
/// Is everything but the layers the same in `other`, as the next version of the document after
|
||||
/// an edit of its art? The id counter and the id index are left out, and art outside the layers
|
||||
/// an edit of its art? The id counter and the caches are left out, and art outside the layers
|
||||
/// (symbols, patterns, graph designs) compares by identity, which an edit keeps for what it
|
||||
/// doesn't change: quick for every frame of an animation.
|
||||
pub fn same_outside_layers(&self, other: &Self) -> bool {
|
||||
@@ -38,7 +38,7 @@ impl Document {
|
||||
text_threads,
|
||||
symbols,
|
||||
graph_designs,
|
||||
guides,
|
||||
saved_guides,
|
||||
views,
|
||||
saved_selections,
|
||||
variables,
|
||||
@@ -77,6 +77,7 @@ impl Document {
|
||||
opened_artboard: _,
|
||||
units_from_file: _,
|
||||
index: _,
|
||||
guide_cache: _,
|
||||
} = self;
|
||||
*version == other.version
|
||||
&& *title == other.title
|
||||
@@ -98,7 +99,7 @@ impl Document {
|
||||
&& symbols.iter().zip(&other.symbols).all(|(a, b)| a.name == b.name && Arc::ptr_eq(&a.art, &b.art))
|
||||
&& graph_designs.len() == other.graph_designs.len()
|
||||
&& graph_designs.iter().zip(&other.graph_designs).all(|(a, b)| a.name == b.name && Arc::ptr_eq(&a.art, &b.art))
|
||||
&& *guides == other.guides
|
||||
&& *saved_guides == other.saved_guides
|
||||
&& *views == other.views
|
||||
&& *saved_selections == other.saved_selections
|
||||
&& *variables == other.variables
|
||||
@@ -221,7 +222,7 @@ impl Document {
|
||||
text_threads: vec![],
|
||||
symbols: vec![],
|
||||
graph_designs: vec![],
|
||||
guides: vec![],
|
||||
saved_guides: vec![],
|
||||
views: vec![],
|
||||
saved_selections: Default::default(),
|
||||
variables: Default::default(),
|
||||
@@ -260,6 +261,7 @@ impl Document {
|
||||
opened_artboard: None,
|
||||
units_from_file: false,
|
||||
index: Default::default(),
|
||||
guide_cache: Default::default(),
|
||||
};
|
||||
let id = d.alloc_id();
|
||||
d.layers.push(Arc::new(Node::layer(id, "Layer 1", LayerColor::Preset(0))));
|
||||
@@ -501,11 +503,13 @@ impl Document {
|
||||
}
|
||||
}
|
||||
|
||||
/// Insert `node` into `parent` at `index` (clamped; `usize::MAX` = top).
|
||||
/// Insert `node` into `parent` at `index` (clamped; `usize::MAX` = top). In a layer, art goes
|
||||
/// below the layer's ruler guides and a ruler guide among them ([`Node::insert_index`]).
|
||||
pub fn insert(&mut self, parent: Option<NodeId>, index: usize, node: Node) -> Result<NodeId, DocError> {
|
||||
let id = node.id;
|
||||
self.check_ruler_guide_place(&node, parent)?;
|
||||
let ch = self.children_mut(parent)?;
|
||||
let i = index.min(ch.len());
|
||||
let i = Node::insert_index(ch, index, &node);
|
||||
ch.insert(i, Arc::new(node));
|
||||
Ok(id)
|
||||
}
|
||||
@@ -522,20 +526,33 @@ impl Document {
|
||||
Ok(ch.remove(idx))
|
||||
}
|
||||
|
||||
/// Move a node to `parent` at `index` (index measured after removal).
|
||||
/// Move a node to `parent` at `index` (index measured after removal), clamped as
|
||||
/// [`Document::insert`] clamps it.
|
||||
pub fn move_node(&mut self, id: NodeId, parent: Option<NodeId>, index: usize) -> Result<(), DocError> {
|
||||
if let Some(p) = parent
|
||||
&& self.ancestry(p).is_some_and(|a| a.contains(&id))
|
||||
{
|
||||
return Err(DocError::Invalid("cannot move an object into itself".into()));
|
||||
}
|
||||
if let Some(n) = self.node(id) {
|
||||
self.check_ruler_guide_place(n, parent)?;
|
||||
}
|
||||
let n = self.remove(id)?;
|
||||
let ch = self.children_mut(parent)?;
|
||||
let i = index.min(ch.len());
|
||||
let i = Node::insert_index(ch, index, &n);
|
||||
ch.insert(i, n);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// An error when `n` is a ruler guide and `parent` is not a layer or sublayer: a ruler guide is
|
||||
/// always a direct child of one ([`NodeKind::RulerGuide`]).
|
||||
fn check_ruler_guide_place(&self, n: &Node, parent: Option<NodeId>) -> Result<(), DocError> {
|
||||
if n.is_ruler_guide() && !parent.and_then(|p| self.node(p)).is_some_and(Node::is_layer) {
|
||||
return Err(DocError::Invalid("a ruler guide goes only directly in a layer or sublayer".into()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Position of `id` within its parent: (parent, index, sibling count).
|
||||
pub fn position(&self, id: NodeId) -> Option<(Option<NodeId>, usize, usize)> {
|
||||
let parent = self.parent_of(id);
|
||||
@@ -739,7 +756,7 @@ impl Document {
|
||||
for c in n.children().into_iter().flatten() {
|
||||
if c.is_layer() {
|
||||
objects(c, out);
|
||||
} else {
|
||||
} else if !c.is_ruler_guide() {
|
||||
out.insert(c.id);
|
||||
}
|
||||
}
|
||||
@@ -757,7 +774,8 @@ impl Document {
|
||||
}
|
||||
|
||||
/// The object of a layer or sublayer that holds node `id`: `id` itself when it is one, else
|
||||
/// the group or other object around it right inside a layer. None for a layer or sublayer.
|
||||
/// the group or other object around it right inside a layer. None for a layer, a sublayer or a
|
||||
/// ruler guide.
|
||||
pub fn layer_object(&self, id: NodeId) -> Option<&Node> {
|
||||
self.layer_object_at(&self.index_path(id)?)
|
||||
}
|
||||
@@ -768,6 +786,9 @@ impl Document {
|
||||
let mut nodes = &self.layers;
|
||||
for &i in path {
|
||||
let n = nodes.get(i)?;
|
||||
if n.is_ruler_guide() {
|
||||
return None;
|
||||
}
|
||||
if !n.is_layer() {
|
||||
return Some(n.as_ref());
|
||||
}
|
||||
|
||||
@@ -56,9 +56,12 @@ impl Document {
|
||||
if self.saved_selections.is_empty() {
|
||||
return;
|
||||
}
|
||||
// A saved selection names objects; ruler guides are not among them.
|
||||
let mut ids = std::collections::HashSet::new();
|
||||
self.walk(|n| {
|
||||
ids.insert(n.id);
|
||||
if !n.is_ruler_guide() {
|
||||
ids.insert(n.id);
|
||||
}
|
||||
});
|
||||
let mut kept: Vec<SavedSelection> = vec![];
|
||||
for s in std::mem::take(&mut self.saved_selections) {
|
||||
@@ -101,12 +104,13 @@ impl Document {
|
||||
/// Leave opacity-mask editing and symbol editing: drop the temporary editing layer, a working
|
||||
/// copy of art the mask or the symbol already holds (the engine syncs it after every edit). The
|
||||
/// layer is removed wherever it is, also as a sublayer of another layer; an id that names
|
||||
/// anything other than a layer is left alone.
|
||||
/// anything other than a layer is left alone. Ruler guides on the layer stay in the document
|
||||
/// ([`Self::remove_edit_layer`]).
|
||||
pub fn drop_edit_modes(&mut self) {
|
||||
let layers = [self.mask_edit.take().map(|me| me.layer), self.symbol_edit.take().map(|se| se.layer)];
|
||||
for id in layers.into_iter().flatten() {
|
||||
if self.node(id).is_some_and(Node::is_layer) {
|
||||
let _ = self.remove(id);
|
||||
let _ = self.remove_edit_layer(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use super::document_ext::free_id;
|
||||
use super::guides::Guide;
|
||||
use super::limits::MAX_NEXT_ID;
|
||||
use super::{Affine, Arc, Document, Node, NodeId, NodeKind, PatternDef, Point, Rect, Selection, SymbolBoxes, Vec2, artboard_holds};
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
|
||||
impl Document {
|
||||
/// Union of the geometric bounds of `ids` (their visual bounds when `visual`), with symbol
|
||||
@@ -151,7 +153,7 @@ impl Document {
|
||||
rest.iter().any(|m| has(m, s, inside, true))
|
||||
}
|
||||
NodeKind::Layer { children, .. } | NodeKind::Group { children, .. } => children.iter().any(|m| has(m, s, rect, exact)),
|
||||
NodeKind::Path { guide: true, .. } => false,
|
||||
NodeKind::Path { guide: true, .. } | NodeKind::RulerGuide { .. } => false,
|
||||
_ => art_box_meets(c, s, rect, exact),
|
||||
}
|
||||
}
|
||||
@@ -171,6 +173,195 @@ impl Document {
|
||||
art
|
||||
}
|
||||
|
||||
/// Put the ruler guide nodes a file holds where ruler guides go: directly in a layer or
|
||||
/// sublayer, above its art. A ruler guide below art in a layer moves up to the layer's other
|
||||
/// guides, keeping their order. A ruler guide anywhere else (at the top level, in a group or
|
||||
/// another object, in opacity-mask art, in a symbol, a graph design or a pattern) is removed.
|
||||
/// Returns how many were removed.
|
||||
pub fn tidy_ruler_guides(&mut self) -> usize {
|
||||
let mut gone = self.layers.len();
|
||||
self.layers.retain(|l| !l.is_ruler_guide());
|
||||
gone -= self.layers.len();
|
||||
for l in &mut self.layers {
|
||||
if layer_untidy(l) {
|
||||
gone += tidy_layer(Arc::make_mut(l));
|
||||
}
|
||||
}
|
||||
let others = self
|
||||
.symbols
|
||||
.iter_mut()
|
||||
.map(|s| &mut s.art)
|
||||
.chain(self.graph_designs.iter_mut().map(|g| &mut g.art))
|
||||
.chain(self.patterns.iter_mut().flat_map(|p| p.art.iter_mut()))
|
||||
.chain(self.pattern_edit.iter_mut().filter_map(|pe| pe.original.as_mut()).flat_map(|p| p.art.iter_mut()));
|
||||
for root in others {
|
||||
if root.is_ruler_guide() {
|
||||
*root = Arc::new(Node::group(root.id, vec![]));
|
||||
gone += 1;
|
||||
} else if any_guide(root) {
|
||||
gone += drop_guides_below(Arc::make_mut(root));
|
||||
}
|
||||
}
|
||||
gone
|
||||
}
|
||||
|
||||
/// Make the saved ruler guides (read from a file: [`Document::guides_to_saved`]'s list) into
|
||||
/// [`NodeKind::RulerGuide`] nodes, each at the top of its layer, in list order. A guide on a
|
||||
/// layer or sublayer the document has goes on it; one on no layer, or on a layer that is gone,
|
||||
/// goes on the top visible, unlocked layer ([`Self::default_layer`]), or on a new "Layer 1"
|
||||
/// when there is none. A guide keeps its saved id when no other node, slice or guide has it,
|
||||
/// and its name, lock and visibility. The saved list is emptied, so running this again does
|
||||
/// nothing. Returns how many guides became nodes.
|
||||
pub fn guides_into_nodes(&mut self) -> usize {
|
||||
let saved = std::mem::take(&mut self.saved_guides);
|
||||
if saved.is_empty() {
|
||||
return 0;
|
||||
}
|
||||
let mut used = HashSet::new();
|
||||
self.walk(|n| {
|
||||
used.insert(n.id);
|
||||
});
|
||||
used.extend(self.slices.iter().map(|s| s.id));
|
||||
used.extend(self.assets.iter().map(|a| NodeId(a.id)));
|
||||
let kept: Vec<Option<NodeId>> = saved.iter().map(|g| g.id.filter(|id| id.0 > 0 && id.0 < MAX_NEXT_ID && used.insert(*id))).collect();
|
||||
if let Some(max) = kept.iter().flatten().map(|id| id.0).max() {
|
||||
self.raise_next_id(max.saturating_add(1));
|
||||
}
|
||||
let mut home = None;
|
||||
// The guides of each layer, in list order.
|
||||
let mut by_layer: BTreeMap<NodeId, Vec<Arc<Node>>> = BTreeMap::new();
|
||||
let mut order = vec![];
|
||||
for (g, id) in saved.iter().zip(kept) {
|
||||
let layer = match g.layer.filter(|l| self.node(*l).is_some_and(Node::is_layer)) {
|
||||
Some(l) => l,
|
||||
None => match home {
|
||||
Some(h) => h,
|
||||
None => {
|
||||
let h = self.default_layer().unwrap_or_else(|| self.add_layer(None));
|
||||
home = Some(h);
|
||||
h
|
||||
}
|
||||
},
|
||||
};
|
||||
let id = id.unwrap_or_else(|| self.alloc_id());
|
||||
let mut n = Node::new(id, NodeKind::RulerGuide { vertical: g.vertical, pos: g.pos, artboard: g.artboard });
|
||||
n.name = g.name.clone();
|
||||
n.locked = g.locked;
|
||||
n.visible = !g.hidden;
|
||||
if !by_layer.contains_key(&layer) {
|
||||
order.push(layer);
|
||||
}
|
||||
by_layer.entry(layer).or_default().push(Arc::new(n));
|
||||
}
|
||||
for layer in order {
|
||||
let guides = by_layer.remove(&layer).unwrap_or_default();
|
||||
if let Ok(ch) = self.children_mut(Some(layer)) {
|
||||
ch.extend(guides);
|
||||
}
|
||||
}
|
||||
saved.len()
|
||||
}
|
||||
|
||||
/// Take the ruler guide nodes out of the layers and put them in the saved list, by ascending
|
||||
/// id, each naming its layer, after any entries not yet made into nodes: the form a file keeps
|
||||
/// them in, readable by versions from before ruler guides were nodes. For saving a copy of
|
||||
/// the document; [`Self::guides_into_nodes`] reverses it.
|
||||
pub fn guides_to_saved(&mut self) {
|
||||
let guides = self.ruler_guides();
|
||||
if guides.is_empty() {
|
||||
return;
|
||||
}
|
||||
for l in &mut self.layers {
|
||||
if l.holds_ruler_guides() {
|
||||
Arc::make_mut(l).strip_ruler_guides();
|
||||
}
|
||||
}
|
||||
self.saved_guides.extend(guides);
|
||||
}
|
||||
|
||||
/// Every ruler guide, by ascending id, with the layer or sublayer it is on, its id, name and
|
||||
/// its own lock and visibility. A guide's place in this list is its index in the guide
|
||||
/// commands.
|
||||
pub fn ruler_guides(&self) -> Vec<Guide> {
|
||||
if let Some(listed) = self.guide_cache.get(&self.layers) {
|
||||
return listed.as_ref().clone();
|
||||
}
|
||||
fn visit(layer: &Node, out: &mut Vec<Guide>) {
|
||||
for c in layer.children().into_iter().flatten() {
|
||||
if let Some(g) = guide_of(c, layer.id) {
|
||||
out.push(g);
|
||||
} else if c.is_layer() {
|
||||
visit(c, out);
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut out = vec![];
|
||||
for l in &self.layers {
|
||||
visit(l, &mut out);
|
||||
}
|
||||
out.sort_by_key(|g| g.id);
|
||||
self.guide_cache.set(&self.layers, &out);
|
||||
out
|
||||
}
|
||||
|
||||
/// Ruler guide `id`, as [`Self::ruler_guides`] lists it. None when `id` is not a ruler guide
|
||||
/// on a layer.
|
||||
pub fn ruler_guide(&self, id: NodeId) -> Option<Guide> {
|
||||
let layer = self.parent_of(id)?;
|
||||
guide_of(self.node(id)?, layer)
|
||||
}
|
||||
|
||||
/// Change ruler guide `id` with `f` (its line, its artboard; the layer, id, name, lock and
|
||||
/// visibility fields are not read back). Returns false when `id` is not a ruler guide.
|
||||
pub fn edit_ruler_guide(&mut self, id: NodeId, f: impl FnOnce(&mut Guide)) -> bool {
|
||||
let Some(mut g) = self.ruler_guide(id) else { return false };
|
||||
f(&mut g);
|
||||
let Some(n) = self.node_mut(id) else { return false };
|
||||
n.kind = NodeKind::RulerGuide { vertical: g.vertical, pos: g.pos, artboard: g.artboard };
|
||||
true
|
||||
}
|
||||
|
||||
/// Add ruler guide `g` as a node with a new id at the top of its layer: `g.layer` when the
|
||||
/// document has that layer or sublayer, else the top visible, unlocked layer
|
||||
/// ([`Self::default_layer`]), or a new "Layer 1" when there is none. Its name, lock and
|
||||
/// visibility come from `g`. Returns the new id.
|
||||
pub fn add_ruler_guide(&mut self, g: &Guide) -> NodeId {
|
||||
let layer = match g.layer.filter(|l| self.node(*l).is_some_and(Node::is_layer)) {
|
||||
Some(l) => l,
|
||||
None => self.default_layer().unwrap_or_else(|| self.add_layer(None)),
|
||||
};
|
||||
let id = self.alloc_id();
|
||||
let mut n = Node::new(id, NodeKind::RulerGuide { vertical: g.vertical, pos: g.pos, artboard: g.artboard });
|
||||
n.name = g.name.clone();
|
||||
n.locked = g.locked;
|
||||
n.visible = !g.hidden;
|
||||
if let Ok(ch) = self.children_mut(Some(layer)) {
|
||||
ch.push(Arc::new(n));
|
||||
}
|
||||
id
|
||||
}
|
||||
|
||||
/// Remove node `id`, the temporary layer of opacity-mask, symbol or pattern editing, keeping the
|
||||
/// ruler guides on it and on its sublayers: they go to the top of the top visible, unlocked
|
||||
/// layer left ([`Self::default_layer`]), or of a new "Layer 1" when no layer is left, where a
|
||||
/// saved guide whose layer is gone opens. Returns the removed node, without the guides.
|
||||
pub fn remove_edit_layer(&mut self, id: NodeId) -> Result<Arc<Node>, crate::DocError> {
|
||||
let mut layer = self.remove(id)?;
|
||||
if !layer.holds_ruler_guides() {
|
||||
return Ok(layer);
|
||||
}
|
||||
let mut guides = vec![];
|
||||
layer.walk(&mut |c| {
|
||||
if c.is_ruler_guide() {
|
||||
guides.push(Arc::new(c.clone()));
|
||||
}
|
||||
});
|
||||
Arc::make_mut(&mut layer).strip_ruler_guides();
|
||||
let home = self.default_layer().unwrap_or_else(|| self.add_layer(None));
|
||||
self.children_mut(Some(home))?.extend(guides);
|
||||
Ok(layer)
|
||||
}
|
||||
|
||||
/// Where ruler guide `g` runs along its line (the y range of a vertical guide): across its
|
||||
/// artboard for an artboard guide, None (the whole canvas) for a canvas guide or one whose
|
||||
/// artboard is gone.
|
||||
@@ -179,79 +370,39 @@ impl Document {
|
||||
Some(if g.vertical { (r.y0, r.y1) } else { (r.x0, r.x1) })
|
||||
}
|
||||
|
||||
/// The ruler guide node `g` names, if this document has it.
|
||||
fn guide_node(&self, g: &Guide) -> Option<NodeId> {
|
||||
g.id.filter(|id| self.node(*id).is_some_and(Node::is_ruler_guide))
|
||||
}
|
||||
|
||||
/// The layer (or sublayer) ruler guide `g` is on, if it is on one this document has.
|
||||
fn guide_layer(&self, g: &Guide) -> Option<NodeId> {
|
||||
g.layer.filter(|l| self.node(*l).is_some_and(Node::is_layer))
|
||||
}
|
||||
|
||||
/// Is ruler guide `g` shown: on no layer, or on a layer that (with the layers around it) is
|
||||
/// visible? A guide whose layer is gone shows as one on no layer.
|
||||
/// Is ruler guide `g` shown: its node, and the layers it is on, visible? A guide that is not a
|
||||
/// node of this document shows unless hidden itself or on a hidden layer.
|
||||
pub fn guide_shown(&self, g: &Guide) -> bool {
|
||||
self.guide_layer(g).is_none_or(|l| self.is_visible(l))
|
||||
}
|
||||
|
||||
/// Can ruler guide `g` be picked, moved and deleted: shown, and its layer unlocked?
|
||||
pub fn guide_editable(&self, g: &Guide) -> bool {
|
||||
self.guide_layer(g).is_none_or(|l| self.is_editable(l))
|
||||
}
|
||||
|
||||
/// Put the ruler guides on no layer this document has (from files saved before guides had
|
||||
/// layers, or of layers merged into `layer`) on layer `layer`. Returns how many.
|
||||
pub fn rehome_guides(&mut self, layer: NodeId) -> usize {
|
||||
let lost: Vec<bool> = self.guides.iter().map(|g| self.guide_layer(g).is_none()).collect();
|
||||
let mut n = 0;
|
||||
for (g, lost) in self.guides.iter_mut().zip(lost) {
|
||||
if lost {
|
||||
g.layer = Some(layer);
|
||||
n += 1;
|
||||
}
|
||||
match self.guide_node(g) {
|
||||
Some(id) => self.is_visible(id),
|
||||
None => !g.hidden && self.guide_layer(g).is_none_or(|l| self.is_visible(l)),
|
||||
}
|
||||
n
|
||||
}
|
||||
|
||||
/// Put the ruler guides on no layer (from files saved before guides had layers), or on one that
|
||||
/// is gone, on the top visible, unlocked layer ([`Self::default_layer`]). Returns how many.
|
||||
pub fn adopt_guides(&mut self) -> usize {
|
||||
self.default_layer().map_or(0, |home| self.rehome_guides(home))
|
||||
/// Can ruler guide `g` be picked, moved and deleted: shown, and neither it nor its layers
|
||||
/// locked?
|
||||
pub fn guide_editable(&self, g: &Guide) -> bool {
|
||||
match self.guide_node(g) {
|
||||
Some(id) => self.is_editable(id),
|
||||
None => !g.hidden && !g.locked && self.guide_layer(g).is_none_or(|l| self.is_editable(l)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete the ruler guides whose layer was deleted (they go with it), keeping `sel`'s guides in
|
||||
/// step. Guides on no layer stay. Returns how many went.
|
||||
pub fn drop_guides_of_deleted_layers(&mut self, sel: &mut Selection) -> usize {
|
||||
let gone: Vec<bool> = self.guides.iter().map(|g| g.layer.is_some() && self.guide_layer(g).is_none()).collect();
|
||||
self.retain_guides(sel, |i, _| !gone.get(i).copied().unwrap_or(false))
|
||||
}
|
||||
|
||||
/// Copy the ruler guides of layer `from` and of its sublayers onto their copies in `copy` (made
|
||||
/// from `from` by [`Self::reid`]: the same tree with fresh ids), as duplicating a layer copies
|
||||
/// the objects on it. Returns how many.
|
||||
pub fn copy_layer_guides(&mut self, from: &Node, copy: &Node) -> usize {
|
||||
let layers = |n: &Node| {
|
||||
let mut out = vec![];
|
||||
n.walk(&mut |c| {
|
||||
if c.is_layer() {
|
||||
out.push(c.id);
|
||||
}
|
||||
});
|
||||
out
|
||||
};
|
||||
let pairs: Vec<(NodeId, NodeId)> = layers(from).into_iter().zip(layers(copy)).collect();
|
||||
let copies: Vec<Guide> = self
|
||||
.guides
|
||||
.iter()
|
||||
.filter_map(|g| {
|
||||
let to = pairs.iter().find(|(a, _)| Some(*a) == g.layer)?.1;
|
||||
Some(Guide { layer: Some(to), ..g.clone() })
|
||||
})
|
||||
.collect();
|
||||
let n = copies.len();
|
||||
self.guides.extend(copies);
|
||||
n
|
||||
}
|
||||
|
||||
/// The ruler guides on layer `layer` (not its sublayers'), by index.
|
||||
pub fn guides_on(&self, layer: NodeId) -> impl Iterator<Item = (usize, &Guide)> + '_ {
|
||||
self.guides.iter().enumerate().filter(move |(_, g)| g.layer == Some(layer))
|
||||
/// The ruler guides directly on layer `layer` (not its sublayers'), by ascending id.
|
||||
pub fn guides_on(&self, layer: NodeId) -> Vec<Guide> {
|
||||
let mut out: Vec<Guide> = self.children(Some(layer)).into_iter().flatten().filter_map(|c| guide_of(c, layer)).collect();
|
||||
out.sort_by_key(|g| g.id);
|
||||
out
|
||||
}
|
||||
|
||||
/// Does ruler guide `g` run past `p` (up to `tol` beyond its ends)?
|
||||
@@ -260,45 +411,86 @@ impl Document {
|
||||
self.guide_span(g).is_none_or(|(a, b)| along >= a - tol && along <= b + tol)
|
||||
}
|
||||
|
||||
/// Keep the ruler guides `keep` accepts (by index), the selected ones left keeping their
|
||||
/// place among the rest (the selected art stays selected). Returns how many went.
|
||||
pub fn retain_guides(&mut self, sel: &mut Selection, keep: impl Fn(usize, &Guide) -> bool) -> usize {
|
||||
let kept: Vec<bool> = self.guides.iter().enumerate().map(|(i, g)| keep(i, g)).collect();
|
||||
// Where each kept guide ends up.
|
||||
let (mut to, mut n) = (Vec::with_capacity(kept.len()), 0);
|
||||
for k in &kept {
|
||||
to.push(k.then_some(n));
|
||||
n += usize::from(*k);
|
||||
/// Keep the ruler guides `keep` accepts, deleting the others; `sel` forgets the deleted ones
|
||||
/// (the selected art stays selected). Returns how many went.
|
||||
pub fn retain_guides(&mut self, sel: &mut Selection, keep: impl Fn(&Guide) -> bool) -> usize {
|
||||
let gone: HashSet<NodeId> = self.ruler_guides().into_iter().filter(|g| !keep(g)).filter_map(|g| g.id).collect();
|
||||
if gone.is_empty() {
|
||||
return 0;
|
||||
}
|
||||
fn drop(layer: &mut Arc<Node>, gone: &HashSet<NodeId>) {
|
||||
fn holds(n: &Node, gone: &HashSet<NodeId>) -> bool {
|
||||
n.is_layer() && n.children().is_some_and(|ch| ch.iter().any(|c| gone.contains(&c.id) || holds(c, gone)))
|
||||
}
|
||||
if !holds(layer, gone) {
|
||||
return;
|
||||
}
|
||||
let Some(ch) = Arc::make_mut(layer).children_mut() else { return };
|
||||
ch.retain(|c| !(c.is_ruler_guide() && gone.contains(&c.id)));
|
||||
for c in ch.iter_mut().filter(|c| c.is_layer()) {
|
||||
drop(c, gone);
|
||||
}
|
||||
}
|
||||
for l in &mut self.layers {
|
||||
drop(l, &gone);
|
||||
}
|
||||
sel.guides.retain(|id| !gone.contains(id));
|
||||
if sel.key_guide.is_some_and(|k| gone.contains(&k)) {
|
||||
sel.key_guide = None;
|
||||
}
|
||||
gone.len()
|
||||
}
|
||||
|
||||
/// Change the line of every ruler guide on artboard `id` with `f` (whether it is vertical, its
|
||||
/// position).
|
||||
fn edit_artboard_guides(&mut self, id: u32, f: &mut impl FnMut(bool, &mut f64)) {
|
||||
fn visit(layer: &mut Arc<Node>, id: u32, f: &mut impl FnMut(bool, &mut f64)) {
|
||||
fn holds(n: &Node, id: u32) -> bool {
|
||||
n.is_layer()
|
||||
&& n.children().is_some_and(|ch| {
|
||||
ch.iter().any(|c| matches!(c.kind, NodeKind::RulerGuide { artboard: Some(a), .. } if a == id) || holds(c, id))
|
||||
})
|
||||
}
|
||||
if !holds(layer, id) {
|
||||
return;
|
||||
}
|
||||
let Some(ch) = Arc::make_mut(layer).children_mut() else { return };
|
||||
for c in ch.iter_mut() {
|
||||
if matches!(c.kind, NodeKind::RulerGuide { artboard: Some(a), .. } if a == id) {
|
||||
if let NodeKind::RulerGuide { vertical, pos, .. } = &mut Arc::make_mut(c).kind {
|
||||
f(*vertical, pos);
|
||||
}
|
||||
} else if c.is_layer() {
|
||||
visit(c, id, f);
|
||||
}
|
||||
}
|
||||
}
|
||||
for l in &mut self.layers {
|
||||
visit(l, id, f);
|
||||
}
|
||||
sel.guides = sel.guides.iter().filter_map(|i| to.get(*i).copied().flatten()).collect();
|
||||
sel.key_guide = sel.key_guide.and_then(|i| to.get(i).copied().flatten());
|
||||
let before = self.guides.len();
|
||||
let mut flags = kept.into_iter();
|
||||
self.guides.retain(|_| flags.next().unwrap_or(true));
|
||||
before - self.guides.len()
|
||||
}
|
||||
|
||||
/// Move the guides of artboard `id` by `d` (along with their artboard).
|
||||
pub fn move_artboard_guides(&mut self, id: u32, d: Vec2) {
|
||||
for g in self.guides.iter_mut().filter(|g| g.artboard == Some(id)) {
|
||||
*g = g.moved(d);
|
||||
}
|
||||
self.edit_artboard_guides(id, &mut |vertical, pos| *pos += if vertical { d.x } else { d.y });
|
||||
}
|
||||
|
||||
/// Map the guides of artboard `id` through `xf`, an axis-aligned scale and move (Scale Artwork
|
||||
/// with Artboard).
|
||||
pub fn map_artboard_guides(&mut self, id: u32, xf: Affine) {
|
||||
for g in self.guides.iter_mut().filter(|g| g.artboard == Some(id)) {
|
||||
g.pos = if g.vertical { (xf * Point::new(g.pos, 0.0)).x } else { (xf * Point::new(0.0, g.pos)).y };
|
||||
}
|
||||
self.edit_artboard_guides(id, &mut |vertical, pos| {
|
||||
*pos = if vertical { (xf * Point::new(*pos, 0.0)).x } else { (xf * Point::new(0.0, *pos)).y };
|
||||
});
|
||||
}
|
||||
|
||||
/// Copy the guides of artboard `from` onto artboard `to`, `d` away (with a copy of their
|
||||
/// artboard).
|
||||
/// artboard), each on its guide's layer with a new id.
|
||||
pub fn copy_artboard_guides(&mut self, from: u32, to: u32, d: Vec2) {
|
||||
let copies: Vec<Guide> =
|
||||
self.guides.iter().filter(|g| g.artboard == Some(from)).map(|g| Guide { artboard: Some(to), ..g.moved(d) }).collect();
|
||||
self.guides.extend(copies);
|
||||
self.ruler_guides().into_iter().filter(|g| g.artboard == Some(from)).map(|g| Guide { artboard: Some(to), ..g.moved(d) }).collect();
|
||||
for g in &copies {
|
||||
self.add_ruler_guide(g);
|
||||
}
|
||||
}
|
||||
|
||||
/// An id for a new artboard: one no artboard has, above the ids given out before and the ids of
|
||||
@@ -367,3 +559,122 @@ impl Document {
|
||||
n
|
||||
}
|
||||
}
|
||||
|
||||
/// Does `n`, or anything below it (opacity-mask art included), hold a ruler guide?
|
||||
fn any_guide(n: &Node) -> bool {
|
||||
let mut any = false;
|
||||
crate::swatches::walk_with_masks(n, &mut |m| any |= m.is_ruler_guide());
|
||||
any
|
||||
}
|
||||
|
||||
/// Remove every ruler guide below `n`, in its members and its opacity-mask art (`n` itself
|
||||
/// stays). Mask art that is a ruler guide becomes an empty group. Returns how many went.
|
||||
fn drop_guides_below(n: &mut Node) -> usize {
|
||||
let mut gone = drop_mask_guides(n);
|
||||
if let Some(ch) = n.children_mut() {
|
||||
let before = ch.len();
|
||||
ch.retain(|c| !c.is_ruler_guide());
|
||||
gone += before - ch.len();
|
||||
for c in ch.iter_mut().filter(|c| any_guide(c)) {
|
||||
gone += drop_guides_below(Arc::make_mut(c));
|
||||
}
|
||||
}
|
||||
gone
|
||||
}
|
||||
|
||||
/// Remove every ruler guide in the opacity-mask art of `n`. Mask art that is a ruler guide
|
||||
/// becomes an empty group. Returns how many went.
|
||||
fn drop_mask_guides(n: &mut Node) -> usize {
|
||||
let Some(mask) = n.extras_if_any().and_then(|e| e.mask.as_deref_mut()) else { return 0 };
|
||||
if mask.art.is_ruler_guide() {
|
||||
mask.art = Arc::new(Node::group(mask.art.id, vec![]));
|
||||
1
|
||||
} else if any_guide(&mask.art) {
|
||||
drop_guides_below(Arc::make_mut(&mut mask.art))
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
/// Does layer `l` hold a ruler guide out of place: below art, in an object or in mask art, or so
|
||||
/// in one of its sublayers?
|
||||
fn layer_untidy(l: &Node) -> bool {
|
||||
let Some(ch) = l.children() else { return false };
|
||||
let start = Node::guide_run_start(ch);
|
||||
l.extras().mask.as_deref().is_some_and(|m| any_guide(&m.art))
|
||||
|| ch.iter().take(start).any(|c| c.is_ruler_guide() || if c.is_layer() { layer_untidy(c) } else { any_guide(c) })
|
||||
}
|
||||
|
||||
/// [`Document::tidy_ruler_guides`] for layer `l`. Returns how many guides were removed.
|
||||
fn tidy_layer(l: &mut Node) -> usize {
|
||||
let mut gone = drop_mask_guides(l);
|
||||
let Some(ch) = l.children_mut() else { return gone };
|
||||
for c in ch.iter_mut() {
|
||||
if c.is_layer() {
|
||||
if layer_untidy(c) {
|
||||
gone += tidy_layer(Arc::make_mut(c));
|
||||
}
|
||||
} else if !c.is_ruler_guide() && any_guide(c) {
|
||||
gone += drop_guides_below(Arc::make_mut(c));
|
||||
}
|
||||
}
|
||||
if ch.iter().take(Node::guide_run_start(ch)).any(|c| c.is_ruler_guide()) {
|
||||
let (guides, art): (Vec<_>, Vec<_>) = std::mem::take(ch).into_iter().partition(|c| c.is_ruler_guide());
|
||||
*ch = art;
|
||||
ch.extend(guides);
|
||||
}
|
||||
gone
|
||||
}
|
||||
|
||||
/// Node `n`, on layer `layer`, as a [`Guide`] value, if it is a ruler guide.
|
||||
fn guide_of(n: &Node, layer: NodeId) -> Option<Guide> {
|
||||
let NodeKind::RulerGuide { vertical, pos, artboard } = n.kind else { return None };
|
||||
Some(Guide { vertical, pos, artboard, layer: Some(layer), id: Some(n.id), name: n.name.clone(), locked: n.locked, hidden: !n.visible })
|
||||
}
|
||||
|
||||
/// The list [`Document::ruler_guides`] last made, with weak pointers to the top-level layers it was
|
||||
/// made from. While a weak pointer to a layer's allocation is held, nothing else can be at that
|
||||
/// address, and the layer can't be changed in place (`Arc::get_mut` refuses it and `Arc::make_mut`
|
||||
/// moves it), so any edit inside a layer, and any layer added, removed or replaced, gives a list of
|
||||
/// layers the cache does not match. Clones of a document share the cached list.
|
||||
#[derive(Default)]
|
||||
pub(crate) struct GuideCache(std::sync::RwLock<Option<Listed>>);
|
||||
|
||||
/// The top-level layers a guide list was made from, and the list.
|
||||
type Listed = (Vec<std::sync::Weak<Node>>, Arc<Vec<Guide>>);
|
||||
|
||||
impl GuideCache {
|
||||
/// The cached list, when it was made from `layers`.
|
||||
fn get(&self, layers: &[Arc<Node>]) -> Option<Arc<Vec<Guide>>> {
|
||||
let g = self.0.read().ok()?;
|
||||
let (from, listed) = g.as_ref()?;
|
||||
let same = from.len() == layers.len() && from.iter().zip(layers).all(|(w, l)| std::ptr::eq(w.as_ptr(), Arc::as_ptr(l)));
|
||||
same.then(|| listed.clone())
|
||||
}
|
||||
|
||||
/// Keep `listed`, made from `layers`.
|
||||
fn set(&self, layers: &[Arc<Node>], listed: &[Guide]) {
|
||||
if let Ok(mut g) = self.0.write() {
|
||||
*g = Some((layers.iter().map(Arc::downgrade).collect(), Arc::new(listed.to_vec())));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for GuideCache {
|
||||
fn clone(&self) -> Self {
|
||||
GuideCache(std::sync::RwLock::new(self.0.read().ok().and_then(|g| g.clone())))
|
||||
}
|
||||
}
|
||||
|
||||
/// A cache, not document content: two documents are equal whatever their caches hold.
|
||||
impl PartialEq for GuideCache {
|
||||
fn eq(&self, _: &Self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for GuideCache {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str("GuideCache")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use super::{Deserialize, NodeId, Serialize, Vec2};
|
||||
|
||||
/// A ruler guide as a value: an entry of the saved `guides` list, or a ruler guide node of a
|
||||
/// document as [`crate::Document::ruler_guides`] lists it. In a document, ruler guides are
|
||||
/// [`crate::NodeKind::RulerGuide`] nodes on their layers.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Guide {
|
||||
/// true = vertical guide at `pos` (x), false = horizontal at `pos` (y).
|
||||
@@ -14,12 +17,25 @@ pub struct Guide {
|
||||
/// from before guides had layers, and from files that don't say), always there.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub layer: Option<NodeId>,
|
||||
/// The id of its node. None in an entry written before ruler guides were nodes: it gets a new
|
||||
/// id when the document is read.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub id: Option<NodeId>,
|
||||
/// The name it shows in the Layers panel, if it was given one.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub name: Option<String>,
|
||||
/// Locked on its own (its layer's lock is separate).
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
pub locked: bool,
|
||||
/// Hidden on its own (its layer's visibility is separate).
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
pub hidden: bool,
|
||||
}
|
||||
|
||||
impl Guide {
|
||||
/// A canvas guide.
|
||||
pub fn new(vertical: bool, pos: f64) -> Self {
|
||||
Self { vertical, pos, artboard: None, layer: None }
|
||||
Self { vertical, pos, artboard: None, layer: None, id: None, name: None, locked: false, hidden: false }
|
||||
}
|
||||
|
||||
/// Moved by `d` (a vertical guide across, a horizontal one down).
|
||||
|
||||
@@ -588,7 +588,8 @@ fn may_hit(parent: &Node, i: usize, c: &Node, p: Point, opt: HitOptions, symbols
|
||||
if !c.visible || editable_only && (c.locked || c.is_template()) {
|
||||
return false;
|
||||
}
|
||||
if opt.skip_guides && matches!(c.kind, NodeKind::Path { guide: true, .. }) {
|
||||
// A ruler guide is picked by the guide commands, never by an object hit test.
|
||||
if c.is_ruler_guide() || opt.skip_guides && matches!(c.kind, NodeKind::Path { guide: true, .. }) {
|
||||
return false;
|
||||
}
|
||||
// (An envelope's content sits where it was, not where the envelope draws it.)
|
||||
@@ -1256,7 +1257,7 @@ fn marquee_with(doc: &Document, r: Rect, scope: Option<NodeId>, leaves: bool, wh
|
||||
// marquee touches none of it, as clicks don't (#5309).
|
||||
Some(s) if !doc.is_editable(s.id) => vec![],
|
||||
Some(s) => match s.children() {
|
||||
Some(c) => c.iter().enumerate().filter(|(i, _)| !shaper_hides(s, *i, scope)).map(|(_, c)| (c, r)).collect(),
|
||||
Some(c) => c.iter().enumerate().filter(|(i, c)| !c.is_ruler_guide() && !shaper_hides(s, *i, scope)).map(|(_, c)| (c, r)).collect(),
|
||||
// A single object isolated: only it.
|
||||
None => doc
|
||||
.parent_of(s.id)
|
||||
@@ -1285,7 +1286,7 @@ fn marquee_with(doc: &Document, r: Rect, scope: Option<NodeId>, leaves: bool, wh
|
||||
if c.visible && !c.locked && !c.is_template() {
|
||||
out.extend(layer_objects(c, r));
|
||||
}
|
||||
} else {
|
||||
} else if !c.is_ruler_guide() {
|
||||
out.push((c, r));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,8 +207,12 @@ pub struct Document {
|
||||
/// Object › Graph › Design…: art graphs can draw as markers.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub graph_designs: Vec<GraphDesign>,
|
||||
#[serde(default)]
|
||||
pub guides: Vec<Guide>,
|
||||
/// The saved form of the ruler guides: read from a file, then made into
|
||||
/// [`NodeKind::RulerGuide`] nodes on their layers ([`Document::guides_into_nodes`]), and
|
||||
/// written from those nodes when saving ([`Document::guides_to_saved`]). Empty in between.
|
||||
/// Each entry read counts as an object against the read's node budget.
|
||||
#[serde(default, rename = "guides", deserialize_with = "crate::node::counted_list")]
|
||||
saved_guides: Vec<Guide>,
|
||||
/// View → New View… (at most [`SavedView::MAX`]); saved with the document.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub views: Vec<SavedView>,
|
||||
@@ -351,6 +355,9 @@ pub struct Document {
|
||||
/// Where each node is in the tree, for lookups by id ([`id_index`]): a cache, never saved.
|
||||
#[serde(skip)]
|
||||
index: id_index::IdIndex,
|
||||
/// The ruler guides as [`Document::ruler_guides`] lists them: a cache, never saved.
|
||||
#[serde(skip)]
|
||||
guide_cache: document_guides::GuideCache,
|
||||
}
|
||||
|
||||
fn ppi72() -> f64 {
|
||||
|
||||
@@ -140,6 +140,8 @@ pub(super) fn map_node(n: &Node, w: &Warper) -> Node {
|
||||
}
|
||||
// Its art is plain, so it warps at this depth.
|
||||
NodeKind::PlacedDocument(_) => return map_node(&expanded_group_hooks(n, w.hooks), w),
|
||||
// A ruler guide has no shape to warp.
|
||||
NodeKind::RulerGuide { .. } => {}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ impl Document {
|
||||
}
|
||||
let mut n = 0;
|
||||
n += vec_bytes(&self.artboards) + self.artboards.iter().map(|a| a.name.capacity()).sum::<usize>();
|
||||
n += vec_bytes(&self.guides) + vec_bytes(&self.views) + vec_bytes(&self.slices) + vec_bytes(&self.assets);
|
||||
n += vec_bytes(&self.saved_guides) + vec_bytes(&self.views) + vec_bytes(&self.slices) + vec_bytes(&self.assets);
|
||||
n += vec_bytes(&self.text_threads) + self.text_threads.iter().map(vec_bytes).sum::<usize>();
|
||||
n += self.images.iter().map(|(k, b)| 4 * size_of::<usize>() + k.capacity() + b.mime.capacity() + size_of_val(b)).sum::<usize>();
|
||||
n += map_bytes(&self.export_settings) + map_bytes(&self.extra) + self.print_setup.as_ref().map_or(0, json_bytes);
|
||||
|
||||
@@ -33,6 +33,7 @@ pub use live_shape::{LiveShape, Scaling};
|
||||
pub use opacity::{Knockout, OpacityMask, whiten};
|
||||
pub use ops::{flattens, keeps_gradient_fit};
|
||||
use wire::NodeWire;
|
||||
pub(crate) use wire::counted_list;
|
||||
pub use wire::{MAX_READ_NODES, with_node_budget};
|
||||
|
||||
fn yes() -> bool {
|
||||
|
||||
@@ -19,7 +19,7 @@ impl Node {
|
||||
|
||||
fn push_clip_shapes(&self, text: Outliner, out: &mut Vec<(BezPath, FillRule)>) {
|
||||
match &self.kind {
|
||||
NodeKind::Path { guide: true, .. } | NodeKind::SymbolInstance { .. } => {}
|
||||
NodeKind::Path { guide: true, .. } | NodeKind::RulerGuide { .. } | NodeKind::SymbolInstance { .. } => {}
|
||||
NodeKind::Path { path, rule, .. } => out.push((path.to_bezpath(), *rule)),
|
||||
NodeKind::Compound { children, rule } => {
|
||||
let mut bp = BezPath::new();
|
||||
|
||||
@@ -122,6 +122,17 @@ pub enum NodeKind {
|
||||
Intertwine {
|
||||
children: Vec<Arc<Node>>,
|
||||
},
|
||||
/// A ruler guide: a straight line across the canvas, or across one artboard, that draws on
|
||||
/// screen only. It is always a direct child of a layer or sublayer, above the layer's other
|
||||
/// children, and has no bounds.
|
||||
RulerGuide {
|
||||
/// true: a vertical guide at x = `pos`; false: a horizontal one at y = `pos`.
|
||||
vertical: bool,
|
||||
pos: f64,
|
||||
/// The [`crate::Artboard::id`] of the artboard it runs across; None: across the canvas.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
artboard: Option<u32>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Reading a [`NodeKind`]. Derived, an internally tagged enum buffers each kind (with every
|
||||
@@ -215,6 +226,12 @@ mod kind_de {
|
||||
Intertwine {
|
||||
children: Vec<Arc<Node>>,
|
||||
},
|
||||
RulerGuide {
|
||||
vertical: bool,
|
||||
pos: f64,
|
||||
#[serde(default)]
|
||||
artboard: Option<u32>,
|
||||
},
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for NodeKind {
|
||||
|
||||
@@ -80,6 +80,16 @@ impl Node {
|
||||
matches!(self.kind, NodeKind::Layer { .. })
|
||||
}
|
||||
|
||||
/// A ruler guide ([`NodeKind::RulerGuide`]).
|
||||
pub fn is_ruler_guide(&self) -> bool {
|
||||
matches!(self.kind, NodeKind::RulerGuide { .. })
|
||||
}
|
||||
|
||||
/// A guide of either kind: a ruler guide, or a path made into a guide.
|
||||
pub fn is_guide(&self) -> bool {
|
||||
matches!(self.kind, NodeKind::RulerGuide { .. } | NodeKind::Path { guide: true, .. })
|
||||
}
|
||||
|
||||
/// A template layer (Layer Options → Template).
|
||||
pub fn is_template(&self) -> bool {
|
||||
matches!(self.kind, NodeKind::Layer { template: true, .. })
|
||||
@@ -91,7 +101,7 @@ impl Node {
|
||||
pub fn layer_art(&self, editable: bool) -> Vec<NodeId> {
|
||||
fn visit(n: &Node, editable: bool, out: &mut Vec<NodeId>) {
|
||||
for c in n.children().into_iter().flatten() {
|
||||
if editable && (!c.visible || c.locked || c.is_template()) {
|
||||
if c.is_ruler_guide() || (editable && (!c.visible || c.locked || c.is_template())) {
|
||||
continue;
|
||||
}
|
||||
if c.is_layer() {
|
||||
@@ -142,6 +152,7 @@ impl Node {
|
||||
NodeKind::Repeat(r) => r.kind.label(),
|
||||
NodeKind::PlacedDocument(_) => "Placed Document",
|
||||
NodeKind::Intertwine { .. } => "Intertwine",
|
||||
NodeKind::RulerGuide { .. } => "Guide",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,6 +321,8 @@ impl Node {
|
||||
NodeKind::PlacedDocument(p) => Some(p.bounds()),
|
||||
// The members drawn over the others are drawn where they are.
|
||||
NodeKind::Intertwine { children } => union(children),
|
||||
// A ruler guide runs across the canvas (or an artboard): it adds nothing to a box.
|
||||
NodeKind::RulerGuide { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -522,6 +535,9 @@ impl Node {
|
||||
}
|
||||
NodeKind::Mesh(m) => m.transform(a),
|
||||
NodeKind::Repeat(r) => r.transform(a, *sc),
|
||||
// A ruler guide stays where it is when the art around it (its layer) is transformed:
|
||||
// the guide commands move it.
|
||||
NodeKind::RulerGuide { .. } => {}
|
||||
}
|
||||
// The frames of the objects attached to a path move with them, and the distances along the
|
||||
// path follow the path as it is now (a live shape's path is regenerated).
|
||||
@@ -557,10 +573,10 @@ impl Node {
|
||||
}
|
||||
}
|
||||
|
||||
/// Count of nodes in this subtree.
|
||||
/// Count of nodes in this subtree, ruler guides left out.
|
||||
pub fn count(&self) -> usize {
|
||||
let mut n = 0;
|
||||
self.walk(&mut |_| n += 1);
|
||||
self.walk(&mut |c| n += usize::from(!c.is_ruler_guide()));
|
||||
n
|
||||
}
|
||||
|
||||
@@ -569,7 +585,7 @@ impl Node {
|
||||
pub fn count_with_masks(&self) -> usize {
|
||||
let mut n = 0;
|
||||
self.walk(&mut |c| {
|
||||
n += 1;
|
||||
n += usize::from(!c.is_ruler_guide());
|
||||
if let Some(m) = &c.extras().mask {
|
||||
n += m.art.count_with_masks();
|
||||
}
|
||||
@@ -688,7 +704,7 @@ impl Node {
|
||||
/// groups ([`Self::passes_knockout_through`]) contribute their children instead of themselves.
|
||||
pub fn knockout_elements(children: &[Arc<Node>]) -> Vec<&Arc<Node>> {
|
||||
fn push<'a>(children: &'a [Arc<Node>], out: &mut Vec<&'a Arc<Node>>) {
|
||||
for c in children.iter().filter(|c| c.visible) {
|
||||
for c in children.iter().filter(|c| c.visible && !c.is_ruler_guide()) {
|
||||
match c.children() {
|
||||
Some(ch) if c.passes_knockout_through() => push(ch, out),
|
||||
_ => out.push(c),
|
||||
@@ -701,8 +717,62 @@ impl Node {
|
||||
}
|
||||
|
||||
/// Whether this is a clip group or a layer with a clipping mask: its first child clips the rest.
|
||||
/// A layer whose first child is a ruler guide holds no art, so a guide never clips it.
|
||||
pub fn clips(&self) -> bool {
|
||||
matches!(self.kind, NodeKind::Group { clip: true, .. } | NodeKind::Layer { clip: true, .. })
|
||||
match &self.kind {
|
||||
NodeKind::Group { clip, .. } => *clip,
|
||||
NodeKind::Layer { clip, children, .. } => *clip && children.first().is_none_or(|c| !c.is_ruler_guide()),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Where the ruler guides at the end of `children` (a layer's) begin: the index past the last
|
||||
/// child that is not one. Art goes below this index and ruler guides at or above it.
|
||||
pub fn guide_run_start(children: &[Arc<Node>]) -> usize {
|
||||
children.iter().rposition(|c| !c.is_ruler_guide()).map_or(0, |i| i + 1)
|
||||
}
|
||||
|
||||
/// Remove the ruler guides of this layer and of its sublayers. Other nodes hold none.
|
||||
pub fn strip_ruler_guides(&mut self) {
|
||||
if !self.holds_ruler_guides() {
|
||||
return;
|
||||
}
|
||||
if let Some(ch) = self.children_mut() {
|
||||
ch.retain(|c| !c.is_ruler_guide());
|
||||
for c in ch.iter_mut().filter(|c| c.holds_ruler_guides()) {
|
||||
Arc::make_mut(c).strip_ruler_guides();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Call `f` on the position (along the axis it crosses) of each ruler guide of this layer and
|
||||
/// of its sublayers, with whether the guide is vertical. Only layers that hold one are copied.
|
||||
pub fn map_ruler_guide_positions(&mut self, f: &mut impl FnMut(bool, &mut f64)) {
|
||||
if !self.holds_ruler_guides() {
|
||||
return;
|
||||
}
|
||||
if let Some(ch) = self.children_mut() {
|
||||
for c in ch.iter_mut().filter(|c| c.is_ruler_guide() || c.holds_ruler_guides()) {
|
||||
let c = Arc::make_mut(c);
|
||||
match &mut c.kind {
|
||||
NodeKind::RulerGuide { vertical, pos, .. } => f(*vertical, pos),
|
||||
_ => c.map_ruler_guide_positions(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this layer or one of its sublayers holds a ruler guide.
|
||||
pub fn holds_ruler_guides(&self) -> bool {
|
||||
self.is_layer() && self.children().is_some_and(|ch| ch.iter().any(|c| c.is_ruler_guide() || c.holds_ruler_guides()))
|
||||
}
|
||||
|
||||
/// The index that an insert into `children` at `index` uses for `node`: art stays below the
|
||||
/// layer's ruler guides and a ruler guide goes among them.
|
||||
pub fn insert_index(children: &[Arc<Node>], index: usize, node: &Node) -> usize {
|
||||
let start = Self::guide_run_start(children);
|
||||
let i = index.min(children.len());
|
||||
if node.is_ruler_guide() { i.max(start) } else { i.min(start) }
|
||||
}
|
||||
|
||||
/// Make this group or layer clip its children by its first child, or stop it.
|
||||
|
||||
@@ -145,9 +145,10 @@ fn count_one<E: serde::de::Error>() -> Result<(), E> {
|
||||
}
|
||||
|
||||
/// A list read from a file (a member's Intertwine areas of tier 2 and up, an instance's symbol
|
||||
/// overrides), each item counting against [`with_node_budget`] as an object does: a compressed
|
||||
/// file of repeated empty items stays small while they take memory once read.
|
||||
fn counted_list<'de, D: serde::Deserializer<'de>, T: Deserialize<'de>>(d: D) -> Result<Vec<T>, D::Error> {
|
||||
/// overrides, a document's saved ruler guides), each item counting against [`with_node_budget`]
|
||||
/// as an object does: a compressed file of repeated empty items stays small while they take
|
||||
/// memory once read.
|
||||
pub(crate) fn counted_list<'de, D: serde::Deserializer<'de>, T: Deserialize<'de>>(d: D) -> Result<Vec<T>, D::Error> {
|
||||
struct Items<T>(std::marker::PhantomData<T>);
|
||||
impl<'de, T: Deserialize<'de>> serde::de::Visitor<'de> for Items<T> {
|
||||
type Value = Vec<T>;
|
||||
|
||||
@@ -584,6 +584,8 @@ impl<'a> NestedCount<'a> {
|
||||
(NodeKind::Repeat(r), _) => r.in_all(self),
|
||||
(NodeKind::SymbolInstance { symbol, .. }, _) => Cost { instances: self.symbol(symbol), copies: 1 },
|
||||
(NodeKind::Path { path, .. }, _) => Cost { instances: 0, copies: path.anchor_count().saturating_add(1) },
|
||||
// A ruler guide draws nothing, so a repeat makes no copies of it.
|
||||
(NodeKind::RulerGuide { .. }, _) => Cost::default(),
|
||||
(_, Some(c)) => self.nodes(c).plus(Cost { instances: 0, copies: 1 }),
|
||||
_ => Cost { instances: 0, copies: 1 },
|
||||
};
|
||||
|
||||
+19
-13
@@ -39,16 +39,16 @@ pub struct Selection {
|
||||
/// clears them.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub slices: Vec<NodeId>,
|
||||
/// Ruler guides selected with a selection tool, as indexes into [`Document::guides`]. A new
|
||||
/// Ruler guides selected with a selection tool, by node id ([`Document::ruler_guides`]). A new
|
||||
/// selection ([`Self::set`]) clears them; Shift-adding or -removing objects keeps them, so
|
||||
/// guides and objects can be selected together, in either order.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub guides: Vec<usize>,
|
||||
/// A selected ruler guide that is the key object for Align instead of [`Self::key`] (an index
|
||||
/// into [`Document::guides`], one of [`Self::guides`]): the objects line up on it. Never set
|
||||
/// with `key`; [`Self::prune`] clears it when no object is selected to align to it.
|
||||
pub guides: Vec<NodeId>,
|
||||
/// A selected ruler guide that is the key object for Align instead of [`Self::key`] (one of
|
||||
/// [`Self::guides`]): the objects line up on it. Never set with `key`; [`Self::prune`] clears
|
||||
/// it when no object is selected to align to it.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub key_guide: Option<usize>,
|
||||
pub key_guide: Option<NodeId>,
|
||||
/// The new art on a locked artboard that this selection keeps editable
|
||||
/// ([`Document::new_art`]). Undo, Redo and a failed batch restore it with the selection, and
|
||||
/// the engine then sets the document's from it. Editing state, never saved.
|
||||
@@ -139,10 +139,10 @@ impl Selection {
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Select ruler guides `indexes` (see [`Selection::guides`]) and nothing else.
|
||||
pub fn set_guides(&mut self, indexes: impl IntoIterator<Item = usize>) {
|
||||
/// Select ruler guides `ids` (see [`Selection::guides`]) and nothing else.
|
||||
pub fn set_guides(&mut self, ids: impl IntoIterator<Item = NodeId>) {
|
||||
self.clear();
|
||||
for i in indexes {
|
||||
for i in ids {
|
||||
if !self.guides.contains(&i) {
|
||||
self.guides.push(i);
|
||||
}
|
||||
@@ -150,7 +150,7 @@ impl Selection {
|
||||
}
|
||||
/// Add ruler guide `i` to the selection, or take it out, keeping the selected objects (a
|
||||
/// Shift-click on a guide).
|
||||
pub fn toggle_guide(&mut self, i: usize) {
|
||||
pub fn toggle_guide(&mut self, i: NodeId) {
|
||||
self.symbol_part = None;
|
||||
if let Some(k) = self.guides.iter().position(|g| *g == i) {
|
||||
self.guides.remove(k);
|
||||
@@ -201,7 +201,9 @@ impl Selection {
|
||||
pub fn prune(&mut self, doc: &Document) {
|
||||
// Every id this looks up, found in one walk of the tree.
|
||||
let ids = self.objects.iter().chain(self.anchors.keys()).chain(self.segments.keys()).copied().chain(self.key).chain(self.target);
|
||||
let found = doc.index_paths(ids);
|
||||
let mut found = doc.index_paths(ids);
|
||||
// A ruler guide is selected in `guides` only, never as an object, an anchor or the key.
|
||||
found.retain(|_, p| !doc.node_at(p).is_some_and(|n| n.is_ruler_guide()));
|
||||
self.objects.retain(|id| found.contains_key(id));
|
||||
self.anchors.retain(|id, _| found.contains_key(id));
|
||||
self.segments.retain(|id, refs| {
|
||||
@@ -209,8 +211,12 @@ impl Selection {
|
||||
refs.retain(|&(si, seg)| path.subpaths.get(si).is_some_and(|sp| seg < sp.segment_count()));
|
||||
!refs.is_empty() && self.objects.contains(id) && self.anchors.contains_key(id)
|
||||
});
|
||||
// Guides on a hidden or locked layer can't stay selected.
|
||||
self.guides.retain(|i| doc.guides.get(*i).is_some_and(|g| doc.guide_editable(g)));
|
||||
// Guides that are gone, or hidden or locked (themselves or by their layer), can't stay
|
||||
// selected.
|
||||
if !self.guides.is_empty() {
|
||||
let editable = doc.editable();
|
||||
self.guides.retain(|id| doc.node(*id).is_some_and(|n| n.is_ruler_guide()) && editable(*id));
|
||||
}
|
||||
// The key object stays while another selected object lies outside it, or while ruler
|
||||
// guides are selected for it to line up (#2143).
|
||||
let keep = self.key.and_then(|k| found.get(&k)).and_then(|p| doc.node_at(p)).is_some_and(|n| {
|
||||
|
||||
@@ -20,25 +20,30 @@ fn basic_ops() {
|
||||
fn guides_are_selected_on_their_own() {
|
||||
let mut s = Selection::default();
|
||||
s.add(NodeId(1));
|
||||
s.set_guides([2, 0, 2]);
|
||||
assert_eq!((s.objects.clone(), s.guides.clone()), (vec![], vec![2, 0]));
|
||||
let (g0, g2) = (NodeId(10), NodeId(12));
|
||||
s.set_guides([g2, g0, g2]);
|
||||
assert_eq!((s.objects.clone(), s.guides.clone()), (vec![], vec![g2, g0]));
|
||||
assert!(s.has_objects_or_guides() && s.is_empty());
|
||||
// Shift-adding an object keeps them; a new selection doesn't.
|
||||
s.add(NodeId(1));
|
||||
assert_eq!(s.guides, vec![2, 0]);
|
||||
assert_eq!(s.guides, vec![g2, g0]);
|
||||
s.set([NodeId(1)]);
|
||||
assert!(s.guides.is_empty());
|
||||
// Toggled, a guide joins (or leaves) the selected objects.
|
||||
s.toggle_guide(0);
|
||||
assert_eq!((s.objects.clone(), s.guides.clone()), (vec![NodeId(1)], vec![0]));
|
||||
s.toggle_guide(0);
|
||||
s.toggle_guide(g0);
|
||||
assert_eq!((s.objects.clone(), s.guides.clone()), (vec![NodeId(1)], vec![g0]));
|
||||
s.toggle_guide(g0);
|
||||
assert_eq!((s.objects.clone(), s.guides.clone()), (vec![NodeId(1)], vec![]));
|
||||
// Pruning drops guides the document no longer has.
|
||||
// Pruning drops guides the document no longer has, ids of other objects, and guides that are
|
||||
// locked or hidden themselves.
|
||||
let mut d = Document::new(100.0, 100.0);
|
||||
d.guides.push(crate::Guide::new(true, 10.0));
|
||||
s.set_guides([0, 1]);
|
||||
let layer = d.layers[0].id;
|
||||
let kept = d.add_ruler_guide(&crate::Guide::new(true, 10.0));
|
||||
let locked = d.add_ruler_guide(&crate::Guide { locked: true, ..crate::Guide::new(true, 20.0) });
|
||||
let hidden = d.add_ruler_guide(&crate::Guide { hidden: true, ..crate::Guide::new(true, 30.0) });
|
||||
s.set_guides([kept, NodeId(9999), layer, locked, hidden]);
|
||||
s.prune(&d);
|
||||
assert_eq!(s.guides, vec![0]);
|
||||
assert_eq!(s.guides, vec![kept]);
|
||||
}
|
||||
|
||||
/// A picked object of a dynamic symbol's instance stays picked while it and the groups and layers
|
||||
@@ -131,16 +136,26 @@ fn guides_show_and_lock_with_their_layer() {
|
||||
assert!(d.guide_shown(&crate::Guide::new(false, 5.0)));
|
||||
let gone = crate::Guide { layer: Some(NodeId(9999)), ..crate::Guide::new(true, 1.0) };
|
||||
assert!(d.guide_shown(&gone) && d.guide_editable(&gone));
|
||||
d.guides.push(g);
|
||||
assert_eq!(d.guides_on(layer).count(), 1);
|
||||
// Guides on no layer, or a gone one, are adopted by the top visible, unlocked layer.
|
||||
// As a node, a guide shows and locks with its layer and on its own.
|
||||
let id = d.add_ruler_guide(&g);
|
||||
let node = d.ruler_guide(id).unwrap();
|
||||
assert_eq!(d.guides_on(layer), std::slice::from_ref(&node));
|
||||
assert!(!d.guide_shown(&node) && !d.guide_editable(&node), "its layer is hidden");
|
||||
d.node_mut(layer).unwrap().visible = true;
|
||||
d.node_mut(layer).unwrap().locked = false;
|
||||
d.guides.push(crate::Guide::new(false, 5.0));
|
||||
d.guides.push(gone);
|
||||
assert_eq!(d.adopt_guides(), 2);
|
||||
assert!(d.guides.iter().all(|g| g.layer == Some(layer)));
|
||||
assert_eq!(d.adopt_guides(), 0, "once");
|
||||
assert!(d.guide_shown(&node) && d.guide_editable(&node));
|
||||
d.node_mut(id).unwrap().locked = true;
|
||||
let node = d.ruler_guide(id).unwrap();
|
||||
assert!(d.guide_shown(&node) && !d.guide_editable(&node), "locked itself");
|
||||
d.node_mut(id).unwrap().visible = false;
|
||||
assert!(!d.guide_shown(&node), "hidden itself");
|
||||
// Saved guides on no layer, or a gone one, go on the top visible, unlocked layer.
|
||||
d.saved_guides.push(crate::Guide::new(false, 5.0));
|
||||
d.saved_guides.push(gone);
|
||||
assert_eq!(d.guides_into_nodes(), 2);
|
||||
assert!(d.ruler_guides().iter().all(|g| g.layer == Some(layer)));
|
||||
assert_eq!(d.ruler_guides().len(), 3);
|
||||
assert_eq!(d.guides_into_nodes(), 0, "once");
|
||||
// A guide saved before guides had layers reads as one on no layer.
|
||||
let old: crate::Guide = serde_json::from_str(r#"{"vertical": true, "pos": 5.0}"#).unwrap();
|
||||
assert_eq!(old.layer, None);
|
||||
@@ -267,7 +282,7 @@ fn random_selection(r: &mut u64, ids: &[NodeId]) -> Selection {
|
||||
s.key = s.objects.get(below(r, s.objects.len() as u64) as usize).copied();
|
||||
s.target = ids.get(below(r, ids.len() as u64) as usize).copied().filter(|_| below(r, 2) == 0);
|
||||
s.slices = ids.iter().copied().take(below(r, 3) as usize).collect();
|
||||
s.guides = vec![1, 0];
|
||||
s.guides = vec![NodeId(1), NodeId(0)];
|
||||
s
|
||||
}
|
||||
|
||||
|
||||
@@ -492,7 +492,8 @@ impl NineSliceMap {
|
||||
/// [`Self::most_placed`] for a container placed by [`Self::container`].
|
||||
fn most_container(&self, n: &Node, boxes: &SymbolBoxes, depth: u32) -> u64 {
|
||||
let own = self.most_mask(n, boxes, depth).saturating_add(1);
|
||||
n.children().into_iter().flatten().fold(own, |sum, c| sum.saturating_add(self.most_at(c, boxes, depth + 1)))
|
||||
// As [`Self::container`] places it: a layer's ruler guides left out.
|
||||
n.children().into_iter().flatten().filter(|c| !c.is_ruler_guide()).fold(own, |sum, c| sum.saturating_add(self.most_at(c, boxes, depth + 1)))
|
||||
}
|
||||
|
||||
/// [`Self::most_placed`] for the opacity mask of `n` as [`Self::place_mask`] places it: a
|
||||
@@ -560,7 +561,8 @@ impl NineSliceMap {
|
||||
let kids = out.children_mut().map(std::mem::take).unwrap_or_default();
|
||||
out.transform(center, false);
|
||||
if let Some(children) = out.children_mut() {
|
||||
*children = kids.iter().map(|c| Arc::new(self.place_at(c, boxes, depth + 1))).collect();
|
||||
// A ruler guide of a layer is not art: it is not placed.
|
||||
*children = kids.iter().filter(|c| !c.is_ruler_guide()).map(|c| Arc::new(self.place_at(c, boxes, depth + 1))).collect();
|
||||
}
|
||||
self.place_mask(n, &mut out, boxes, depth);
|
||||
out
|
||||
|
||||
@@ -91,6 +91,268 @@ fn an_inch_ruler_steps_in_binary_fractions() {
|
||||
assert!([0.1, 0.2, 0.5, 1.0, 2.0, 5.0, 10.0, 25.0].contains(&step(Unit::Millimeters, 5.0)), "{}", step(Unit::Millimeters, 5.0));
|
||||
}
|
||||
|
||||
/// Ruler guide nodes on a layer and a sublayer are not art: Select All, a layer's target circle,
|
||||
/// the object count, the art's bounds, clicks, the marquee and a knockout layer's elements are
|
||||
/// the same as without them, and transforming the layer leaves them where they are.
|
||||
#[test]
|
||||
fn ruler_guide_nodes_are_not_art() {
|
||||
use crate::hit::{HitOptions, hit_test, marquee, objects_at};
|
||||
let (mut d, a, b) = doc_with_rects();
|
||||
let layer = d.layers[0].id;
|
||||
let sub = d.alloc_id();
|
||||
d.insert(Some(layer), usize::MAX, Node::layer(sub, "Sub", LayerColor::Preset(2))).unwrap();
|
||||
let s1 = d.alloc_id();
|
||||
d.insert(Some(sub), usize::MAX, Node::path(s1, shapes::rectangle(Rect::new(40.0, 0.0, 50.0, 10.0)), Appearance::default_art())).unwrap();
|
||||
let opt = HitOptions::default();
|
||||
let all = Rect::new(-100.0, -100.0, 700.0, 900.0);
|
||||
let before = (
|
||||
d.selectable_art(),
|
||||
d.layers[0].layer_art(false),
|
||||
d.node_count(),
|
||||
d.art_bounds(),
|
||||
marquee(&d, all, None, false),
|
||||
hit_test(&d, Point::new(5.0, 5.0), opt).map(|h| h.top_object(None)),
|
||||
);
|
||||
let guide = |id, vertical, pos| Node::new(id, NodeKind::RulerGuide { vertical, pos, artboard: None });
|
||||
let (g1, g2, g3) = (d.alloc_id(), d.alloc_id(), d.alloc_id());
|
||||
d.insert(Some(layer), usize::MAX, guide(g1, true, 5.0)).unwrap();
|
||||
d.insert(Some(layer), usize::MAX, guide(g2, false, 5.0)).unwrap();
|
||||
d.insert(Some(sub), usize::MAX, guide(g3, true, 45.0)).unwrap();
|
||||
let after = (
|
||||
d.selectable_art(),
|
||||
d.layers[0].layer_art(false),
|
||||
d.node_count(),
|
||||
d.art_bounds(),
|
||||
marquee(&d, all, None, false),
|
||||
hit_test(&d, Point::new(5.0, 5.0), opt).map(|h| h.top_object(None)),
|
||||
);
|
||||
assert_eq!(after, before);
|
||||
assert_eq!(after.0, [a, b, s1]);
|
||||
for p in [Point::new(5.0, 300.0), Point::new(300.0, 5.0), Point::new(45.0, 300.0)] {
|
||||
assert!(hit_test(&d, p, opt).is_none(), "nothing to click on the guide at {p:?}");
|
||||
assert!(objects_at(&d, p, opt, None).is_empty());
|
||||
}
|
||||
let mut sel = Selection::default();
|
||||
sel.set_target(&d, layer);
|
||||
assert_eq!(sel.objects, [a, b, s1]);
|
||||
let children = d.layers[0].children().unwrap().clone();
|
||||
assert_eq!(Node::knockout_elements(&children).iter().map(|c| c.id).collect::<Vec<_>>(), [a, b, s1]);
|
||||
assert!(d.node(g1).is_some_and(Node::is_ruler_guide) && d.node(g1).is_some_and(Node::is_guide));
|
||||
assert_eq!(d.node(g1).map(Node::geometric_bounds), Some(None));
|
||||
let mut moved = (*d.layers[0]).clone();
|
||||
moved.transform(Affine::translate((10.0, 10.0)), true);
|
||||
let g = moved.children().unwrap().iter().find(|c| c.id == g1).unwrap();
|
||||
assert!(matches!(g.kind, NodeKind::RulerGuide { pos, .. } if pos == 5.0));
|
||||
}
|
||||
|
||||
/// Art inserted or moved onto a layer goes below the layer's ruler guides, a ruler guide goes
|
||||
/// among them, and a clipped layer whose art is gone does not clip by a guide.
|
||||
#[test]
|
||||
fn layer_art_stays_below_its_ruler_guides() {
|
||||
let (mut d, a, b) = doc_with_rects();
|
||||
let layer = d.layers[0].id;
|
||||
let guide = |id, pos| Node::new(id, NodeKind::RulerGuide { vertical: true, pos, artboard: None });
|
||||
let (g1, g2) = (d.alloc_id(), d.alloc_id());
|
||||
d.insert(Some(layer), usize::MAX, guide(g1, 5.0)).unwrap();
|
||||
// Asked for the bottom, a guide still goes above the art.
|
||||
d.insert(Some(layer), 0, guide(g2, 7.0)).unwrap();
|
||||
let ids = |d: &Document| d.layers[0].children().unwrap().iter().map(|c| c.id).collect::<Vec<_>>();
|
||||
assert_eq!(ids(&d), [a, b, g2, g1]);
|
||||
let c = d.alloc_id();
|
||||
d.insert(Some(layer), usize::MAX, Node::path(c, shapes::rectangle(Rect::new(0.0, 0.0, 5.0, 5.0)), Appearance::default_art())).unwrap();
|
||||
assert_eq!(ids(&d), [a, b, c, g2, g1]);
|
||||
d.move_node(a, Some(layer), usize::MAX).unwrap();
|
||||
assert_eq!(ids(&d), [b, c, a, g2, g1]);
|
||||
d.move_node(g1, Some(layer), 0).unwrap();
|
||||
assert_eq!(ids(&d), [b, c, a, g1, g2]);
|
||||
assert!(d.layer_object(g1).is_none());
|
||||
assert_eq!(d.layer_object(a).map(|n| n.id), Some(a));
|
||||
|
||||
// A clipped layer clips by its first art object; with only guides left it clips nothing.
|
||||
if let Some(NodeKind::Layer { clip, .. }) = d.node_mut(layer).map(|n| &mut n.kind) {
|
||||
*clip = true;
|
||||
}
|
||||
assert!(d.node(layer).is_some_and(Node::clips));
|
||||
for id in [a, b, c] {
|
||||
d.remove(id).unwrap();
|
||||
}
|
||||
assert!(!d.node(layer).is_some_and(Node::clips));
|
||||
|
||||
// Reading a file that clips a layer by a path beyond the canvas empties the layer of its art
|
||||
// only: its guides stay.
|
||||
let far = d.alloc_id();
|
||||
d.insert(Some(layer), 0, Node::path(far, shapes::rectangle(Rect::new(0.0, 0.0, 1e9, 10.0)), Appearance::default_art())).unwrap();
|
||||
assert!(d.node(layer).is_some_and(Node::clips));
|
||||
let gone = d.drop_beyond_canvas();
|
||||
assert_eq!(gone.objects, 1);
|
||||
assert_eq!(ids(&d), [g1, g2]);
|
||||
}
|
||||
|
||||
/// `ruler_guides` keeps its list while the layers are unchanged, and lists again after any change
|
||||
/// inside a layer, made in place or on a shared copy, and after layers are added, removed or
|
||||
/// reordered.
|
||||
#[test]
|
||||
fn ruler_guides_follow_every_edit() {
|
||||
let (mut d, _, _) = doc_with_rects();
|
||||
let layer = d.layers[0].id;
|
||||
let sub = d.alloc_id();
|
||||
d.insert(Some(layer), usize::MAX, Node::layer(sub, "Sub", LayerColor::Preset(1))).unwrap();
|
||||
let g1 = d.add_ruler_guide(&Guide { layer: Some(layer), ..Guide::new(true, 5.0) });
|
||||
let g2 = d.add_ruler_guide(&Guide { layer: Some(sub), ..Guide::new(false, 6.0) });
|
||||
let line = |d: &Document| d.ruler_guides().iter().map(|g| (g.id, g.pos, g.hidden, g.layer)).collect::<Vec<_>>();
|
||||
assert_eq!(line(&d), [(Some(g1), 5.0, false, Some(layer)), (Some(g2), 6.0, false, Some(sub))]);
|
||||
// Changed in place: the document is the only holder of its layers.
|
||||
assert!(d.edit_ruler_guide(g2, |g| g.pos = 9.0));
|
||||
assert_eq!(line(&d)[1], (Some(g2), 9.0, false, Some(sub)));
|
||||
d.node_mut(g1).unwrap().visible = false;
|
||||
assert_eq!(line(&d)[0], (Some(g1), 5.0, true, Some(layer)));
|
||||
// A copy shares the list until one of them changes.
|
||||
let copy = d.clone();
|
||||
assert_eq!(line(©), line(&d));
|
||||
d.remove(g1).unwrap();
|
||||
assert_eq!(line(&d), [(Some(g2), 9.0, false, Some(sub))]);
|
||||
assert_eq!(line(©).len(), 2);
|
||||
// A new layer with a guide, then the layers reordered and one removed.
|
||||
let top = d.add_layer(None);
|
||||
let g3 = d.add_ruler_guide(&Guide { layer: Some(top), ..Guide::new(true, 1.0) });
|
||||
assert_eq!(line(&d).len(), 2);
|
||||
d.layers.reverse();
|
||||
assert_eq!(line(&d), [(Some(g2), 9.0, false, Some(sub)), (Some(g3), 1.0, false, Some(top))]);
|
||||
d.layers.retain(|l| l.id != layer);
|
||||
assert_eq!(line(&d), [(Some(g3), 1.0, false, Some(top))]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strip_ruler_guides_leaves_a_layers_art() {
|
||||
let (mut d, a, b) = doc_with_rects();
|
||||
let layer = d.layers[0].id;
|
||||
let guide = |id| Node::new(id, NodeKind::RulerGuide { vertical: false, pos: 3.0, artboard: None });
|
||||
let sub = d.alloc_id();
|
||||
d.insert(Some(layer), usize::MAX, Node::layer(sub, "Sub", LayerColor::Preset(1))).unwrap();
|
||||
let (g1, g2) = (d.alloc_id(), d.alloc_id());
|
||||
d.insert(Some(layer), usize::MAX, guide(g1)).unwrap();
|
||||
d.insert(Some(sub), usize::MAX, guide(g2)).unwrap();
|
||||
let mut n = (*d.layers[0]).clone();
|
||||
assert!(n.holds_ruler_guides());
|
||||
n.strip_ruler_guides();
|
||||
assert!(!n.holds_ruler_guides());
|
||||
let mut left = vec![];
|
||||
n.walk(&mut |c| left.push(c.id));
|
||||
assert_eq!(left, [layer, a, b, sub]);
|
||||
// Other nodes hold none and are left as they are.
|
||||
let mut p = d.node(a).unwrap().clone();
|
||||
p.strip_ruler_guides();
|
||||
assert_eq!(&p, d.node(a).unwrap());
|
||||
}
|
||||
|
||||
/// The saved `guides` list becomes ruler guide nodes and is written back from them unchanged:
|
||||
/// guides on a layer, a sublayer, no layer and a deleted layer, with an artboard guide, keep their
|
||||
/// order, ids, names and their own lock and visibility; a saved id that another node has is
|
||||
/// replaced; and the saved form of an older entry still reads.
|
||||
#[test]
|
||||
fn saved_guides_become_nodes_and_are_saved_back_the_same() {
|
||||
let (mut d, a, _) = doc_with_rects();
|
||||
let layer = d.layers[0].id;
|
||||
let top = d.add_layer(Some("Top"));
|
||||
let sub = d.alloc_id();
|
||||
d.insert(Some(layer), usize::MAX, Node::layer(sub, "Sub", LayerColor::Preset(1))).unwrap();
|
||||
let json = serde_json::json!([
|
||||
{"vertical": true, "pos": 10.0, "layer": layer},
|
||||
{"vertical": false, "pos": 20.0, "layer": sub, "artboard": 1},
|
||||
{"vertical": true, "pos": 30.0},
|
||||
{"vertical": false, "pos": 40.0, "layer": 9999},
|
||||
{"vertical": true, "pos": 50.0, "layer": layer, "id": 500, "name": "Edge", "locked": true, "hidden": true},
|
||||
{"vertical": true, "pos": 60.0, "layer": layer, "id": a},
|
||||
]);
|
||||
d.saved_guides = serde_json::from_value(json).unwrap();
|
||||
let before = d.peek_next_id();
|
||||
assert_eq!(d.guides_into_nodes(), 6);
|
||||
assert!(d.saved_guides.is_empty());
|
||||
let guides = d.ruler_guides();
|
||||
let lines: Vec<(f64, Option<NodeId>)> = guides.iter().map(|g| (g.pos, g.layer)).collect();
|
||||
// A guide on no layer, or on a deleted one, goes on the top visible, unlocked layer.
|
||||
assert_eq!(lines[..4], [(50.0, Some(layer)), (10.0, Some(layer)), (20.0, Some(sub)), (30.0, Some(top))], "by id: the kept id first");
|
||||
assert_eq!(lines[4..], [(40.0, Some(top)), (60.0, Some(layer))]);
|
||||
assert_eq!(guides[0].id, Some(NodeId(500)));
|
||||
assert!(guides[1..].iter().all(|g| g.id.unwrap().0 > 500), "new ids start above a kept one");
|
||||
assert_ne!(guides[5].id, Some(a), "an id another object has is not reused");
|
||||
assert!(d.peek_next_id() > 500 && before <= 500);
|
||||
let edge = d.node(NodeId(500)).unwrap();
|
||||
assert!(edge.locked && !edge.visible && edge.name.as_deref() == Some("Edge"));
|
||||
assert_eq!(guides[2].artboard, Some(1));
|
||||
// Each layer's guides sit at its top, above its art and its sublayer.
|
||||
let kids: Vec<bool> = d.node(layer).unwrap().children().unwrap().iter().map(|c| c.is_ruler_guide()).collect();
|
||||
assert_eq!(kids, [false, false, false, true, true, true]);
|
||||
assert_eq!(d.guides_into_nodes(), 0, "a second run does nothing");
|
||||
|
||||
// Saved back: the guides leave the layers and the list names each one's layer and id.
|
||||
let mut saved = d.clone();
|
||||
saved.guides_to_saved();
|
||||
assert!(saved.ruler_guides().is_empty());
|
||||
assert_eq!(saved.node_count(), d.node_count());
|
||||
assert_eq!(saved.saved_guides, guides);
|
||||
let json = serde_json::to_value(&saved).unwrap();
|
||||
assert_eq!(json["guides"].as_array().map(Vec::len), Some(6));
|
||||
assert_eq!(
|
||||
json["guides"][0],
|
||||
serde_json::json!({"vertical": true, "pos": 50.0, "layer": layer, "id": 500, "name": "Edge", "locked": true, "hidden": true})
|
||||
);
|
||||
// Read again, the same guides come back with the same ids.
|
||||
let mut back: Document = serde_json::from_value(json).unwrap();
|
||||
back.fix_next_id();
|
||||
back.guides_into_nodes();
|
||||
assert_eq!(back.ruler_guides(), guides);
|
||||
// An older reader of the list ignores the new fields.
|
||||
#[derive(serde::Deserialize, PartialEq, Debug)]
|
||||
struct Old {
|
||||
vertical: bool,
|
||||
pos: f64,
|
||||
#[serde(default)]
|
||||
artboard: Option<u32>,
|
||||
#[serde(default)]
|
||||
layer: Option<NodeId>,
|
||||
}
|
||||
let old: Vec<Old> = serde_json::from_value(serde_json::to_value(&saved.saved_guides).unwrap()).unwrap();
|
||||
assert_eq!(old[0], Old { vertical: true, pos: 50.0, artboard: None, layer: Some(layer) });
|
||||
|
||||
// A document with no layer gets a "Layer 1" for its guides.
|
||||
let mut empty = Document::new(10.0, 10.0);
|
||||
empty.layers.clear();
|
||||
empty.saved_guides = vec![Guide::new(true, 1.0)];
|
||||
assert_eq!(empty.guides_into_nodes(), 1);
|
||||
assert_eq!(empty.layers.len(), 1);
|
||||
assert_eq!(empty.layers[0].name.as_deref(), Some("Layer 1"));
|
||||
assert_eq!(empty.ruler_guides().len(), 1);
|
||||
}
|
||||
|
||||
/// Editing, deleting and the artboard moves of ruler guide nodes, and the selection keeping step.
|
||||
#[test]
|
||||
fn ruler_guide_nodes_are_edited_by_id() {
|
||||
let (mut d, a, _) = doc_with_rects();
|
||||
let layer = d.layers[0].id;
|
||||
let g1 = d.add_ruler_guide(&Guide { layer: Some(layer), ..Guide::new(true, 10.0) });
|
||||
let g2 = d.add_ruler_guide(&Guide { artboard: Some(1), ..Guide::new(false, 20.0) });
|
||||
assert!(d.edit_ruler_guide(g1, |g| g.pos = 15.0));
|
||||
assert!(!d.edit_ruler_guide(a, |g| g.pos = 15.0), "not a guide");
|
||||
assert_eq!(d.ruler_guide(g1).map(|g| g.pos), Some(15.0));
|
||||
d.move_artboard_guides(1, Vec2::new(5.0, 7.0));
|
||||
assert_eq!(d.ruler_guide(g2).map(|g| g.pos), Some(27.0));
|
||||
d.map_artboard_guides(1, Affine::scale(2.0));
|
||||
assert_eq!(d.ruler_guide(g2).map(|g| g.pos), Some(54.0));
|
||||
assert_eq!(d.ruler_guide(g1).map(|g| g.pos), Some(15.0), "a canvas guide stays");
|
||||
d.copy_artboard_guides(1, 2, Vec2::new(0.0, 100.0));
|
||||
let copies: Vec<(f64, Option<u32>)> = d.ruler_guides().iter().filter(|g| g.artboard == Some(2)).map(|g| (g.pos, g.artboard)).collect();
|
||||
assert_eq!(copies, [(154.0, Some(2))]);
|
||||
let mut sel = Selection::default();
|
||||
sel.set_guides([g1, g2]);
|
||||
sel.key_guide = Some(g2);
|
||||
assert_eq!(d.retain_guides(&mut sel, |g| g.artboard.is_none()), 2);
|
||||
assert_eq!(sel.guides, [g1]);
|
||||
assert_eq!(sel.key_guide, None);
|
||||
assert_eq!(d.ruler_guides().iter().map(|g| g.id).collect::<Vec<_>>(), [Some(g1)]);
|
||||
assert_eq!(d.retain_guides(&mut sel, |_| true), 0);
|
||||
}
|
||||
|
||||
fn doc_with_rects() -> (Document, NodeId, NodeId) {
|
||||
let mut d = Document::new(612.0, 792.0);
|
||||
let layer = d.layers[0].id;
|
||||
|
||||
@@ -107,7 +107,8 @@ fn path_kind(d: &mut Document, path: PathData, rule: FillRule) -> NodeKind {
|
||||
/// `n` with its geometry effects evaluated (`None` = unchanged).
|
||||
fn bake_node(d: &mut Document, n: &Node) -> Option<Node> {
|
||||
// A cancelled export leaves the rest of the document as it is: the caller drops the result.
|
||||
if !needs_bake(n) || vectorcraft_doc::cancel::is_cancelled() {
|
||||
// A ruler guide draws nothing and stays as it is, last in its layer.
|
||||
if n.is_ruler_guide() || !needs_bake(n) || vectorcraft_doc::cancel::is_cancelled() {
|
||||
return None;
|
||||
}
|
||||
// Colour adjustments: the object recoloured (inside it too), then baked as it is.
|
||||
|
||||
@@ -455,6 +455,17 @@ pub fn pathfinder_children(doc: Option<&Document>, group: &Node, hook: Option<Ou
|
||||
return None;
|
||||
}
|
||||
let source: &[Arc<Node>] = group.children()?;
|
||||
// A layer's ruler guides take no part in the operation: they stay above its result.
|
||||
let art = Node::guide_run_start(source);
|
||||
if art < source.len() {
|
||||
let mut without = group.clone();
|
||||
if let Some(ch) = without.children_mut() {
|
||||
ch.truncate(art);
|
||||
}
|
||||
let mut out = pathfinder_children(doc, &without, hook)?;
|
||||
out.extend(source.iter().skip(art).cloned());
|
||||
return Some(out);
|
||||
}
|
||||
let mut stack: Vec<Arc<Node>> = if group.is_compound_shape() {
|
||||
// The group's own appearance paints the area ([`evaluate_container`]), so the art has none.
|
||||
match compound_shape_area(doc, group, hook) {
|
||||
@@ -727,9 +738,11 @@ pub fn evaluate_container(doc: Option<&Document>, n: &Node) -> Option<Node> {
|
||||
return None;
|
||||
}
|
||||
let mut m = n.clone();
|
||||
// A layer's ruler guides take no part: they go back above the evaluated art at the end.
|
||||
let guides = m.children_mut().map(|ch| ch.split_off(Node::guide_run_start(ch))).unwrap_or_default();
|
||||
if pathfinder {
|
||||
let hook = |c: &Node| crate::outline_object(doc, c);
|
||||
let result = pathfinder_children(doc, n, Some(&hook)).unwrap_or_default();
|
||||
let result = pathfinder_children(doc, &m, Some(&hook)).unwrap_or_default();
|
||||
if let Some(ch) = m.children_mut() {
|
||||
*ch = result;
|
||||
}
|
||||
@@ -762,6 +775,9 @@ pub fn evaluate_container(doc: Option<&Document>, n: &Node) -> Option<Node> {
|
||||
}
|
||||
m.appearance.items.clear();
|
||||
m.appearance.contents_index = None;
|
||||
if let Some(ch) = m.children_mut() {
|
||||
ch.extend(guides);
|
||||
}
|
||||
Some(m)
|
||||
}
|
||||
|
||||
|
||||
@@ -820,3 +820,45 @@ fn nested_compound_shapes_take_part_as_their_area() {
|
||||
assert!((area(&out[0]) - 50.0).abs() < 0.01);
|
||||
assert_eq!(out[0].appearance.fill_paint().color(), Some(Color::rgb(0.0, 1.0, 0.0)), "styled like the compound shape");
|
||||
}
|
||||
|
||||
/// The ruler guides of a layer with a Pathfinder effect, a Transform effect that copies its art, or
|
||||
/// fills and strokes of its own take no part: they stay last in the layer, once and unchanged.
|
||||
#[test]
|
||||
fn a_layers_ruler_guides_stay_last_through_its_effects() {
|
||||
use vectorcraft_doc::LayerColor;
|
||||
let guide = |id, pos| Arc::new(Node::new(NodeId(id), NodeKind::RulerGuide { vertical: true, pos, artboard: None }));
|
||||
let layer = |effect: &str, params: serde_json::Value| {
|
||||
let mut l = Node::layer(NodeId(10), "Layer 1", LayerColor::Preset(0));
|
||||
if let Some(ch) = l.children_mut() {
|
||||
*ch = vec![sq(1, 0.0, Color::rgb(1.0, 0.0, 0.0)), sq(2, 5.0, Color::rgb(0.0, 0.0, 1.0)), guide(20, 3.0), guide(21, 8.0)];
|
||||
}
|
||||
l.appearance.effects.push(Effect { id: effect.into(), params, visible: true });
|
||||
l
|
||||
};
|
||||
let tail = |n: &Node| {
|
||||
let ch = n.children().unwrap();
|
||||
assert_eq!(ch.iter().filter(|c| c.is_ruler_guide()).count(), 2, "each guide once");
|
||||
ch.iter().rev().take(2).map(|c| c.kind.clone()).collect::<Vec<_>>()
|
||||
};
|
||||
let guides = vec![guide(21, 8.0).kind.clone(), guide(20, 3.0).kind.clone()];
|
||||
|
||||
let add = layer("pathfinder.add", serde_json::json!({}));
|
||||
let out = pathfinder_children(None, &add, None).unwrap();
|
||||
assert_eq!(out.len(), 3, "the united area, then the two guides");
|
||||
assert!((area(&out[0]) - 150.0).abs() < 0.01);
|
||||
assert!(out[1].is_ruler_guide() && out[2].is_ruler_guide());
|
||||
assert_eq!(tail(&evaluate_container(None, &add).unwrap()), guides);
|
||||
|
||||
let copies = layer("distort.transform", serde_json::json!({"moveH": 30, "copies": 2}));
|
||||
let art = evaluate_container(None, &copies).unwrap();
|
||||
assert_eq!(tail(&art), guides);
|
||||
assert_eq!(art.children().unwrap().len(), 5, "three groups of the art, then the two guides");
|
||||
let mut art = copies.clone();
|
||||
crate::reshape::reshape_leaves(&mut art, &copies.appearance.effects.iter().collect::<Vec<_>>(), &copies.appearance.effects, None);
|
||||
assert_eq!(tail(&art), guides);
|
||||
assert_eq!(art.children().unwrap().len(), 5, "reshaped as members too");
|
||||
|
||||
let mut painted = layer("distort.transform", serde_json::json!({"moveH": 30}));
|
||||
painted.appearance.items = Appearance::basic(Paint::solid(Color::BLACK), Paint::None, 0.0).items;
|
||||
assert_eq!(tail(&evaluate_container(None, &painted).unwrap()), guides);
|
||||
}
|
||||
|
||||
@@ -974,7 +974,7 @@ fn effects_cost(leaf: &Node, path: &PathData) -> Budget {
|
||||
/// Remove the guides anywhere under `n`.
|
||||
fn without_guides(n: &mut Node) {
|
||||
let Some(children) = n.children_mut() else { return };
|
||||
children.retain(|c| !matches!(c.kind, NodeKind::Path { guide: true, .. }));
|
||||
children.retain(|c| !c.is_guide());
|
||||
children.iter_mut().for_each(|c| without_guides(Arc::make_mut(c)));
|
||||
}
|
||||
|
||||
@@ -984,7 +984,9 @@ fn without_guides(n: &mut Node) {
|
||||
/// [`crate::distort::MAX_COPY_OBJECTS`] nodes in all, the art of opacity masks counted. Returns what
|
||||
/// the art costs after.
|
||||
fn copy_art(art: &mut Node, m: Affine, copies: usize, scaling: vectorcraft_doc::Scaling, budget: Budget) -> Budget {
|
||||
let Some(members) = art.children().cloned() else { return art_cost(art) };
|
||||
let Some(mut members) = art.children().cloned() else { return art_cost(art) };
|
||||
// A layer's ruler guides are not copied: they stay above the copies, once.
|
||||
let guides = members.split_off(Node::guide_run_start(&members));
|
||||
let original = Node::group(art.id, members);
|
||||
let mut made = copy_cost(&original);
|
||||
let mut drawn = original.clone();
|
||||
@@ -1014,6 +1016,7 @@ fn copy_art(art: &mut Node, m: Affine, copies: usize, scaling: vectorcraft_doc::
|
||||
// The last copy is the lowest.
|
||||
placements.reverse();
|
||||
placements.push(Arc::new(original));
|
||||
placements.extend(guides);
|
||||
if let Some(children) = art.children_mut() {
|
||||
*children = placements;
|
||||
}
|
||||
|
||||
@@ -348,7 +348,7 @@ pub(crate) fn members(n: &Node, label: &str) -> Result<Vec<Profile>, String> {
|
||||
fn walk(n: &Node, opacity: f32, scopes: &[Scope], w: &mut Walk) -> Result<(), String> {
|
||||
let label = w.label;
|
||||
for c in n.children().into_iter().flatten() {
|
||||
if !c.visible || matches!(c.kind, NodeKind::Path { guide: true, .. }) {
|
||||
if !c.visible || c.is_guide() {
|
||||
continue;
|
||||
}
|
||||
let opacity = opacity * c.opacity;
|
||||
|
||||
@@ -191,7 +191,7 @@ impl Resolver<'_> {
|
||||
let mut changed = false;
|
||||
for c in children {
|
||||
self.spend(1)?;
|
||||
if !c.visible || matches!(c.kind, NodeKind::Path { guide: true, .. }) {
|
||||
if !c.visible || c.is_guide() {
|
||||
continue;
|
||||
}
|
||||
if matches!(c.kind, NodeKind::Image(_)) {
|
||||
@@ -279,7 +279,7 @@ impl Resolver<'_> {
|
||||
kids = nest(n, cut, 0);
|
||||
} else {
|
||||
for c in n.children().into_iter().flatten() {
|
||||
if !c.visible || matches!(c.kind, NodeKind::Path { guide: true, .. }) {
|
||||
if !c.visible || c.is_guide() {
|
||||
continue;
|
||||
}
|
||||
if let Some(c) = self.clipped(c, clip, depth + 1)? {
|
||||
|
||||
@@ -526,10 +526,12 @@ pub(crate) fn checked_ids_or_selection(s: &Session, p: &Value, command: &str) ->
|
||||
|
||||
pub(crate) fn checked_id(s: &Session, value: &Value, command: &str) -> Result<NodeId> {
|
||||
let id = value.as_u64().map(NodeId).ok_or_else(|| bad(command, format!("invalid object id {value}: expected a non-negative integer")))?;
|
||||
if s.doc()?.doc.node(id).is_none() {
|
||||
return Err(bad(command, format!("no such object id {value}")));
|
||||
match s.doc()?.doc.node(id) {
|
||||
None => Err(bad(command, format!("no such object id {value}"))),
|
||||
// The guide commands take ruler guides by index.
|
||||
Some(n) if n.is_ruler_guide() => Err(bad(command, format!("id {value} is a ruler guide, not an object"))),
|
||||
Some(_) => Ok(id),
|
||||
}
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
/// `v` as exactly `N` finite numbers (`[x, y]`, `[x, y, width, height]`…): `None` when it isn't an
|
||||
@@ -689,10 +691,11 @@ pub(crate) fn leaf_targets(s: &Session, ids: &[NodeId]) -> Result<Vec<NodeId>> {
|
||||
let compound = |id: NodeId| d.parent_of(id).filter(|p| d.node(*p).is_some_and(owns));
|
||||
let mut out = vec![];
|
||||
for id in ids {
|
||||
let Some(n) = d.node(*id) else { continue };
|
||||
// A ruler guide has no appearance to paint, given or in a layer.
|
||||
let Some(n) = d.node(*id).filter(|n| !n.is_ruler_guide()) else { continue };
|
||||
match &n.kind {
|
||||
NodeKind::Group { .. } | NodeKind::Layer { .. } if !n.is_compound_shape() => n.walk(&mut |c| {
|
||||
if !c.is_container() || owns(c) {
|
||||
if (!c.is_container() && !c.is_ruler_guide()) || owns(c) {
|
||||
out.push(c.id)
|
||||
}
|
||||
}),
|
||||
@@ -742,6 +745,7 @@ pub(crate) fn unique_name_from(base: &str, next: &mut std::collections::HashMap<
|
||||
/// The most presets one presets file imports: each is looked up among the saved ones.
|
||||
pub(crate) const MAX_IMPORTED_PRESETS: usize = 1000;
|
||||
|
||||
pub use docmenu::guide_indexes;
|
||||
pub(crate) use edit::{settle_interaction, typing_in_progress};
|
||||
pub(crate) use object::bounds_rewrite;
|
||||
pub(crate) use object::{keep_aligned_in, refresh_pixel_aligned};
|
||||
|
||||
@@ -185,7 +185,7 @@ fn objects<'a>(d: &'a Document, scope: &Scope) -> Vec<(&'a Node, Place)> {
|
||||
}
|
||||
|
||||
fn visit<'a>(n: &'a Node, place: Place, out: &mut Vec<(&'a Node, Place)>) {
|
||||
if !n.visible || n.locked || out.len() >= MAX_OBJECTS {
|
||||
if !n.visible || n.locked || n.is_ruler_guide() || out.len() >= MAX_OBJECTS {
|
||||
return;
|
||||
}
|
||||
let inner = |clip: bool, i: usize, top: bool| Place { clips: clip && i == 0, top, in_compound: false };
|
||||
@@ -216,7 +216,7 @@ fn is_generated(n: &Node) -> bool {
|
||||
}
|
||||
|
||||
fn is_guide_or_clip(n: &Node) -> bool {
|
||||
matches!(n.kind, NodeKind::Path { guide: true, .. } | NodeKind::Path { clipping: true, .. })
|
||||
n.is_guide() || matches!(n.kind, NodeKind::Path { clipping: true, .. })
|
||||
}
|
||||
|
||||
fn is_shape(n: &Node) -> bool {
|
||||
@@ -484,7 +484,7 @@ fn artboards(d: &mut Document, sel: &mut Selection, scope: &Scope) -> Result<Out
|
||||
if i < d.artboards.len() {
|
||||
let id = d.artboards.remove(i).id;
|
||||
d.retire_artboard_id(id);
|
||||
d.retain_guides(sel, |_, g| g.artboard != Some(id));
|
||||
d.retain_guides(sel, |g| g.artboard != Some(id));
|
||||
}
|
||||
}
|
||||
Ok(Outcome { count: empty.len(), ids: vec![], artboards: empty })
|
||||
|
||||
@@ -107,7 +107,7 @@ impl Fixes {
|
||||
let white = usize::from(self.remove_white_overprint && self.unset_white(n));
|
||||
let blacks = self.overprint_black > 0.0 && (!self.text_only || matches!(n.kind, NodeKind::Text(_)));
|
||||
let mut counts = (white, usize::from(blacks && black.apply(n, spots)));
|
||||
for c in n.children_mut().into_iter().flatten() {
|
||||
for c in n.children_mut().into_iter().flatten().filter(|c| !c.is_ruler_guide()) {
|
||||
let (w, b) = self.overprint(Arc::make_mut(c), black, spots);
|
||||
counts = (counts.0 + w, counts.1 + b);
|
||||
}
|
||||
|
||||
@@ -164,11 +164,11 @@ fn export_all_open(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
|
||||
/// `n` with each layer in it, and itself, made a group of the same art and clipping (a symbol holds
|
||||
/// no layers).
|
||||
/// no layers, and no ruler guides: a layer's are left out).
|
||||
fn without_layers(n: &Node) -> Node {
|
||||
let mut n = n.clone();
|
||||
if let NodeKind::Layer { children, clip, .. } = &n.kind {
|
||||
n.kind = NodeKind::Group { children: children.clone(), clip: *clip };
|
||||
n.kind = NodeKind::Group { children: children.iter().filter(|c| !c.is_ruler_guide()).cloned().collect(), clip: *clip };
|
||||
}
|
||||
if let Some(children) = n.children_mut() {
|
||||
for c in children.iter_mut().filter(|c| matches!(c.kind, NodeKind::Layer { .. })) {
|
||||
@@ -203,8 +203,10 @@ fn symbols_from_layers(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let (mut made, mut skipped) = (vec![], vec![]);
|
||||
for layer in layers.iter().filter_map(|id| d.layers.iter().find(|l| l.id == *id)) {
|
||||
let name = layer.display_name();
|
||||
let roots: Vec<NodeId> = layer.children().map(|c| c.iter().map(|n| n.id).collect()).unwrap_or_default();
|
||||
let art = match layer.children().map(Vec::as_slice).unwrap_or_default() {
|
||||
// The layer's ruler guides stay where they are.
|
||||
let children: Vec<Arc<Node>> = layer.children().into_iter().flatten().filter(|n| !n.is_ruler_guide()).cloned().collect();
|
||||
let roots: Vec<NodeId> = children.iter().map(|n| n.id).collect();
|
||||
let art = match children.as_slice() {
|
||||
[] => None,
|
||||
[one] => Some(without_layers(one)),
|
||||
many => Some(without_layers(&Node::group(NodeId(0), many.to_vec()))),
|
||||
|
||||
@@ -1189,7 +1189,7 @@ fn target_contents(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
.iter()
|
||||
.filter_map(|id| d.node(*id))
|
||||
.filter(|n| matches!(n.kind, NodeKind::Group { .. } | NodeKind::Layer { .. }))
|
||||
.flat_map(|n| n.children().into_iter().flatten().filter(|c| c.visible && !c.locked).map(|c| c.id))
|
||||
.flat_map(|n| n.children().into_iter().flatten().filter(|c| c.visible && !c.locked && !c.is_ruler_guide()).map(|c| c.id))
|
||||
.collect();
|
||||
if ids.is_empty() {
|
||||
return Err(bad("appearance.targetContents", "select a group or layer with visible, unlocked members"));
|
||||
|
||||
@@ -116,13 +116,15 @@ pub(crate) fn art_under(doc: &vectorcraft_doc::Document, id: NodeId, out: &mut V
|
||||
let Some(n) = doc.node(id) else { return };
|
||||
if n.is_layer() {
|
||||
if let Some(kids) = n.children() {
|
||||
for c in kids {
|
||||
for c in kids.iter().filter(|c| !c.is_ruler_guide()) {
|
||||
art_under(doc, c.id, out);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
out.push(id);
|
||||
if !n.is_ruler_guide() {
|
||||
out.push(id);
|
||||
}
|
||||
}
|
||||
|
||||
fn add(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
@@ -137,7 +137,7 @@ fn path_param(p: &Value, art: &Node, cmd: &str) -> Result<Vec<u32>> {
|
||||
/// stroke has a Convert to Shape or Outline Object effect.
|
||||
fn paint_refusal(n: &Node, key: &str, cmd: &str) -> Option<EngineError> {
|
||||
Some(match &n.kind {
|
||||
NodeKind::Path { guide: true, .. } => bad(cmd, format!("{key} cannot be set on a guide")),
|
||||
NodeKind::Path { guide: true, .. } | NodeKind::RulerGuide { .. } => bad(cmd, format!("{key} cannot be set on a guide")),
|
||||
NodeKind::Path { .. } | NodeKind::Compound { .. } | NodeKind::Text(_) | NodeKind::Group { .. } | NodeKind::Layer { .. } => return None,
|
||||
NodeKind::Image(_) => {
|
||||
let effects = if key == "fill" { n.appearance.fill().map(|f| &f.effects) } else { n.appearance.stroke().map(|s| &s.effects) };
|
||||
|
||||
@@ -322,7 +322,18 @@ impl Clipboard {
|
||||
/// Every object of `src` (each top-level layer's contents) with its resources, for Place and
|
||||
/// pasting outside SVG. Their layers don't count for Paste Remembers Layers.
|
||||
pub fn from_document(src: &Document) -> Self {
|
||||
let nodes = src.layers.iter().flat_map(|l| l.children().into_iter().flatten()).map(|n| (**n).clone()).collect();
|
||||
// Ruler guides are not objects: the ones of `src`'s layers and sublayers stay behind.
|
||||
let nodes = src
|
||||
.layers
|
||||
.iter()
|
||||
.flat_map(|l| l.children().into_iter().flatten())
|
||||
.filter(|n| !n.is_ruler_guide())
|
||||
.map(|n| {
|
||||
let mut n = (**n).clone();
|
||||
n.strip_ruler_guides();
|
||||
n
|
||||
})
|
||||
.collect();
|
||||
Self::gather(src, nodes)
|
||||
}
|
||||
|
||||
|
||||
@@ -462,7 +462,7 @@ fn run_pass(s: &mut Session, p: &Value, cmd: &str, label: &str, pass: Recolor) -
|
||||
pub(crate) fn left_out(d: &Document, iso: Option<NodeId>, roots: &[NodeId]) -> Vec<(NodeId, bool)> {
|
||||
fn visit(n: &Node, hidden: bool, locked: bool, all: &HashSet<NodeId>, board_locked: &dyn Fn(&Node) -> bool, out: &mut Vec<(NodeId, bool)>) {
|
||||
for c in n.children().into_iter().flatten() {
|
||||
if c.is_template() {
|
||||
if c.is_template() || c.is_ruler_guide() {
|
||||
continue;
|
||||
}
|
||||
let (hidden, locked) = (hidden || !c.visible, locked || c.locked);
|
||||
|
||||
@@ -582,7 +582,7 @@ fn map_more_colors(n: &mut vectorcraft_doc::Node, f: &mut dyn FnMut(&Color, proo
|
||||
}
|
||||
}
|
||||
if let Some(ch) = n.children_mut() {
|
||||
for c in ch.iter_mut() {
|
||||
for c in ch.iter_mut().filter(|c| !c.is_ruler_guide()) {
|
||||
map_more_colors(Arc::make_mut(c), f);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -370,7 +370,8 @@ fn info(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
return;
|
||||
}
|
||||
let k = match &n.kind {
|
||||
NodeKind::Layer { .. } => None,
|
||||
// The Guides row counts paths made into guides; ruler guides are not objects.
|
||||
NodeKind::Layer { .. } | NodeKind::RulerGuide { .. } => None,
|
||||
NodeKind::Group { clip: true, .. } => Some("clipGroups"),
|
||||
NodeKind::Group { .. } => Some("groups"),
|
||||
NodeKind::Path { guide: true, .. } => Some("guides"),
|
||||
|
||||
@@ -666,7 +666,7 @@ fn release_guides(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if (index.is_some() || ids.is_some()) && s.menu.guides_locked {
|
||||
return Err(EngineError::Other("guides are locked".into()));
|
||||
}
|
||||
let (rulers, paths): (Vec<usize>, Vec<NodeId>) = if index.is_some() || ids.is_some() {
|
||||
let (rulers, paths): (Vec<NodeId>, Vec<NodeId>) = if index.is_some() || ids.is_some() {
|
||||
let rulers = match index {
|
||||
Some(v) => vec![editable_guide_index(s, Some(v), C)?],
|
||||
None => vec![],
|
||||
@@ -689,10 +689,10 @@ fn release_guides(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let st = s.doc()?;
|
||||
let all = guide_paths(&st.doc);
|
||||
let sel: Vec<NodeId> = all.iter().copied().filter(|id| st.selection.contains(*id)).collect();
|
||||
let rulers: Vec<usize> = if s.menu.guides_locked {
|
||||
let rulers: Vec<NodeId> = if s.menu.guides_locked {
|
||||
vec![]
|
||||
} else {
|
||||
st.selection.guides.iter().copied().filter(|i| st.doc.guides.get(*i).is_some_and(|g| st.doc.guide_editable(g))).collect()
|
||||
st.selection.guides.iter().copied().filter(|id| st.doc.ruler_guide(*id).is_some_and(|g| st.doc.guide_editable(&g))).collect()
|
||||
};
|
||||
let editable = st.doc.editable();
|
||||
if sel.is_empty() && rulers.is_empty() { (vec![], all.into_iter().filter(|id| editable(*id)).collect()) } else { (rulers, sel) }
|
||||
@@ -702,8 +702,8 @@ fn release_guides(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let artboards = st.doc.artboards.iter().map(|a| a.rect).reduce(|a, b| a.union(b));
|
||||
let mut lines = Vec::with_capacity(rulers.len());
|
||||
for &i in &rulers {
|
||||
let Some(g) = st.doc.guides.get(i) else { continue };
|
||||
let span = st.doc.guide_span(g).or_else(|| artboards.map(|r| if g.vertical { (r.y0, r.y1) } else { (r.x0, r.x1) }));
|
||||
let Some(g) = st.doc.ruler_guide(i) else { continue };
|
||||
let span = st.doc.guide_span(&g).or_else(|| artboards.map(|r| if g.vertical { (r.y0, r.y1) } else { (r.x0, r.x1) }));
|
||||
let Some((a, b)) = span else { continue };
|
||||
let (a, b) = if g.vertical { (Point::new(g.pos, a), Point::new(g.pos, b)) } else { (Point::new(a, g.pos), Point::new(b, g.pos)) };
|
||||
let parent = match g.layer.filter(|l| st.doc.node(*l).is_some_and(Node::is_layer)) {
|
||||
@@ -729,7 +729,7 @@ fn release_guides(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
d.insert(parent, usize::MAX, Node::path(id, vectorcraft_geom::shapes::line(a, b), look))?;
|
||||
made.push(id);
|
||||
}
|
||||
d.retain_guides(sel, |i, _| !lines.iter().any(|l| l.0 == i));
|
||||
d.retain_guides(sel, |g| !lines.iter().any(|l| Some(l.0) == g.id));
|
||||
sel.set(paths.iter().copied().chain(made));
|
||||
Ok(())
|
||||
})?;
|
||||
@@ -745,12 +745,12 @@ fn lock_guides(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
fn clear_guides(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
let st = s.doc()?;
|
||||
let paths = guide_paths(&st.doc);
|
||||
let n = paths.len() + st.doc.guides.len();
|
||||
let n = paths.len() + st.doc.ruler_guides().len();
|
||||
if n == 0 {
|
||||
return Ok(json!({ "count": 0 }));
|
||||
}
|
||||
s.edit("Clear Guides", |d, _| {
|
||||
d.guides.clear();
|
||||
s.edit("Clear Guides", |d, sel| {
|
||||
d.retain_guides(sel, |_| false);
|
||||
let mut gone = Beside::replacing(d, &paths);
|
||||
for id in &paths {
|
||||
if d.node(*id).is_some() {
|
||||
@@ -780,17 +780,29 @@ fn guide_add(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
// Where new art goes (refused when no layer is shown and unlocked), or that group's layer.
|
||||
None => st.target_parent()?.and_then(|p| if st.doc.node(p).is_some_and(Node::is_layer) { Some(p) } else { st.doc.layer_containing(p) }),
|
||||
};
|
||||
let i = s.edit("New Guide", |d, _| {
|
||||
d.guides.push(Guide { artboard, layer, ..Guide::new(vertical, pos) });
|
||||
Ok(d.guides.len() - 1)
|
||||
})?;
|
||||
let id = s.edit("New Guide", |d, _| Ok(d.add_ruler_guide(&Guide { artboard, layer, ..Guide::new(vertical, pos) })))?;
|
||||
let i = guide_indexes(&s.doc()?.doc, &[id]).first().copied();
|
||||
Ok(json!({ "index": i }))
|
||||
}
|
||||
|
||||
/// The ids of the active document's ruler guides by index: the order of `guide.list` (ascending
|
||||
/// id).
|
||||
pub(crate) fn guide_ids(d: &vectorcraft_doc::Document) -> Vec<NodeId> {
|
||||
d.ruler_guides().into_iter().filter_map(|g| g.id).collect()
|
||||
}
|
||||
|
||||
/// The `guide.list` indexes of ruler guides `ids`, in the order of `ids` (an id that is not a ruler
|
||||
/// guide is left out).
|
||||
pub fn guide_indexes(d: &vectorcraft_doc::Document, ids: &[NodeId]) -> Vec<usize> {
|
||||
let all = guide_ids(d);
|
||||
ids.iter().filter_map(|id| all.binary_search(id).ok()).collect()
|
||||
}
|
||||
|
||||
fn guide_list(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
let st = s.doc()?;
|
||||
let row = |(i, g): (usize, &Guide)| {
|
||||
let mut r = json!({ "index": i, "vertical": g.vertical, "pos": g.pos, "selected": st.selection.guides.contains(&i) });
|
||||
let selected = g.id.is_some_and(|id| st.selection.guides.contains(&id));
|
||||
let mut r = json!({ "index": i, "vertical": g.vertical, "pos": g.pos, "selected": selected });
|
||||
if let Some(ab) = st.doc.artboards.iter().position(|a| Some(a.id) == g.artboard) {
|
||||
r["artboard"] = json!(ab);
|
||||
}
|
||||
@@ -801,31 +813,38 @@ fn guide_list(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
r["editable"] = json!(st.doc.guide_editable(g));
|
||||
r
|
||||
};
|
||||
Ok(Value::Array(st.doc.guides.iter().enumerate().map(row).collect()))
|
||||
Ok(Value::Array(st.doc.ruler_guides().iter().enumerate().map(row).collect()))
|
||||
}
|
||||
|
||||
/// Ruler guide `index` of the active document (`index` validated).
|
||||
fn guide_index(s: &Session, v: Option<&Value>, cmd: &str) -> Result<usize> {
|
||||
/// The id of ruler guide `index` in `ids` ([`guide_ids`]; `index` validated).
|
||||
fn guide_index_in(ids: &[NodeId], v: Option<&Value>, cmd: &str) -> Result<NodeId> {
|
||||
let i = v.and_then(Value::as_u64).and_then(|i| usize::try_from(i).ok()).ok_or_else(|| bad(cmd, "an index must be a whole number"))?;
|
||||
if i >= s.doc()?.doc.guides.len() {
|
||||
return Err(bad(cmd, format!("no guide {i}")));
|
||||
ids.get(i).copied().ok_or_else(|| bad(cmd, format!("no guide {i}")))
|
||||
}
|
||||
|
||||
/// The id of ruler guide `index` of the active document (`index` validated).
|
||||
fn guide_index(s: &Session, v: Option<&Value>, cmd: &str) -> Result<NodeId> {
|
||||
guide_index_in(&guide_ids(&s.doc()?.doc), v, cmd)
|
||||
}
|
||||
|
||||
/// Is ruler guide `id` one that can be moved or deleted: not on a hidden or locked layer?
|
||||
fn check_editable_guide(s: &Session, id: NodeId) -> Result<NodeId> {
|
||||
let d = &s.doc()?.doc;
|
||||
if !d.ruler_guide(id).is_some_and(|g| d.guide_editable(&g)) {
|
||||
return Err(EngineError::Other("the guide is on a hidden or locked layer".into()));
|
||||
}
|
||||
Ok(i)
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
/// [`guide_index`] of a guide that can be moved or deleted: not on a hidden or locked layer.
|
||||
fn editable_guide_index(s: &Session, v: Option<&Value>, cmd: &str) -> Result<usize> {
|
||||
let i = guide_index(s, v, cmd)?;
|
||||
let d = &s.doc()?.doc;
|
||||
if !d.guides.get(i).is_some_and(|g| d.guide_editable(g)) {
|
||||
return Err(EngineError::Other("the guide is on a hidden or locked layer".into()));
|
||||
}
|
||||
Ok(i)
|
||||
fn editable_guide_index(s: &Session, v: Option<&Value>, cmd: &str) -> Result<NodeId> {
|
||||
check_editable_guide(s, guide_index(s, v, cmd)?)
|
||||
}
|
||||
|
||||
fn guide_select(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let list = p.get("indexes").and_then(Value::as_array).ok_or_else(|| bad("guide.select", "missing indexes"))?;
|
||||
let picked = list.iter().map(|v| guide_index(s, Some(v), "guide.select")).collect::<Result<Vec<_>>>()?;
|
||||
let ids = guide_ids(&s.doc()?.doc);
|
||||
let picked = list.iter().map(|v| guide_index_in(&ids, Some(v), "guide.select")).collect::<Result<Vec<_>>>()?;
|
||||
if bool_or(p, "toggle", false) {
|
||||
// Added to (or taken from) what is selected, the art included.
|
||||
s.select(|_, sel| {
|
||||
@@ -836,12 +855,13 @@ fn guide_select(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
} else {
|
||||
s.select(|_, sel| sel.set_guides(picked))?;
|
||||
}
|
||||
Ok(json!({ "selected": s.doc()?.selection.guides }))
|
||||
let st = s.doc()?;
|
||||
Ok(json!({ "selected": guide_indexes(&st.doc, &st.selection.guides) }))
|
||||
}
|
||||
|
||||
/// Delete ruler guide `index`, else the selected guides, in one undo step → {count}.
|
||||
pub(crate) fn guide_remove(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let gone: Vec<usize> = match p.get("index") {
|
||||
let gone: Vec<NodeId> = match p.get("index") {
|
||||
Some(v) => vec![editable_guide_index(s, Some(v), "guide.remove")?],
|
||||
None => s.doc()?.selection.guides.clone(),
|
||||
};
|
||||
@@ -850,7 +870,7 @@ pub(crate) fn guide_remove(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
let label = if gone.len() == 1 { "Delete Guide" } else { "Delete Guides" };
|
||||
s.edit(label, |d, sel| {
|
||||
d.retain_guides(sel, |i, _| !gone.contains(&i));
|
||||
d.retain_guides(sel, |g| !g.id.is_some_and(|id| gone.contains(&id)));
|
||||
Ok(())
|
||||
})?;
|
||||
Ok(json!({ "count": gone.len() }))
|
||||
@@ -866,18 +886,22 @@ fn guide_set_layer(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if super::layer::locked_within(d, layer) {
|
||||
return Err(EngineError::Other("the layer or group is locked".into()));
|
||||
}
|
||||
let moving: Vec<usize> = match (p.get("index"), p.get("indexes").and_then(Value::as_array)) {
|
||||
let moving: Vec<NodeId> = match (p.get("index"), p.get("indexes").and_then(Value::as_array)) {
|
||||
(Some(v), _) => vec![editable_guide_index(s, Some(v), C)?],
|
||||
(None, Some(list)) => list.iter().take(10_000).map(|v| editable_guide_index(s, Some(v), C)).collect::<Result<Vec<_>>>()?,
|
||||
(None, Some(list)) => {
|
||||
let ids = guide_ids(&s.doc()?.doc);
|
||||
list.iter().take(10_000).map(|v| check_editable_guide(s, guide_index_in(&ids, Some(v), C)?)).collect::<Result<Vec<_>>>()?
|
||||
}
|
||||
(None, None) => s.doc()?.selection.guides.clone(),
|
||||
};
|
||||
if moving.is_empty() {
|
||||
return Err(bad(C, "no guide selected (or give an index)"));
|
||||
}
|
||||
s.edit("Move Guide to Layer", |d, _| {
|
||||
for i in &moving {
|
||||
if let Some(g) = d.guides.get_mut(*i) {
|
||||
g.layer = Some(layer);
|
||||
// Each keeps its id (and so its index), at the top of the layer.
|
||||
for id in &moving {
|
||||
if d.parent_of(*id) != Some(layer) && d.ruler_guide(*id).is_some() {
|
||||
d.move_node(*id, Some(layer), usize::MAX)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -891,9 +915,7 @@ pub(crate) fn guide_move(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let i = editable_guide_index(s, Some(v), "guide.move")?;
|
||||
let pos = f64_req(p, "pos", "guide.move")?;
|
||||
s.edit("Move Guide", |d, _| {
|
||||
if let Some(g) = d.guides.get_mut(i) {
|
||||
g.pos = pos;
|
||||
}
|
||||
d.edit_ruler_guide(i, |g| g.pos = pos);
|
||||
Ok(())
|
||||
})?;
|
||||
return ok();
|
||||
@@ -917,18 +939,14 @@ pub(crate) fn guide_move(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
s.edit(if copy { "Copy Guide" } else { "Move Guide" }, |d, sel| {
|
||||
let mut copies = vec![];
|
||||
for &i in &moving {
|
||||
let Some(g) = d.guides.get_mut(i) else { continue };
|
||||
let moved = g.moved(Vec2::new(dx, dy));
|
||||
if copy {
|
||||
copies.push(moved);
|
||||
copies.extend(d.ruler_guide(i).map(|g| d.add_ruler_guide(&g.moved(Vec2::new(dx, dy)))));
|
||||
} else {
|
||||
*g = moved;
|
||||
d.edit_ruler_guide(i, |g| *g = g.moved(Vec2::new(dx, dy)));
|
||||
}
|
||||
}
|
||||
if copy {
|
||||
let first = d.guides.len();
|
||||
d.guides.extend(copies);
|
||||
sel.set_guides(first..d.guides.len());
|
||||
sel.set_guides(copies);
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
@@ -991,8 +991,8 @@ fn copy(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
.selection
|
||||
.guides
|
||||
.iter()
|
||||
.filter_map(|i| st.doc.guides.get(*i))
|
||||
.map(|g| vectorcraft_doc::Guide { layer: None, artboard: None, ..g.clone() })
|
||||
.filter_map(|i| st.doc.ruler_guide(*i))
|
||||
.map(|g| vectorcraft_doc::Guide { layer: None, artboard: None, id: None, ..g })
|
||||
.collect();
|
||||
let roots = if st.selection.is_empty() && !guides.is_empty() { vec![] } else { targets };
|
||||
let mut clip = Clipboard::copy(st, &roots, direct);
|
||||
@@ -1004,7 +1004,7 @@ fn copy(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
// Guides alone: the artboard the first one belongs to, or crosses, is where in-place pastes
|
||||
// measure from, as the objects' artboard is.
|
||||
if clip.nodes.is_empty()
|
||||
&& let Some(g) = st.selection.guides.first().and_then(|i| st.doc.guides.get(*i))
|
||||
&& let Some(g) = st.selection.guides.first().and_then(|i| st.doc.ruler_guide(*i))
|
||||
{
|
||||
let board = g.artboard.and_then(|id| st.doc.artboards.iter().find(|a| a.id == id)).or_else(|| {
|
||||
st.doc
|
||||
@@ -1053,7 +1053,7 @@ fn cut_direct_selection(s: &mut Session, roots: &[NodeId]) -> Result<Value> {
|
||||
s.edit("Cut", |d, selection| {
|
||||
// The selected ruler guides were copied too (#3226): they go in the same undo step.
|
||||
let gone = selection.guides.clone();
|
||||
d.retain_guides(selection, |i, _| !gone.contains(&i));
|
||||
d.retain_guides(selection, |g| !g.id.is_some_and(|id| gone.contains(&id)));
|
||||
let mut survivors = vec![];
|
||||
let mut pieces = super::pathops::Beside::replacing(d, roots);
|
||||
for id in roots {
|
||||
@@ -1153,7 +1153,7 @@ fn clear(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
if with_guides {
|
||||
let gone = sel.guides.clone();
|
||||
d.retain_guides(sel, |i, _| !gone.contains(&i));
|
||||
d.retain_guides(sel, |g| !g.id.is_some_and(|id| gone.contains(&id)));
|
||||
}
|
||||
if is_explicit {
|
||||
for id in &ids {
|
||||
@@ -1438,8 +1438,7 @@ fn paste_clip(s: &mut Session, p: &Value, mode: PasteMode, clip: &Clipboard, cho
|
||||
for g in &clip.guides {
|
||||
let pos = g.pos + if g.vertical { dv.x } else { dv.y };
|
||||
if pos.is_finite() {
|
||||
sel.guides.push(d.guides.len());
|
||||
d.guides.push(vectorcraft_doc::Guide { pos, layer: guide_layer, ..g.clone() });
|
||||
sel.guides.push(d.add_ruler_guide(&vectorcraft_doc::Guide { pos, layer: guide_layer, ..g.clone() }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -889,6 +889,10 @@ fn expand_pieces(
|
||||
let mut seen = std::collections::HashSet::from([id]);
|
||||
let mut expanded = std::collections::HashMap::new();
|
||||
for c in m.children_mut().into_iter().flatten() {
|
||||
// A layer's ruler guides are not art: they keep their ids and expand to nothing.
|
||||
if c.is_ruler_guide() {
|
||||
continue;
|
||||
}
|
||||
let c = Arc::make_mut(c);
|
||||
let mut listed = None;
|
||||
if all || c.id == id {
|
||||
@@ -1084,6 +1088,11 @@ fn expanded_node(d: &mut Document, mut n: Node, brushes: &[vectorcraft_brush::Br
|
||||
// The place of each member in the walk of `n`: after `n` and the members before it.
|
||||
let mut at = 1;
|
||||
for c in n.children_mut().into_iter().flatten() {
|
||||
if c.is_ruler_guide() {
|
||||
// A layer's ruler guide expands to nothing, but the walk of `n` still visits it.
|
||||
at += 1;
|
||||
continue;
|
||||
}
|
||||
let from = out.len();
|
||||
match members.remove(&c.id) {
|
||||
Some(listed) => out.extend(listed),
|
||||
@@ -1097,7 +1106,8 @@ fn expanded_node(d: &mut Document, mut n: Node, brushes: &[vectorcraft_brush::Br
|
||||
for (_, place) in &mut out[from..] {
|
||||
*place += at;
|
||||
}
|
||||
at += c.count();
|
||||
// Every node the walk visits, ruler guides in a sublayer too (`Node::count` leaves them out).
|
||||
c.walk(&mut |_| at += 1);
|
||||
}
|
||||
}
|
||||
changed.then_some(n)
|
||||
|
||||
@@ -534,7 +534,7 @@ fn expand_piece_leaves(
|
||||
}
|
||||
return;
|
||||
}
|
||||
for c in n.children_mut().into_iter().flatten() {
|
||||
for c in n.children_mut().into_iter().flatten().filter(|c| !c.is_ruler_guide()) {
|
||||
expand_piece_leaves(d, Arc::make_mut(c), left, expanded, expand);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -306,10 +306,10 @@ pub(super) fn outlined_if_asked(mut doc: Document, warnings: Vec<String>, opts:
|
||||
(doc, warnings)
|
||||
}
|
||||
|
||||
/// Does `doc` hold no art: only layers, with nothing in them?
|
||||
/// Does `doc` hold no art: only layers, with nothing in them but ruler guides?
|
||||
fn draws_nothing(doc: &Document) -> bool {
|
||||
let mut art = false;
|
||||
doc.walk(|n| art |= !n.is_layer());
|
||||
doc.walk(|n| art |= !n.is_layer() && !n.is_ruler_guide());
|
||||
!art
|
||||
}
|
||||
|
||||
|
||||
@@ -621,7 +621,7 @@ pub(crate) fn flatten_expanded(doc: &Document, o: &FlattenOptions) -> Result<Opt
|
||||
let mut roots = vec![];
|
||||
for l in doc.layers.iter().filter(|l| l.visible && !matches!(l.kind, NodeKind::Layer { template: true, .. })) {
|
||||
let children = l.children().map_or(&[][..], Vec::as_slice);
|
||||
roots.extend(children.iter().skip(usize::from(l.clips())).filter(|c| c.visible).map(|c| c.id));
|
||||
roots.extend(children.iter().skip(usize::from(l.clips())).filter(|c| c.visible && !c.is_ruler_guide()).map(|c| c.id));
|
||||
}
|
||||
let plan = plan(doc, &roots, o, true)?;
|
||||
if plan.flat.is_empty() && !plan.kept.iter().any(|id| doc.node(*id).is_some_and(|n| kept_work(n, o))) {
|
||||
@@ -640,6 +640,8 @@ pub(super) fn apply(d: &mut Document, sel: &mut vectorcraft_doc::Selection, plan
|
||||
// The groups replace their objects in one pass at the end. When an object is listed twice or
|
||||
// lies inside another listed one, each group replaces its objects as it is made instead.
|
||||
let one_pass = separate(d, &plan.flat.iter().flat_map(|f| f.roots.iter().copied()).collect::<Vec<_>>());
|
||||
// A sublayer that becomes flat art gives its ruler guides to the layer that held it.
|
||||
let guides = held_guides(d, plan.flat.iter().flat_map(|f| f.roots.iter().copied()));
|
||||
for f in plan.flat {
|
||||
// A group always has objects.
|
||||
let Some(top) = place_of(d, &f.roots) else { continue };
|
||||
@@ -703,6 +705,9 @@ pub(super) fn apply(d: &mut Document, sel: &mut vectorcraft_doc::Selection, plan
|
||||
if !flat.is_empty() {
|
||||
d.insert_beside(flat, true)?;
|
||||
}
|
||||
for (layer, guide) in guides {
|
||||
d.insert(Some(layer), usize::MAX, guide)?;
|
||||
}
|
||||
let brushes = if o.strokes_to_outlines { vectorcraft_brush::library(d) } else { vec![] };
|
||||
for mut id in plan.kept {
|
||||
if o.text_to_outlines {
|
||||
@@ -747,6 +752,23 @@ fn place_of(d: &Document, roots: &[NodeId]) -> Option<NodeId> {
|
||||
Some(place.map_or(top, |(r, _)| *r))
|
||||
}
|
||||
|
||||
/// The ruler guides of each of `roots` that is a sublayer (in its sublayers too), with the layer
|
||||
/// that holds it, in order. Other objects hold none.
|
||||
fn held_guides(d: &Document, roots: impl IntoIterator<Item = NodeId>) -> Vec<(NodeId, Node)> {
|
||||
let mut out = vec![];
|
||||
for id in roots {
|
||||
let (Some(n), Some(parent)) = (d.node(id), d.parent_of(id)) else { continue };
|
||||
if n.holds_ruler_guides() {
|
||||
n.walk(&mut |c| {
|
||||
if c.is_ruler_guide() {
|
||||
out.push((parent, c.clone()));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// An image of `r` (in a clip group when it is clipped).
|
||||
fn raster_node(d: &mut Document, r: Raster) -> Node {
|
||||
let key = unique_key(d, "flattened");
|
||||
@@ -829,3 +851,30 @@ fn drop_overprints(n: &mut Node) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
|
||||
use super::*;
|
||||
use crate::Session;
|
||||
|
||||
#[test]
|
||||
fn a_flattened_sublayer_gives_its_ruler_guides_to_its_layer() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 400, "height": 300})).unwrap();
|
||||
let layer = s.doc().unwrap().doc.layers[0].id;
|
||||
let sub = NodeId(s.execute("layer.newSublayer", &json!({"parent": layer.0})).unwrap()["id"].as_u64().unwrap());
|
||||
s.execute("layer.setCurrent", &json!({"id": sub.0})).unwrap();
|
||||
let r = s.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 20, "height": 20})).unwrap()["id"].clone();
|
||||
s.execute("transparency.set", &json!({"ids": [r], "opacity": 50})).unwrap();
|
||||
let d = Arc::make_mut(&mut s.doc_mut().unwrap().doc);
|
||||
let g = Node::new(d.alloc_id(), NodeKind::RulerGuide { vertical: true, pos: 5.0, artboard: None });
|
||||
let g = d.insert(Some(sub), usize::MAX, g).unwrap();
|
||||
let flat = flatten_document(&s.doc().unwrap().doc, &FlattenOptions::default()).unwrap().unwrap();
|
||||
assert!(flat.node(sub).is_none(), "the sublayer became flat art");
|
||||
assert!(flat.node(g).is_some_and(Node::is_ruler_guide), "its guide is kept");
|
||||
assert_eq!(flat.parent_of(g), Some(layer));
|
||||
assert_eq!(flat.children(Some(layer)).and_then(|c| c.last()).map(|c| c.id), Some(g));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,9 @@ pub(super) fn target_roots(s: &Session, p: &Value) -> Result<Vec<NodeId>> {
|
||||
let mut ids = vec![];
|
||||
for id in targets(s, p)? {
|
||||
match d.node(id) {
|
||||
Some(n) if n.is_layer() => ids.extend(n.children().into_iter().flatten().map(|c| c.id)),
|
||||
// Ruler guides are not art: they stay where they are.
|
||||
Some(n) if n.is_layer() => ids.extend(n.children().into_iter().flatten().filter(|c| !c.is_ruler_guide()).map(|c| c.id)),
|
||||
Some(n) if n.is_ruler_guide() => {}
|
||||
Some(_) => ids.push(id),
|
||||
None => {}
|
||||
}
|
||||
|
||||
@@ -243,7 +243,8 @@ pub(crate) fn accepts(d: &Document, parent: Option<NodeId>, n: &Node) -> bool {
|
||||
None => n.is_layer(),
|
||||
Some(p) => match d.node(p).map(|x| &x.kind) {
|
||||
Some(NodeKind::Layer { .. }) => true,
|
||||
Some(NodeKind::Group { .. }) => !n.is_layer(),
|
||||
// A ruler guide stays directly in a layer or sublayer.
|
||||
Some(NodeKind::Group { .. }) => !n.is_layer() && !n.is_ruler_guide(),
|
||||
_ => false,
|
||||
},
|
||||
}
|
||||
@@ -478,8 +479,12 @@ fn clip_toggle(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
super::object::release_clip(d, id);
|
||||
return Ok(false);
|
||||
}
|
||||
let top =
|
||||
n.children().and_then(|c| c.last()).map(|c| c.id).ok_or_else(|| EngineError::Other("the layer has no objects to clip by".into()))?;
|
||||
// The topmost object: a layer's ruler guides above its art do not clip.
|
||||
let top = n
|
||||
.children()
|
||||
.and_then(|c| c.iter().rev().find(|c| !c.is_ruler_guide()))
|
||||
.map(|c| c.id)
|
||||
.ok_or_else(|| EngineError::Other("the layer has no objects to clip by".into()))?;
|
||||
super::object::make_clipping_path(d, top)?;
|
||||
d.move_node(top, Some(id), 0)?;
|
||||
if let Some(n) = d.node_mut(id) {
|
||||
@@ -556,9 +561,8 @@ fn delete_rows(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
return Err(EngineError::Other("a document needs at least one layer".into()));
|
||||
}
|
||||
s.edit("Delete Selection", |d, sel| {
|
||||
d.remove_many(&ids)?;
|
||||
// The guides on the deleted layers go with them.
|
||||
d.drop_guides_of_deleted_layers(sel);
|
||||
d.remove_many(&ids)?;
|
||||
sel.prune(d);
|
||||
Ok(())
|
||||
})?;
|
||||
@@ -577,10 +581,10 @@ fn duplicate_rows(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
return Err(bad("layer.duplicate", "no such rows"));
|
||||
}
|
||||
let copies = s.edit("Duplicate Selection", |d, _| {
|
||||
super::edit::copy_above(d, &ids, |d, n, c| {
|
||||
// A copied layer's ruler guides are copied with it.
|
||||
super::edit::copy_above(d, &ids, |_, n, c| {
|
||||
if n.is_layer() {
|
||||
c.name = Some(format!("{} copy", n.display_name()));
|
||||
d.copy_layer_guides(n, c);
|
||||
}
|
||||
})
|
||||
})?;
|
||||
@@ -706,6 +710,9 @@ fn select_art(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
vec![]
|
||||
} else if n.is_layer() {
|
||||
n.layer_art(true)
|
||||
} else if n.is_ruler_guide() {
|
||||
// A ruler guide row selects the guide through `guide.select`, not as art.
|
||||
vec![]
|
||||
} else {
|
||||
vec![id]
|
||||
};
|
||||
@@ -784,11 +791,8 @@ fn move_rows(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let mut nodes = vec![];
|
||||
for id in &ids {
|
||||
let n = d.node(*id).cloned().ok_or(EngineError::NoNode(*id))?;
|
||||
let c = d.reid(&n);
|
||||
if n.is_layer() {
|
||||
d.copy_layer_guides(&n, &c);
|
||||
}
|
||||
nodes.push(Arc::new(c));
|
||||
// A copied layer's ruler guides are copied with it.
|
||||
nodes.push(Arc::new(d.reid(&n)));
|
||||
}
|
||||
nodes
|
||||
} else {
|
||||
@@ -810,6 +814,8 @@ fn move_rows(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let ch = d.children_mut(parent)?;
|
||||
let index = index.min(ch.len());
|
||||
ch.splice(index..index, nodes);
|
||||
// A layer's ruler guides stay above its art, each in the order it now has.
|
||||
ch.sort_by_key(|c| c.is_ruler_guide());
|
||||
// Copied type whose layout caches `reid` dropped has them again.
|
||||
if copy {
|
||||
super::typecmd::refresh_bounds_of(d, &out);
|
||||
@@ -972,7 +978,7 @@ fn remove_artboards(d: &mut Document, sel: &mut vectorcraft_doc::Selection, indi
|
||||
for &i in indices.iter().rev() {
|
||||
let id = d.artboards.remove(i).id;
|
||||
d.retire_artboard_id(id);
|
||||
d.retain_guides(sel, |_, g| g.artboard != Some(id));
|
||||
d.retain_guides(sel, |g| g.artboard != Some(id));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -255,9 +255,9 @@ fn merge_into(d: &mut Document, into: NodeId, others: &[NodeId]) -> Result<()> {
|
||||
let mut all = std::mem::take(ch);
|
||||
all.extend(taken);
|
||||
all.sort_by(|a, b| keys.get(&a.id).cmp(&keys.get(&b.id)));
|
||||
// Ruler guides (the merged layers' among them) stay above the art, each kind in that order.
|
||||
all.sort_by_key(|c| c.is_ruler_guide());
|
||||
*ch = all;
|
||||
// The merged layers' guides go to it too.
|
||||
d.rehome_guides(into);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -318,9 +318,8 @@ fn flatten(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let others: Vec<NodeId> = d.layers.iter().filter(|l| l.id != into && l.visible && !l.is_template()).map(|l| l.id).collect();
|
||||
let hidden: Vec<NodeId> = d.layers.iter().filter(|l| l.id != into && !l.visible && !l.is_template()).map(|l| l.id).collect();
|
||||
s.edit("Flatten Artwork", |d, sel| {
|
||||
d.remove_many(&hidden)?;
|
||||
// Hidden layers are discarded with their guides.
|
||||
d.drop_guides_of_deleted_layers(sel);
|
||||
d.remove_many(&hidden)?;
|
||||
merge_into(d, into, &others)?;
|
||||
sel.prune(d);
|
||||
sel.deselect_uneditable(d);
|
||||
@@ -349,8 +348,8 @@ fn release(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
NodeKind::Group { .. } => true,
|
||||
_ => return Err(bad(C, "release a layer or a group")),
|
||||
};
|
||||
// Its objects, bottom first (sublayers stay as they are).
|
||||
let objects: Vec<NodeId> = n.children().into_iter().flatten().filter(|c| !c.is_layer()).map(|c| c.id).collect();
|
||||
// Its objects, bottom first (sublayers and ruler guides stay as they are).
|
||||
let objects: Vec<NodeId> = n.children().into_iter().flatten().filter(|c| !c.is_layer() && !c.is_ruler_guide()).map(|c| c.id).collect();
|
||||
if objects.is_empty() {
|
||||
return Err(EngineError::Other("there are no objects to release".into()));
|
||||
}
|
||||
@@ -386,7 +385,8 @@ fn release(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
let new: Vec<NodeId> = layers.iter().map(|l| l.id).collect();
|
||||
let ch = d.children_mut(Some(parent))?;
|
||||
let at = index.min(ch.len());
|
||||
// Below the parent's ruler guides.
|
||||
let at = index.min(Node::guide_run_start(ch));
|
||||
ch.splice(at..at, layers);
|
||||
// Copied type whose layout caches `reid` dropped has them again.
|
||||
if build {
|
||||
|
||||
@@ -259,10 +259,12 @@ pub(crate) fn follow_history(st: &mut crate::DocState, was: Option<NodeId>) {
|
||||
/// mode when the object or the layer is gone (e.g. deleted).
|
||||
pub(crate) fn sync(d: &mut Document) {
|
||||
let Some(me) = d.mask_edit else { return };
|
||||
let art: Option<Vec<Arc<Node>>> = d.node(me.layer).and_then(|l| l.children().cloned());
|
||||
// The layer's art: ruler guides on it are not part of the mask.
|
||||
let art: Option<Vec<Arc<Node>>> =
|
||||
d.node(me.layer).and_then(|l| l.children()).map(|ch| ch.iter().filter(|c| !c.is_ruler_guide()).cloned().collect());
|
||||
let (Some(art), true) = (art, d.node(me.object).is_some_and(|n| n.extras().mask.is_some())) else {
|
||||
d.mask_edit = None;
|
||||
let _ = d.remove(me.layer);
|
||||
let _ = d.remove_edit_layer(me.layer);
|
||||
return;
|
||||
};
|
||||
let new_art = match art.as_slice() {
|
||||
|
||||
@@ -102,15 +102,15 @@ fn items_bounds(d: &Document, ids: &[NodeId], m: Measure) -> Vec<(NodeId, Rect)>
|
||||
ids.iter().filter_map(|id| Some((*id, m.of(d, *id)?))).collect()
|
||||
}
|
||||
|
||||
/// Move each object of `moves` by its offset and each ruler guide of `guides` (index, position)
|
||||
/// Move each object of `moves` by its offset and each ruler guide of `guides` (id, position)
|
||||
/// to its position, as undo step `label`; pattern tiles move too when [`transform_patterns`] says
|
||||
/// so for `ids`. When every move is a rounding error (1e-9 pt or less on both axes), the document
|
||||
/// and its undo history stay as they are.
|
||||
fn apply_moves(s: &mut Session, label: &str, p: &Value, ids: &[NodeId], moves: &[(NodeId, Vec2)], guides: &[(usize, f64)]) -> Result<Value> {
|
||||
fn apply_moves(s: &mut Session, label: &str, p: &Value, ids: &[NodeId], moves: &[(NodeId, Vec2)], guides: &[(NodeId, f64)]) -> Result<Value> {
|
||||
let sc = Scaling { patterns: transform_patterns(s, p, ids)?, ..Scaling::default() };
|
||||
let guides: Vec<(usize, f64)> = {
|
||||
let guides: Vec<(NodeId, f64)> = {
|
||||
let d = &s.doc()?.doc;
|
||||
guides.iter().copied().filter(|&(i, pos)| d.guides.get(i).is_some_and(|g| (g.pos - pos).abs() > 1e-9)).collect()
|
||||
guides.iter().copied().filter(|&(i, pos)| d.ruler_guide(i).is_some_and(|g| (g.pos - pos).abs() > 1e-9)).collect()
|
||||
};
|
||||
if moves.iter().all(|(_, dv)| dv.x.abs() <= 1e-9 && dv.y.abs() <= 1e-9) && guides.is_empty() {
|
||||
return ok();
|
||||
@@ -124,9 +124,7 @@ fn apply_moves(s: &mut Session, label: &str, p: &Value, ids: &[NodeId], moves: &
|
||||
super::pixelgrid::keep_compound_aligned(d, *id, sc);
|
||||
}
|
||||
for &(i, pos) in &guides {
|
||||
if let Some(g) = d.guides.get_mut(i) {
|
||||
g.pos = pos;
|
||||
}
|
||||
d.edit_ruler_guide(i, |g| g.pos = pos);
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
@@ -143,7 +141,7 @@ fn align_to<'a>(s: &Session, p: &'a Value) -> Option<&'a str> {
|
||||
/// [`vectorcraft_doc::Selection::key_guide`]): its position, and whether it is vertical.
|
||||
fn key_guide(s: &Session, p: &Value) -> Option<(f64, bool)> {
|
||||
let st = s.doc().ok()?;
|
||||
let g = st.doc.guides.get(st.selection.key_guide?)?;
|
||||
let g = st.doc.ruler_guide(st.selection.key_guide?)?;
|
||||
(align_to(s, p) == Some("key")).then_some((g.pos, g.vertical))
|
||||
}
|
||||
|
||||
@@ -164,7 +162,7 @@ fn align_reference(s: &Session, p: &Value, ids: &[NodeId], m: Measure, own: Opti
|
||||
/// Bounds of the selected ruler guides (each guide is a zero-thickness line), for Align to
|
||||
/// Selection when no objects are selected.
|
||||
fn selected_guides_bounds(st: &crate::DocState) -> Option<Rect> {
|
||||
st.selection.guides.iter().filter_map(|&i| st.doc.guides.get(i)).fold(None, |acc: Option<Rect>, g| {
|
||||
st.selection.guides.iter().filter_map(|&i| st.doc.ruler_guide(i)).fold(None, |acc: Option<Rect>, g| {
|
||||
let r = if g.vertical { Rect::new(g.pos, 0.0, g.pos, 0.0) } else { Rect::new(0.0, g.pos, 0.0, g.pos) };
|
||||
Some(acc.map(|a| a.union(r)).unwrap_or(r))
|
||||
})
|
||||
@@ -264,7 +262,7 @@ pub(super) fn align(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
/// `v` names. Align to the key object (or key guide) moves them too, and the key guide stays put.
|
||||
/// Align to the artboard moves them (#3463). Align to the selection moves them when only guides are
|
||||
/// selected; with objects in the selection they stay put.
|
||||
fn guide_moves(s: &Session, p: &Value, r: Rect, h: Option<&str>, v: Option<&str>) -> Result<Vec<(usize, f64)>> {
|
||||
fn guide_moves(s: &Session, p: &Value, r: Rect, h: Option<&str>, v: Option<&str>) -> Result<Vec<(NodeId, f64)>> {
|
||||
let st = s.doc()?;
|
||||
let to = align_to(s, p);
|
||||
let move_guides = match to {
|
||||
@@ -290,7 +288,7 @@ fn guide_moves(s: &Session, p: &Value, r: Rect, h: Option<&str>, v: Option<&str>
|
||||
.guides
|
||||
.iter()
|
||||
.filter(|&&i| !skip_key || Some(i) != st.selection.key_guide)
|
||||
.filter_map(|&i| Some((i, at(st.doc.guides.get(i)?.vertical)?)))
|
||||
.filter_map(|&i| Some((i, at(st.doc.ruler_guide(i)?.vertical)?)))
|
||||
.collect())
|
||||
}
|
||||
|
||||
@@ -361,7 +359,7 @@ pub(super) fn can_distribute(s: &Session) -> std::result::Result<(), String> {
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
enum Spaced {
|
||||
Object(NodeId),
|
||||
Guide(usize),
|
||||
Guide(NodeId),
|
||||
}
|
||||
|
||||
/// The selected objects `ids` with their bounds (measured by `m`), and the selected
|
||||
@@ -371,7 +369,7 @@ fn spaced_items(s: &Session, ids: &[NodeId], m: Measure, horiz: bool) -> Result<
|
||||
let st = s.doc()?;
|
||||
let objects = items_bounds(&st.doc, ids, m).into_iter().map(|(id, r)| (Spaced::Object(id), r));
|
||||
let guides = st.selection.guides.iter().filter_map(|&i| {
|
||||
let g = st.doc.guides.get(i).filter(|g| g.vertical == horiz)?;
|
||||
let g = st.doc.ruler_guide(i).filter(|g| g.vertical == horiz)?;
|
||||
Some((Spaced::Guide(i), Rect::new(g.pos, g.pos, g.pos, g.pos)))
|
||||
});
|
||||
Ok(objects.chain(guides).collect())
|
||||
@@ -404,7 +402,7 @@ fn apply_spaced(s: &mut Session, label: &str, p: &Value, ids: &[NodeId], horiz:
|
||||
for &(it, delta) in deltas {
|
||||
match it {
|
||||
Spaced::Object(id) => moves.push((id, along(delta))),
|
||||
Spaced::Guide(i) => guides.extend(d.guides.get(i).map(|g| (i, g.pos + delta))),
|
||||
Spaced::Guide(i) => guides.extend(d.ruler_guide(i).map(|g| (i, g.pos + delta))),
|
||||
}
|
||||
}
|
||||
apply_moves(s, label, p, ids, &moves, &guides)
|
||||
|
||||
@@ -39,7 +39,9 @@ pub(super) fn arrange(s: &mut Session, how: Arrange) -> Result<Value> {
|
||||
group.retain(|id| d.position(*id).is_some_and(|(_, idx, _)| idx >= floor));
|
||||
}
|
||||
for (k, id) in group.iter().enumerate() {
|
||||
let Some((_, idx, len)) = d.position(*id) else { continue };
|
||||
let Some((_, idx, _)) = d.position(*id) else { continue };
|
||||
// The parent's art, without the ruler guides that stay above it.
|
||||
let len = d.children(par).map_or(0, |ch| Node::guide_run_start(ch));
|
||||
if len <= floor {
|
||||
continue;
|
||||
}
|
||||
@@ -73,12 +75,16 @@ pub(super) fn send_to_current_layer(s: &mut Session, _: &Value) -> Result<Value>
|
||||
}
|
||||
// When the selected objects are already the topmost in the current layer, in paint order, the
|
||||
// document and its undo history stay as they are.
|
||||
if st.doc.children(Some(layer)).is_some_and(|c| c.iter().rev().map(|n| n.id).take(ids.len()).eq(ids.iter().rev().copied())) {
|
||||
// (The layer's ruler guides stay above its art.)
|
||||
let art = |c: &&std::sync::Arc<Node>| !c.is_ruler_guide();
|
||||
if st.doc.children(Some(layer)).is_some_and(|c| c.iter().rev().filter(art).map(|n| n.id).take(ids.len()).eq(ids.iter().rev().copied())) {
|
||||
return ok();
|
||||
}
|
||||
s.edit("Send to Current Layer", |d, _| {
|
||||
let moved = d.remove_many(&ids)?;
|
||||
d.children_mut(Some(layer))?.extend(moved);
|
||||
let ch = d.children_mut(Some(layer))?;
|
||||
let at = Node::guide_run_start(ch);
|
||||
ch.splice(at..at, moved);
|
||||
Ok(())
|
||||
})?;
|
||||
ok()
|
||||
@@ -176,7 +182,7 @@ fn selected_lock_hide_roots(s: &Session) -> Result<Vec<NodeId>> {
|
||||
fn collect_flag(d: &Document, f: impl Fn(&Node) -> bool) -> Vec<NodeId> {
|
||||
let mut v = vec![];
|
||||
d.walk(|n| {
|
||||
if !n.is_layer() && f(n) {
|
||||
if !n.is_layer() && !n.is_ruler_guide() && f(n) {
|
||||
v.push(n.id)
|
||||
}
|
||||
});
|
||||
@@ -198,7 +204,7 @@ fn collect_flag_in_scope(s: &Session, cmd: &str, p: &Value, f: impl Fn(&Node) ->
|
||||
if let Some(node) = d.node(root) {
|
||||
for child in node.children().into_iter().flatten() {
|
||||
child.walk(&mut |n| {
|
||||
if !n.is_layer() && f(n) {
|
||||
if !n.is_layer() && !n.is_ruler_guide() && f(n) {
|
||||
ids.push(n.id);
|
||||
}
|
||||
});
|
||||
@@ -222,7 +228,7 @@ fn collect_flag_in_scope(s: &Session, cmd: &str, p: &Value, f: impl Fn(&Node) ->
|
||||
}
|
||||
for c in d.node(*g).and_then(Node::children).into_iter().flatten() {
|
||||
c.walk(&mut |n| {
|
||||
if !n.is_layer() && f(n) {
|
||||
if !n.is_layer() && !n.is_ruler_guide() && f(n) {
|
||||
v.push(n.id)
|
||||
}
|
||||
});
|
||||
|
||||
@@ -145,16 +145,14 @@ fn apply_transform_moving_guides(
|
||||
}
|
||||
None if copy => {
|
||||
let guide_copies = if with_guides {
|
||||
let copies: Vec<_> = sel.guides.iter().filter_map(|&i| d.guides.get(i).cloned()).collect();
|
||||
let copies: Vec<_> = sel.guides.iter().filter_map(|&i| d.ruler_guide(i)).collect();
|
||||
(!copies.is_empty()).then_some(copies)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let new_ids = duplicate_in(d, sel, &ids, Affine::IDENTITY)?;
|
||||
if let Some(copies) = guide_copies {
|
||||
let first = d.guides.len();
|
||||
d.guides.extend(copies);
|
||||
sel.guides = (first..d.guides.len()).collect();
|
||||
sel.guides = copies.iter().map(|g| d.add_ruler_guide(g)).collect();
|
||||
}
|
||||
new_ids
|
||||
}
|
||||
@@ -177,9 +175,7 @@ fn apply_transform_moving_guides(
|
||||
}
|
||||
if with_guides && (delta.x != 0.0 || delta.y != 0.0) {
|
||||
for i in sel.guides.clone() {
|
||||
if let Some(g) = d.guides.get_mut(i) {
|
||||
*g = g.moved(delta);
|
||||
}
|
||||
d.edit_ruler_guide(i, |g| *g = g.moved(delta));
|
||||
}
|
||||
}
|
||||
Ok(targets)
|
||||
@@ -224,10 +220,8 @@ pub(crate) fn preview_copies(s: &mut Session, roots: &[NodeId], with_guides: boo
|
||||
let ids = duplicate_in(Arc::make_mut(&mut doc), &mut selection, roots, Affine::IDENTITY)?;
|
||||
if has_guide_copies {
|
||||
let d = Arc::make_mut(&mut doc);
|
||||
let first = d.guides.len();
|
||||
let copies: Vec<_> = st.selection.guides.iter().filter_map(|&i| d.guides.get(i).cloned()).collect();
|
||||
d.guides.extend(copies);
|
||||
selection.guides = (first..d.guides.len()).collect();
|
||||
let copies: Vec<_> = st.selection.guides.iter().filter_map(|&i| d.ruler_guide(i)).collect();
|
||||
selection.guides = copies.iter().map(|g| d.add_ruler_guide(g)).collect();
|
||||
}
|
||||
let k = Arc::new(crate::PreviewCopies { from: it.doc.clone(), roots: roots.to_vec(), doc, selection, ids });
|
||||
it.copies = Some(k.clone());
|
||||
|
||||
@@ -214,7 +214,7 @@ fn attributes(s: &Session, p: &Value) -> Result<AttributesInfo> {
|
||||
/// Overprint Black on `n` and its descendants; returns the objects changed.
|
||||
fn overprint_black_tree(n: &mut Node, op: &OverprintBlack, spots: &[String]) -> usize {
|
||||
let mut changed = usize::from(op.apply(n, spots));
|
||||
for c in n.children_mut().into_iter().flatten() {
|
||||
for c in n.children_mut().into_iter().flatten().filter(|c| !c.is_ruler_guide()) {
|
||||
changed += overprint_black_tree(Arc::make_mut(c), op, spots);
|
||||
}
|
||||
changed
|
||||
|
||||
@@ -242,6 +242,10 @@ fn same_in_proxy(a: &Paint, b: &Paint) -> bool {
|
||||
/// change ([`leaf_targets`]): groups and layers stand for their contents (and so do blends and
|
||||
/// other containers inside them), a compound path for its parts.
|
||||
pub(crate) fn painted<'a>(n: &'a Node, top: bool, out: &mut Vec<&'a Node>) {
|
||||
// A ruler guide has no paints.
|
||||
if n.is_ruler_guide() {
|
||||
return;
|
||||
}
|
||||
let expand = match n.kind {
|
||||
NodeKind::Group { .. } | NodeKind::Layer { .. } => true,
|
||||
NodeKind::Compound { .. } => false,
|
||||
|
||||
@@ -352,7 +352,7 @@ pub(crate) fn artboard_copy(s: &mut Session, p: &Value, cut: bool) -> Result<Val
|
||||
for &i in indices.iter().rev() {
|
||||
let gone = d.artboards.remove(i).id;
|
||||
d.retire_artboard_id(gone);
|
||||
d.retain_guides(sel, |_, g| g.artboard != Some(gone));
|
||||
d.retain_guides(sel, |g| g.artboard != Some(gone));
|
||||
}
|
||||
d.remove_many(&art)?;
|
||||
sel.prune(d);
|
||||
|
||||
@@ -289,9 +289,7 @@ pub(crate) fn transform_anchors(s: &mut Session, label: &str, xf: vectorcraft_ge
|
||||
}
|
||||
if is_translation && (delta.x != 0.0 || delta.y != 0.0) {
|
||||
for &i in &sel.guides {
|
||||
if let Some(g) = doc.guides.get_mut(i) {
|
||||
*g = g.moved(delta);
|
||||
}
|
||||
doc.edit_ruler_guide(i, |g| *g = g.moved(delta));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -1232,7 +1230,7 @@ pub(crate) fn delete_anchors(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
s.edit("Clear", |d, selection| {
|
||||
if with_guides {
|
||||
let gone = selection.guides.clone();
|
||||
d.retain_guides(selection, |i, _| !gone.contains(&i));
|
||||
d.retain_guides(selection, |g| !g.id.is_some_and(|id| gone.contains(&id)));
|
||||
}
|
||||
let targets: BTreeSet<NodeId> = whole.into_iter().filter(|id| d.node(*id).is_some_and(|n| !n.is_layer())).collect();
|
||||
let whole_ids: Vec<NodeId> = targets
|
||||
|
||||
@@ -1351,7 +1351,9 @@ fn split_into_grid(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let node = shape_node(d, c, Some(&top));
|
||||
out.push(d.insert(par, idx + k, node)?);
|
||||
}
|
||||
d.guides.extend(edges.iter().map(|&(vertical, pos)| vectorcraft_doc::Guide { layer, ..vectorcraft_doc::Guide::new(vertical, pos) }));
|
||||
for &(vertical, pos) in &edges {
|
||||
d.add_ruler_guide(&vectorcraft_doc::Guide { layer, ..vectorcraft_doc::Guide::new(vertical, pos) });
|
||||
}
|
||||
sel.set(out.iter().copied());
|
||||
Ok(out)
|
||||
})?;
|
||||
|
||||
@@ -271,8 +271,9 @@ fn enter_edit(d: &mut Document, sel: &mut Selection, name: &str, original: Optio
|
||||
/// Leave editing mode: drop the layer, return the edit record.
|
||||
fn leave_edit(d: &mut Document, sel: &mut Selection) -> Result<(PatternEdit, Vec<Arc<Node>>)> {
|
||||
let pe = d.pattern_edit.take().ok_or_else(|| EngineError::Other("not in pattern editing mode".into()))?;
|
||||
let art = match d.remove(pe.layer) {
|
||||
Ok(l) => l.children().cloned().unwrap_or_default(),
|
||||
// The tile art: ruler guides on the edit layer are not part of it and stay in the document.
|
||||
let art = match d.remove_edit_layer(pe.layer) {
|
||||
Ok(l) => l.children().into_iter().flatten().cloned().collect(),
|
||||
Err(_) => vec![],
|
||||
};
|
||||
sel.clear();
|
||||
@@ -427,7 +428,8 @@ fn pattern_save_copy(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let name = s.edit("Save a Copy", |d, _| {
|
||||
let pe = d.pattern_edit.clone().ok_or_else(|| EngineError::Other("not in pattern editing mode".into()))?;
|
||||
let mut def = d.pattern(&pe.pattern).cloned().ok_or_else(|| EngineError::Other("pattern missing".into()))?;
|
||||
let art: Vec<Arc<Node>> = d.node(pe.layer).and_then(|l| l.children().cloned()).unwrap_or_default();
|
||||
let art: Vec<Arc<Node>> =
|
||||
d.node(pe.layer).into_iter().flat_map(|l| l.children().into_iter().flatten()).filter(|c| !c.is_ruler_guide()).cloned().collect();
|
||||
let name = unique_pattern_name(d, wanted.as_deref().unwrap_or(&format!("{} copy", pe.pattern.trim())));
|
||||
def.name = name.clone();
|
||||
def.art = copies(d, &art).into_iter().map(Arc::new).collect();
|
||||
@@ -481,7 +483,8 @@ fn pattern_options(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let out_name = new_name.clone().unwrap_or(name.clone());
|
||||
s.edit("Pattern Options", |d, _| {
|
||||
// While editing, the tile art is the edit layer's.
|
||||
let live_art: Option<Vec<Arc<Node>>> = edit_layer.and_then(|l| d.node(l)).and_then(|l| l.children().cloned());
|
||||
let live_art: Option<Vec<Arc<Node>>> =
|
||||
edit_layer.and_then(|l| d.node(l)).and_then(|l| l.children()).map(|ch| ch.iter().filter(|c| !c.is_ruler_guide()).cloned().collect());
|
||||
let def = d.pattern_mut(&name).ok_or_else(|| EngineError::Other("pattern missing".into()))?;
|
||||
def.tile_type = tt;
|
||||
def.overlap = overlap;
|
||||
|
||||
@@ -377,7 +377,7 @@ fn placeable(l: &Node) -> bool {
|
||||
fn art_of<'a>(nodes: impl Iterator<Item = &'a Arc<Node>>) -> Vec<Node> {
|
||||
nodes
|
||||
.filter_map(|c| match &c.kind {
|
||||
NodeKind::Path { guide: true, .. } => None,
|
||||
_ if c.is_guide() => None,
|
||||
NodeKind::Layer { children, clip, .. } => placeable(c).then(|| {
|
||||
let mut g = (**c).clone();
|
||||
g.kind = NodeKind::Group { children: art_of(children.iter()).into_iter().map(Arc::new).collect(), clip: *clip };
|
||||
|
||||
@@ -48,12 +48,13 @@ pub(crate) fn prepare(cmd: &str, doc: &Document, set: &PrintSettings, saved: &[F
|
||||
Ok(Some(flatten_document(&printed, &o)?.unwrap_or_else(|| printed.into_owned())))
|
||||
}
|
||||
|
||||
/// Leave guide paths out of `nodes` (the renderer draws them; they never print).
|
||||
/// Leave guides out of `nodes`: guide paths (the renderer draws them; they never print) and ruler
|
||||
/// guides.
|
||||
fn drop_guides(nodes: &mut Vec<Arc<Node>>) {
|
||||
fn has_guides(n: &Node) -> bool {
|
||||
matches!(n.kind, NodeKind::Path { guide: true, .. }) || n.children().is_some_and(|c| c.iter().any(|c| has_guides(c)))
|
||||
n.is_guide() || n.children().is_some_and(|c| c.iter().any(|c| has_guides(c)))
|
||||
}
|
||||
nodes.retain(|n| !matches!(n.kind, NodeKind::Path { guide: true, .. }));
|
||||
nodes.retain(|n| !n.is_guide());
|
||||
for n in nodes.iter_mut().filter(|n| has_guides(n)) {
|
||||
if let Some(children) = Arc::make_mut(n).children_mut() {
|
||||
drop_guides(children);
|
||||
|
||||
@@ -263,7 +263,7 @@ fn retain_effects(n: &mut Node, members: bool, keep: &dyn Fn(&Effect) -> bool) {
|
||||
it.effects_mut().retain(keep);
|
||||
}
|
||||
if members {
|
||||
for c in n.children_mut().into_iter().flatten() {
|
||||
for c in n.children_mut().into_iter().flatten().filter(|c| !c.is_ruler_guide()) {
|
||||
retain_effects(Arc::make_mut(c), true, keep);
|
||||
}
|
||||
}
|
||||
@@ -367,7 +367,9 @@ pub(crate) fn replace_with_image(mut v: Node, image: Node) -> Node {
|
||||
v.appearance = Default::default();
|
||||
v.set_clips(false);
|
||||
if let Some(ch) = v.children_mut() {
|
||||
*ch = vec![Arc::new(image)];
|
||||
// Its ruler guides stay above the image.
|
||||
let guides = ch.split_off(Node::guide_run_start(ch));
|
||||
*ch = std::iter::once(Arc::new(image)).chain(guides).collect();
|
||||
}
|
||||
v
|
||||
}
|
||||
|
||||
@@ -442,15 +442,16 @@ fn key(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if id.is_some() {
|
||||
return Err(bad("select.key", "give `id` or `guide`, not both"));
|
||||
}
|
||||
if !sel.guides.contains(&g) {
|
||||
let gid = super::docmenu::guide_ids(&s.doc()?.doc).get(g).copied();
|
||||
let Some(gid) = gid.filter(|id| sel.guides.contains(id)) else {
|
||||
return Err(bad("select.key", format!("guide {g} is not selected")));
|
||||
}
|
||||
};
|
||||
if sel.is_empty() {
|
||||
return Err(bad("select.key", "select objects to align to the guide"));
|
||||
}
|
||||
s.select(|_, sel| {
|
||||
sel.key = None;
|
||||
sel.key_guide = Some(g);
|
||||
sel.key_guide = Some(gid);
|
||||
})?;
|
||||
return key_result(s);
|
||||
}
|
||||
@@ -472,9 +473,9 @@ fn key(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
/// `{key}`, with `guide` when a guide is the key.
|
||||
fn key_result(s: &Session) -> Result<Value> {
|
||||
let sel = &s.doc()?.selection;
|
||||
let mut out = json!({ "key": sel.key });
|
||||
if let Some(g) = sel.key_guide {
|
||||
let st = s.doc()?;
|
||||
let mut out = json!({ "key": st.selection.key });
|
||||
if let Some(g) = st.selection.key_guide.and_then(|id| super::docmenu::guide_indexes(&st.doc, &[id]).first().copied()) {
|
||||
out["guide"] = json!(g);
|
||||
}
|
||||
Ok(out)
|
||||
|
||||
@@ -340,8 +340,9 @@ fn release(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
fn from_guides(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
let d = &s.doc()?.doc;
|
||||
let region = d.slice_clip().ok_or_else(|| EngineError::Other("Create from Guides: the document has no artboard".into()))?;
|
||||
let guides = d.ruler_guides();
|
||||
let cuts = |vertical: bool, lo: f64, hi: f64| -> Vec<f64> {
|
||||
let mut v: Vec<f64> = d.guides.iter().filter(|g| g.vertical == vertical && g.pos > lo && g.pos < hi).map(|g| g.pos).collect();
|
||||
let mut v: Vec<f64> = guides.iter().filter(|g| g.vertical == vertical && g.pos > lo && g.pos < hi).map(|g| g.pos).collect();
|
||||
v.extend([lo, hi]);
|
||||
v.sort_by(f64::total_cmp);
|
||||
v.dedup();
|
||||
|
||||
@@ -153,7 +153,8 @@ fn descends(n: &Node, top: bool) -> bool {
|
||||
/// The objects whose appearance a style applied to `n` sets: `n`, or its painted contents.
|
||||
fn painted<'a>(n: &'a Node, top: bool, out: &mut Vec<&'a Node>) {
|
||||
match n.children() {
|
||||
Some(ch) if descends(n, top) => ch.iter().for_each(|c| painted(c, false, out)),
|
||||
// A layer's ruler guides have no appearance to set.
|
||||
Some(ch) if descends(n, top) => ch.iter().filter(|c| !c.is_ruler_guide()).for_each(|c| painted(c, false, out)),
|
||||
_ => out.push(n),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,7 +248,7 @@ pub(crate) fn leave(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
/// the instance editing was entered from. The symbol already holds the layer's art ([`sync`]).
|
||||
pub(crate) fn finish(d: &mut Document, sel: &mut Selection) {
|
||||
let Some(se) = d.symbol_edit.take() else { return };
|
||||
let _ = d.remove(se.layer);
|
||||
let _ = d.remove_edit_layer(se.layer);
|
||||
match se.instance.filter(|i| d.node(*i).is_some()) {
|
||||
Some(i) => sel.set([i]),
|
||||
None => sel.clear(),
|
||||
@@ -480,18 +480,24 @@ fn follow_objects(d: &mut Document, name: &str, old: (&[Arc<Node>], bool), new:
|
||||
/// an instance of the symbol itself.
|
||||
pub(crate) fn sync(before: &Document, d: &mut Document) -> Result<()> {
|
||||
let Some(se) = d.symbol_edit.clone() else { return Ok(()) };
|
||||
let art: Option<Vec<Arc<Node>>> = d.node(se.layer).and_then(|l| l.children().cloned());
|
||||
// The layer's art: ruler guides on it are not part of the symbol.
|
||||
let art_of = |d: &Document| -> Option<Vec<Arc<Node>>> {
|
||||
d.node(se.layer).and_then(|l| l.children()).map(|ch| ch.iter().filter(|c| !c.is_ruler_guide()).cloned().collect())
|
||||
};
|
||||
let art = art_of(d);
|
||||
let (Some(art), Some(index)) = (art, d.symbols.iter().position(|x| x.name == se.symbol)) else {
|
||||
d.symbol_edit = None;
|
||||
let _ = d.remove(se.layer);
|
||||
let _ = d.remove_edit_layer(se.layer);
|
||||
return Ok(());
|
||||
};
|
||||
// Entering, and edits that left the layer's art alone, write nothing.
|
||||
let same = |b: &[Arc<Node>]| b.len() == art.len() && b.iter().zip(&art).all(|(a, b)| Arc::ptr_eq(a, b));
|
||||
let (old, old_group) = match before.symbol_edit.as_ref().filter(|b| b.layer == se.layer) {
|
||||
None => return Ok(()),
|
||||
Some(_) if before.node(se.layer).and_then(|l| l.children()).is_some_and(|b| same(b)) => return Ok(()),
|
||||
Some(b) => (before.node(se.layer).and_then(|l| l.children().cloned()).unwrap_or_default(), b.group),
|
||||
Some(b) => match art_of(before) {
|
||||
Some(old) if same(&old) => return Ok(()),
|
||||
old => (old.unwrap_or_default(), b.group),
|
||||
},
|
||||
};
|
||||
if art.iter().any(|n| holds_symbol(d, n, &se.symbol, &mut HashSet::new())) {
|
||||
return Err(EngineError::Other("a symbol can't hold an instance of itself".into()));
|
||||
|
||||
@@ -137,7 +137,7 @@ fn paintable(doc: &Document) -> Vec<NodeId> {
|
||||
if matches!(n.kind, NodeKind::Compound { .. }) {
|
||||
compounds.push(n.id);
|
||||
out.push(n.id);
|
||||
} else if !n.is_container() {
|
||||
} else if !n.is_container() && !n.is_ruler_guide() {
|
||||
out.push(n.id);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -112,6 +112,8 @@ pub fn node_summary_opts(n: &Node, opts: SummaryOpts, symbols: &SymbolBoxes) ->
|
||||
_ => {}
|
||||
}
|
||||
if let Some(ch) = n.children() {
|
||||
// A layer's ruler guides are listed by `guide.list`, not as its objects.
|
||||
let ch: Vec<&std::sync::Arc<Node>> = ch.iter().filter(|c| !c.is_ruler_guide()).collect();
|
||||
let total = ch.len();
|
||||
let take_n = opts.child_limit.map_or(total, |l| l.min(total as u64) as usize);
|
||||
if opts.depth.is_none_or(|d| d > 0) {
|
||||
@@ -203,6 +205,10 @@ fn ancestors(d: &Document, id: vectorcraft_doc::NodeId) -> Vec<u64> {
|
||||
}
|
||||
|
||||
fn collect_kind<'a>(n: &'a Node, kind: &str, path: &mut Vec<u64>, out: &mut Vec<Hit<'a>>) {
|
||||
// Ruler guides are not objects (`guide.list` lists them).
|
||||
if n.is_ruler_guide() {
|
||||
return;
|
||||
}
|
||||
if n.kind_label().eq_ignore_ascii_case(kind) {
|
||||
out.push((n, path.clone()));
|
||||
}
|
||||
@@ -295,7 +301,7 @@ fn document_base(s: &Session) -> Value {
|
||||
// The key object Align aligns to and Distribute Spacing spaces from (`select.key`).
|
||||
"key": st.selection.key.map(|k| k.0),
|
||||
// The ruler guide that is the key instead (`select.key {guide}`): an index into `guide.list`.
|
||||
"keyGuide": st.selection.key_guide,
|
||||
"keyGuide": st.selection.key_guide.and_then(|id| crate::cmd::guide_indexes(d, &[id]).first().copied()),
|
||||
"history": st.history.undo.iter().map(|h| h.label.clone()).collect::<Vec<_>>(),
|
||||
"redo": st.history.redo.iter().rev().map(|h| h.label.clone()).collect::<Vec<_>>(),
|
||||
"objects": d.node_count(),
|
||||
@@ -432,7 +438,8 @@ pub fn find_nodes(doc: &Document, filter: &FindFilter, limit: u64) -> (Vec<Value
|
||||
ids: Vec<NodeId>,
|
||||
}
|
||||
fn walk(nodes: &[Arc<Node>], w: &mut Walk) {
|
||||
for n in nodes.iter().rev() {
|
||||
// Ruler guides are not objects (`guide.list` lists them).
|
||||
for n in nodes.iter().rev().filter(|n| !n.is_ruler_guide()) {
|
||||
if w.filter.hits(n, &w.symbols) {
|
||||
w.ids.push(n.id);
|
||||
if (w.out.len() as u64) < w.limit {
|
||||
|
||||
@@ -45,7 +45,7 @@ pub(crate) fn doc_sane(d: &Document, sel: &Selection) -> bool {
|
||||
&& d.ruler_origin.x.abs() <= MAX_COORD
|
||||
&& d.ruler_origin.y.abs() <= MAX_COORD
|
||||
&& d.artboards.iter().all(|a| rect_in_range(a.rect))
|
||||
&& d.guides.iter().all(|g| g.pos.is_finite() && g.pos.abs() <= MAX_COORD)
|
||||
&& d.ruler_guides().iter().all(|g| g.pos.is_finite() && g.pos.abs() <= MAX_COORD)
|
||||
&& objects_in_range(d, &sel.objects)
|
||||
}
|
||||
|
||||
|
||||
@@ -300,8 +300,8 @@ impl Session {
|
||||
cmd::compositefonts::filter_loaded(&mut doc);
|
||||
// Inline graphics in text take their size from their symbols' art (not saved).
|
||||
doc.resolve_inline_art();
|
||||
// Guides from files saved before guides had layers go on a layer.
|
||||
doc.adopt_guides();
|
||||
// Ruler guides a reader left in the saved list become nodes on their layers.
|
||||
doc.guides_into_nodes();
|
||||
cmd::dimension::conform(&mut doc);
|
||||
self.reset_tool_for_doc_switch();
|
||||
let mut st = DocState::new(doc, path);
|
||||
@@ -328,7 +328,7 @@ impl Session {
|
||||
doc.limit_depth();
|
||||
cmd::compositefonts::filter_loaded(&mut doc);
|
||||
doc.resolve_inline_art();
|
||||
doc.adopt_guides();
|
||||
doc.guides_into_nodes();
|
||||
cmd::dimension::conform(&mut doc);
|
||||
// In the back, type set in a font named like a composite font is measured when the document
|
||||
// comes to the front and its own composite fonts are registered, not in the front
|
||||
|
||||
@@ -99,6 +99,13 @@ impl Session {
|
||||
// the artboards the journal recorded need only a document too, whatever is selected and
|
||||
// whichever tool is chosen.
|
||||
let given = given_ids(spec.params, params);
|
||||
// The guide commands take ruler guides by index; an object command given one's id is refused.
|
||||
if let (Some(ids), Some(st)) = (&given, self.active())
|
||||
&& let Some(guide) = ids.iter().find(|i| st.doc.node(**i).is_some_and(|n| n.is_ruler_guide()))
|
||||
{
|
||||
let value = guide.0;
|
||||
return Err(cmd::bad(id, format!("id {value} is a ruler guide, not an object")));
|
||||
}
|
||||
let enabled = if (id == "edit.clear" && params.get("ids").is_some())
|
||||
|| (matches!(id, "edit.copy" | "edit.cut") && params.get("selectedArtboards").is_some())
|
||||
{
|
||||
|
||||
@@ -215,6 +215,18 @@ fn rect(s: &mut Session, x: f64, y: f64, w: f64, h: f64) -> NodeId {
|
||||
NodeId(r["id"].as_u64().unwrap())
|
||||
}
|
||||
|
||||
/// The active document's selected ruler guides, by their `guide.list` indexes.
|
||||
pub(crate) fn selected_guides(s: &Session) -> Vec<usize> {
|
||||
let st = s.doc().unwrap();
|
||||
crate::cmd::guide_indexes(&st.doc, &st.selection.guides)
|
||||
}
|
||||
|
||||
/// The active document's key guide, by its `guide.list` index.
|
||||
pub(crate) fn key_guide(s: &Session) -> Option<usize> {
|
||||
let st = s.doc().unwrap();
|
||||
st.selection.key_guide.and_then(|k| crate::cmd::guide_indexes(&st.doc, &[k]).first().copied())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_and_undo_redo() {
|
||||
let mut s = session();
|
||||
|
||||
@@ -78,7 +78,7 @@ fn constrain_angle_turns_the_arrow_keys() {
|
||||
let g = s.execute("guide.add", &json!({"vertical": true, "pos": 50})).unwrap()["index"].as_u64().unwrap();
|
||||
s.execute("guide.select", &json!({"indexes": [g]})).unwrap();
|
||||
s.execute("object.nudge", &json!({"dx": 1, "dy": 0})).unwrap();
|
||||
let pos = s.doc().unwrap().doc.guides[g as usize].pos;
|
||||
let pos = s.doc().unwrap().doc.ruler_guides()[g as usize].pos;
|
||||
assert!(close(pos, 60.0), "a vertical guide moves 10 right: {pos}");
|
||||
}
|
||||
|
||||
@@ -167,7 +167,7 @@ fn constrain_angle_keeps_guides_selected_with_art_on_the_page_axes() {
|
||||
let h = s.execute("guide.add", &json!({"vertical": false, "pos": 50})).unwrap()["index"].as_u64().unwrap() as usize;
|
||||
s.execute("select.set", &json!({"ids": [id.0]})).unwrap();
|
||||
s.execute("guide.select", &json!({"indexes": [v, h], "toggle": true})).unwrap();
|
||||
let pos = |s: &Session| s.doc().unwrap().doc.guides.iter().map(|g| g.pos).collect::<Vec<_>>();
|
||||
let pos = |s: &Session| s.doc().unwrap().doc.ruler_guides().iter().map(|g| g.pos).collect::<Vec<_>>();
|
||||
let at = centre(&s, id);
|
||||
s.execute("object.nudge", &json!({"dx": 1, "dy": 0})).unwrap();
|
||||
assert_eq!(centre(&s, id) - at, Vec2::new(0.0, -10.0), "the art moves along the turned axis");
|
||||
|
||||
@@ -12,7 +12,7 @@ fn run(s: &mut Session, id: &str, p: Value) -> Value {
|
||||
|
||||
/// The active document's guides: (vertical, pos, layer).
|
||||
fn guides(s: &Session) -> Vec<(bool, f64, Option<NodeId>)> {
|
||||
s.doc().unwrap().doc.guides.iter().map(|g| (g.vertical, g.pos, g.layer)).collect()
|
||||
s.doc().unwrap().doc.ruler_guides().iter().map(|g| (g.vertical, g.pos, g.layer)).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -31,7 +31,7 @@ fn ruler_guides_copy_and_paste_into_another_document() {
|
||||
let r = run(&mut s, "edit.paste", json!({}));
|
||||
assert_eq!(r["guides"], 2);
|
||||
assert_eq!(guides(&s), [(true, 120.0, Some(layer)), (false, 80.0, Some(layer))], "where they were, not offset");
|
||||
assert_eq!(s.doc().unwrap().selection.guides, [0, 1]);
|
||||
assert_eq!(super::selected_guides(&s), [0, 1]);
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), undo + 1);
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
assert!(guides(&s).is_empty());
|
||||
@@ -104,9 +104,9 @@ fn two_pastes_add_their_own_guides_on_the_current_layer_and_svg_has_none() {
|
||||
assert!(s.clipboard.flavour_types(true, true).is_empty(), "nor any other format (the app empties the system clipboard)");
|
||||
let layer = NodeId(run(&mut s, "layer.new", json!({}))["id"].as_u64().unwrap());
|
||||
run(&mut s, "edit.paste", json!({}));
|
||||
assert_eq!(s.doc().unwrap().selection.guides, [1], "the first paste selects its own guide");
|
||||
assert_eq!(super::selected_guides(&s), [1], "the first paste selects its own guide");
|
||||
run(&mut s, "edit.paste", json!({}));
|
||||
assert_eq!(s.doc().unwrap().selection.guides, [2], "the second paste selects its own guide");
|
||||
assert_eq!(super::selected_guides(&s), [2], "the second paste selects its own guide");
|
||||
let g = guides(&s);
|
||||
assert_eq!(g.len(), 3, "two pastes, two more guides: {g:?}");
|
||||
assert_eq!((g[1].2, g[2].2), (Some(layer), Some(layer)), "on the layer new art goes to");
|
||||
@@ -116,7 +116,7 @@ fn two_pastes_add_their_own_guides_on_the_current_layer_and_svg_has_none() {
|
||||
// Hiding that layer hides the guides pasted on it: they follow its visibility (and lock).
|
||||
run(&mut s, "layer.setProps", json!({"id": layer.0, "visible": false}));
|
||||
let st = s.doc().unwrap();
|
||||
let shown: Vec<bool> = st.doc.guides.iter().map(|g| st.doc.guide_shown(g)).collect();
|
||||
let shown: Vec<bool> = st.doc.ruler_guides().iter().map(|g| st.doc.guide_shown(g)).collect();
|
||||
assert_eq!(shown, [true, false, false]);
|
||||
// With an object, the SVG holds the object and no guide.
|
||||
run(&mut s, "layer.setProps", json!({"id": layer.0, "visible": true}));
|
||||
|
||||
@@ -37,14 +37,14 @@ fn a_clicked_guide_is_the_key_and_align_lines_the_objects_up_on_it() {
|
||||
// Click an object, Shift-click the guide, then click the guide again.
|
||||
click(&mut s, 110.0, 110.0, false);
|
||||
click(&mut s, 500.0, 400.0, true);
|
||||
assert_eq!((s.doc().unwrap().selection.objects.clone(), s.doc().unwrap().selection.guides.clone()), (vec![a], vec![0]));
|
||||
assert_eq!((s.doc().unwrap().selection.objects.clone(), super::selected_guides(&s)), (vec![a], vec![0]));
|
||||
click(&mut s, 500.0, 400.0, false);
|
||||
assert_eq!(s.doc().unwrap().selection.key_guide, Some(0), "the guide is the key");
|
||||
assert_eq!(super::key_guide(&s), Some(0), "the guide is the key");
|
||||
assert_eq!(s.execute("document.inspect", &json!({})).unwrap()["keyGuide"], 0);
|
||||
// Shift-click a third object: the guide stays the key.
|
||||
click(&mut s, 310.0, 260.0, true);
|
||||
assert_eq!(s.doc().unwrap().selection.objects, vec![a, b]);
|
||||
assert_eq!(s.doc().unwrap().selection.key_guide, Some(0));
|
||||
assert_eq!(super::key_guide(&s), Some(0));
|
||||
let (ya, yb) = (bounds(&s, a).y0, bounds(&s, b).y0);
|
||||
s.execute("object.align", &json!({"horizontal": "center", "vertical": "center"})).unwrap();
|
||||
// Both centred on the guide's x; a vertical guide fixes no y, so they keep theirs.
|
||||
@@ -52,12 +52,12 @@ fn a_clicked_guide_is_the_key_and_align_lines_the_objects_up_on_it() {
|
||||
let r = bounds(&s, id);
|
||||
assert!((r.center().x - 500.0).abs() < 1e-6 && (r.y0 - y).abs() < 1e-6, "{id:?}: {r:?}");
|
||||
}
|
||||
assert_eq!(s.doc().unwrap().doc.guides[0].pos, 500.0, "the guide stays put");
|
||||
assert_eq!(positions(&s)[0], 500.0, "the guide stays put");
|
||||
s.execute("object.align", &json!({"horizontal": "left"})).unwrap();
|
||||
assert!((bounds(&s, a).x0 - 500.0).abs() < 1e-6 && (bounds(&s, b).x0 - 500.0).abs() < 1e-6);
|
||||
// A click on the key guide again: no key.
|
||||
click(&mut s, 500.0, 400.0, false);
|
||||
assert_eq!(s.doc().unwrap().selection.key_guide, None);
|
||||
assert_eq!(super::key_guide(&s), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -96,22 +96,22 @@ fn select_key_takes_only_a_selected_guide_with_objects() {
|
||||
s.execute("select.key", &json!({"guide": 1})).unwrap();
|
||||
// An object key replaces the guide key, and the other way round.
|
||||
s.execute("select.key", &json!({"id": b.0})).unwrap();
|
||||
assert_eq!((s.doc().unwrap().selection.key, s.doc().unwrap().selection.key_guide), (Some(b), None));
|
||||
assert_eq!((s.doc().unwrap().selection.key, super::key_guide(&s)), (Some(b), None));
|
||||
s.execute("select.key", &json!({"guide": 1})).unwrap();
|
||||
assert_eq!((s.doc().unwrap().selection.key, s.doc().unwrap().selection.key_guide), (None, Some(1)));
|
||||
assert_eq!((s.doc().unwrap().selection.key, super::key_guide(&s)), (None, Some(1)));
|
||||
// Deleting a guide before it keeps the key on the same guide.
|
||||
s.execute("guide.remove", &json!({"index": 0})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().selection.key_guide, Some(0));
|
||||
assert_eq!(s.doc().unwrap().doc.guides[0].pos, 600.0);
|
||||
assert_eq!(super::key_guide(&s), Some(0));
|
||||
assert_eq!(positions(&s)[0], 600.0);
|
||||
// Taking the guide out of the selection, or every object, lets the key go.
|
||||
s.execute("guide.select", &json!({"indexes": [0], "toggle": true})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().selection.key_guide, None);
|
||||
assert_eq!(super::key_guide(&s), None);
|
||||
s.execute("guide.select", &json!({"indexes": [0], "toggle": true})).unwrap();
|
||||
s.execute("select.key", &json!({"guide": 0})).unwrap();
|
||||
s.execute("select.toggle", &json!({"id": a.0})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().selection.key_guide, Some(0), "one object is enough");
|
||||
assert_eq!(super::key_guide(&s), Some(0), "one object is enough");
|
||||
s.execute("select.toggle", &json!({"id": b.0})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().selection.key_guide, None, "no object to align");
|
||||
assert_eq!(super::key_guide(&s), None, "no object to align");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -141,12 +141,12 @@ fn selected_guides_align_to_a_clicked_key_object() {
|
||||
click(&mut s, 110.0, 110.0, false);
|
||||
click(&mut s, 500.0, 300.0, true);
|
||||
click(&mut s, 300.0, 400.0, true);
|
||||
assert_eq!(s.doc().unwrap().selection.guides, vec![0, 1]);
|
||||
assert_eq!(super::selected_guides(&s), vec![0, 1]);
|
||||
click(&mut s, 110.0, 110.0, false);
|
||||
assert_eq!(s.doc().unwrap().selection.key, Some(a), "the object is the key");
|
||||
let before = bounds(&s, a);
|
||||
s.execute("object.align", &json!({"horizontal": "center", "vertical": "center"})).unwrap();
|
||||
let guides = |s: &Session| s.doc().unwrap().doc.guides.iter().map(|g| g.pos).collect::<Vec<_>>();
|
||||
let guides = positions;
|
||||
assert_eq!(guides(&s), [120.0, 110.0], "the vertical guide on the centre's x, the horizontal one on its y");
|
||||
assert_eq!(bounds(&s, a), before, "the key stays put");
|
||||
s.execute("object.align", &json!({"horizontal": "right"})).unwrap();
|
||||
@@ -174,15 +174,15 @@ fn guides_align_to_a_key_object_among_others_and_parallel_ones_to_a_key_guide()
|
||||
s.execute("select.key", &json!({"id": b.0})).unwrap();
|
||||
s.execute("object.align", &json!({"horizontal": "left"})).unwrap();
|
||||
assert_eq!(bounds(&s, a).x0, 300.0);
|
||||
assert!(s.doc().unwrap().doc.guides.iter().all(|g| g.pos == 300.0));
|
||||
assert!(positions(&s).iter().all(|p| *p == 300.0));
|
||||
// Aligning to the selection leaves guides alone.
|
||||
s.execute("object.align", &json!({"horizontal": "right", "to": "selection"})).unwrap();
|
||||
assert!(s.doc().unwrap().doc.guides.iter().all(|g| g.pos == 300.0));
|
||||
assert!(positions(&s).iter().all(|p| *p == 300.0));
|
||||
// A key guide stays put, and the other vertical guide lines up on it.
|
||||
s.execute("guide.move", &json!({"index": 1, "pos": 700})).unwrap();
|
||||
s.execute("select.key", &json!({"guide": 1})).unwrap();
|
||||
s.execute("object.align", &json!({"horizontal": "center"})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc.guides.iter().map(|g| g.pos).collect::<Vec<_>>(), [700.0, 700.0]);
|
||||
assert_eq!(positions(&s), [700.0, 700.0]);
|
||||
}
|
||||
|
||||
/// Objects 40 pt wide at `xs` (y 100) and a vertical guide at `pos` that is their key.
|
||||
@@ -208,7 +208,7 @@ fn distribute_with_a_spacing_spaces_the_objects_from_a_key_guide() {
|
||||
for (h, want) in [("left", [330.0, 340.0, 360.0]), ("center", [310.0, 320.0, 340.0]), ("right", [290.0, 300.0, 320.0])] {
|
||||
s.execute("object.distribute", &json!({"horizontal": h, "spacing": 10})).unwrap();
|
||||
assert_eq!(lefts(&s, &ids), want, "{h}");
|
||||
assert_eq!(s.doc().unwrap().doc.guides[0].pos, 350.0, "{h}: the guide stays put");
|
||||
assert_eq!(positions(&s)[0], 350.0, "{h}: the guide stays put");
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(lefts(&s, &ids), [100.0, 300.0, 500.0], "{h}: one undo step");
|
||||
}
|
||||
@@ -300,14 +300,14 @@ fn guides_alone(vertical: &[f64], horizontal: &[f64]) -> Session {
|
||||
/// Selects the objects `ids` and the ruler guides `guides`, and nothing else.
|
||||
fn select_with_guides(s: &mut Session, ids: &[NodeId], guides: &[usize]) {
|
||||
s.execute("select.set", &json!({"ids": ids.iter().map(|id| id.0).collect::<Vec<_>>()})).unwrap();
|
||||
let now = s.doc().unwrap().selection.guides.clone();
|
||||
let flip: Vec<usize> = (0..s.doc().unwrap().doc.guides.len()).filter(|i| now.contains(i) != guides.contains(i)).collect();
|
||||
let now = super::selected_guides(s);
|
||||
let flip: Vec<usize> = (0..positions(s).len()).filter(|i| now.contains(i) != guides.contains(i)).collect();
|
||||
s.execute("guide.select", &json!({"indexes": flip, "toggle": true})).unwrap();
|
||||
assert_eq!((s.doc().unwrap().selection.objects.len(), s.doc().unwrap().selection.guides.len()), (ids.len(), guides.len()));
|
||||
}
|
||||
|
||||
fn positions(s: &Session) -> Vec<f64> {
|
||||
s.doc().unwrap().doc.guides.iter().map(|g| g.pos).collect()
|
||||
s.doc().unwrap().doc.ruler_guides().iter().map(|g| g.pos).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -3686,7 +3686,7 @@ fn make_release_clear_guides() {
|
||||
s.execute("guide.add", &json!({"vertical": true, "pos": 100})).unwrap();
|
||||
let n = undo_len(&s);
|
||||
assert_eq!(s.execute("view.guides.clear", &json!({})).unwrap()["count"], 2);
|
||||
assert!(s.doc().unwrap().doc.guides.is_empty());
|
||||
assert!(s.doc().unwrap().doc.ruler_guides().is_empty());
|
||||
assert!(s.doc().unwrap().doc.node(a).is_none());
|
||||
assert_eq!(undo_len(&s), n + 1);
|
||||
}
|
||||
@@ -3751,13 +3751,13 @@ fn ruler_guides_and_lock() {
|
||||
let mut s = session();
|
||||
assert_eq!(s.execute("guide.add", &json!({"vertical": false, "pos": 50})).unwrap()["index"], 0);
|
||||
s.execute("guide.move", &json!({"index": 0, "pos": 70})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc.guides[0].pos, 70.0);
|
||||
assert_eq!(s.doc().unwrap().doc.ruler_guides()[0].pos, 70.0);
|
||||
assert_eq!(s.execute("view.guides.lock", &json!({})).unwrap()["locked"], true);
|
||||
assert!(s.guides_locked());
|
||||
assert!(matches!(s.execute("guide.remove", &json!({"index": 0})), Err(EngineError::Disabled(..))));
|
||||
s.execute("view.guides.lock", &json!({})).unwrap();
|
||||
s.execute("guide.remove", &json!({"index": 0})).unwrap();
|
||||
assert!(s.doc().unwrap().doc.guides.is_empty());
|
||||
assert!(s.doc().unwrap().doc.ruler_guides().is_empty());
|
||||
assert!(s.execute("guide.remove", &json!({"index": 0})).is_err());
|
||||
}
|
||||
|
||||
@@ -3804,7 +3804,7 @@ fn invalid_guide_lock_parameters_keep_guides_selected() {
|
||||
for params in [json!({"locked": "false"}), json!({"locked": 1})] {
|
||||
assert!(s.execute("view.guides.lock", ¶ms).is_err(), "accepted {params}");
|
||||
assert!(!s.guides_locked(), "locked guides for {params}");
|
||||
assert_eq!(s.doc().unwrap().selection.guides, vec![0], "cleared selection for {params}");
|
||||
assert_eq!(selected_guides(&s), vec![0], "cleared selection for {params}");
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), before_undo, "recorded undo for {params}");
|
||||
}
|
||||
// Left out, or null as for every command, it toggles.
|
||||
@@ -3987,11 +3987,11 @@ fn a_failed_preview_leaves_locked_guides_deselected() {
|
||||
}
|
||||
|
||||
fn guides(s: &Session) -> Vec<(bool, f64)> {
|
||||
s.doc().unwrap().doc.guides.iter().map(|g| (g.vertical, g.pos)).collect()
|
||||
s.doc().unwrap().doc.ruler_guides().iter().map(|g| (g.vertical, g.pos)).collect()
|
||||
}
|
||||
|
||||
fn selected_guides(s: &Session) -> Vec<usize> {
|
||||
s.doc().unwrap().selection.guides.clone()
|
||||
super::selected_guides(s)
|
||||
}
|
||||
|
||||
/// #414: ruler guides are selected, moved, copied, nudged and deleted like art, over commands.
|
||||
@@ -4219,10 +4219,12 @@ fn guides_and_art_later_cases(s: &mut Session, a: NodeId) {
|
||||
assert_eq!(guides(s).len(), 4);
|
||||
assert_eq!(guides(s)[2], (true, 210.0));
|
||||
assert_eq!(guides(s)[3], (false, 320.0));
|
||||
assert_eq!(s.doc().unwrap().doc.layers[0].children().unwrap().len(), 2);
|
||||
// The layer's art (its ruler guides are listed after it).
|
||||
let art = |s: &Session| s.doc().unwrap().doc.layers[0].children().unwrap().iter().filter(|c| !c.is_ruler_guide()).count();
|
||||
assert_eq!(art(s), 2);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(guides(s).len(), 2);
|
||||
assert_eq!(s.doc().unwrap().doc.layers[0].children().unwrap().len(), 1);
|
||||
assert_eq!(art(s), 1);
|
||||
}
|
||||
|
||||
/// #451: an artboard guide (`guide.add {artboard}`) runs across its artboard only, and is moved,
|
||||
@@ -4244,7 +4246,7 @@ fn artboard_guides_go_with_their_artboard() {
|
||||
);
|
||||
let spans = |s: &Session| {
|
||||
let d = &s.doc().unwrap().doc;
|
||||
d.guides.iter().map(|g| d.guide_span(g)).collect::<Vec<_>>()
|
||||
d.ruler_guides().iter().map(|g| d.guide_span(g)).collect::<Vec<_>>()
|
||||
};
|
||||
assert_eq!(spans(&s), [Some((0.0, 300.0)), None]);
|
||||
for bad in [json!(5), json!("x"), json!(-1)] {
|
||||
@@ -4270,7 +4272,7 @@ fn artboard_guides_go_with_their_artboard() {
|
||||
// Rearranged, they follow their artboards.
|
||||
s.execute("artboard.rearrange", &json!({"columns": 4})).unwrap();
|
||||
let d = &s.doc().unwrap().doc;
|
||||
for (g, ab) in d.guides.iter().filter_map(|g| Some((g, d.artboards.iter().find(|a| Some(a.id) == g.artboard)?))) {
|
||||
for (g, ab) in d.ruler_guides().iter().filter_map(|g| Some((g, d.artboards.iter().find(|a| Some(a.id) == g.artboard)?))) {
|
||||
assert_eq!(g.pos - ab.rect.x0, 160.5, "{g:?} on {ab:?}");
|
||||
}
|
||||
// Deleted, the artboard takes its guides; the selected ones left stay selected.
|
||||
@@ -4405,13 +4407,16 @@ fn cmd_ruler_drags_make_guides_of_the_active_artboard_when_the_preference_is_on(
|
||||
#[test]
|
||||
fn guides_from_older_files_are_put_on_a_layer() {
|
||||
let mut s = Session::new();
|
||||
let mut d = vectorcraft_doc::Document::new(400.0, 300.0);
|
||||
d.guides.push(vectorcraft_doc::Guide::new(true, 50.0));
|
||||
d.guides.push(vectorcraft_doc::Guide::new(false, 80.0));
|
||||
// A document read with its guides on no layer, as such a file holds them.
|
||||
let mut v = serde_json::to_value(vectorcraft_doc::Document::new(400.0, 300.0)).unwrap();
|
||||
v["guides"] = json!([{"vertical": true, "pos": 50.0}, {"vertical": false, "pos": 80.0}]);
|
||||
let d: vectorcraft_doc::Document = serde_json::from_value(v).unwrap();
|
||||
s.add_document(d, None);
|
||||
let st = s.doc().unwrap();
|
||||
let top = st.doc.default_layer();
|
||||
assert!(top.is_some() && st.doc.guides.iter().all(|g| g.layer == top), "{:?}", st.doc.guides);
|
||||
let guides = st.doc.ruler_guides();
|
||||
assert_eq!(guides.len(), 2);
|
||||
assert!(top.is_some() && guides.iter().all(|g| g.layer == top), "{guides:?}");
|
||||
}
|
||||
|
||||
/// Ruler guides are layer objects: made on the current layer, hidden, locked and deleted with it,
|
||||
@@ -4421,10 +4426,10 @@ fn ruler_guides_live_on_layers() {
|
||||
let mut s = session();
|
||||
let first = s.doc().unwrap().current_layer().unwrap();
|
||||
s.execute("guide.add", &json!({"vertical": true, "pos": 200})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc.guides[0].layer, Some(first), "on the current layer");
|
||||
assert_eq!(s.doc().unwrap().doc.ruler_guides()[0].layer, Some(first), "on the current layer");
|
||||
let second = id_of(&s.execute("layer.new", &json!({"name": "Guides"})).unwrap());
|
||||
s.execute("guide.add", &json!({"vertical": false, "pos": 300})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc.guides[1].layer, Some(second), "the new layer is current");
|
||||
assert_eq!(s.doc().unwrap().doc.ruler_guides()[1].layer, Some(second), "the new layer is current");
|
||||
assert!(s.execute("guide.add", &json!({"vertical": true, "pos": 1, "layer": 99999})).is_err());
|
||||
// Hidden or locked with its layer.
|
||||
s.execute("layer.setProps", &json!({"ids": [second.0], "visible": false})).unwrap();
|
||||
@@ -4435,12 +4440,12 @@ fn ruler_guides_live_on_layers() {
|
||||
s.execute("layer.setProps", &json!({"ids": [second.0], "locked": false})).unwrap();
|
||||
// Moved to the first layer, then back.
|
||||
s.execute("guide.setLayer", &json!({"index": 1, "layer": first.0})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc.guides[1].layer, Some(first));
|
||||
assert_eq!(s.doc().unwrap().doc.ruler_guides()[1].layer, Some(first));
|
||||
s.execute("guide.setLayer", &json!({"index": 1, "layer": second.0})).unwrap();
|
||||
assert!(s.execute("guide.setLayer", &json!({"index": 1, "layer": 99999})).is_err());
|
||||
// Several at once, as dragging their rows onto a layer's row does.
|
||||
s.execute("guide.setLayer", &json!({"indexes": [0, 1], "layer": first.0})).unwrap();
|
||||
assert!(s.doc().unwrap().doc.guides.iter().all(|g| g.layer == Some(first)));
|
||||
assert!(s.doc().unwrap().doc.ruler_guides().iter().all(|g| g.layer == Some(first)));
|
||||
s.execute("guide.setLayer", &json!({"indexes": [1], "layer": second.0})).unwrap();
|
||||
assert!(s.execute("guide.setLayer", &json!({"indexes": [7], "layer": first.0})).is_err());
|
||||
// Deleting a layer deletes its guides; undo brings them back.
|
||||
@@ -4476,7 +4481,7 @@ fn guides_on_hidden_or_locked_layers_are_left_alone() {
|
||||
s.execute("layer.setProps", &json!({"ids": [first.0], "locked": false})).unwrap();
|
||||
// The current layer locked: the new guide goes on the top shown, unlocked layer.
|
||||
s.execute("guide.add", &json!({"vertical": true, "pos": 20})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc.guides[2].layer, Some(first));
|
||||
assert_eq!(s.doc().unwrap().doc.ruler_guides()[2].layer, Some(first));
|
||||
assert_eq!(guides(&s).len(), 3);
|
||||
}
|
||||
|
||||
@@ -4489,7 +4494,7 @@ fn guides_go_with_their_layers_when_duplicated_merged_and_flattened() {
|
||||
s.execute("guide.add", &json!({"vertical": true, "pos": 200})).unwrap();
|
||||
let second = id_of(&s.execute("layer.new", &json!({})).unwrap());
|
||||
s.execute("guide.add", &json!({"vertical": false, "pos": 300})).unwrap();
|
||||
let on = |s: &Session, l: NodeId| s.doc().unwrap().doc.guides_on(l).map(|(_, g)| (g.vertical, g.pos)).collect::<Vec<_>>();
|
||||
let on = |s: &Session, l: NodeId| s.doc().unwrap().doc.guides_on(l).iter().map(|g| (g.vertical, g.pos)).collect::<Vec<_>>();
|
||||
// Duplicate Layer copies its guides onto the copy.
|
||||
let copy = s.execute("layer.duplicate", &json!({"ids": [second.0]})).unwrap()["ids"][0].as_u64().map(NodeId).unwrap();
|
||||
assert_eq!((on(&s, second), on(&s, copy)), (vec![(false, 300.0)], vec![(false, 300.0)]));
|
||||
|
||||
@@ -798,7 +798,7 @@ fn copied_artboards_take_a_free_id_and_keep_their_own_guides() {
|
||||
let mut kept = vec![];
|
||||
for i in [1, 0] {
|
||||
s.execute("artboard.delete", &json!({"index": i})).unwrap();
|
||||
kept.push(s.doc().unwrap().doc.guides.iter().map(|g| g.artboard).collect::<Vec<_>>());
|
||||
kept.push(s.doc().unwrap().doc.ruler_guides().iter().map(|g| g.artboard).collect::<Vec<_>>());
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
}
|
||||
got.push((cmd, s.doc().unwrap().doc.artboards.iter().map(|a| a.id).collect::<Vec<_>>(), kept));
|
||||
@@ -1675,12 +1675,12 @@ fn delete_empty_artboards_deletes_those_with_no_art_in_one_step() {
|
||||
assert_eq!(s.execute("artboard.deleteEmpty", &json!({})).unwrap(), json!({"deleted": [0, 2]}));
|
||||
let st = s.doc().unwrap();
|
||||
assert_eq!(artboard_names(&s), ["Artboard 2"]);
|
||||
assert_eq!(st.doc.guides.len(), 1, "the deleted artboards' guides go with them");
|
||||
assert_eq!(st.doc.ruler_guides().len(), 1, "the deleted artboards' guides go with them");
|
||||
assert_eq!(st.doc.node(art).map(|n| n.geometric_bounds()), before.node(art).map(|n| n.geometric_bounds()), "the art stays");
|
||||
assert_eq!(st.history.undo.len(), steps + 1);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(artboard_names(&s), ["Artboard 1", "Artboard 2", "Artboard 3"]);
|
||||
assert_eq!(s.doc().unwrap().doc.guides.len(), 3);
|
||||
assert_eq!(s.doc().unwrap().doc.ruler_guides().len(), 3);
|
||||
}
|
||||
|
||||
/// What counts as art: hidden and locked objects, an object straddling two artboards and one
|
||||
|
||||
@@ -656,13 +656,13 @@ fn simplify_refreshes_changed_path_selection_and_undo() {
|
||||
let kept = NodeId(s.execute("path.create", &json!({"anchors": [{"x": 350, "y": 180}]})).unwrap()["id"].as_u64().unwrap());
|
||||
let unrelated = rect(&mut s, 400.0, 200.0, 40.0, 30.0);
|
||||
s.edit("selection fixture", |d, sel| {
|
||||
d.guides.push(vectorcraft_doc::Guide::new(true, 30.0));
|
||||
let guide = d.add_ruler_guide(&vectorcraft_doc::Guide::new(true, 30.0));
|
||||
sel.set([kept, id]);
|
||||
sel.anchors.insert(id, [(0, 19)].into_iter().collect());
|
||||
sel.anchors.insert(kept, [(0, 0)].into_iter().collect());
|
||||
sel.key = Some(kept);
|
||||
sel.target = Some(id);
|
||||
sel.guides.push(0);
|
||||
sel.guides.push(guide);
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
@@ -781,7 +781,7 @@ fn split_into_grid_options() {
|
||||
rect(&mut s, 10.0, 20.0, 110.0, 50.0);
|
||||
let r = s.execute("object.path.splitIntoGrid", &p).unwrap();
|
||||
let cells: Vec<Rect> = ids(&r).iter().map(|id| bounds(&s, *id)).collect();
|
||||
let guides: Vec<(bool, f64)> = s.doc().unwrap().doc.guides.iter().map(|g| (g.vertical, g.pos)).collect();
|
||||
let guides: Vec<(bool, f64)> = s.doc().unwrap().doc.ruler_guides().iter().map(|g| (g.vertical, g.pos)).collect();
|
||||
assert_eq!(r["guides"], guides.len());
|
||||
(cells, guides)
|
||||
};
|
||||
@@ -812,13 +812,16 @@ fn split_into_grid_options() {
|
||||
assert!(close(bounds(&s, r[0]), Rect::new(0.0, 0.0, 40.0, 60.0), 1e-9) && close(bounds(&s, r[1]), Rect::new(60.0, 0.0, 100.0, 60.0), 1e-9));
|
||||
let st = s.doc().unwrap();
|
||||
assert!(r.iter().all(|id| node(&s, *id).appearance == node(&s, r[0]).appearance) && st.doc.node(back).is_none() && st.doc.node(front).is_none());
|
||||
assert_eq!(st.doc.layers[0].children().unwrap().iter().map(|n| n.id).collect::<Vec<_>>(), [kept, r[0], r[1]], "above the path left alone");
|
||||
let art: Vec<NodeId> = st.doc.layers[0].children().unwrap().iter().filter(|n| !n.is_ruler_guide()).map(|n| n.id).collect();
|
||||
assert_eq!(art, [kept, r[0], r[1]], "above the path left alone");
|
||||
let children = st.doc.layers[0].children().unwrap();
|
||||
assert!(children.len() == 9 && children[3..].iter().all(|n| n.is_ruler_guide()), "the six guides are above the art");
|
||||
assert_eq!(st.selection.objects, r);
|
||||
assert!(st.doc.guides.iter().all(|g| g.layer == Some(st.doc.layers[0].id)));
|
||||
assert!(st.doc.ruler_guides().iter().all(|g| g.layer == Some(st.doc.layers[0].id)));
|
||||
assert_eq!(st.history.undo.len(), undo + 1);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
let st = s.doc().unwrap();
|
||||
assert!(st.doc.node(back).is_some() && st.doc.node(front).is_some() && st.doc.guides.is_empty());
|
||||
assert!(st.doc.node(back).is_some() && st.doc.node(front).is_some() && st.doc.ruler_guides().is_empty());
|
||||
// A grid with no room for its gutters is refused and changes nothing.
|
||||
assert!(s.execute("object.path.splitIntoGrid", &json!({"columns": 4, "gutter": 50})).is_err());
|
||||
assert!(s.execute("object.path.splitIntoGrid", &json!({"width": -5})).is_err());
|
||||
|
||||
@@ -1990,9 +1990,12 @@ fn real_time_drawing_off_still_shows_a_dragged_guide() {
|
||||
set_pref(&mut s, "realTimeDrawing", json!(false));
|
||||
s.pointer(&PointerEvent::new(PointerKind::Down, 100.0, 200.0), v).unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Drag, 150.0, 200.0), v).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc.guides[0].pos, 150.0, "the guide moves while dragged");
|
||||
assert_eq!(s.doc().unwrap().doc.ruler_guides()[0].pos, 150.0, "the guide moves while dragged");
|
||||
s.pointer(&PointerEvent::new(PointerKind::Up, 150.0, 200.0), v).unwrap();
|
||||
assert_eq!((s.doc().unwrap().doc.guides[0].pos, s.doc().unwrap().history.undo.last().map(|e| e.label.as_str())), (150.0, Some("Move Guide")));
|
||||
assert_eq!(
|
||||
(s.doc().unwrap().doc.ruler_guides()[0].pos, s.doc().unwrap().history.undo.last().map(|e| e.label.as_str())),
|
||||
(150.0, Some("Move Guide"))
|
||||
);
|
||||
}
|
||||
|
||||
/// With Real-time Drawing off, a 9-slice guide of the edited symbol moved with the Selection tool
|
||||
|
||||
@@ -579,7 +579,7 @@ fn units_and_ruler_guides_survive_save_and_reopen() {
|
||||
s.execute("file.close", &json!({})).unwrap();
|
||||
s.execute("document.open", &json!({"path": p})).unwrap();
|
||||
let doc = &s.doc().unwrap().doc;
|
||||
let guides: Vec<_> = doc.guides.iter().map(|g| (g.vertical, g.pos)).collect();
|
||||
let guides: Vec<_> = doc.ruler_guides().iter().map(|g| (g.vertical, g.pos)).collect();
|
||||
assert_eq!((doc.units, guides), (vectorcraft_doc::Unit::Millimeters, vec![(true, 50.0), (false, 30.0)]), "{ext}");
|
||||
}
|
||||
let _ = std::fs::remove_dir_all(d);
|
||||
|
||||
@@ -93,7 +93,7 @@ fn too_many_guide_pieces_are_refused() {
|
||||
let doc = std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc);
|
||||
for i in 1..3000 {
|
||||
for vertical in [true, false] {
|
||||
doc.guides.push(vectorcraft_doc::Guide::new(vertical, f64::from(i) * 0.1));
|
||||
doc.add_ruler_guide(&vectorcraft_doc::Guide::new(vertical, f64::from(i) * 0.1));
|
||||
}
|
||||
}
|
||||
assert!(s.execute("object.slice.fromGuides", &json!({})).unwrap_err().to_string().contains("more than"));
|
||||
@@ -440,9 +440,9 @@ fn a_canceled_undo_scope_leaves_locked_or_hidden_slices_deselected() {
|
||||
|
||||
/// Select slice `id` and the first ruler guide together, through [`Session::select`].
|
||||
fn pick_slice_and_guide(s: &mut Session, id: u64) {
|
||||
s.select(|_, sel| {
|
||||
s.select(|d, sel| {
|
||||
sel.slices = vec![NodeId(id)];
|
||||
sel.guides = vec![0];
|
||||
sel.guides = d.ruler_guides().first().and_then(|g| g.id).into_iter().collect();
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
@@ -1016,7 +1016,7 @@ fn dropped_type_press_restores_the_original_text_and_selection() {
|
||||
let a = at(&s, first);
|
||||
click(&mut s, a.x + 0.1, a.y);
|
||||
assert!(s.tool_wants_text());
|
||||
s.select(|_, selection| selection.guides.push(0)).unwrap();
|
||||
s.select(|d, selection| selection.guides.extend(d.ruler_guides().first().and_then(|g| g.id))).unwrap();
|
||||
let before = s.doc().unwrap().selection.clone();
|
||||
let b = at(&s, second);
|
||||
s.pointer(&PointerEvent::new(PointerKind::Down, b.x + 0.1, b.y), v).unwrap();
|
||||
|
||||
@@ -766,7 +766,7 @@ fn stroke_width(s: &Session, id: NodeId) -> f64 {
|
||||
}
|
||||
|
||||
fn guide_positions(s: &Session) -> Vec<f64> {
|
||||
s.doc().unwrap().doc.guides.iter().map(|g| g.pos).collect()
|
||||
s.doc().unwrap().doc.ruler_guides().iter().map(|g| g.pos).collect()
|
||||
}
|
||||
|
||||
/// Scale Artwork with Artboard (#602): `artboard.setProps {scaleArt}` takes the art fully inside the
|
||||
|
||||
@@ -51,7 +51,7 @@ fn an_artboard_a_guide_or_a_ruler_origin_beyond_the_canvas_no_longer_blocks_edit
|
||||
v["document"]["ruler_origin"] = json!([1e17, 0.0]);
|
||||
let (s, warnings) = open_and_move(&v, "setup");
|
||||
let d = &s.doc().unwrap().doc;
|
||||
assert_eq!((d.artboards.len(), d.guides.len(), d.ruler_origin.x), (1, 1, 0.0));
|
||||
assert_eq!((d.artboards.len(), d.ruler_guides().len(), d.ruler_origin.x), (1, 1, 0.0));
|
||||
assert!(warnings.to_string().contains("1 artboard, 1 guide beyond the canvas"), "{warnings}");
|
||||
assert!(warnings.to_string().contains("ruler origin"), "{warnings}");
|
||||
}
|
||||
|
||||
@@ -177,12 +177,12 @@ fn clear_guides_and_empty_explicit_targets_keep_their_separate_behavior() {
|
||||
s.execute("edit.clear", &json!({"ids": []})).unwrap();
|
||||
assert_eq!(s.active().unwrap().history.undo.len(), history);
|
||||
assert_eq!(s.active().unwrap().selection, selection);
|
||||
assert_eq!(s.active().unwrap().doc.guides.len(), 1);
|
||||
assert_eq!(s.active().unwrap().doc.ruler_guides().len(), 1);
|
||||
s.execute("edit.clear", &json!({})).unwrap();
|
||||
assert!(s.active().unwrap().doc.guides.is_empty());
|
||||
assert!(s.active().unwrap().doc.ruler_guides().is_empty());
|
||||
assert!(s.active().unwrap().doc.node(a).is_some());
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(s.active().unwrap().doc.guides.len(), 1);
|
||||
assert_eq!(s.active().unwrap().doc.ruler_guides().len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -27,6 +27,8 @@ mod param_keys;
|
||||
mod paste_many;
|
||||
mod pen_click_after_smooth;
|
||||
mod pen_endpoint_picking;
|
||||
mod ruler_guide_nodes;
|
||||
mod ruler_guide_perf;
|
||||
mod ruler_origin;
|
||||
mod selection_commands;
|
||||
mod stroke_visibility;
|
||||
|
||||
@@ -0,0 +1,439 @@
|
||||
//! Ruler guides held as nodes of a layer: commands that work on a layer's art leave them out and
|
||||
//! keep them above the art.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde_json::json;
|
||||
use vectorcraft_doc::{Node, NodeId, NodeKind};
|
||||
use vectorcraft_engine::Session;
|
||||
|
||||
fn id(v: &serde_json::Value) -> NodeId {
|
||||
NodeId(v["id"].as_u64().unwrap())
|
||||
}
|
||||
|
||||
fn rect(s: &mut Session, x: f64) -> NodeId {
|
||||
id(&s.execute("shape.rectangle", &json!({"x": x, "y": 10, "width": 20, "height": 20})).unwrap())
|
||||
}
|
||||
|
||||
/// A ruler guide added on top of `layer`'s contents.
|
||||
fn guide(s: &mut Session, layer: NodeId, pos: f64) -> NodeId {
|
||||
let d = Arc::make_mut(&mut s.doc_mut().unwrap().doc);
|
||||
let g = Node::new(d.alloc_id(), NodeKind::RulerGuide { vertical: true, pos, artboard: None });
|
||||
d.insert(Some(layer), usize::MAX, g).unwrap()
|
||||
}
|
||||
|
||||
fn children(s: &Session, layer: NodeId) -> Vec<NodeId> {
|
||||
s.doc().unwrap().doc.children(Some(layer)).unwrap().iter().map(|c| c.id).collect()
|
||||
}
|
||||
|
||||
/// A document with a layer holding two rectangles and two ruler guides: (layer, [art], [guides]).
|
||||
fn session() -> (Session, NodeId, Vec<NodeId>, Vec<NodeId>) {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 400, "height": 300})).unwrap();
|
||||
let layer = s.doc().unwrap().doc.layers[0].id;
|
||||
let art = vec![rect(&mut s, 10.0), rect(&mut s, 50.0)];
|
||||
let guides = vec![guide(&mut s, layer, 5.0), guide(&mut s, layer, 7.0)];
|
||||
(s, layer, art, guides)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layer_commands_keep_ruler_guides_above_the_art() {
|
||||
// Send to Current Layer puts the object below the current layer's guides.
|
||||
let (mut s, a, art, guides) = session();
|
||||
let b = id(&s.execute("layer.new", &json!({})).unwrap());
|
||||
let moved = rect(&mut s, 90.0);
|
||||
s.execute("layer.setCurrent", &json!({"id": a.0})).unwrap();
|
||||
s.execute("select.set", &json!({"ids": [moved.0]})).unwrap();
|
||||
s.execute("object.arrange.sendToCurrentLayer", &json!({})).unwrap();
|
||||
assert_eq!(children(&s, a), [art[0], art[1], moved, guides[0], guides[1]]);
|
||||
// Already on top of the art: nothing changes and no undo step is added.
|
||||
let steps = s.doc().unwrap().history.undo.len();
|
||||
s.execute("object.arrange.sendToCurrentLayer", &json!({})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), steps);
|
||||
|
||||
// Moving a row on top of a layer's contents puts it below the guides.
|
||||
let other = rect(&mut s, 130.0);
|
||||
s.execute("layer.move", &json!({"id": other.0, "target": a.0})).unwrap();
|
||||
assert_eq!(children(&s, a), [art[0], art[1], moved, other, guides[0], guides[1]]);
|
||||
|
||||
// Merging layers keeps every guide above the merged art.
|
||||
let gb = guide(&mut s, b, 9.0);
|
||||
let late = {
|
||||
s.execute("layer.setCurrent", &json!({"id": b.0})).unwrap();
|
||||
rect(&mut s, 170.0)
|
||||
};
|
||||
s.execute("layer.merge", &json!({"ids": [a.0, b.0]})).unwrap();
|
||||
let merged = children(&s, b);
|
||||
let first_guide = merged.iter().position(|c| [guides[0], guides[1], gb].contains(c)).unwrap();
|
||||
assert_eq!(merged.len() - first_guide, 3, "{merged:?}");
|
||||
assert!(merged.contains(&late));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_and_clipping_leave_ruler_guides_alone() {
|
||||
let (mut s, layer, art, guides) = session();
|
||||
// Make Clipping Mask clips by the top object, not by a guide above it.
|
||||
assert_eq!(s.execute("layer.clippingMask.toggle", &json!({"id": layer.0})).unwrap()["clip"], json!(true));
|
||||
assert_eq!(children(&s, layer), [art[1], art[0], guides[0], guides[1]]);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
|
||||
// Release to Layers makes layers of the art only, below the guides.
|
||||
s.execute("layer.releaseToLayers", &json!({"id": layer.0})).unwrap();
|
||||
let ch = children(&s, layer);
|
||||
assert_eq!(ch.len(), 4);
|
||||
assert_eq!(ch[2..], [guides[0], guides[1]]);
|
||||
let d = &s.doc().unwrap().doc;
|
||||
assert!(ch[..2].iter().all(|c| d.node(*c).is_some_and(Node::is_layer)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ruler_guides_are_not_art_to_select_paint_or_inspect() {
|
||||
let (mut s, layer, art, guides) = session();
|
||||
// Target Contents selects the layer's objects only.
|
||||
let out = s.execute("appearance.targetContents", &json!({"ids": [layer.0]})).unwrap();
|
||||
assert_eq!(out["ids"], json!([art[0].0, art[1].0]));
|
||||
// Painting the layer paints its objects and leaves the guides with no appearance.
|
||||
s.execute("paint.setFill", &json!({"ids": [layer.0], "color": "#ff0000"})).unwrap();
|
||||
let d = &s.doc().unwrap().doc;
|
||||
for g in &guides {
|
||||
assert_eq!(d.node(*g).unwrap().appearance, vectorcraft_doc::Appearance::default());
|
||||
}
|
||||
// Document inspection lists the objects, not the guides.
|
||||
assert_eq!(s.execute("document.find", &json!({"kind": "Guide"})).unwrap()["total"], json!(0));
|
||||
let layer_row = s.execute("document.inspect", &json!({"ids": [layer.0]})).unwrap();
|
||||
let listed = layer_row.to_string();
|
||||
assert!(guides.iter().all(|g| !listed.contains(&format!("\"id\":{}", g.0))), "{listed}");
|
||||
// Unlock All does not reach a locked guide.
|
||||
Arc::make_mut(&mut s.doc_mut().unwrap().doc).node_mut(guides[0]).unwrap().locked = true;
|
||||
s.execute("object.unlockAll", &json!({})).unwrap();
|
||||
assert!(s.doc().unwrap().doc.node(guides[0]).unwrap().locked);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flattening_a_sublayers_art_keeps_its_ruler_guides() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 400, "height": 300})).unwrap();
|
||||
let layer = s.doc().unwrap().doc.layers[0].id;
|
||||
let sub = id(&s.execute("layer.newSublayer", &json!({"parent": layer.0})).unwrap());
|
||||
s.execute("layer.setCurrent", &json!({"id": sub.0})).unwrap();
|
||||
let r = rect(&mut s, 10.0);
|
||||
s.execute("transparency.set", &json!({"ids": [r.0], "opacity": 50})).unwrap();
|
||||
let g = guide(&mut s, sub, 5.0);
|
||||
s.execute("object.flattenTransparency", &json!({"ids": [sub.0]})).unwrap();
|
||||
// The sublayer stands for its art: the art is flattened and the guide stays on top of it.
|
||||
let d = &s.doc().unwrap().doc;
|
||||
assert!(d.node(r).is_none(), "the art was flattened");
|
||||
assert!(d.node(g).is_some_and(Node::is_ruler_guide), "the guide is kept");
|
||||
assert_eq!(d.parent_of(g), Some(sub));
|
||||
assert_eq!(children(&s, sub).len(), 2);
|
||||
assert_eq!(children(&s, sub).last(), Some(&g));
|
||||
assert_eq!(d.parent_of(sub), Some(layer));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn object_commands_refuse_a_ruler_guide_id() {
|
||||
let (mut s, _, art, guides) = session();
|
||||
s.execute("select.set", &json!({"ids": [art[0].0]})).unwrap();
|
||||
let before = (s.doc().unwrap().doc.clone(), s.doc().unwrap().selection.clone(), s.doc().unwrap().history.undo.len());
|
||||
for (command, params) in [
|
||||
("select.set", json!({"ids": [guides[0].0]})),
|
||||
("select.set", json!({"ids": [art[1].0, guides[1].0]})),
|
||||
("edit.clear", json!({"ids": [guides[0].0]})),
|
||||
("object.lock", json!({"ids": [guides[0].0]})),
|
||||
("object.move", json!({"ids": [guides[0].0], "dx": 5, "dy": 0})),
|
||||
("paint.setFill", json!({"ids": [guides[1].0], "color": "#ff0000"})),
|
||||
] {
|
||||
let e = s.execute(command, ¶ms).unwrap_err().to_string();
|
||||
assert!(e.contains("is a ruler guide, not an object"), "{command}: {e}");
|
||||
let d = s.doc().unwrap();
|
||||
assert_eq!((&d.doc, &d.selection, d.history.undo.len()), (&before.0, &before.1, before.2), "{command} changed something");
|
||||
}
|
||||
// A selection holding a guide id as an object loses it when pruned; the guide stays.
|
||||
let d = &s.doc().unwrap().doc;
|
||||
let mut sel = vectorcraft_doc::Selection { objects: vec![art[0], guides[0]], key: Some(guides[1]), ..Default::default() };
|
||||
sel.prune(d);
|
||||
assert_eq!((sel.objects, sel.key), (vec![art[0]], None));
|
||||
assert!(d.node(guides[0]).is_some_and(Node::is_ruler_guide));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_ruler_guide_stays_directly_in_a_layer() {
|
||||
let (mut s, layer, art, guides) = session();
|
||||
s.execute("select.set", &json!({"ids": [art[0].0, art[1].0]})).unwrap();
|
||||
let group = id(&s.execute("object.group", &json!({})).unwrap());
|
||||
let before = (s.doc().unwrap().doc.clone(), s.doc().unwrap().history.undo.len());
|
||||
// Reorder refuses a group as a guide's parent, and the top level.
|
||||
for parent in [json!(group.0), json!(null)] {
|
||||
assert!(s.execute("node.move", &json!({"id": guides[0].0, "parent": parent, "index": 0})).is_err(), "{parent}");
|
||||
assert_eq!((&s.doc().unwrap().doc, s.doc().unwrap().history.undo.len()), (&before.0, before.1));
|
||||
}
|
||||
// So do the document's own insert and move.
|
||||
let d = Arc::make_mut(&mut s.doc_mut().unwrap().doc);
|
||||
assert!(d.move_node(guides[1], Some(group), 0).is_err());
|
||||
let loose = Node::new(d.alloc_id(), NodeKind::RulerGuide { vertical: false, pos: 3.0, artboard: None });
|
||||
assert!(d.insert(Some(group), 0, loose.clone()).is_err());
|
||||
assert!(d.insert(None, 0, loose.clone()).is_err());
|
||||
assert_eq!(d.ruler_guides().iter().filter_map(|g| g.id).collect::<Vec<_>>(), guides);
|
||||
assert_eq!(children(&s, layer), [group, guides[0], guides[1]]);
|
||||
// Within its layer it still moves among the guides.
|
||||
s.execute("node.move", &json!({"id": guides[1].0, "parent": layer.0, "index": 0})).unwrap();
|
||||
assert_eq!(children(&s, layer), [group, guides[1], guides[0]]);
|
||||
}
|
||||
|
||||
/// Arrange counts only a layer's art: the guides above it are no place to move to.
|
||||
#[test]
|
||||
fn arranging_counts_only_a_layers_art() {
|
||||
let (mut s, layer, art, guides) = session();
|
||||
let top = rect(&mut s, 90.0);
|
||||
assert_eq!(children(&s, layer), [art[0], art[1], top, guides[0], guides[1]]);
|
||||
s.execute("select.set", &json!({"ids": [art[0].0, top.0]})).unwrap();
|
||||
s.execute("object.arrange.bringToFront", &json!({})).unwrap();
|
||||
assert_eq!(children(&s, layer), [art[1], art[0], top, guides[0], guides[1]], "the selection keeps its order at the top of the art");
|
||||
// The topmost art brought to the front or forward: nothing changes and no undo step is added.
|
||||
s.execute("select.set", &json!({"ids": [top.0]})).unwrap();
|
||||
let steps = s.doc().unwrap().history.undo.len();
|
||||
for command in ["object.arrange.bringToFront", "object.arrange.bringForward"] {
|
||||
s.execute(command, &json!({})).unwrap();
|
||||
assert_eq!(children(&s, layer), [art[1], art[0], top, guides[0], guides[1]], "{command}");
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), steps, "{command}");
|
||||
}
|
||||
s.execute("select.set", &json!({"ids": [art[1].0]})).unwrap();
|
||||
s.execute("object.arrange.bringForward", &json!({})).unwrap();
|
||||
assert_eq!(children(&s, layer), [art[0], art[1], top, guides[0], guides[1]]);
|
||||
}
|
||||
|
||||
/// A guide made while an opacity mask, a pattern or a symbol is edited goes on the temporary
|
||||
/// editing layer, and stays in the document when editing ends; a save made during mask editing
|
||||
/// keeps it.
|
||||
#[test]
|
||||
fn guides_made_while_editing_a_mask_a_pattern_or_a_symbol_stay() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 400, "height": 300})).unwrap();
|
||||
rect(&mut s, 10.0);
|
||||
s.execute("transparency.makeOpacityMask", &json!({})).unwrap();
|
||||
let edit_layer = s.doc().unwrap().doc.mask_edit.expect("editing the mask").layer;
|
||||
s.execute("guide.add", &json!({"vertical": true, "pos": 40})).unwrap();
|
||||
let made = s.doc().unwrap().doc.ruler_guides();
|
||||
assert_eq!(made.iter().map(|g| (g.layer, g.pos)).collect::<Vec<_>>(), [(Some(edit_layer), 40.0)]);
|
||||
let saved = s.doc().unwrap().doc.without_edit_modes().ruler_guides();
|
||||
assert_eq!(saved.iter().map(|g| (g.id, g.pos)).collect::<Vec<_>>(), [(made[0].id, 40.0)], "a save keeps the guide");
|
||||
s.execute("transparency.stopEditingOpacityMask", &json!({})).unwrap();
|
||||
let d = &s.doc().unwrap().doc;
|
||||
let kept = d.ruler_guides();
|
||||
assert_eq!(kept.iter().map(|g| (g.id, g.pos)).collect::<Vec<_>>(), [(made[0].id, 40.0)]);
|
||||
assert!(kept[0].layer.is_some_and(|l| l != edit_layer && d.node(l).is_some_and(Node::is_layer)));
|
||||
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 400, "height": 300})).unwrap();
|
||||
rect(&mut s, 10.0);
|
||||
s.execute("object.pattern.make", &json!({})).unwrap();
|
||||
let tile_layer = s.doc().unwrap().doc.pattern_edit.as_ref().expect("editing the pattern").layer;
|
||||
s.execute("guide.add", &json!({"vertical": false, "pos": 25})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc.ruler_guides()[0].layer, Some(tile_layer));
|
||||
s.execute("object.pattern.done", &json!({})).unwrap();
|
||||
let d = &s.doc().unwrap().doc;
|
||||
let kept = d.ruler_guides();
|
||||
assert_eq!(kept.iter().map(|g| g.pos).collect::<Vec<_>>(), [25.0]);
|
||||
assert!(kept[0].layer.is_some_and(|l| l != tile_layer && d.node(l).is_some_and(Node::is_layer)));
|
||||
let tile = &d.patterns.last().expect("the new pattern").art;
|
||||
assert!(tile.iter().all(|n| !n.is_ruler_guide()), "the guide is not part of the tile");
|
||||
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 400, "height": 300})).unwrap();
|
||||
rect(&mut s, 10.0);
|
||||
let instance = id(&s.execute("symbol.new", &json!({"name": "Mark"})).unwrap());
|
||||
s.execute("symbol.edit", &json!({"ids": [instance.0]})).unwrap();
|
||||
let edit_layer = s.doc().unwrap().doc.symbol_edit.as_ref().expect("editing the symbol").layer;
|
||||
s.execute("guide.add", &json!({"vertical": true, "pos": 15})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc.ruler_guides()[0].layer, Some(edit_layer));
|
||||
s.execute("symbol.exitEditing", &json!({})).unwrap();
|
||||
let d = &s.doc().unwrap().doc;
|
||||
let kept = d.ruler_guides();
|
||||
assert_eq!(kept.iter().map(|g| g.pos).collect::<Vec<_>>(), [15.0]);
|
||||
assert!(kept[0].layer.is_some_and(|l| l != edit_layer && d.node(l).is_some_and(Node::is_layer)));
|
||||
let art = &d.symbols.iter().find(|x| x.name == "Mark").expect("the symbol").art;
|
||||
let mut in_symbol = false;
|
||||
art.walk(&mut |n| in_symbol |= n.is_ruler_guide());
|
||||
assert!(!in_symbol, "the guide is not part of the symbol");
|
||||
}
|
||||
|
||||
/// The bytes of each export format (PDF without the embedded native document, which keeps the
|
||||
/// guides), and the box Fit All in Window fits (the art and every artboard).
|
||||
fn exports(s: &mut Session) -> (Vec<(String, String)>, Option<vectorcraft_geom::Rect>) {
|
||||
let files = [json!({"format": "svg"}), json!({"format": "pdf", "preserveEditing": false}), json!({"format": "png"}), json!({"format": "psd"})]
|
||||
.iter()
|
||||
.map(|p| {
|
||||
let out = s.execute("document.export", p).unwrap();
|
||||
(p["format"].as_str().unwrap().to_string(), out["dataBase64"].as_str().unwrap().to_string())
|
||||
})
|
||||
.collect();
|
||||
let d = &s.doc().unwrap().doc;
|
||||
let fit = d.art_bounds().map(|a| d.artboards.iter().fold(a, |r, ab| r.union(ab.rect)));
|
||||
(files, fit)
|
||||
}
|
||||
|
||||
/// Ruler guides on a layer, on a sublayer and on an artboard change no export and not the box
|
||||
/// Fit All in Window fits.
|
||||
#[test]
|
||||
fn exports_and_fit_all_are_the_same_with_ruler_guides() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 200, "height": 150, "artboards": 2})).unwrap();
|
||||
let layer = s.doc().unwrap().doc.layers[0].id;
|
||||
rect(&mut s, 10.0);
|
||||
let sub = id(&s.execute("layer.newSublayer", &json!({"parent": layer.0})).unwrap());
|
||||
s.execute("layer.setCurrent", &json!({"id": sub.0})).unwrap();
|
||||
s.execute("shape.ellipse", &json!({"x": 60, "y": 30, "width": 40, "height": 30})).unwrap();
|
||||
let without = exports(&mut s);
|
||||
assert_eq!(exports(&mut s), without, "the exports are repeatable");
|
||||
// Guides far outside the art too: they must not widen any box.
|
||||
s.execute("guide.add", &json!({"vertical": true, "pos": -5000, "layer": layer.0})).unwrap();
|
||||
s.execute("guide.add", &json!({"vertical": false, "pos": 9000, "layer": sub.0})).unwrap();
|
||||
s.execute("guide.add", &json!({"vertical": true, "pos": 120, "artboard": 1})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc.ruler_guides().len(), 3);
|
||||
let with = exports(&mut s);
|
||||
for ((format, a), (_, b)) in without.0.iter().zip(&with.0) {
|
||||
assert!(a == b, "{format} export changed with ruler guides present");
|
||||
}
|
||||
assert_eq!(with.1, without.1, "Fit All");
|
||||
// A PDF that preserves editing carries them in its native document, and opens with them.
|
||||
let guides = s.doc().unwrap().doc.ruler_guides();
|
||||
let pdf = s.execute("document.export", &json!({"format": "pdf"})).unwrap()["dataBase64"].clone();
|
||||
s.execute("document.open", &json!({"name": "guides.pdf", "dataBase64": pdf})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc.ruler_guides(), guides);
|
||||
}
|
||||
|
||||
/// One step of [`guide_commands_follow_a_list_model`].
|
||||
#[derive(Clone, Debug)]
|
||||
enum GuideOp {
|
||||
Add { vertical: bool, pos: u16, layer: usize },
|
||||
Remove(usize),
|
||||
Copy(usize),
|
||||
Paste,
|
||||
SetCurrent(usize),
|
||||
Undo,
|
||||
Redo,
|
||||
}
|
||||
|
||||
fn guide_op() -> impl proptest::strategy::Strategy<Value = GuideOp> {
|
||||
use proptest::prelude::*;
|
||||
prop_oneof![
|
||||
3 => (any::<bool>(), 0u16..400, 0usize..3).prop_map(|(vertical, pos, layer)| GuideOp::Add { vertical, pos, layer }),
|
||||
2 => (0usize..16).prop_map(GuideOp::Remove),
|
||||
1 => (0usize..16).prop_map(GuideOp::Copy),
|
||||
1 => Just(GuideOp::Paste),
|
||||
1 => (0usize..3).prop_map(GuideOp::SetCurrent),
|
||||
2 => Just(GuideOp::Undo),
|
||||
1 => Just(GuideOp::Redo),
|
||||
]
|
||||
}
|
||||
|
||||
/// A guide as `guide.list` reports it: (vertical, pos, layer).
|
||||
type Line = (bool, f64, u64);
|
||||
|
||||
fn listed(s: &mut Session) -> Vec<Line> {
|
||||
let rows = s.execute("guide.list", &json!({})).unwrap();
|
||||
rows.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, r)| {
|
||||
assert_eq!(r["index"], json!(i));
|
||||
(r["vertical"].as_bool().unwrap(), r["pos"].as_f64().unwrap(), r["layer"].as_u64().unwrap())
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Runs `ops` on a document with two layers and a sublayer, checking `guide.list` against a plain
|
||||
/// list after every step: Add appends (a new guide takes the highest id), Remove takes the entry
|
||||
/// out, Paste appends the copied guide on the current layer at the same place, and Undo and
|
||||
/// Redo bring back the list as it was, the same indexes included.
|
||||
fn run_guide_ops(ops: &[GuideOp]) -> Result<(), proptest::test_runner::TestCaseError> {
|
||||
use proptest::prop_assert_eq;
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 400, "height": 300})).unwrap();
|
||||
let bottom = s.doc().unwrap().doc.layers[0].id;
|
||||
let top = id(&s.execute("layer.new", &json!({})).unwrap());
|
||||
let sub = id(&s.execute("layer.newSublayer", &json!({"parent": bottom.0})).unwrap());
|
||||
let layers = [bottom, top, sub];
|
||||
s.execute("layer.setCurrent", &json!({"id": bottom.0})).unwrap();
|
||||
let mut current = bottom;
|
||||
let mut model: Vec<Line> = vec![];
|
||||
let mut clip: Option<(bool, f64)> = None;
|
||||
let (mut undo, mut redo): (Vec<Vec<Line>>, Vec<Vec<Line>>) = (vec![], vec![]);
|
||||
for (step, op) in ops.iter().enumerate() {
|
||||
let steps = s.doc().unwrap().history.undo.len();
|
||||
let before = model.clone();
|
||||
// Whether the step is an edit (one undo step).
|
||||
let edit = match *op {
|
||||
GuideOp::Add { vertical, pos, layer } => {
|
||||
let l = layers[layer];
|
||||
let r = s.execute("guide.add", &json!({"vertical": vertical, "pos": pos, "layer": l.0})).unwrap();
|
||||
model.push((vertical, f64::from(pos), l.0));
|
||||
prop_assert_eq!(&r["index"], &json!(model.len() - 1), "step {}", step);
|
||||
true
|
||||
}
|
||||
GuideOp::Remove(i) if !model.is_empty() => {
|
||||
let i = i % model.len();
|
||||
s.execute("guide.remove", &json!({"index": i})).unwrap();
|
||||
model.remove(i);
|
||||
true
|
||||
}
|
||||
GuideOp::Copy(i) if !model.is_empty() => {
|
||||
let i = i % model.len();
|
||||
s.execute("guide.select", &json!({"indexes": [i]})).unwrap();
|
||||
s.execute("edit.copy", &json!({})).unwrap();
|
||||
clip = Some((model[i].0, model[i].1));
|
||||
false
|
||||
}
|
||||
GuideOp::Paste => match clip {
|
||||
Some((vertical, pos)) => {
|
||||
s.execute("edit.paste", &json!({})).unwrap();
|
||||
model.push((vertical, pos, current.0));
|
||||
true
|
||||
}
|
||||
None => false,
|
||||
},
|
||||
GuideOp::SetCurrent(l) => {
|
||||
current = layers[l];
|
||||
s.execute("layer.setCurrent", &json!({"id": current.0})).unwrap();
|
||||
false
|
||||
}
|
||||
GuideOp::Undo => {
|
||||
if let Some(m) = undo.pop() {
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
redo.push(std::mem::replace(&mut model, m));
|
||||
}
|
||||
false
|
||||
}
|
||||
GuideOp::Redo => {
|
||||
if let Some(m) = redo.pop() {
|
||||
s.execute("edit.redo", &json!({})).unwrap();
|
||||
undo.push(std::mem::replace(&mut model, m));
|
||||
}
|
||||
false
|
||||
}
|
||||
GuideOp::Remove(_) | GuideOp::Copy(_) => false,
|
||||
};
|
||||
let after = s.doc().unwrap().history.undo.len();
|
||||
if edit {
|
||||
prop_assert_eq!(after, steps + 1, "step {} {:?}: one undo step", step, op);
|
||||
undo.push(before);
|
||||
redo.clear();
|
||||
} else if !matches!(op, GuideOp::Undo | GuideOp::Redo) {
|
||||
prop_assert_eq!(after, steps, "step {} {:?}: no undo step", step, op);
|
||||
}
|
||||
prop_assert_eq!(listed(&mut s), model.clone(), "step {} {:?}", step, op);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
proptest::proptest! {
|
||||
#![proptest_config(proptest::test_runner::Config { cases: 64, failure_persistence: None, ..proptest::test_runner::Config::default() })]
|
||||
|
||||
/// Add, remove, copy, paste, undo and redo keep `guide.list` in step with a plain list, on
|
||||
/// three layers.
|
||||
#[test]
|
||||
fn guide_commands_follow_a_list_model(ops in proptest::collection::vec(guide_op(), 1..40)) {
|
||||
run_guide_ops(&ops)?;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
//! Timings of a document with 10,000 ruler guides and 100,000 objects: save, load, open, the guide
|
||||
//! list, hovering and dragging with the Selection tool, an edit that does not touch guides and one
|
||||
//! that does. Ignored by default; it prints its timings and sets no bound on them:
|
||||
//! `cargo test -p vectorcraft-engine --test it ruler_guide_perf -- --ignored --nocapture`.
|
||||
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_color::Color;
|
||||
use vectorcraft_doc::Document;
|
||||
use vectorcraft_engine::{Session, ViewInfo};
|
||||
use vectorcraft_geom::Rect;
|
||||
use vectorcraft_testkit::fixtures::DocBuilder;
|
||||
use vectorcraft_tools::{PointerEvent, PointerKind};
|
||||
|
||||
const GUIDES: usize = 10_000;
|
||||
const LAYERS: usize = 4;
|
||||
const GROUPS_PER_LAYER: usize = 250;
|
||||
const RECTS_PER_GROUP: usize = 100;
|
||||
|
||||
/// The `.vectorcraft` file of the document: 100,000 rectangles in groups of 100 on four layers,
|
||||
/// and the ruler guides in its saved list, spread over the layers.
|
||||
fn file() -> Vec<u8> {
|
||||
let mut b = DocBuilder::new(2000.0, 2000.0);
|
||||
let mut layers = vec![b.layer];
|
||||
for l in 0..LAYERS {
|
||||
if l > 0 {
|
||||
layers.push(b.layer(&format!("Layer {}", l + 1)));
|
||||
}
|
||||
for g in 0..GROUPS_PER_LAYER {
|
||||
let kids = (0..RECTS_PER_GROUP)
|
||||
.map(|r| {
|
||||
let (x, y) = (((g * RECTS_PER_GROUP + r) % 400) as f64 * 5.0, (l * GROUPS_PER_LAYER + g) as f64 * 2.0);
|
||||
b.rect_node(Rect::new(x, y, x + 3.0, y + 1.5), Color::rgb(0.2, 0.4, 0.6))
|
||||
})
|
||||
.collect();
|
||||
b.group(kids, false);
|
||||
}
|
||||
}
|
||||
let d: Document = b.build();
|
||||
let mut v: Value = serde_json::from_slice(&vectorcraft_format::save(&d, false)).unwrap();
|
||||
v["document"]["guides"] =
|
||||
(0..GUIDES).map(|i| json!({"vertical": i % 2 == 0, "pos": (i / 2) as f64 * 0.37 + 0.1, "layer": layers[i % LAYERS]})).collect();
|
||||
serde_json::to_vec(&v).unwrap()
|
||||
}
|
||||
|
||||
/// The median of `n` runs of `f`.
|
||||
fn median(n: usize, mut f: impl FnMut()) -> Duration {
|
||||
let mut t: Vec<Duration> = (0..n)
|
||||
.map(|_| {
|
||||
let start = Instant::now();
|
||||
f();
|
||||
start.elapsed()
|
||||
})
|
||||
.collect();
|
||||
t.sort();
|
||||
t[n / 2]
|
||||
}
|
||||
|
||||
fn report(what: &str, d: Duration) {
|
||||
eprintln!("ruler_guide_perf: {what:<44} {:>10.3} ms", d.as_secs_f64() * 1000.0);
|
||||
}
|
||||
|
||||
fn pointer(s: &mut Session, kind: PointerKind, x: f64, y: f64) {
|
||||
s.pointer(&PointerEvent::new(kind, x, y), ViewInfo::default()).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "a benchmark: run with --ignored --nocapture"]
|
||||
fn ruler_guide_perf() {
|
||||
let bytes = file();
|
||||
eprintln!("ruler_guide_perf: file of {} bytes", bytes.len());
|
||||
report("load (format::load), median of 5", median(5, || drop(vectorcraft_format::load(&bytes).unwrap())));
|
||||
let doc = vectorcraft_format::load(&bytes).unwrap();
|
||||
report("save (format::save), median of 5", median(5, || drop(vectorcraft_format::save(&doc, false))));
|
||||
|
||||
let mut s = Session::new();
|
||||
let data = vectorcraft_format::base64_encode(&bytes);
|
||||
let start = Instant::now();
|
||||
s.execute("document.open", &json!({"name": "perf.vectorcraft", "dataBase64": data})).unwrap();
|
||||
report("document.open", start.elapsed());
|
||||
let listed = s.execute("guide.list", &json!({})).unwrap().as_array().map_or(0, Vec::len);
|
||||
assert_eq!(listed, GUIDES);
|
||||
report("guide.list, median of 11", median(11, || drop(s.execute("guide.list", &json!({})).unwrap())));
|
||||
|
||||
s.select_tool("selection", ViewInfo::default()).unwrap();
|
||||
let mut i = 0.0;
|
||||
report(
|
||||
"hover (Selection tool move), median of 201",
|
||||
median(201, || {
|
||||
i += 1.0;
|
||||
pointer(&mut s, PointerKind::Move, 600.0 + i * 0.13, 300.0 + i * 0.07);
|
||||
}),
|
||||
);
|
||||
// A drag of the first rectangle's group (the rectangle is at 0,0 to 3,1.5), with its
|
||||
// snapping: one undo step.
|
||||
let steps = s.doc().unwrap().history.undo.len();
|
||||
pointer(&mut s, PointerKind::Down, 1.5, 0.75);
|
||||
let mut j = 0.0;
|
||||
report(
|
||||
"drag (Selection tool drag step), median of 51",
|
||||
median(51, || {
|
||||
j += 1.0;
|
||||
pointer(&mut s, PointerKind::Drag, 1.5 + j * 0.9, 0.75 + j * 0.4);
|
||||
}),
|
||||
);
|
||||
let start = Instant::now();
|
||||
pointer(&mut s, PointerKind::Up, 1.5 + j * 0.9, 0.75 + j * 0.4);
|
||||
report("drag release (commit)", start.elapsed());
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), steps + 1, "the drag moved the group");
|
||||
|
||||
let mut k = 0.0;
|
||||
report(
|
||||
"edit without guides (shape.rectangle), median of 21",
|
||||
median(21, || {
|
||||
k += 1.0;
|
||||
s.execute("shape.rectangle", &json!({"x": k, "y": 1900, "width": 5, "height": 5})).unwrap();
|
||||
}),
|
||||
);
|
||||
report("undo, median of 11", median(11, || drop(s.execute("edit.undo", &json!({})).unwrap())));
|
||||
let mut m = 0.0;
|
||||
report(
|
||||
"guide edit (guide.move), median of 21",
|
||||
median(21, || {
|
||||
m += 1.0;
|
||||
s.execute("guide.move", &json!({"index": GUIDES / 2, "pos": 100.0 + m})).unwrap();
|
||||
}),
|
||||
);
|
||||
report("file.saveCopy, median of 3", median(3, || drop(s.execute("file.saveCopy", &json!({})).unwrap())));
|
||||
}
|
||||
@@ -78,7 +78,7 @@ fn native_versions_and_compression_keep_the_origin_and_old_files_default_to_zero
|
||||
assert_eq!(back.ruler_origin_for(0), doc.ruler_origin_for(0), "v{version}, compression {compress}");
|
||||
assert_eq!((back.rulers, &back.artboard_ruler_origins), (doc.rulers, &doc.artboard_ruler_origins), "v{version}, compression {compress}");
|
||||
assert_eq!(back.layers, doc.layers);
|
||||
assert_eq!(back.guides, doc.guides);
|
||||
assert_eq!(back.ruler_guides(), doc.ruler_guides());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -460,6 +460,9 @@ impl<'a> Writer<'a> {
|
||||
let flag = |b: bool| u8::from(b);
|
||||
let dim = dim_images.unwrap_or(50);
|
||||
let name = n.name.as_deref().unwrap_or("Layer");
|
||||
// Ruler guides are kept in the document's native copy only.
|
||||
let children: Vec<Arc<Node>> = children.iter().filter(|c| !c.is_ruler_guide()).cloned().collect();
|
||||
let children = &children;
|
||||
// The last flag makes the layer a clipping layer: its top object clips the rest. Only a
|
||||
// clipping path or compound path, outside any art cut to a clipping object, is written as
|
||||
// one ([`Self::clip_group`] writes the others).
|
||||
@@ -583,6 +586,10 @@ impl<'a> Writer<'a> {
|
||||
if let NodeKind::Layer { .. } = n.kind {
|
||||
return self.write_layer(n, &[]);
|
||||
}
|
||||
// Ruler guides are kept in the document's native copy only: no object, state or style.
|
||||
if n.is_ruler_guide() {
|
||||
return;
|
||||
}
|
||||
if self.in_symbol {
|
||||
self.wrote_fixed |= symbols::redrawn(n);
|
||||
}
|
||||
@@ -697,6 +704,8 @@ impl<'a> Writer<'a> {
|
||||
}
|
||||
self.end_group("U", xy);
|
||||
}
|
||||
// Ruler guides are kept in the document's native copy only.
|
||||
NodeKind::RulerGuide { .. } => {}
|
||||
}
|
||||
if self.body.len() > start {
|
||||
// A group's dictionary goes between its end and its state block.
|
||||
|
||||
@@ -28,6 +28,10 @@ pub(in crate::ai_editing) const STYLE_LINKS: &str = "links to graphic styles are
|
||||
/// How many objects of `doc`'s layers, opacity masks and symbols are linked to a graphic style.
|
||||
pub(in crate::ai_editing) fn style_links(doc: &Document) -> usize {
|
||||
fn count(n: &Node) -> usize {
|
||||
// Ruler guides are not written as objects.
|
||||
if n.is_ruler_guide() {
|
||||
return 0;
|
||||
}
|
||||
let own = usize::from(n.extras().graphic_style.is_some());
|
||||
let mask = n.extras().mask.as_ref().map_or(0, |m| count(&m.art));
|
||||
own + mask + n.children().map_or(0, |c| c.iter().map(|m| count(m)).sum())
|
||||
|
||||
@@ -1244,9 +1244,9 @@ impl<'a> Reader<'a> {
|
||||
None => None,
|
||||
};
|
||||
let pos = if vertical { p.x } else { p.y };
|
||||
pos.is_finite().then_some(Guide { vertical, pos, artboard, layer: None })
|
||||
pos.is_finite().then_some(Guide { artboard, ..Guide::new(vertical, pos) })
|
||||
};
|
||||
take_ruler_guides(&mut doc.layers, &pick, &mut doc.guides, 0);
|
||||
take_ruler_guides(&mut doc.layers, &pick, 0);
|
||||
}
|
||||
let mut brushes = self.brush_library();
|
||||
if let Some((lib, _)) = &brushes {
|
||||
@@ -1429,9 +1429,9 @@ fn ruler_guide(a: Point, b: Point, centre: Option<(f64, f64)>, boards: &[Rect])
|
||||
}
|
||||
}
|
||||
|
||||
/// Moves the children of the layers and sublayers of `nodes` that `pick` makes a guide of into
|
||||
/// `guides`, each on its layer.
|
||||
fn take_ruler_guides(nodes: &mut [Arc<Node>], pick: &dyn Fn(NodeId) -> Option<Guide>, guides: &mut Vec<Guide>, depth: usize) {
|
||||
/// Makes the children of the layers and sublayers of `nodes` that `pick` makes a guide of into
|
||||
/// ruler guide nodes with the same ids, moved to the top of their layer in their order.
|
||||
fn take_ruler_guides(nodes: &mut [Arc<Node>], pick: &dyn Fn(NodeId) -> Option<Guide>, depth: usize) {
|
||||
if depth > MAX_DEPTH {
|
||||
return;
|
||||
}
|
||||
@@ -1439,16 +1439,17 @@ fn take_ruler_guides(nodes: &mut [Arc<Node>], pick: &dyn Fn(NodeId) -> Option<Gu
|
||||
if !matches!(n.kind, NodeKind::Layer { .. }) {
|
||||
continue;
|
||||
}
|
||||
let id = n.id;
|
||||
let Some(kids) = Arc::make_mut(n).children_mut() else { continue };
|
||||
let mut guides = vec![];
|
||||
kids.retain(|k| match pick(k.id) {
|
||||
Some(g) => {
|
||||
guides.push(Guide { layer: Some(id), ..g });
|
||||
guides.push(Arc::new(Node::new(k.id, NodeKind::RulerGuide { vertical: g.vertical, pos: g.pos, artboard: g.artboard })));
|
||||
false
|
||||
}
|
||||
None => true,
|
||||
});
|
||||
take_ruler_guides(kids, pick, guides, depth + 1);
|
||||
take_ruler_guides(kids, pick, depth + 1);
|
||||
kids.extend(guides);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -62,9 +62,14 @@ fn arrays_nested_past_the_depth_limit_are_refused() {
|
||||
assert!(nested(100_000));
|
||||
}
|
||||
|
||||
/// The children of the first layer.
|
||||
/// The children of the first layer, its ruler guides left out.
|
||||
fn art(doc: &Document) -> Vec<Arc<Node>> {
|
||||
doc.layers.first().and_then(|l| l.children()).cloned().unwrap_or_default()
|
||||
doc.layers.first().and_then(|l| l.children()).into_iter().flatten().filter(|c| !c.is_ruler_guide()).cloned().collect()
|
||||
}
|
||||
|
||||
/// The ruler guides of `doc` by their line, artboard and layer.
|
||||
fn guide_lines(doc: &Document) -> Vec<Guide> {
|
||||
doc.ruler_guides().into_iter().map(|g| Guide { artboard: g.artboard, layer: g.layer, ..Guide::new(g.vertical, g.pos) }).collect()
|
||||
}
|
||||
|
||||
fn fill(n: &Node) -> Paint {
|
||||
@@ -8459,17 +8464,16 @@ fn canvas_wide_guide_paths_are_ruler_guides_of_their_layer() {
|
||||
);
|
||||
let doc = read(canvas_stream(&layer(&body)).as_bytes()).unwrap().doc;
|
||||
let top = &doc.layers[0];
|
||||
let kids = top.children().unwrap();
|
||||
let kids = art(&doc);
|
||||
assert_eq!(kids.len(), 2, "the square and the sublayer");
|
||||
assert!(matches!(kids[0].kind, NodeKind::Path { guide: false, .. }));
|
||||
let sub = &kids[1];
|
||||
assert!(sub.children().unwrap().is_empty());
|
||||
let want = [
|
||||
Guide { vertical: false, pos: 60.0, artboard: None, layer: Some(top.id) },
|
||||
Guide { vertical: true, pos: 25.0, artboard: None, layer: Some(top.id) },
|
||||
Guide { vertical: false, pos: 10.0, artboard: None, layer: Some(sub.id) },
|
||||
];
|
||||
assert_eq!(doc.guides, want);
|
||||
assert!(sub.children().unwrap().iter().all(|c| c.is_ruler_guide()));
|
||||
let at = |vertical, pos, layer| Guide { layer: Some(layer), ..Guide::new(vertical, pos) };
|
||||
let want = [at(false, 60.0, top.id), at(true, 25.0, top.id), at(false, 10.0, sub.id)];
|
||||
assert_eq!(guide_lines(&doc), want);
|
||||
// The guides sit above the layers' art.
|
||||
assert!(top.children().unwrap()[2..].iter().all(|c| c.is_ruler_guide()));
|
||||
}
|
||||
|
||||
/// #779: a straight `(N) *` guide path from edge to edge of one artboard is a ruler guide of that
|
||||
@@ -8478,7 +8482,7 @@ fn canvas_wide_guide_paths_are_ruler_guides_of_their_layer() {
|
||||
fn artboard_wide_guide_paths_are_ruler_guides_of_their_artboard() {
|
||||
// The crop marks' artboard, 0 0 200 100.
|
||||
let doc = read(canvas_stream(&layer(&guide_line(0.0, 30.0, 200.0, 30.0, "N"))).as_bytes()).unwrap().doc;
|
||||
assert_eq!(doc.guides, [Guide { vertical: false, pos: 70.0, artboard: Some(1), layer: Some(doc.layers[0].id) }]);
|
||||
assert_eq!(guide_lines(&doc), [Guide { artboard: Some(1), layer: Some(doc.layers[0].id), ..Guide::new(false, 70.0) }]);
|
||||
assert!(art(&doc).is_empty());
|
||||
// The second of two artboards: art space x 400 to 600, y -150 to 0.
|
||||
let boards = "%_/Document :\n%_/Dictionary :\n%_/Array :\n\
|
||||
@@ -8487,7 +8491,7 @@ fn artboard_wide_guide_paths_are_ruler_guides_of_their_artboard() {
|
||||
%_; (ArtboardArray) ,\n%_; /NotRecorded ,\n%_;\n";
|
||||
let doc = read_ok(&format!("{boards}{}", layer(&guide_line(500.0, 0.0, 500.0, -150.0, "N")))).doc;
|
||||
assert_eq!(doc.artboards[1].id, 2);
|
||||
assert_eq!(doc.guides, [Guide { vertical: true, pos: 500.0, artboard: Some(2), layer: Some(doc.layers[0].id) }]);
|
||||
assert_eq!(guide_lines(&doc), [Guide { artboard: Some(2), layer: Some(doc.layers[0].id), ..Guide::new(true, 500.0) }]);
|
||||
}
|
||||
|
||||
/// Guide paths that aren't ruler guides stay guide paths: `(S) *` ones (guides made from objects),
|
||||
@@ -8506,7 +8510,7 @@ fn other_guide_paths_stay_guide_paths() {
|
||||
];
|
||||
for body in cases {
|
||||
let doc = read(canvas_stream(&layer(&body)).as_bytes()).unwrap().doc;
|
||||
assert!(doc.guides.is_empty(), "{body}");
|
||||
assert!(doc.ruler_guides().is_empty(), "{body}");
|
||||
assert_eq!(art(&doc).len(), 1, "{body}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1669,7 +1669,7 @@ fn page_boards(doc: &Document, visible: &Document, first: usize, pages: &[Rect])
|
||||
/// Drops the guides of artboards `doc` no longer has: an artboard guide goes with its artboard.
|
||||
fn drop_guides_off_boards(doc: &mut Document) {
|
||||
let kept: Vec<u32> = doc.artboards.iter().map(|a| a.id).collect();
|
||||
doc.guides.retain(|g| g.artboard.is_none_or(|id| kept.contains(&id)));
|
||||
doc.retain_guides(&mut vectorcraft_doc::Selection::default(), |g| g.artboard.is_none_or(|id| kept.contains(&id)));
|
||||
}
|
||||
|
||||
/// Draw `doc`'s layers, artboards and guides `k` times as large, from the origin, with their
|
||||
@@ -1682,13 +1682,12 @@ pub(crate) fn scale_document(doc: &mut Document, k: f64, styles: bool) {
|
||||
let layer = Arc::make_mut(layer);
|
||||
layer.transform(a, scaling);
|
||||
scale_wrap_offsets(layer, k);
|
||||
// A transform leaves ruler guides where they are: their positions scale here.
|
||||
layer.map_ruler_guide_positions(&mut |_, pos| *pos *= k);
|
||||
}
|
||||
for board in &mut doc.artboards {
|
||||
board.rect = a.transform_rect_bbox(board.rect);
|
||||
}
|
||||
for guide in &mut doc.guides {
|
||||
guide.pos *= k;
|
||||
}
|
||||
if styles {
|
||||
for g in doc.graphic_styles.iter_mut() {
|
||||
g.appearance.scale_strokes(k);
|
||||
|
||||
@@ -122,7 +122,7 @@ pub(super) fn comparison_samples(doc: &Document) -> u32 {
|
||||
let (mut images, mut vector) = (false, false);
|
||||
doc.walk(|n| match n.kind {
|
||||
NodeKind::Image(_) => images = true,
|
||||
NodeKind::Layer { .. } | NodeKind::Group { .. } | NodeKind::Path { clipping: true, .. } => {}
|
||||
NodeKind::Layer { .. } | NodeKind::Group { .. } | NodeKind::Path { clipping: true, .. } | NodeKind::RulerGuide { .. } => {}
|
||||
_ => vector = true,
|
||||
});
|
||||
if images && !vector { 4 } else { 1 }
|
||||
|
||||
@@ -77,6 +77,14 @@ fn names(d: &Document) -> Vec<String> {
|
||||
d.layers.iter().map(|l| l.name.clone().unwrap_or_default()).collect()
|
||||
}
|
||||
|
||||
/// The ruler guides of `d` by their line, artboard and layer.
|
||||
fn guide_lines(d: &Document) -> Vec<vectorcraft_doc::Guide> {
|
||||
d.ruler_guides()
|
||||
.into_iter()
|
||||
.map(|g| vectorcraft_doc::Guide { artboard: g.artboard, layer: g.layer, ..vectorcraft_doc::Guide::new(g.vertical, g.pos) })
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ruler_units_in_the_program_header_alone_are_kept() {
|
||||
let art = layer("Art", true, &square(10, 10));
|
||||
@@ -2219,11 +2227,11 @@ fn an_ai_files_ruler_guides_come_in_on_their_layers() {
|
||||
assert!(read, "{w:?}");
|
||||
let jig = d.layers[0].id;
|
||||
let want = [
|
||||
vectorcraft_doc::Guide { vertical: false, pos: 60.0 * k, artboard: None, layer: Some(jig) },
|
||||
vectorcraft_doc::Guide { vertical: true, pos: 20.0 * k, artboard: Some(d.artboards[0].id), layer: Some(jig) },
|
||||
vectorcraft_doc::Guide { layer: Some(jig), ..vectorcraft_doc::Guide::new(false, 60.0 * k) },
|
||||
vectorcraft_doc::Guide { artboard: Some(d.artboards[0].id), layer: Some(jig), ..vectorcraft_doc::Guide::new(true, 20.0 * k) },
|
||||
];
|
||||
assert_eq!(d.guides, want);
|
||||
assert_eq!(d.layers[0].children().map_or(0, |c| c.len()), 1, "the square alone");
|
||||
assert_eq!(guide_lines(&d), want);
|
||||
assert_eq!(d.layers[0].children().map_or(0, |c| c.iter().filter(|c| !c.is_ruler_guide()).count()), 1, "the square alone");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2241,7 +2249,10 @@ fn an_eps_of_its_art_box_keeps_the_guides_of_its_first_artboard() {
|
||||
assert_eq!(names(&r.document), ["Art"], "{:?}", r.warnings);
|
||||
assert_eq!(r.document.artboards.len(), 1);
|
||||
let (board, layer) = (r.document.artboards[0].id, r.document.layers[0].id);
|
||||
assert_eq!(r.document.guides, [vectorcraft_doc::Guide { vertical: true, pos: 25.0, artboard: Some(board), layer: Some(layer) }]);
|
||||
assert_eq!(
|
||||
guide_lines(&r.document),
|
||||
[vectorcraft_doc::Guide { artboard: Some(board), layer: Some(layer), ..vectorcraft_doc::Guide::new(true, 25.0) }]
|
||||
);
|
||||
}
|
||||
|
||||
/// A PDF part read from a `UserUnit` page cropped to the art: the art's box, not the artboard, is
|
||||
|
||||
@@ -152,7 +152,9 @@ impl<'a> Writer<'a> {
|
||||
self.locked = false;
|
||||
self.line("0 A");
|
||||
self.in_hidden = !n.visible;
|
||||
self.contents(children, *clip, false);
|
||||
// Ruler guides are kept in the document's native copy only.
|
||||
let art: Vec<Arc<Node>> = children.iter().filter(|c| !c.is_ruler_guide()).cloned().collect();
|
||||
self.contents(&art, *clip, false);
|
||||
self.in_hidden = false;
|
||||
self.line("LB\n%AI5_EndLayer--");
|
||||
true
|
||||
@@ -209,6 +211,10 @@ impl<'a> Writer<'a> {
|
||||
|
||||
/// One object (inside a container locked when `locked`).
|
||||
fn node(&mut self, n: &Node, locked: bool) {
|
||||
// Ruler guides are kept in the document's native copy only.
|
||||
if n.is_ruler_guide() {
|
||||
return;
|
||||
}
|
||||
if !n.visible && !self.in_hidden {
|
||||
match self.hidden {
|
||||
HiddenObjects::Omit => {
|
||||
@@ -292,6 +298,8 @@ impl<'a> Writer<'a> {
|
||||
self.nest -= 1;
|
||||
}
|
||||
}
|
||||
// Ruler guides are kept in the document's native copy only.
|
||||
NodeKind::RulerGuide { .. } => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -389,6 +389,8 @@ impl<'a> Scene<'a> {
|
||||
self.node(c, false);
|
||||
}
|
||||
}
|
||||
// Ruler guides draw on screen only.
|
||||
NodeKind::RulerGuide { .. } => {}
|
||||
}
|
||||
if self.body.len() > MAX_BODY {
|
||||
self.body.truncate(was);
|
||||
|
||||
@@ -239,6 +239,9 @@ struct Body {
|
||||
fn body(doc: &Document, o: &SaveOptions) -> Result<Body, FormatError> {
|
||||
let version = o.version;
|
||||
let mut d = doc.without_edit_modes().into_owned();
|
||||
// Ruler guides are saved in the document's `guides` list, as before they were nodes, so every
|
||||
// version reads them.
|
||||
d.guides_to_saved();
|
||||
// Older apps don't know placed documents: they get the art the objects show, with its
|
||||
// resources. Otherwise resources only exporting adds are never saved.
|
||||
if version < VERSION && d.has_placed() {
|
||||
@@ -602,6 +605,12 @@ fn load_file_within(bytes: &[u8], max_nodes: usize, max_values: usize) -> Result
|
||||
)));
|
||||
}
|
||||
depth_notes.extend(beyond.note());
|
||||
// Untrusted: ruler guide nodes only directly in layers, above their art (after the depth limit:
|
||||
// the walk recurses).
|
||||
doc.tidy_ruler_guides();
|
||||
// The saved ruler guides become nodes on their layers (after the guides beyond the canvas are
|
||||
// dropped, and after `fix_next_id`: new ids start above the file's).
|
||||
doc.guides_into_nodes();
|
||||
// Untrusted: the type's sizes, leading, baseline shifts and scales, its indents and paragraph
|
||||
// spacing, and its tab stops, each within its range (after the depth limit: the walk recurses).
|
||||
doc.keep_type_values_in_range();
|
||||
@@ -1203,6 +1212,66 @@ mod tests {
|
||||
assert_eq!(load(&bytes).unwrap().node_count(), n, "the real budget opens ordinary documents");
|
||||
}
|
||||
|
||||
/// The saved ruler guides become objects when read, so each counts against the same budget.
|
||||
#[test]
|
||||
fn saved_ruler_guides_count_against_the_object_budget() {
|
||||
let mut d = Document::new(100.0, 100.0);
|
||||
let l = d.add_layer(None);
|
||||
let id = d.alloc_id();
|
||||
d.insert(Some(l), 0, Node::path(id, shapes::ellipse(Rect::new(0.0, 0.0, 10.0, 10.0)), Appearance::default_art())).unwrap();
|
||||
for i in 0..50 {
|
||||
d.add_ruler_guide(&vectorcraft_doc::Guide::new(i % 2 == 0, f64::from(i)));
|
||||
}
|
||||
let n = d.node_count() + d.ruler_guides().len();
|
||||
let bytes = save_with(&d, &SaveOptions { compress: true, ..SaveOptions::default() }).unwrap();
|
||||
let opened = load_file_within(&bytes, n, vectorcraft_doc::node::MAX_BUFFERED_VALUES).unwrap().doc;
|
||||
assert_eq!(opened.ruler_guides().len(), 50, "a document of exactly the budget opens with its guides");
|
||||
let Err(FormatError::NotVectorcraft(why)) = load_file_within(&bytes, n - 1, vectorcraft_doc::node::MAX_BUFFERED_VALUES) else {
|
||||
panic!("one guide past the budget must be refused")
|
||||
};
|
||||
assert!(why.contains(&format!("more than {} objects", n - 1)), "{why}");
|
||||
}
|
||||
|
||||
/// Ruler guide nodes a file holds out of place are put right as it is read: one below a layer's
|
||||
/// art moves up to the layer's guides, and one in a group, beside the layers or in a symbol is
|
||||
/// removed. Saving again writes no ruler guide node, which builds from before ruler guides
|
||||
/// were nodes could not read.
|
||||
#[test]
|
||||
fn ruler_guide_nodes_out_of_place_in_a_file_are_put_right() {
|
||||
let mut d = Document::new(100.0, 100.0);
|
||||
let l = d.add_layer(None);
|
||||
let (a, b, group) = (d.alloc_id(), d.alloc_id(), d.alloc_id());
|
||||
let path = |id| Node::path(id, shapes::ellipse(Rect::new(0.0, 0.0, 10.0, 10.0)), Appearance::default_art());
|
||||
d.insert(Some(l), 0, path(a)).unwrap();
|
||||
d.insert(Some(l), usize::MAX, Node::group(group, vec![Arc::new(path(b))])).unwrap();
|
||||
let li = d.layers.iter().position(|x| x.id == l).unwrap();
|
||||
let mut v: serde_json::Value = serde_json::from_slice(&save(&d, false)).unwrap();
|
||||
let guide = |id: u64| serde_json::json!({"id": id, "kind": {"type": "rulerguide", "vertical": true, "pos": 5.0}});
|
||||
let kids = &mut v["document"]["layers"][li]["kind"]["children"];
|
||||
kids.as_array_mut().unwrap().insert(0, guide(50));
|
||||
kids[2]["kind"]["children"].as_array_mut().unwrap().push(guide(51));
|
||||
v["document"]["layers"].as_array_mut().unwrap().push(guide(52));
|
||||
let opened = load(&serde_json::to_vec(&v).unwrap()).unwrap();
|
||||
let listed: Vec<u64> = opened.ruler_guides().iter().filter_map(|g| g.id).map(|id| id.0).collect();
|
||||
assert_eq!(listed, [50]);
|
||||
let layer = opened.layers.iter().find(|x| x.id == l).unwrap();
|
||||
assert_eq!(layer.children().unwrap().iter().map(|c| c.id.0).collect::<Vec<_>>(), [a.0, group.0, 50], "art first, then the guide");
|
||||
assert_eq!(opened.node(group).unwrap().children().unwrap().len(), 1, "the group keeps only its path");
|
||||
assert!(opened.layers.iter().all(|x| !x.is_ruler_guide()));
|
||||
assert!(!String::from_utf8(save(&opened, false)).unwrap().contains("rulerguide"));
|
||||
|
||||
// A ruler guide in a symbol's art is removed too.
|
||||
let mut d = Document::new(100.0, 100.0);
|
||||
let sym = d.alloc_id();
|
||||
let art = Node::group(sym, vec![Arc::new(path(d.alloc_id()))]);
|
||||
d.symbols.push(vectorcraft_doc::Symbol { name: "S".into(), art: Arc::new(art), dynamic: false });
|
||||
let mut v: serde_json::Value = serde_json::from_slice(&save(&d, false)).unwrap();
|
||||
v["document"]["symbols"][0]["art"]["kind"]["children"].as_array_mut().unwrap().push(guide(60));
|
||||
let opened = load(&serde_json::to_vec(&v).unwrap()).unwrap();
|
||||
assert_eq!(opened.symbols[0].art.children().unwrap().len(), 1);
|
||||
assert!(!opened.symbols[0].art.children().unwrap().iter().any(|c| c.is_ruler_guide()));
|
||||
}
|
||||
|
||||
/// #3587: a reordered file (its objects' fields before their `type`, as a JSON tool that
|
||||
/// sorts keys writes them) still opens; one whose fields before a `type` hold more values than
|
||||
/// the budget, such as a long array of numbers, is refused as they are read.
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
|
||||
mod links;
|
||||
mod native_guides;
|
||||
mod native_v3;
|
||||
mod native_v4;
|
||||
mod save_options;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user