mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-14 21:44:57 +00:00
Merge pull request #7449 from dotCooCoo/fix/links-stored-brush-art
Links: placed images in stored brush art get Links rows
This commit is contained in:
@@ -13,6 +13,7 @@
|
||||
mod calli;
|
||||
mod colorize;
|
||||
mod defaults;
|
||||
pub mod stored_art;
|
||||
pub mod track;
|
||||
mod warp;
|
||||
|
||||
@@ -799,9 +800,15 @@ fn number_repeated_names(lib: &mut [Brush]) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Store `lib` as the document's brush library.
|
||||
/// Store `lib` as the document's brush library. When the art of a brush holds images or placed
|
||||
/// documents, each object in that art whose id is 0 or repeats another object's is given a new
|
||||
/// id ([`stored_art::StoredArt::persist_ids`]), so the Links panel lists it by the same id until it
|
||||
/// changes.
|
||||
pub fn store(doc: &mut Document, lib: &[Brush]) {
|
||||
doc.unknown.insert(DOC_KEY.into(), serde_json::to_value(lib).unwrap_or_default());
|
||||
if lib.iter().any(|b| b.art().into_iter().any(stored_art::holds_files)) {
|
||||
stored_art::StoredArt::persist_ids(doc);
|
||||
}
|
||||
}
|
||||
|
||||
/// Find a brush by name.
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
//! The art of the stored brushes (a scatter or art brush's art, a pattern brush's tiles) as stored
|
||||
//! art of the document ([`Document::mounted_art`]): set on the document while a command looks it up
|
||||
//! by id or changes it, then written back into the library entries whose art changed. The Links
|
||||
//! panel finds the images and placed documents in that art this way.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::{Document, Node, NodeKind};
|
||||
|
||||
use crate::{Brush, BrushKind, DOC_KEY};
|
||||
|
||||
/// The brushes of a document's stored library whose art holds images or placed documents.
|
||||
#[derive(Default)]
|
||||
pub struct StoredArt {
|
||||
entries: Vec<Entry>,
|
||||
/// How many entries the stored library had when it was read.
|
||||
read_entries: usize,
|
||||
}
|
||||
|
||||
/// A library entry whose brush's art holds images or placed documents.
|
||||
struct Entry {
|
||||
/// Its index in the stored library.
|
||||
slot: usize,
|
||||
/// The entry as it was read.
|
||||
raw: Value,
|
||||
brush: Brush,
|
||||
/// The brush's art ([`Brush::art`]), each root with the `owner` the Links panel names for it.
|
||||
art: Vec<(Arc<Node>, Value)>,
|
||||
}
|
||||
|
||||
/// What names each root of [`Brush::art`] among the brush's art: a pattern brush's tile, in the
|
||||
/// same order; nothing for the one root of a scatter or art brush.
|
||||
fn tiles(b: &Brush) -> Vec<Option<&'static str>> {
|
||||
match &b.kind {
|
||||
BrushKind::Pattern(p) => std::iter::once(Some("side"))
|
||||
.chain(
|
||||
[
|
||||
(p.outer_corner.is_some(), "outerCorner"),
|
||||
(p.inner_corner.is_some(), "innerCorner"),
|
||||
(p.start.is_some(), "start"),
|
||||
(p.end.is_some(), "end"),
|
||||
]
|
||||
.into_iter()
|
||||
.filter(|(held, _)| *held)
|
||||
.map(|(_, tile)| Some(tile)),
|
||||
)
|
||||
.collect(),
|
||||
_ => vec![None],
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether `n` or its subtree, opacity-mask art included, holds an image or a placed document.
|
||||
pub(crate) fn holds_files(n: &Node) -> bool {
|
||||
let mut found = false;
|
||||
vectorcraft_doc::swatches::walk_with_masks(n, &mut |c| found |= matches!(c.kind, NodeKind::Image(_) | NodeKind::PlacedDocument(_)));
|
||||
found
|
||||
}
|
||||
|
||||
impl StoredArt {
|
||||
/// The brushes of `doc`'s stored library whose art holds an image or a placed document (opacity
|
||||
/// masks included). An entry that does not read as a brush is left out, and so is left as it
|
||||
/// is. A root's owner is
|
||||
/// `{kind: "brush", name, index, tile?}`, with the name and index the brush has in the library as
|
||||
/// the Brushes panel lists it ([`crate::library`], a repeated name numbered).
|
||||
pub fn read(doc: &Document) -> Self {
|
||||
let Some(stored) = doc.unknown.get(DOC_KEY) else { return Self::default() };
|
||||
let Some(list) = stored.as_array() else { return Self::default() };
|
||||
let listed = crate::parse_library(stored);
|
||||
let parsed = list.iter().enumerate().filter_map(|(slot, raw)| Brush::from_json(raw.clone()).ok().map(|b| (slot, raw, b)));
|
||||
let mut entries = vec![];
|
||||
for (index, (slot, raw, brush)) in parsed.enumerate() {
|
||||
if !brush.art().into_iter().any(holds_files) {
|
||||
continue;
|
||||
}
|
||||
let name = listed.get(index).map_or(brush.name.as_str(), |b| b.name.as_str());
|
||||
let art: Vec<(Arc<Node>, Value)> = brush
|
||||
.art()
|
||||
.into_iter()
|
||||
.zip(tiles(&brush))
|
||||
.map(|(n, tile)| {
|
||||
let mut owner = json!({ "kind": "brush", "name": name, "index": index });
|
||||
if let Some(tile) = tile {
|
||||
owner["tile"] = json!(tile);
|
||||
}
|
||||
(Arc::new(n.clone()), owner)
|
||||
})
|
||||
.collect();
|
||||
if !art.is_empty() {
|
||||
entries.push(Entry { slot, raw: raw.clone(), brush, art });
|
||||
}
|
||||
}
|
||||
Self { entries, read_entries: list.len() }
|
||||
}
|
||||
|
||||
/// Gives the objects of the stored brushes' art that holds images or placed documents ids of
|
||||
/// their own in `doc`'s library, as [`Self::mount`] gives them, and writes the brushes whose
|
||||
/// ids changed back. Does nothing while art is set on `doc` ([`Document::mounted_art`]).
|
||||
pub fn persist_ids(doc: &mut Document) {
|
||||
if !doc.mounted_art.is_empty() {
|
||||
return;
|
||||
}
|
||||
let art = Self::read(doc);
|
||||
if !art.is_empty() {
|
||||
art.mount(doc);
|
||||
art.write_entries(doc);
|
||||
}
|
||||
}
|
||||
|
||||
/// No stored brush's art holds an image or a placed document.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.entries.is_empty()
|
||||
}
|
||||
|
||||
/// Sets the art on `doc` as [`Document::mounted_art`], each object with an id of its own
|
||||
/// ([`Document::number_mounted_art`]: an id another stored object has, or none, is replaced, and
|
||||
/// the id counter stands above them all).
|
||||
pub fn mount(&self, doc: &mut Document) {
|
||||
doc.mounted_art = self.entries.iter().flat_map(|e| e.art.iter().cloned()).collect();
|
||||
doc.number_mounted_art();
|
||||
}
|
||||
|
||||
/// Takes [`Document::mounted_art`] off `doc` and writes each brush whose art changed (new ids
|
||||
/// included) back into its library entry: the entry at its index when that entry is as it was
|
||||
/// read or reads as the brush as it was apart from its name, else the first entry that reads as
|
||||
/// the brush as it was, else the first that does apart from its name. An entry found apart from
|
||||
/// its name keeps that name (the library was written again meanwhile, as placing a document's
|
||||
/// brushes into it does, numbering a repeated name). The other entries are left as
|
||||
/// they are. Then the art of brushes added meanwhile gets ids of its own
|
||||
/// ([`Self::persist_ids`]).
|
||||
pub fn write_back(self, doc: &mut Document) {
|
||||
let read_entries = self.read_entries;
|
||||
self.write_entries(doc);
|
||||
if doc.unknown.get(DOC_KEY).and_then(Value::as_array).map(Vec::len) != Some(read_entries) {
|
||||
Self::persist_ids(doc);
|
||||
}
|
||||
}
|
||||
|
||||
/// [`Self::write_back`] without giving ids to the art of brushes added meanwhile.
|
||||
fn write_entries(self, doc: &mut Document) {
|
||||
let mut mounted = std::mem::take(&mut doc.mounted_art).into_iter();
|
||||
let mut changed = vec![];
|
||||
for e in self.entries {
|
||||
let now: Vec<Arc<Node>> = mounted.by_ref().take(e.art.len()).map(|(n, _)| n).collect();
|
||||
if now.len() != e.art.len() || now.iter().zip(&e.art).all(|(a, (b, _))| Arc::ptr_eq(a, b)) {
|
||||
continue;
|
||||
}
|
||||
let mut brush = e.brush.clone();
|
||||
for (slot, art) in brush.art_mut().into_iter().zip(now) {
|
||||
*slot = Arc::unwrap_or_clone(art);
|
||||
}
|
||||
changed.push((e.slot, e.raw, e.brush, brush));
|
||||
}
|
||||
let Some(list) = doc.unknown.get_mut(DOC_KEY).and_then(Value::as_array_mut) else { return };
|
||||
for (slot, raw, was, mut brush) in changed {
|
||||
// The entry's name when it reads as `was` apart from its name.
|
||||
let name_of = |entry: &Value| {
|
||||
Brush::from_json(entry.clone()).ok().and_then(|mut b| {
|
||||
let name = std::mem::replace(&mut b.name, was.name.clone());
|
||||
(b == was).then_some(name)
|
||||
})
|
||||
};
|
||||
let at = match list.get(slot) {
|
||||
Some(entry) if *entry == raw || name_of(entry).is_some() => Some(slot),
|
||||
_ => list.iter().position(|e| name_of(e).as_ref() == Some(&was.name)).or_else(|| list.iter().position(|e| name_of(e).is_some())),
|
||||
};
|
||||
let Some(entry) = at.and_then(|i| list.get_mut(i)) else { continue };
|
||||
if let Some(name) = name_of(entry) {
|
||||
brush.name = name;
|
||||
}
|
||||
if let Ok(v) = serde_json::to_value(&brush) {
|
||||
*entry = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1017,6 +1017,159 @@ fn repeated_brush_names_are_numbered_when_read() {
|
||||
assert_eq!(names.iter().collect::<std::collections::HashSet<_>>().len(), names.len());
|
||||
}
|
||||
|
||||
/// Art of `w`×`h` points holding a linked image: a group of [`rect_art`] and the image, both with id 0.
|
||||
fn linked_art(w: f64, h: f64) -> Node {
|
||||
let image = Node::new(
|
||||
NodeId(0),
|
||||
NodeKind::Image(Box::new(vectorcraft_doc::ImageObject {
|
||||
key: "k".into(),
|
||||
width: 4,
|
||||
height: 4,
|
||||
xf: Affine::IDENTITY,
|
||||
link: Some(vectorcraft_doc::LinkInfo::new("/art/a.png")),
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
})),
|
||||
);
|
||||
Node::group(NodeId(0), vec![std::sync::Arc::new(rect_art(w, h)), std::sync::Arc::new(image)])
|
||||
}
|
||||
|
||||
/// The art of the stored brushes that holds images mounts on the document as stored art, each tile
|
||||
/// with the brush, its place in the library and the tile as owner, its objects with ids of their
|
||||
/// own; writing it back changes only the entries whose art changed, and leaves an entry that does
|
||||
/// not read as a brush, a brush without art and a brush whose art holds no image as they were stored.
|
||||
#[test]
|
||||
fn stored_brush_art_mounts_and_writes_back_only_what_changed() {
|
||||
use stored_art::StoredArt;
|
||||
let mut d = Document::new(100.0, 100.0);
|
||||
let layer_object = d.layers[0].id;
|
||||
let mut side = linked_art(10.0, 4.0);
|
||||
side.id = layer_object;
|
||||
let pattern =
|
||||
Brush { name: "Frame".into(), kind: BrushKind::Pattern(PatternBrush { side, start: Some(linked_art(4.0, 4.0)), ..Default::default() }) };
|
||||
let scatter = Brush {
|
||||
name: "Dots".into(),
|
||||
kind: BrushKind::Scatter(Scatter { art: linked_art(2.0, 2.0), colorization: Colorization::None, ..Default::default() }),
|
||||
};
|
||||
// Written as a file holds it, without the ids `store` gives.
|
||||
let lib = [calli(0.0, 100.0, 3.0), pattern, scatter, art_brush(rect_art(10.0, 4.0), ArtScale::Stretch)];
|
||||
d.unknown.insert(DOC_KEY.into(), serde_json::to_value(lib).unwrap());
|
||||
let odd = serde_json::json!({ "name": "Odd", "type": "unknown" });
|
||||
if let Some(list) = d.unknown.get_mut(DOC_KEY).and_then(serde_json::Value::as_array_mut) {
|
||||
list.insert(1, odd.clone());
|
||||
}
|
||||
let stored = d.unknown[DOC_KEY].clone();
|
||||
let art = StoredArt::read(&d);
|
||||
art.mount(&mut d);
|
||||
let owners: Vec<&serde_json::Value> = d.mounted_art.iter().map(|(_, o)| o).collect();
|
||||
assert_eq!(
|
||||
owners,
|
||||
[
|
||||
&serde_json::json!({ "kind": "brush", "name": "Frame", "index": 1, "tile": "side" }),
|
||||
&serde_json::json!({ "kind": "brush", "name": "Frame", "index": 1, "tile": "start" }),
|
||||
&serde_json::json!({ "kind": "brush", "name": "Dots", "index": 2 }),
|
||||
]
|
||||
);
|
||||
let ids: Vec<NodeId> = d.mounted_art.iter().map(|(n, _)| n.id).collect();
|
||||
let mut all = vec![];
|
||||
d.visit_stored(|n, _| all.push(n.id));
|
||||
assert!(all.iter().all(|id| id.0 != 0) && !all[1..].contains(&layer_object), "{all:?}");
|
||||
assert_eq!(all.iter().collect::<std::collections::HashSet<_>>().len(), all.len(), "{all:?}");
|
||||
// The scatter brush's art changes; the pattern brush's tiles keep the ids mounting gave them.
|
||||
d.stored_node_mut(ids[2]).unwrap().name = Some("changed".into());
|
||||
art.write_back(&mut d);
|
||||
assert!(d.mounted_art.is_empty());
|
||||
let now = d.unknown[DOC_KEY].as_array().unwrap();
|
||||
assert_eq!(now.len(), 5);
|
||||
assert_eq!((&now[0], &now[1], &now[4]), (&stored[0], &odd, &stored[4]), "the entries without images stay as stored");
|
||||
assert_ne!(now[2], stored[2], "the pattern brush keeps the ids its art was given");
|
||||
let lib = library(&d);
|
||||
let Some(BrushKind::Pattern(p)) = lib.iter().find(|b| b.name == "Frame").map(|b| &b.kind) else { panic!("no pattern brush") };
|
||||
assert_eq!((p.side.id, p.start.as_ref().map(|n| n.id)), (ids[0], Some(ids[1])));
|
||||
assert_eq!(find(&d, "Dots").unwrap().art()[0].name.as_deref(), Some("changed"));
|
||||
// Mounting again gives the same ids and changes nothing, so writing back leaves the library as it is.
|
||||
let again = d.unknown[DOC_KEY].clone();
|
||||
let art = StoredArt::read(&d);
|
||||
art.mount(&mut d);
|
||||
assert_eq!(d.mounted_art.iter().map(|(n, _)| n.id).collect::<Vec<_>>(), ids);
|
||||
art.write_back(&mut d);
|
||||
assert_eq!(d.unknown[DOC_KEY], again);
|
||||
}
|
||||
|
||||
/// A brush whose stored name repeats an earlier brush's is named in its art's owner as the Brushes
|
||||
/// panel lists it, numbered.
|
||||
#[test]
|
||||
fn stored_brush_art_owner_names_a_repeated_name_as_listed() {
|
||||
use stored_art::StoredArt;
|
||||
let mut d = Document::new(100.0, 100.0);
|
||||
let scatter = Brush { name: "Dots".into(), kind: BrushKind::Scatter(Scatter { art: linked_art(2.0, 2.0), ..Default::default() }) };
|
||||
store(&mut d, &[scatter.clone(), scatter]);
|
||||
StoredArt::read(&d).mount(&mut d);
|
||||
let names: Vec<&serde_json::Value> = d.mounted_art.iter().map(|(_, o)| &o["name"]).collect();
|
||||
assert_eq!(names, ["Dots", "Dots 2"]);
|
||||
}
|
||||
|
||||
/// Storing a library whose brush art holds an image gives that art ids of its own, so mounting it
|
||||
/// later gives the same ids whatever the document allocated in between; art without images keeps
|
||||
/// the ids it was stored with.
|
||||
#[test]
|
||||
fn storing_brush_art_with_images_keeps_ids_of_its_own() {
|
||||
use stored_art::StoredArt;
|
||||
let mut d = Document::new(100.0, 100.0);
|
||||
let mut art = linked_art(2.0, 2.0);
|
||||
art.id = d.layers[0].id;
|
||||
let scatter = Brush { name: "Dots".into(), kind: BrushKind::Scatter(Scatter { art, ..Default::default() }) };
|
||||
store(&mut d, &[scatter, art_brush(rect_art(10.0, 4.0), ArtScale::Stretch)]);
|
||||
let lib = library(&d);
|
||||
let stored = lib[0].art()[0].id;
|
||||
assert_ne!(stored, d.layers[0].id);
|
||||
assert_eq!(lib[1].art()[0].id, NodeId(0), "art without images is stored as it was");
|
||||
d.alloc_id();
|
||||
StoredArt::read(&d).mount(&mut d);
|
||||
assert_eq!(d.mounted_art[0].0.id, stored);
|
||||
}
|
||||
|
||||
/// When the library is written again while the art is mounted, numbering a repeated name and
|
||||
/// adding a brush (as placing a document's brushes into it does), writing back changes the entry
|
||||
/// at its own place that reads as the brush apart from its new name, keeping that name, even when
|
||||
/// an earlier brush is the same as the one it was, and gives the added brush's art ids of its own.
|
||||
#[test]
|
||||
fn brush_art_written_back_finds_its_renamed_entry_and_numbers_added_art() {
|
||||
use stored_art::StoredArt;
|
||||
/// `n` with ids from `next` on, in paint order.
|
||||
fn numbered(mut n: Node, next: &mut u64) -> Node {
|
||||
n.id = NodeId(*next);
|
||||
*next += 1;
|
||||
for c in n.children_mut().into_iter().flatten() {
|
||||
*c = std::sync::Arc::new(numbered((**c).clone(), next));
|
||||
}
|
||||
n
|
||||
}
|
||||
let mut d = Document::new(100.0, 100.0);
|
||||
let scatter =
|
||||
|n: &str| Brush { name: n.into(), kind: BrushKind::Scatter(Scatter { art: numbered(linked_art(2.0, 2.0), &mut 50), ..Default::default() }) };
|
||||
// Two brushes the same apart from the ids mounting gives the second.
|
||||
d.unknown.insert(DOC_KEY.into(), serde_json::to_value([scatter("Dots"), scatter("Dots")]).unwrap());
|
||||
let art = StoredArt::read(&d);
|
||||
art.mount(&mut d);
|
||||
assert_eq!(d.mounted_art[0].0.id, NodeId(50));
|
||||
let second = d.mounted_art[1].0.id;
|
||||
// The library is written again: the repeated name numbered, a brush whose art repeats an id added.
|
||||
let mut added = scatter("New");
|
||||
if let BrushKind::Scatter(s) = &mut added.kind {
|
||||
s.art.id = d.layers[0].id;
|
||||
}
|
||||
let lib = [library(&d), vec![added]].concat();
|
||||
d.unknown.insert(DOC_KEY.into(), serde_json::to_value(&lib).unwrap());
|
||||
assert_eq!(lib[1].name, "Dots 2");
|
||||
d.stored_node_mut(second).unwrap().name = Some("changed".into());
|
||||
art.write_back(&mut d);
|
||||
let lib = library(&d);
|
||||
assert_eq!(lib.iter().map(|b| b.name.as_str()).collect::<Vec<_>>(), ["Dots", "Dots 2", "New"]);
|
||||
assert_eq!((lib[0].art()[0].name.as_deref(), lib[1].art()[0].name.as_deref()), (None, Some("changed")));
|
||||
assert_ne!(lib[2].art()[0].id, d.layers[0].id, "the added brush's art has ids of its own");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn brushes_serde_round_trip() {
|
||||
let lib = defaults().to_vec();
|
||||
|
||||
@@ -79,6 +79,7 @@ impl Document {
|
||||
index: _,
|
||||
guide_cache: _,
|
||||
inline_cycles: _,
|
||||
mounted_art: _,
|
||||
} = self;
|
||||
*version == other.version
|
||||
&& *title == other.title
|
||||
@@ -264,6 +265,7 @@ impl Document {
|
||||
index: Default::default(),
|
||||
guide_cache: Default::default(),
|
||||
inline_cycles: Default::default(),
|
||||
mounted_art: vec![],
|
||||
};
|
||||
let id = d.alloc_id();
|
||||
d.layers.push(Arc::new(Node::layer(id, "Layer 1", LayerColor::Preset(0))));
|
||||
|
||||
@@ -128,8 +128,8 @@ impl Document {
|
||||
}
|
||||
|
||||
/// The roots of every tree of the document's art: the layers, symbol definitions, graph designs,
|
||||
/// pattern swatches and the original of the pattern being edited. Opacity masks hang off nodes
|
||||
/// in these trees.
|
||||
/// pattern swatches, the original of the pattern being edited and [`Self::mounted_art`].
|
||||
/// Opacity masks hang off nodes in these trees.
|
||||
pub fn art_roots(&self) -> impl Iterator<Item = &Arc<Node>> {
|
||||
self.layers
|
||||
.iter()
|
||||
@@ -137,6 +137,7 @@ impl Document {
|
||||
.chain(self.graph_designs.iter().map(|g| &g.art))
|
||||
.chain(self.patterns.iter().flat_map(|p| p.art.iter()))
|
||||
.chain(self.pattern_edit.iter().filter_map(|pe| pe.original.as_ref()).flat_map(|p| p.art.iter()))
|
||||
.chain(self.mounted_art.iter().map(|(n, _)| n))
|
||||
}
|
||||
|
||||
/// [`Self::art_roots`], to change.
|
||||
@@ -147,6 +148,7 @@ impl Document {
|
||||
.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()))
|
||||
.chain(self.mounted_art.iter_mut().map(|(n, _)| n))
|
||||
}
|
||||
|
||||
/// Does `wants` hold for a node anywhere in the document's art ([`Self::art_roots`]) or its
|
||||
|
||||
@@ -367,6 +367,12 @@ pub struct Document {
|
||||
/// saved.
|
||||
#[serde(skip)]
|
||||
inline_cycles: inline::InlineCycles,
|
||||
/// Art kept elsewhere in the document's data (the art of the stored brushes, which
|
||||
/// `vectorcraft-brush` reads from [`Self::unknown`]) while it is set here, each root with the
|
||||
/// `owner` the Links panel names for it, so the stored-art lookups ([`StoredRoot::Mounted`])
|
||||
/// find its objects too. Empty otherwise; never saved.
|
||||
#[serde(skip)]
|
||||
pub mounted_art: Vec<(Arc<Node>, serde_json::Value)>,
|
||||
}
|
||||
|
||||
fn ppi72() -> f64 {
|
||||
|
||||
@@ -248,14 +248,15 @@ fn walk_all<'a>(n: &'a Node, f: &mut impl FnMut(&'a Node)) {
|
||||
|
||||
impl Document {
|
||||
/// Visit every image object: in the layers, then in symbol definitions and pattern swatches,
|
||||
/// opacity-mask art included.
|
||||
/// then in [`Document::mounted_art`], opacity-mask art included.
|
||||
pub fn visit_images<'a>(&'a self, mut f: impl FnMut(&'a Node, &'a ImageObject)) {
|
||||
let roots = self
|
||||
.layers
|
||||
.iter()
|
||||
.chain(self.symbols.iter().map(|s| &s.art))
|
||||
.chain(self.graph_designs.iter().map(|g| &g.art))
|
||||
.chain(self.patterns.iter().flat_map(|p| &p.art));
|
||||
.chain(self.patterns.iter().flat_map(|p| &p.art))
|
||||
.chain(self.mounted_art.iter().map(|(n, _)| n));
|
||||
for root in roots {
|
||||
walk_all(root, &mut |n| {
|
||||
if let NodeKind::Image(im) = &n.kind {
|
||||
@@ -384,7 +385,8 @@ impl Align {
|
||||
|
||||
impl Document {
|
||||
/// Rewrite linked file locations in both raster images and placed VectorCraft documents,
|
||||
/// including those inside symbols, patterns and opacity masks. Used to make packages portable.
|
||||
/// including those inside symbols, patterns, opacity masks and [`Document::mounted_art`]. Used to
|
||||
/// make packages portable.
|
||||
pub fn update_links(&mut self, mut f: impl FnMut(&mut LinkInfo)) {
|
||||
fn walk(n: &mut Arc<Node>, f: &mut dyn FnMut(&mut LinkInfo)) {
|
||||
let n = Arc::make_mut(n);
|
||||
@@ -409,7 +411,8 @@ impl Document {
|
||||
.iter_mut()
|
||||
.chain(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| &mut p.art));
|
||||
.chain(self.patterns.iter_mut().flat_map(|p| &mut p.art))
|
||||
.chain(self.mounted_art.iter_mut().map(|(n, _)| n));
|
||||
for root in roots {
|
||||
walk(root, &mut f);
|
||||
}
|
||||
@@ -471,7 +474,8 @@ impl Document {
|
||||
.iter_mut()
|
||||
.chain(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| &mut p.art));
|
||||
.chain(self.patterns.iter_mut().flat_map(|p| &mut p.art))
|
||||
.chain(self.mounted_art.iter_mut().map(|(n, _)| n));
|
||||
for root in roots {
|
||||
update(root, &pick, &mut f);
|
||||
}
|
||||
|
||||
@@ -166,13 +166,15 @@ fn walk_all<'a>(n: &'a Node, f: &mut impl FnMut(&'a Node)) {
|
||||
}
|
||||
}
|
||||
|
||||
/// The roots of `d`'s art: layers, symbol definitions and pattern swatches.
|
||||
/// The roots of `d`'s art: layers, symbol definitions, pattern swatches and
|
||||
/// [`Document::mounted_art`].
|
||||
fn roots(d: &Document) -> impl Iterator<Item = &Arc<Node>> {
|
||||
d.layers
|
||||
.iter()
|
||||
.chain(d.symbols.iter().map(|s| &s.art))
|
||||
.chain(d.graph_designs.iter().map(|g| &g.art))
|
||||
.chain(d.patterns.iter().flat_map(|p| &p.art))
|
||||
.chain(d.mounted_art.iter().map(|(n, _)| n))
|
||||
}
|
||||
|
||||
fn roots_mut(d: &mut Document) -> impl Iterator<Item = &mut Arc<Node>> {
|
||||
@@ -181,6 +183,7 @@ fn roots_mut(d: &mut Document) -> impl Iterator<Item = &mut Arc<Node>> {
|
||||
.chain(d.symbols.iter_mut().map(|s| &mut s.art))
|
||||
.chain(d.graph_designs.iter_mut().map(|g| &mut g.art))
|
||||
.chain(d.patterns.iter_mut().flat_map(|p| &mut p.art))
|
||||
.chain(d.mounted_art.iter_mut().map(|(n, _)| n))
|
||||
}
|
||||
|
||||
impl Document {
|
||||
|
||||
@@ -20,6 +20,8 @@ pub enum StoredRoot {
|
||||
GraphDesign(usize),
|
||||
/// Object `art` of pattern swatch `pattern` of [`Document::patterns`].
|
||||
Pattern { pattern: usize, art: usize },
|
||||
/// Root `index` of [`Document::mounted_art`].
|
||||
Mounted(usize),
|
||||
}
|
||||
|
||||
/// Where a stored object is: its root and, inside opacity-mask art, the object that mask belongs
|
||||
@@ -170,10 +172,12 @@ impl Document {
|
||||
.iter()
|
||||
.enumerate()
|
||||
.flat_map(|(p, def)| def.art.iter().enumerate().map(move |(a, n)| (n, StoredRoot::Pattern { pattern: p, art: a })));
|
||||
symbols.chain(designs).chain(patterns)
|
||||
let mounted = self.mounted_art.iter().enumerate().map(|(i, (n, _))| (n, StoredRoot::Mounted(i)));
|
||||
symbols.chain(designs).chain(patterns).chain(mounted)
|
||||
}
|
||||
|
||||
/// Every root of stored art: the layers, then the art of symbols, graph designs and patterns.
|
||||
/// Every root of stored art: the layers, then the art of symbols, graph designs and patterns,
|
||||
/// then [`Document::mounted_art`].
|
||||
fn stored_roots(&self) -> impl Iterator<Item = (&Arc<Node>, StoredRoot)> {
|
||||
self.layers.iter().enumerate().map(|(i, l)| (l, StoredRoot::Layer(i))).chain(self.definition_roots())
|
||||
}
|
||||
@@ -185,12 +189,13 @@ impl Document {
|
||||
StoredRoot::Symbol(i) => self.symbols.get_mut(i).map(|s| &mut s.art),
|
||||
StoredRoot::GraphDesign(i) => self.graph_designs.get_mut(i).map(|g| &mut g.art),
|
||||
StoredRoot::Pattern { pattern, art } => self.patterns.get_mut(pattern)?.art.get_mut(art),
|
||||
StoredRoot::Mounted(i) => self.mounted_art.get_mut(i).map(|(n, _)| n),
|
||||
}
|
||||
}
|
||||
|
||||
/// Visit every stored object with its place: the layer tree with the opacity-mask art of its
|
||||
/// objects, then the art of symbols, graph designs and pattern swatches (their definitions,
|
||||
/// once each, never the copies instances and tiles draw).
|
||||
/// once each, never the copies instances and tiles draw), then [`Document::mounted_art`].
|
||||
pub fn visit_stored<'a>(&'a self, mut f: impl FnMut(&'a Node, StoredPlace)) {
|
||||
self.visit_stored_arcs(|n, place| f(n, place));
|
||||
}
|
||||
@@ -238,6 +243,29 @@ impl Document {
|
||||
/// visited before them (the layer tree's own objects first) a new id, so every stored object
|
||||
/// can be named by its id. Returns how many changed; the id counter then stands above them.
|
||||
pub(crate) fn renumber_stored_duplicates(&mut self) -> usize {
|
||||
self.renumber_duplicates_in(|_| true)
|
||||
}
|
||||
|
||||
/// Raises the id counter above the ids of [`Document::mounted_art`]'s objects, then gives those
|
||||
/// objects whose id is 0, past the id range, or already taken by an object visited before them
|
||||
/// (every other stored object first) a new id, as [`Self::renumber_stored_duplicates`] gives
|
||||
/// them. Returns how many changed.
|
||||
pub fn number_mounted_art(&mut self) -> usize {
|
||||
let mut max = 0;
|
||||
for (i, (root, _)) in self.mounted_art.iter().enumerate() {
|
||||
visit(root, StoredPlace { root: StoredRoot::Mounted(i), mask_of: None }, &mut |n, _| {
|
||||
if n.id.0 < MAX_NEXT_ID {
|
||||
max = max.max(n.id.0);
|
||||
}
|
||||
});
|
||||
}
|
||||
self.raise_next_id(max.saturating_add(1));
|
||||
self.renumber_duplicates_in(|r| matches!(r, StoredRoot::Mounted(_)))
|
||||
}
|
||||
|
||||
/// [`Self::renumber_stored_duplicates`] for the roots `pick` holds for only; the objects of the
|
||||
/// others still count as taken.
|
||||
fn renumber_duplicates_in(&mut self, pick: impl Fn(StoredRoot) -> bool) -> usize {
|
||||
let mut seen: HashSet<NodeId> = HashSet::new();
|
||||
for l in &self.layers {
|
||||
l.walk(&mut |n| {
|
||||
@@ -250,9 +278,9 @@ impl Document {
|
||||
// Per root, the visiting indices of the nodes that take a new id.
|
||||
let mut fresh: Vec<(StoredRoot, HashSet<usize>)> = vec![];
|
||||
for (root, r) in self.stored_roots() {
|
||||
let (mut i, mut here) = (0, HashSet::new());
|
||||
let (mut i, mut here, take) = (0, HashSet::new(), pick(r));
|
||||
visit(root, StoredPlace { root: r, mask_of: None }, &mut |n, place| {
|
||||
if !place.in_layers() && (n.id.0 == 0 || n.id.0 >= MAX_NEXT_ID || !seen.insert(n.id)) {
|
||||
if !place.in_layers() && (n.id.0 == 0 || n.id.0 >= MAX_NEXT_ID || !seen.insert(n.id)) && take {
|
||||
here.insert(i);
|
||||
}
|
||||
i += 1;
|
||||
@@ -355,6 +383,30 @@ mod tests {
|
||||
assert_eq!(d, again);
|
||||
}
|
||||
|
||||
/// Mounted art is found and changed by id after the other stored art, and numbering it gives
|
||||
/// its objects that repeat another stored object's id, or have none, new ids above the counter,
|
||||
/// leaving every other object's id and its own unique ones as they are.
|
||||
#[test]
|
||||
fn mounted_art_is_found_and_numbered_after_the_rest() {
|
||||
let mut d = doc([12, 13, 14]);
|
||||
let owner = serde_json::json!({ "kind": "brush" });
|
||||
d.mounted_art = vec![(Arc::new(masked(10, rect(0))), owner.clone()), (Arc::new(rect(15)), owner)];
|
||||
assert_eq!(d.stored_node(NodeId(15)).map(|(_, p)| p.root), Some(StoredRoot::Mounted(1)));
|
||||
let before = d.clone();
|
||||
assert_eq!(d.number_mounted_art(), 2);
|
||||
let mut ids = vec![];
|
||||
d.visit_stored(|n, _| ids.push(n.id.0));
|
||||
let unique: HashSet<u64> = ids.iter().copied().collect();
|
||||
assert_eq!(unique.len(), ids.len(), "{ids:?}");
|
||||
assert_eq!(&ids[..6], &[d.layers[0].id.0, 10, 11, 12, 13, 14], "the other art keeps its ids");
|
||||
assert_eq!(ids[8], 15, "a unique id is kept");
|
||||
assert!(ids.iter().all(|id| *id < d.peek_next_id()), "{ids:?} {}", d.peek_next_id());
|
||||
assert!(Arc::ptr_eq(&d.symbols[0].art, &before.symbols[0].art) && Arc::ptr_eq(&d.mounted_art[1].0, &before.mounted_art[1].0));
|
||||
d.stored_node_mut(NodeId(15)).unwrap().name = Some("changed".into());
|
||||
assert_eq!(d.mounted_art[1].0.name.as_deref(), Some("changed"));
|
||||
assert_eq!(d.number_mounted_art(), 0, "a second pass changes nothing");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn renumbered_objects_on_path_keep_their_path_and_members() {
|
||||
// A symbol's Objects on Path group whose path repeats the layer's object 10 and whose
|
||||
|
||||
@@ -320,7 +320,8 @@ fn fresh(doc: &Document, n: &Node, cmd: &str) -> Option<Result<(ColorMask, Optio
|
||||
|
||||
/// After an edit from `before` (`None`: everything is new, as after a document's links are read):
|
||||
/// the color masks of images in `doc`'s art ([`Document::art_roots`]: layers, symbols, graph
|
||||
/// designs, patterns and the original of the pattern being edited, opacity mask art included)
|
||||
/// designs, patterns, the original of the pattern being edited and the art of the stored brushes
|
||||
/// while it is set on the document, opacity mask art included)
|
||||
/// whose own pixels were replaced are made again for the new pixels ([`Refresh::made`]). Only the
|
||||
/// art that changed since `before` is looked at.
|
||||
pub(crate) fn refresh(before: Option<&Document>, doc: &mut Document) {
|
||||
|
||||
@@ -144,9 +144,10 @@ fn find(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
None | Some(Value::Null) => None,
|
||||
Some(_) => {
|
||||
// Images and placed documents in opacity-mask art and in the art of symbols, graph
|
||||
// designs and patterns are named by their ids too (`links.list`).
|
||||
// designs, patterns and stored brushes are named by their ids too (`links.list`).
|
||||
let ids = ids_param(p, "ids", C)?.unwrap_or_default();
|
||||
let kind = |id: &NodeId| st.doc.stored_node(*id).map(|(n, _)| &n.kind);
|
||||
let doc = super::links::with_brush_art(&st.doc);
|
||||
let kind = |id: &NodeId| doc.stored_node(*id).map(|(n, _)| &n.kind);
|
||||
if let Some(value) = ids.iter().find(|id| kind(id).is_none()).map(|id| id.0) {
|
||||
return Err(bad(C, format!("no such object id {value}")));
|
||||
}
|
||||
|
||||
+153
-69
@@ -27,6 +27,7 @@ use std::path::{Component, Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_brush::stored_art::StoredArt;
|
||||
use vectorcraft_doc::links::{Align, Preserve, hash_bytes};
|
||||
use vectorcraft_doc::{
|
||||
Appearance, DataValue, Document, ImageBlob, ImageObject, LinkInfo, Node, NodeId, NodeKind, PageBox, PlacedDocument, PlacementOptions,
|
||||
@@ -44,7 +45,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Check Links",
|
||||
[],
|
||||
None,
|
||||
"{ids?: [image or placed document ids] (default: every linked image and placed document, in the layers and in opacity-mask art, symbols, graph designs and patterns)} where each linked file is and whether it changed since it was read → {links: [{name, path?, status: ok|modified|missing, ids, found?: path (found away from its path: relative to the document, or by name in its folder), preview: true (showing the low-resolution preview, not the file), refused?: why the path is not followed (another computer, a device or a URL: it counts as missing)}], missing, modified}; under automation read roots, path and found are left out when the file they name is outside the roots",
|
||||
"{ids?: [image or placed document ids] (default: every linked image and placed document, in the layers and in opacity-mask art, symbols, graph designs, patterns and the art of stored brushes)} where each linked file is and whether it changed since it was read → {links: [{name, path?, status: ok|modified|missing, ids, found?: path (found away from its path: relative to the document, or by name in its folder), preview: true (showing the low-resolution preview, not the file), refused?: why the path is not followed (another computer, a device or a URL: it counts as missing)}], missing, modified}; under automation read roots, path and found are left out when the file they name is outside the roots",
|
||||
has_doc,
|
||||
check
|
||||
),
|
||||
@@ -53,7 +54,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Update Links",
|
||||
[],
|
||||
None,
|
||||
"{ids?: [image or placed document ids] (default: the linked images whose file was modified or is showing its preview, and placed documents whose file was modified)} read their linked files again; each keeps its bounds. Images and placed documents inside symbols, graph designs, patterns and opacity masks have ids of their own, as links.list gives them; those that show the same content as one read again are read again too. One undo step → {updated: [ids], missing: [ids], warnings?: [a DCS file's spot plate whose ink a swatch of the document already names, as file.place warns]}",
|
||||
"{ids?: [image or placed document ids] (default: the linked images whose file was modified or is showing its preview, and placed documents whose file was modified)} read their linked files again; each keeps its bounds. Images and placed documents inside symbols, graph designs, patterns, opacity masks and the art of stored brushes have ids of their own, as links.list gives them; those that show the same content as one read again are read again too. One undo step → {updated: [ids], missing: [ids], warnings?: [a DCS file's spot plate whose ink a swatch of the document already names, as file.place warns]}",
|
||||
has_doc,
|
||||
update
|
||||
),
|
||||
@@ -71,7 +72,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Relink",
|
||||
[],
|
||||
None,
|
||||
"{ids?: [image or placed document ids] (default: the selected ones; with folder and nothing selected, every missing link), path | folder, scope?: selected (default: ids) | all (ids and every other object linked to the same file as one of them, in the layers, opacity-mask art, symbols, graph designs and patterns) | missingInFolder (with path: ids, then every other missing link whose file name is in path's folder, each to the file of its name there; names not there are in notFound; with copy, only images are looked for) | each (with paths), paths?: [{ids, path}] (with scope each, in place of ids, path and folder: each link to its own file; an id in two entries is an error), copy?: false (with path or paths, for images of a saved document: copy the file into the document's folder and link to the copy; the names Name.png, Name 2.png … Name 99.png are tried in turn until one is free, which gets the copy, or is a file with the same contents, which is used; an error for a DCS file that reads plate files of its own), preserve?: transforms|bounds|fileDimensions|fit|fill (how the new file takes each image's place this time, as links.placementOptions describes; default: each image's Placement Options; fit keeps the new file's aspect ratio inside the old bounds: Replace)} link images (or placed documents, each showing the same artboard or page of the new file) to the file at path, or each to the file of its link's name in folder; embedded images become linked; each keeps its bounds. One undo step → {relinked: [ids], notFound: [file names], copied?: [{from, to}], kept?: [{from, to}] (with copy; to is the file name alone when the document's folder is outside the automation read roots), warnings?: (as links.update's)}",
|
||||
"{ids?: [image or placed document ids] (default: the selected ones; with folder and nothing selected, every missing link), path | folder, scope?: selected (default: ids) | all (ids and every other object linked to the same file as one of them, in the layers, opacity-mask art, symbols, graph designs, patterns and the art of stored brushes) | missingInFolder (with path: ids, then every other missing link whose file name is in path's folder, each to the file of its name there; names not there are in notFound; with copy, only images are looked for) | each (with paths), paths?: [{ids, path}] (with scope each, in place of ids, path and folder: each link to its own file; an id in two entries is an error), copy?: false (with path or paths, for images of a saved document: copy the file into the document's folder and link to the copy; the names Name.png, Name 2.png … Name 99.png are tried in turn until one is free, which gets the copy, or is a file with the same contents, which is used; an error for a DCS file that reads plate files of its own), preserve?: transforms|bounds|fileDimensions|fit|fill (how the new file takes each image's place this time, as links.placementOptions describes; default: each image's Placement Options; fit keeps the new file's aspect ratio inside the old bounds: Replace)} link images (or placed documents, each showing the same artboard or page of the new file) to the file at path, or each to the file of its link's name in folder; embedded images become linked; each keeps its bounds. One undo step → {relinked: [ids], notFound: [file names], copied?: [{from, to}], kept?: [{from, to}] (with copy; to is the file name alone when the document's folder is outside the automation read roots), warnings?: (as links.update's)}",
|
||||
has_doc,
|
||||
relink
|
||||
),
|
||||
@@ -80,7 +81,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Links",
|
||||
[],
|
||||
None,
|
||||
"{show?: all (default)|missing|modified|embedded, sort?: name|kind (file format)|status (missing, modified, ok, embedded); default: stacking order; top first, dcsTransparency?: false (Show DCS Transparency Interactions: mark the images placed from DCS files that are transparent, themselves or a group or layer holding them, or that overlap a visible object with its own transparency; it looks at every object, so it takes longer)} every image object and placed document the document stores, as the Links panel lists them: those in the layers, then those in opacity-mask art and in the art of symbols, graph designs and patterns (one row per stored object, never one per symbol instance or pattern tile) → {links: [{id, name, linked, status: ok|modified|missing|embedded, format (DCS for an image placed from a DCS file), pixelWidth, pixelHeight, path?, found?: path (found away from its path), page?, preview?: true (showing the saved preview), note?: said of an image past the decode allowance (shown at reduced size, or too large to draw), document?: true (a placed document: pageWidth and pageHeight in pt instead of pixels), dcsTransparency?: true (with dcsTransparency), owner?: {kind: layers|symbol|graphDesign|pattern, name? (of the symbol, graph design or pattern), maskOf?: id (in the opacity-mask art of that object)} (an object outside the layer tree)}], missing, modified, embedded}; under automation read roots, path and found are left out when the file they name is outside the roots",
|
||||
"{show?: all (default)|missing|modified|embedded, sort?: name|kind (file format)|status (missing, modified, ok, embedded); default: stacking order; top first, dcsTransparency?: false (Show DCS Transparency Interactions: mark the images placed from DCS files that are transparent, themselves or a group or layer holding them, or that overlap a visible object with its own transparency; it looks at every object, so it takes longer)} every image object and placed document the document stores, as the Links panel lists them: those in the layers, then those in opacity-mask art and in the art of symbols, graph designs, patterns and stored brushes (one row per stored object, never one per symbol instance, pattern tile or brushed stroke) → {links: [{id, name, linked, status: ok|modified|missing|embedded, format (DCS for an image placed from a DCS file), pixelWidth, pixelHeight, path?, found?: path (found away from its path), page?, preview?: true (showing the saved preview), note?: said of an image past the decode allowance (shown at reduced size, or too large to draw), document?: true (a placed document: pageWidth and pageHeight in pt instead of pixels), dcsTransparency?: true (with dcsTransparency), owner?: {kind: layers|symbol|graphDesign|pattern|brush, name? (of the symbol, graph design, pattern or brush), index? (a brush's place in the Brushes panel, from 0), tile?: side|outerCorner|innerCorner|start|end (a pattern brush's tile), maskOf?: id (in the opacity-mask art of that object)} (an object outside the layer tree)}], missing, modified, embedded}; under automation read roots, path and found are left out when the file they name is outside the roots",
|
||||
has_doc,
|
||||
list
|
||||
),
|
||||
@@ -89,7 +90,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Go To Link",
|
||||
[],
|
||||
None,
|
||||
"{id?} select image or placed document `id` (default: the first selected one); the Links panel scrolls it into view → {id, bounds: [x0, y0, x1, y1], onCanvas: true}; one in opacity-mask art or in the art of a symbol, graph design or pattern has no place of its own on the canvas: the selection is cleared → {id, onCanvas: false, owner} (owner as links.list gives it)",
|
||||
"{id?} select image or placed document `id` (default: the first selected one); the Links panel scrolls it into view → {id, bounds: [x0, y0, x1, y1], onCanvas: true}; one in opacity-mask art or in the art of a symbol, graph design, pattern or stored brush has no place of its own on the canvas: the selection is cleared → {id, onCanvas: false, owner} (owner as links.list gives it)",
|
||||
has_doc,
|
||||
go_to
|
||||
),
|
||||
@@ -198,6 +199,8 @@ pub(crate) fn linked_groups(d: &Document) -> Vec<(String, Vec<u64>)> {
|
||||
/// visited before it has (art being edited on a layer and its definition) counts once, by its
|
||||
/// first place; without `ids` its content is still grouped.
|
||||
fn groups(d: &Document, ids: Option<&[NodeId]>) -> Vec<Group> {
|
||||
let view = with_brush_art(d);
|
||||
let d = &*view;
|
||||
let mut out: Vec<Group> = vec![];
|
||||
let mut index: BTreeMap<(String, String, bool), usize> = BTreeMap::new();
|
||||
// Looked up for every linked node: a set, not the list (which may repeat an id many times).
|
||||
@@ -234,13 +237,52 @@ fn stored(d: &Document, id: NodeId) -> Option<&Node> {
|
||||
d.stored_node(id).map(|(n, _)| &**n)
|
||||
}
|
||||
|
||||
/// `d` with the art of its stored brushes set on it ([`vectorcraft_brush::stored_art`]), so that
|
||||
/// the images and placed documents in that art are visited and found by their ids as stored art;
|
||||
/// `d` itself when no stored brush's art holds one or the art is set on it already.
|
||||
pub(crate) fn with_brush_art(d: &Document) -> Cow<'_, Document> {
|
||||
if !d.mounted_art.is_empty() {
|
||||
return Cow::Borrowed(d);
|
||||
}
|
||||
let art = StoredArt::read(d);
|
||||
if art.is_empty() {
|
||||
return Cow::Borrowed(d);
|
||||
}
|
||||
let mut d = d.clone();
|
||||
art.mount(&mut d);
|
||||
Cow::Owned(d)
|
||||
}
|
||||
|
||||
/// `f` run on `d` with the art of its stored brushes set on it as [`with_brush_art`] sets it; the
|
||||
/// brushes whose art `f` changed are then written back into the library, the color masks of its
|
||||
/// images made for their pixels first. The ids it gives that art are the ones [`with_brush_art`]
|
||||
/// gives it in the same document.
|
||||
pub(crate) fn editing_brush_art<T>(d: &mut Document, f: impl FnOnce(&mut Document) -> T) -> T {
|
||||
if !d.mounted_art.is_empty() {
|
||||
return f(d);
|
||||
}
|
||||
let art = StoredArt::read(d);
|
||||
if art.is_empty() {
|
||||
return f(d);
|
||||
}
|
||||
art.mount(d);
|
||||
let before: Vec<Arc<Node>> = d.mounted_art.iter().map(|(n, _)| n.clone()).collect();
|
||||
let out = f(d);
|
||||
if d.mounted_art.len() != before.len() || d.mounted_art.iter().zip(&before).any(|((n, _), b)| !Arc::ptr_eq(n, b)) {
|
||||
super::refresh_color_masks(None, d);
|
||||
}
|
||||
art.write_back(d);
|
||||
out
|
||||
}
|
||||
|
||||
/// Of `ids`, the objects of `d` outside the layer tree (where [`Document::node`] finds none).
|
||||
fn nested_ids(d: &Document, ids: &[NodeId]) -> std::collections::HashSet<NodeId> {
|
||||
ids.iter().copied().filter(|id| d.node(*id).is_none()).collect()
|
||||
}
|
||||
|
||||
/// `links.list`'s `owner` of an object stored at `place` of `d`, when it is not in the layer tree
|
||||
/// itself: `{kind: layers|symbol|graphDesign|pattern, name?, maskOf?}`.
|
||||
/// itself: `{kind: layers|symbol|graphDesign|pattern, name?, maskOf?}`, or for one in the art of a
|
||||
/// stored brush, `{kind: brush, name, index, tile?, maskOf?}`.
|
||||
fn owner_json(d: &Document, place: StoredPlace) -> Option<Value> {
|
||||
if place.in_layers() {
|
||||
return None;
|
||||
@@ -250,6 +292,7 @@ fn owner_json(d: &Document, place: StoredPlace) -> Option<Value> {
|
||||
StoredRoot::Symbol(i) => json!({ "kind": "symbol", "name": d.symbols.get(i).map(|s| s.name.as_str()) }),
|
||||
StoredRoot::GraphDesign(i) => json!({ "kind": "graphDesign", "name": d.graph_designs.get(i).map(|g| g.name.as_str()) }),
|
||||
StoredRoot::Pattern { pattern, .. } => json!({ "kind": "pattern", "name": d.patterns.get(pattern).map(|p| p.name.as_str()) }),
|
||||
StoredRoot::Mounted(i) => d.mounted_art.get(i).map_or_else(|| json!({}), |(_, owner)| owner.clone()),
|
||||
};
|
||||
if let Some(m) = place.mask_of {
|
||||
owner["maskOf"] = json!(m.0);
|
||||
@@ -479,7 +522,8 @@ pub(crate) fn linked_files(d: &Document, base: LinkBase) -> Vec<LinkedFile> {
|
||||
/// again (Preferences › Use Low Resolution Proxy for Linked EPS keeps the preview on screen only).
|
||||
pub(crate) fn full_linked_images<'a>(d: &'a Document, base: LinkBase) -> (Cow<'a, Document>, Vec<String>) {
|
||||
let mut keys = BTreeSet::new();
|
||||
d.visit_images(|_, im| {
|
||||
// The art of the stored brushes draws its images too.
|
||||
with_brush_art(d).visit_images(|_, im| {
|
||||
if im.link.is_some() && d.images.get(&im.key).is_some_and(ImageBlob::is_proxy) {
|
||||
keys.insert(im.key.clone());
|
||||
}
|
||||
@@ -763,7 +807,7 @@ struct Nested<'a> {
|
||||
|
||||
impl Nested<'_> {
|
||||
/// Every root of `d`: the layers (whose own objects are left alone), then the art of symbols,
|
||||
/// graph designs and patterns.
|
||||
/// graph designs and patterns, then [`Document::mounted_art`].
|
||||
fn run(mut self, d: &mut Document) {
|
||||
let first = d.peek_next_id();
|
||||
self.next = first;
|
||||
@@ -774,7 +818,7 @@ impl Nested<'_> {
|
||||
for root in d.symbols.iter_mut().map(|s| &mut s.art).chain(d.graph_designs.iter_mut().map(|g| &mut g.art)) {
|
||||
self.root(root);
|
||||
}
|
||||
for root in d.patterns.iter_mut().flat_map(|p| &mut p.art) {
|
||||
for root in d.patterns.iter_mut().flat_map(|p| &mut p.art).chain(d.mounted_art.iter_mut().map(|(n, _)| n)) {
|
||||
self.root(root);
|
||||
}
|
||||
for _ in first..self.next {
|
||||
@@ -1306,8 +1350,14 @@ impl Resolved {
|
||||
/// found away from their path take the new path. A link nothing is known of whose image, or placed
|
||||
/// document, shows a preview (a file another app wrote kept only that) was never read: its file is
|
||||
/// read now, into the object's bounds, which isn't a change to report. A placed document whose
|
||||
/// file's bytes are gone isn't showing a preview. Nothing here is an undo step.
|
||||
/// file's bytes are gone isn't showing a preview. Links in the art of the stored brushes count too,
|
||||
/// and that art keeps the ids they are listed by. Nothing here is an undo step.
|
||||
pub fn resolve(d: &mut Document, base: LinkBase, update: bool, low_res_proxy_eps: bool) -> Resolved {
|
||||
editing_brush_art(d, |d| resolve_links(d, base, update, low_res_proxy_eps))
|
||||
}
|
||||
|
||||
/// [`resolve`] in the art `d` stores.
|
||||
fn resolve_links(d: &mut Document, base: LinkBase, update: bool, low_res_proxy_eps: bool) -> Resolved {
|
||||
let mut out = Resolved::default();
|
||||
// The image (with its preview) and hash of each file read for a preview here, by path: images
|
||||
// of other keys linking to the same file share them, rather than each reading, decoding and
|
||||
@@ -1458,8 +1508,8 @@ fn set_links(d: &mut Document, ids: &[NodeId], link: &LinkInfo) {
|
||||
}
|
||||
|
||||
/// Placed documents of the document saved at `path` (document `saved` of `s`) in the other open
|
||||
/// documents, in the layers, symbols, graph designs, patterns and opacity masks: read again from
|
||||
/// the new file, each as one Update Links step of its document.
|
||||
/// documents, in the layers, symbols, graph designs, patterns, opacity masks and the art of the
|
||||
/// stored brushes: read again from the new file, each as one Update Links step of its document.
|
||||
pub(crate) fn refresh_placed(s: &mut Session, saved: u64, path: &str) {
|
||||
let path = absolute_path(path);
|
||||
let targets: Vec<usize> = s
|
||||
@@ -1469,7 +1519,7 @@ pub(crate) fn refresh_placed(s: &mut Session, saved: u64, path: &str) {
|
||||
.filter(|(_, st)| st.uid != saved)
|
||||
.filter(|(_, st)| {
|
||||
let mut placed = false;
|
||||
st.doc.visit_placed(|_, p| placed |= p.link.path == path);
|
||||
with_brush_art(&st.doc).visit_placed(|_, p| placed |= p.link.path == path);
|
||||
placed
|
||||
})
|
||||
.map(|(i, _)| i)
|
||||
@@ -1526,7 +1576,7 @@ fn link_target(link: &LinkInfo, base: LinkBase) -> PathBuf {
|
||||
/// `d` (looking from `base`) as saved to `dest`: the links' paths relative to its folder written
|
||||
/// in; `None` when they are already. A link whose file is not at its path is written relative to
|
||||
/// where its file is found, else to the place its relative path names. Links in the layers,
|
||||
/// symbols, graph designs, patterns and opacity masks are all set.
|
||||
/// symbols, graph designs, patterns, opacity masks and the art of the stored brushes are all set.
|
||||
pub fn with_relative_paths(d: &Document, base: LinkBase, dest: &str) -> Option<Document> {
|
||||
let dest = absolute_path(dest);
|
||||
let dir = Path::new(&dest).parent()?;
|
||||
@@ -1535,23 +1585,29 @@ pub fn with_relative_paths(d: &Document, base: LinkBase, dest: &str) -> Option<D
|
||||
let mut look = |l: &LinkInfo| {
|
||||
wanted.entry((l.path.clone(), l.relative.clone())).or_insert_with(|| relative_path(&link_target(l, base), dir));
|
||||
};
|
||||
d.visit_images(|_, im| im.link.iter().for_each(&mut look));
|
||||
d.visit_placed(|_, p| look(&p.link));
|
||||
let view = with_brush_art(d);
|
||||
view.visit_images(|_, im| im.link.iter().for_each(&mut look));
|
||||
view.visit_placed(|_, p| look(&p.link));
|
||||
wanted.retain(|(_, stored), rel| rel != stored);
|
||||
if wanted.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let key = |l: &LinkInfo| (l.path.clone(), l.relative.clone());
|
||||
let mut d = d.clone();
|
||||
d.update_links_where(|l| wanted.contains_key(&key(l)), |l| l.relative = wanted.get(&key(l)).cloned().flatten());
|
||||
editing_brush_art(&mut d, |d| d.update_links_where(|l| wanted.contains_key(&key(l)), |l| l.relative = wanted.get(&key(l)).cloned().flatten()));
|
||||
Some(d)
|
||||
}
|
||||
|
||||
/// Sets each link of `d`, a copy of a document looking from `base`, to the path where that
|
||||
/// document finds its file, and its relative path to it as that document's links have them, so
|
||||
/// the copy finds the same files. Links in the layers, symbols, graph designs, patterns and opacity
|
||||
/// masks are all set.
|
||||
/// the copy finds the same files. Links in the layers, symbols, graph designs, patterns, opacity
|
||||
/// masks and the art of the stored brushes are all set.
|
||||
pub(crate) fn with_found_paths(d: &mut Document, base: LinkBase) {
|
||||
editing_brush_art(d, |d| set_found_paths(d, base));
|
||||
}
|
||||
|
||||
/// [`with_found_paths`] in the art `d` stores.
|
||||
fn set_found_paths(d: &mut Document, base: LinkBase) {
|
||||
// The new path and relative path of each link by what `locate` reads of it: its path, relative
|
||||
// path and name.
|
||||
type PathAndRelative = (String, Option<String>);
|
||||
@@ -1706,7 +1762,9 @@ fn update_where(s: &mut Session, ids: Option<Vec<NodeId>>, path: Option<&str>) -
|
||||
let updated: Vec<u64> = reads.iter().flat_map(|(g, _)| ids_json(&g.ids)).collect();
|
||||
let warnings = spot_swatch_notes(&st.doc, reads.iter().map(|(_, read)| read));
|
||||
if !reads.is_empty() {
|
||||
s.edit("Update Links", |d, _| reads.into_iter().try_for_each(|(g, read)| install_read(d, &g.ids, read, None, Some(&g))))?;
|
||||
s.edit("Update Links", |d, _| {
|
||||
editing_brush_art(d, |d| reads.into_iter().try_for_each(|(g, read)| install_read(d, &g.ids, read, None, Some(&g))))
|
||||
})?;
|
||||
}
|
||||
let mut out = json!({ "updated": updated, "missing": missing });
|
||||
if !warnings.is_empty() {
|
||||
@@ -1833,9 +1891,10 @@ fn relink(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Some(Path::new(&doc).parent().map(Path::to_path_buf).ok_or_else(no_folder)?)
|
||||
}
|
||||
};
|
||||
let doc = with_brush_art(&st.doc);
|
||||
// An image or a placed document, with its link.
|
||||
let image = |id: &NodeId| {
|
||||
stored(&st.doc, *id).and_then(|n| match &n.kind {
|
||||
stored(&doc, *id).and_then(|n| match &n.kind {
|
||||
NodeKind::Image(im) => Some(im.link.as_ref()),
|
||||
NodeKind::PlacedDocument(p) => Some(Some(&p.link)),
|
||||
_ => None,
|
||||
@@ -1865,7 +1924,7 @@ fn relink(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let paths: BTreeSet<&str> = ids.iter().filter_map(|id| image(id).flatten()).map(|l| l.path.as_str()).collect();
|
||||
let mut given: BTreeSet<NodeId> = ids.iter().copied().collect();
|
||||
let mut more = vec![];
|
||||
st.doc.visit_stored(|n, _| {
|
||||
doc.visit_stored(|n, _| {
|
||||
if let Some((l, _, _)) = linked(n)
|
||||
&& paths.contains(l.path.as_str())
|
||||
&& n.id.0 != 0
|
||||
@@ -1876,7 +1935,7 @@ fn relink(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
});
|
||||
ids.extend(more);
|
||||
}
|
||||
if copy_to.is_some() && ids.iter().any(|id| matches!(stored(&st.doc, *id).map(|n| &n.kind), Some(NodeKind::PlacedDocument(_)))) {
|
||||
if copy_to.is_some() && ids.iter().any(|id| matches!(stored(&doc, *id).map(|n| &n.kind), Some(NodeKind::PlacedDocument(_)))) {
|
||||
return Err(bad(C, "copy takes images only: relink placed documents where their files are"));
|
||||
}
|
||||
// (ids, the file) per file to read.
|
||||
@@ -1912,7 +1971,7 @@ fn relink(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let mut images = vec![];
|
||||
let mut documents: Vec<((u32, bool, PageBox), Vec<NodeId>)> = vec![];
|
||||
for id in ids {
|
||||
match stored(&st.doc, id).map(|n| &n.kind) {
|
||||
match stored(&doc, id).map(|n| &n.kind) {
|
||||
Some(NodeKind::PlacedDocument(pl)) => {
|
||||
let k = (pl.page(), pl.bounding, pl.page_box);
|
||||
match documents.iter_mut().find(|(e, _)| *e == k) {
|
||||
@@ -1963,9 +2022,10 @@ fn relink(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
}
|
||||
let relinked: Vec<u64> = reads.iter().flat_map(|(ids, _)| ids_json(ids)).collect();
|
||||
drop(doc);
|
||||
let warnings = spot_swatch_notes(&s.doc()?.doc, reads.iter().map(|(_, read)| read));
|
||||
if !reads.is_empty() {
|
||||
s.edit("Relink", |d, _| reads.into_iter().try_for_each(|(ids, read)| install_read(d, &ids, read, preserve, None)))?;
|
||||
s.edit("Relink", |d, _| editing_brush_art(d, |d| reads.into_iter().try_for_each(|(ids, read)| install_read(d, &ids, read, preserve, None))))?;
|
||||
}
|
||||
not_found.sort();
|
||||
not_found.dedup();
|
||||
@@ -2075,14 +2135,15 @@ fn selected_images(st: &crate::DocState, keep: impl Fn(&ImageObject) -> bool) ->
|
||||
st.selection.objects.iter().copied().filter(|id| image_of(&st.doc, *id).is_some_and(&keep)).collect()
|
||||
}
|
||||
|
||||
/// `id`, else the first selected image.
|
||||
fn image_param(st: &crate::DocState, p: &Value, cmd: &str) -> Result<NodeId> {
|
||||
/// `id` (looked up in `doc`, the document of `st` as [`with_brush_art`] gives it), else the first
|
||||
/// selected image.
|
||||
fn image_param(st: &crate::DocState, doc: &Document, p: &Value, cmd: &str) -> Result<NodeId> {
|
||||
let id = object_param(p, "id", cmd)?
|
||||
.or_else(|| selected_images(st, |_| true).first().copied())
|
||||
.ok_or_else(|| bad(cmd, "select an image, or pass id"))?;
|
||||
match image_of(&st.doc, id) {
|
||||
match image_of(doc, id) {
|
||||
Some(_) => Ok(id),
|
||||
None if stored(&st.doc, id).is_none() => Err(crate::EngineError::NoNode(id)),
|
||||
None if stored(doc, id).is_none() => Err(crate::EngineError::NoNode(id)),
|
||||
None => Err(bad(cmd, format!("object {} is not an image", id.0))),
|
||||
}
|
||||
}
|
||||
@@ -2105,13 +2166,14 @@ fn placement_of(d: &Document, id: NodeId) -> Option<PlacementOptions> {
|
||||
}
|
||||
}
|
||||
|
||||
/// `id`, else the first selected image or placed document.
|
||||
fn linked_param(st: &crate::DocState, p: &Value, cmd: &str) -> Result<NodeId> {
|
||||
let first = || st.selection.objects.iter().copied().find(|id| placement_of(&st.doc, *id).is_some());
|
||||
/// `id` (looked up in `doc`, the document of `st` as [`with_brush_art`] gives it), else the first
|
||||
/// selected image or placed document.
|
||||
fn linked_param(st: &crate::DocState, doc: &Document, p: &Value, cmd: &str) -> Result<NodeId> {
|
||||
let first = || st.selection.objects.iter().copied().find(|id| placement_of(doc, *id).is_some());
|
||||
let id = object_param(p, "id", cmd)?.or_else(first).ok_or_else(|| bad(cmd, "select an image, or pass id"))?;
|
||||
match placement_of(&st.doc, id) {
|
||||
match placement_of(doc, id) {
|
||||
Some(_) => Ok(id),
|
||||
None if stored(&st.doc, id).is_none() => Err(crate::EngineError::NoNode(id)),
|
||||
None if stored(doc, id).is_none() => Err(crate::EngineError::NoNode(id)),
|
||||
None => Err(bad(cmd, format!("object {} is not an image or a linked file", id.0))),
|
||||
}
|
||||
}
|
||||
@@ -2218,10 +2280,12 @@ fn list(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let interactions = if switch_or(p, "dcsTransparency", false, C)? { dcs_transparency(&st.doc) } else { BTreeSet::new() };
|
||||
let statuses = statuses(st);
|
||||
// The layer tree's objects, top first as the Layers panel lists them, then the others in the
|
||||
// order they are stored (opacity-mask art, symbols, graph designs, patterns), each once.
|
||||
// order they are stored (opacity-mask art, symbols, graph designs, patterns, the art of the
|
||||
// stored brushes), each once.
|
||||
let doc = with_brush_art(&st.doc);
|
||||
let (mut objects, mut nested) = (vec![], vec![]);
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
st.doc.visit_stored(|n, place| {
|
||||
doc.visit_stored(|n, place| {
|
||||
if matches!(n.kind, NodeKind::Image(_) | NodeKind::PlacedDocument(_)) && n.id.0 != 0 && seen.insert(n.id) {
|
||||
if place.in_layers() { objects.push((n, place)) } else { nested.push((n, place)) }
|
||||
}
|
||||
@@ -2273,7 +2337,7 @@ fn list(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if preview {
|
||||
row["preview"] = json!(true);
|
||||
}
|
||||
if let Some(owner) = owner_json(&st.doc, place) {
|
||||
if let Some(owner) = owner_json(&doc, place) {
|
||||
row["owner"] = owner;
|
||||
}
|
||||
if interactions.contains(&n.id) {
|
||||
@@ -2295,10 +2359,13 @@ fn list(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
fn go_to(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let st = s.doc()?;
|
||||
let id = linked_param(st, p, "links.goTo")?;
|
||||
// An object in mask art or a definition has no place of its own on the canvas: the selection
|
||||
// is cleared, and the reply names what holds it.
|
||||
if let Some(owner) = st.doc.stored_node(id).and_then(|(_, place)| owner_json(&st.doc, place)) {
|
||||
let doc = with_brush_art(&st.doc);
|
||||
let id = linked_param(st, &doc, p, "links.goTo")?;
|
||||
// An object in mask art, a definition or a stored brush's art has no place of its own on the
|
||||
// canvas: the selection is cleared, and the reply names what holds it.
|
||||
let owner = doc.stored_node(id).and_then(|(_, place)| owner_json(&doc, place));
|
||||
drop(doc);
|
||||
if let Some(owner) = owner {
|
||||
s.select(|_, sel| sel.set([]))?;
|
||||
return Ok(json!({ "id": id.0, "onCanvas": false, "owner": owner }));
|
||||
}
|
||||
@@ -2310,7 +2377,8 @@ fn go_to(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
fn embed(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "links.embed";
|
||||
let st = s.doc()?;
|
||||
let kind = |id: &NodeId| stored(&st.doc, *id).map(|n| &n.kind);
|
||||
let doc = with_brush_art(&st.doc);
|
||||
let kind = |id: &NodeId| stored(&doc, *id).map(|n| &n.kind);
|
||||
let linked = |id: &NodeId| match kind(id) {
|
||||
Some(NodeKind::Image(im)) => im.link.is_some(),
|
||||
Some(NodeKind::PlacedDocument(_)) => true,
|
||||
@@ -2362,17 +2430,20 @@ fn embed(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
_ => missing.extend(g.ids),
|
||||
}
|
||||
}
|
||||
drop(doc);
|
||||
if !done.is_empty() || !documents.is_empty() {
|
||||
s.edit("Embed", |d, _| {
|
||||
for (ids, read) in reads {
|
||||
install(d, &ids, read, Some(Preserve::Bounds), None)?;
|
||||
}
|
||||
for id in &done {
|
||||
if let Some(Node { kind: NodeKind::Image(im), .. }) = d.stored_node_mut(*id) {
|
||||
im.link = None;
|
||||
editing_brush_art(d, |d| {
|
||||
for (ids, read) in reads {
|
||||
install(d, &ids, read, Some(Preserve::Bounds), None)?;
|
||||
}
|
||||
}
|
||||
documents.iter().try_for_each(|id| super::place::document::expand(d, *id, C))
|
||||
for id in &done {
|
||||
if let Some(Node { kind: NodeKind::Image(im), .. }) = d.stored_node_mut(*id) {
|
||||
im.link = None;
|
||||
}
|
||||
}
|
||||
documents.iter().try_for_each(|id| super::place::document::expand(d, *id, C))
|
||||
})
|
||||
})?;
|
||||
}
|
||||
done.extend(documents);
|
||||
@@ -2395,9 +2466,10 @@ fn convert_image(bytes: &[u8], ext: &str) -> std::result::Result<Vec<u8>, String
|
||||
fn unembed(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "links.unembed";
|
||||
let st = s.doc()?;
|
||||
let id = image_param(st, p, C)?;
|
||||
let n = stored(&st.doc, id).ok_or(crate::EngineError::NoNode(id))?;
|
||||
let im = image_of(&st.doc, id).ok_or_else(|| bad(C, format!("object {} is not an image", id.0)))?;
|
||||
let doc = with_brush_art(&st.doc);
|
||||
let id = image_param(st, &doc, p, C)?;
|
||||
let n = stored(&doc, id).ok_or(crate::EngineError::NoNode(id))?;
|
||||
let im = image_of(&doc, id).ok_or_else(|| bad(C, format!("object {} is not an image", id.0)))?;
|
||||
if im.link.is_some() {
|
||||
return Err(bad(C, format!("image {} is linked already", id.0)));
|
||||
}
|
||||
@@ -2422,7 +2494,8 @@ fn unembed(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let img = linked_image(&bytes, Some(&path))?;
|
||||
write_file(&path, &bytes)?;
|
||||
let read = (link_info(&path, &bytes), img);
|
||||
s.edit("Unembed", |d, _| install(d, &[id], read, Some(Preserve::Bounds), None))?;
|
||||
drop(doc);
|
||||
s.edit("Unembed", |d, _| editing_brush_art(d, |d| install(d, &[id], read, Some(Preserve::Bounds), None)))?;
|
||||
Ok(json!({ "id": id.0, "path": path }))
|
||||
}
|
||||
|
||||
@@ -2480,9 +2553,10 @@ fn scale_rotation(xf: Affine, (px, py): (f64, f64)) -> (Value, Value) {
|
||||
(json!([a.hypot(b) / px * 100.0, c.hypot(d) / py * 100.0]), json!((-b).atan2(a).to_degrees() + 0.0))
|
||||
}
|
||||
|
||||
/// Link Info of placed document `id`.
|
||||
fn placed_info(st: &crate::DocState, id: NodeId) -> Result<Value> {
|
||||
let n = stored(&st.doc, id).ok_or(crate::EngineError::NoNode(id))?;
|
||||
/// Link Info of placed document `id` (looked up in `doc`, the document of `st` as
|
||||
/// [`with_brush_art`] gives it).
|
||||
fn placed_info(st: &crate::DocState, doc: &Document, id: NodeId) -> Result<Value> {
|
||||
let n = stored(doc, id).ok_or(crate::EngineError::NoNode(id))?;
|
||||
let NodeKind::PlacedDocument(pl) = &n.kind else { return Err(bad("links.info", format!("object {} is not a placed document", id.0))) };
|
||||
let size = n.geometric_bounds().unwrap_or_default();
|
||||
let (scale, rotation) = scale_rotation(pl.xf, (1.0, 1.0));
|
||||
@@ -2507,13 +2581,19 @@ fn placed_info(st: &crate::DocState, id: NodeId) -> Result<Value> {
|
||||
|
||||
fn info(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "links.info";
|
||||
let id = linked_param(s.doc()?, p, C)?;
|
||||
if image_of(&s.doc()?.doc, id).is_none() {
|
||||
return placed_info(s.doc()?, id);
|
||||
}
|
||||
let id = {
|
||||
let st = s.doc()?;
|
||||
let doc = with_brush_art(&st.doc);
|
||||
let id = linked_param(st, &doc, p, C)?;
|
||||
if image_of(&doc, id).is_none() {
|
||||
return placed_info(st, &doc, id);
|
||||
}
|
||||
id
|
||||
};
|
||||
let mut out = super::place::image_info(s, &json!({ "id": id.0 }))?;
|
||||
let st = s.doc()?;
|
||||
let im = image_of(&st.doc, id).ok_or_else(|| bad(C, format!("object {} is not an image", id.0)))?;
|
||||
let doc = with_brush_art(&st.doc);
|
||||
let im = image_of(&doc, id).ok_or_else(|| bad(C, format!("object {} is not an image", id.0)))?;
|
||||
let (px, py) = file_pt(&st.doc, &im.key);
|
||||
out["effectivePpi"] = out["ppi"].take();
|
||||
out["ppi"] = json!([72.0 / px, 72.0 / py]);
|
||||
@@ -2530,9 +2610,10 @@ fn info(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
fn placement_options(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "links.placementOptions";
|
||||
let st = s.doc()?;
|
||||
let doc = with_brush_art(&st.doc);
|
||||
let ids =
|
||||
ids_param(p, "ids", C)?.unwrap_or_else(|| st.selection.objects.iter().copied().filter(|id| placement_of(&st.doc, *id).is_some()).collect());
|
||||
if let Some(id) = ids.iter().find(|id| placement_of(&st.doc, **id).is_none()) {
|
||||
ids_param(p, "ids", C)?.unwrap_or_else(|| st.selection.objects.iter().copied().filter(|id| placement_of(&doc, *id).is_some()).collect());
|
||||
if let Some(id) = ids.iter().find(|id| placement_of(&doc, **id).is_none()) {
|
||||
return Err(bad(C, format!("object {} is not an image or a linked file", id.0)));
|
||||
}
|
||||
if ids.is_empty() {
|
||||
@@ -2551,7 +2632,8 @@ fn placement_options(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
None | Some(Value::Null) => None,
|
||||
Some(v) => Some(v.as_bool().ok_or_else(|| bad(C, "clip must be true or false"))?),
|
||||
};
|
||||
let first = ids.first().and_then(|id| placement_of(&st.doc, *id)).unwrap_or_default();
|
||||
let first = ids.first().and_then(|id| placement_of(&doc, *id)).unwrap_or_default();
|
||||
drop(doc);
|
||||
if preserve.is_none() && align.is_none() && clip.is_none() {
|
||||
return Ok(json!({ "placement": placement_json(first) }));
|
||||
}
|
||||
@@ -2561,11 +2643,13 @@ fn placement_options(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
o.clip = clip.unwrap_or(o.clip);
|
||||
};
|
||||
s.edit("Placement Options", |d, _| {
|
||||
for id in &ids {
|
||||
if let Some(o) = d.stored_node_mut(*id).and_then(placement_mut) {
|
||||
set(o);
|
||||
editing_brush_art(d, |d| {
|
||||
for id in &ids {
|
||||
if let Some(o) = d.stored_node_mut(*id).and_then(placement_mut) {
|
||||
set(o);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
})?;
|
||||
let mut shown = first;
|
||||
|
||||
@@ -229,7 +229,7 @@ impl<'a> Collector<'a> {
|
||||
Some((d, base)) => super::links::linked_files(d, base),
|
||||
None => super::links::linked_files(doc, LinkBase::file(Some(source))),
|
||||
};
|
||||
doc.visit_placed(|_, p| {
|
||||
super::links::with_brush_art(doc).visit_placed(|_, p| {
|
||||
let page = (p.link.page.unwrap_or(1), p.bounding, p.page_box);
|
||||
let pages = placed.entry(p.link.path.clone()).or_default();
|
||||
if !pages.contains(&page) {
|
||||
@@ -389,16 +389,20 @@ impl<'a> Collector<'a> {
|
||||
}
|
||||
if self.opts.relink {
|
||||
// The child files have already been rewritten; the hash and size of each
|
||||
// target must reflect the *packaged* bytes rather than the original.
|
||||
doc.update_links(|link: &mut LinkInfo| {
|
||||
if let Some(copy) = local.get(&link.path) {
|
||||
let relative = relative_in_package(destination, ©.name);
|
||||
link.path = self.root.as_ref().map_or_else(|| copy.name.clone(), |r| r.join(©.name).to_string_lossy().into_owned());
|
||||
link.relative = Some(relative);
|
||||
link.size = copy.size;
|
||||
link.hash = copy.hash.clone();
|
||||
link.modified = None;
|
||||
}
|
||||
// target must reflect the *packaged* bytes rather than the original. The links in the
|
||||
// art of the stored brushes are written back into the brush library.
|
||||
let root = &self.root;
|
||||
super::links::editing_brush_art(doc, |doc| {
|
||||
doc.update_links(|link: &mut LinkInfo| {
|
||||
if let Some(copy) = local.get(&link.path) {
|
||||
let relative = relative_in_package(destination, ©.name);
|
||||
link.path = root.as_ref().map_or_else(|| copy.name.clone(), |r| r.join(©.name).to_string_lossy().into_owned());
|
||||
link.relative = Some(relative);
|
||||
link.size = copy.size;
|
||||
link.hash = copy.hash.clone();
|
||||
link.modified = None;
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
if let Some(s) = self.sections.get_mut(section) {
|
||||
|
||||
@@ -962,8 +962,10 @@ pub(crate) fn image_info(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
(None, [id]) => *id,
|
||||
_ => return Err(bad(C, "select one image or pass id")),
|
||||
};
|
||||
// An image in opacity-mask art or a definition's art is found by its id too (`links.list`).
|
||||
let n = st.doc.stored_node(id).map(|(n, _)| &**n).ok_or(EngineError::NoNode(id))?;
|
||||
// An image in opacity-mask art, a definition's art or a stored brush's art is found by its id
|
||||
// too (`links.list`).
|
||||
let doc = super::links::with_brush_art(&st.doc);
|
||||
let n = doc.stored_node(id).map(|(n, _)| &**n).ok_or(EngineError::NoNode(id))?;
|
||||
let NodeKind::Image(im) = &n.kind else { return Err(bad(C, format!("object {} is not an image", id.0))) };
|
||||
// Effective resolution: 72 pt per inch over the points one pixel spans along each axis.
|
||||
let [a, b, c, d, ..] = im.xf.as_coeffs();
|
||||
|
||||
@@ -330,7 +330,8 @@ pub(crate) fn node(d: &mut Document, s: &Source, link: LinkInfo, bounding: bool)
|
||||
}
|
||||
|
||||
/// `d` for output: linked images and placed documents that keep only their previews get their
|
||||
/// files back, else output as their previews, with a warning.
|
||||
/// files back, else output as their previews, with a warning. The art of the stored brushes counts
|
||||
/// too.
|
||||
pub(crate) fn full_documents(d: &Document) -> (Cow<'_, Document>, Vec<String>) {
|
||||
let (d, mut warnings) = super::super::links::full_linked_images(d, Default::default());
|
||||
let doc = match &d {
|
||||
@@ -339,7 +340,7 @@ pub(crate) fn full_documents(d: &Document) -> (Cow<'_, Document>, Vec<String>) {
|
||||
};
|
||||
let mut need: Vec<PlacedDocument> = vec![];
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
doc.visit_placed(|_, p| {
|
||||
super::super::links::with_brush_art(doc).visit_placed(|_, p| {
|
||||
if doc.images.get(&p.key).is_some_and(ImageBlob::is_proxy) && seen.insert(p.key.clone()) {
|
||||
need.push(p.clone());
|
||||
}
|
||||
@@ -370,7 +371,7 @@ pub(crate) fn full_documents(d: &Document) -> (Cow<'_, Document>, Vec<String>) {
|
||||
"{n} placed documents output as their previews: their files can't be found, or changed since they were read (relink or update them)"
|
||||
),
|
||||
});
|
||||
out.placed_as_previews(&previews);
|
||||
super::super::links::editing_brush_art(&mut out, |out| out.placed_as_previews(&previews));
|
||||
}
|
||||
(Cow::Owned(out), warnings)
|
||||
}
|
||||
|
||||
@@ -768,6 +768,223 @@ fn nested_objects_that_repeat_ids_open_with_ids_of_their_own() {
|
||||
assert!(image(&s, layer_image).link.is_some(), "the image in the layers stays linked");
|
||||
}
|
||||
|
||||
/// The files of the linked images that the stored brushes of [`brush_links`] are made of: a scatter
|
||||
/// brush's, an art brush's and a pattern brush's side tile.
|
||||
const BRUSHED: [(&str, &str, &str); 3] = [("scatter.png", "scatter", "Specks"), ("art.png", "art", "Band"), ("side.png", "pattern", "Frame")];
|
||||
|
||||
/// A document whose stored brushes are made of [`BRUSHED`]'s linked images in `dir`. The art
|
||||
/// brush's image is still in the layers too, so its brush art repeats that object's id; the other
|
||||
/// two images were cleared from the layers.
|
||||
fn brush_links(dir: &Folder) -> (Session, NodeId) {
|
||||
for (i, (name, ..)) in (0u8..).zip(BRUSHED) {
|
||||
write(&dir.file(name), &png(20, 10, [30 + 60 * i, 120, 90]));
|
||||
}
|
||||
let mut s = session();
|
||||
let mut kept = NodeId(0);
|
||||
for (name, kind, brush) in BRUSHED {
|
||||
let id = place(&mut s, &dir.file(name));
|
||||
s.execute("brush.new", &json!({"type": kind, "name": brush, "ids": [id.0]})).unwrap();
|
||||
if kind == "art" {
|
||||
kept = id;
|
||||
} else {
|
||||
s.execute("edit.clear", &json!({"ids": [id.0]})).unwrap();
|
||||
}
|
||||
}
|
||||
s.execute("select.set", &json!({"ids": []})).unwrap();
|
||||
(s, kept)
|
||||
}
|
||||
|
||||
/// The image in the art of stored brush `name`, as the document stores it.
|
||||
fn brush_image(s: &Session, name: &str) -> ImageObject {
|
||||
let b = vectorcraft_brush::find(&s.doc().unwrap().doc, name).unwrap_or_else(|| panic!("no brush {name}"));
|
||||
let mut found = None;
|
||||
b.art()[0].walk(&mut |n| {
|
||||
if let (None, NodeKind::Image(im)) = (&found, &n.kind) {
|
||||
found = Some((**im).clone());
|
||||
}
|
||||
});
|
||||
found.unwrap_or_else(|| panic!("no image in brush {name}"))
|
||||
}
|
||||
|
||||
/// The id of the image in the art of stored brush `name`, as the document stores it.
|
||||
fn brush_image_id(s: &Session, name: &str) -> u64 {
|
||||
let b = vectorcraft_brush::find(&s.doc().unwrap().doc, name).unwrap();
|
||||
let mut found = None;
|
||||
b.art()[0].walk(&mut |n| {
|
||||
if found.is_none() && matches!(n.kind, NodeKind::Image(_)) {
|
||||
found = Some(n.id.0);
|
||||
}
|
||||
});
|
||||
found.unwrap()
|
||||
}
|
||||
|
||||
/// Images in the art of stored brushes are listed after the others, one row per image in each brush tile,
|
||||
/// each with an id no other object has (the layer tree keeps its own), the brush named as what holds it; Check
|
||||
/// Links and the missing filter count them, and the ids stay the same from one list to the next.
|
||||
#[test]
|
||||
fn links_in_stored_brush_art_are_listed_and_checked_with_ids() {
|
||||
let dir = Folder::new("brush-ids");
|
||||
let (mut s, kept) = brush_links(&dir);
|
||||
let list = s.execute("links.list", &json!({})).unwrap();
|
||||
let rows = list["links"].as_array().unwrap();
|
||||
let owners: Vec<(&str, &Value)> = rows.iter().map(|r| (r["name"].as_str().unwrap(), &r["owner"])).collect();
|
||||
let library = vectorcraft_brush::library(&s.doc().unwrap().doc);
|
||||
let index = |name: &str| library.iter().position(|b| b.name == name).unwrap();
|
||||
assert_eq!(
|
||||
owners,
|
||||
[
|
||||
("art.png", &Value::Null),
|
||||
("scatter.png", &json!({"kind": "brush", "name": "Specks", "index": index("Specks")})),
|
||||
("art.png", &json!({"kind": "brush", "name": "Band", "index": index("Band")})),
|
||||
("side.png", &json!({"kind": "brush", "name": "Frame", "index": index("Frame"), "tile": "side"})),
|
||||
],
|
||||
"{list}"
|
||||
);
|
||||
assert_eq!(rows[0]["id"], json!(kept.0), "the layer tree keeps its ids: {list}");
|
||||
let ids: std::collections::BTreeSet<u64> = rows.iter().map(|r| r["id"].as_u64().unwrap()).collect();
|
||||
assert_eq!(ids.len(), 4, "{list}");
|
||||
assert!(!ids.contains(&0), "{list}");
|
||||
assert!(rows.iter().all(|r| r["status"] == "ok" && r["linked"] == true), "{list}");
|
||||
assert_eq!(s.execute("links.list", &json!({})).unwrap(), list, "the same ids each time");
|
||||
let other = super::rect(&mut s, 0.0, 0.0, 5.0, 5.0);
|
||||
s.execute("edit.clear", &json!({"ids": [other.0]})).unwrap();
|
||||
assert_eq!(s.execute("links.list", &json!({})).unwrap(), list, "the same ids after other objects are made");
|
||||
let check = s.execute("links.check", &json!({})).unwrap();
|
||||
assert_eq!(check["links"].as_array().unwrap().len(), 3, "{check}");
|
||||
std::fs::remove_file(dir.file("side.png")).unwrap();
|
||||
let check = s.execute("links.check", &json!({})).unwrap();
|
||||
assert_eq!(check["missing"].as_u64(), Some(1), "{check}");
|
||||
let missing = s.execute("links.list", &json!({"show": "missing"})).unwrap();
|
||||
assert_eq!(missing["links"][0]["id"], rows[3]["id"], "{missing}");
|
||||
}
|
||||
|
||||
/// Every links command takes the id of an image in a stored brush's art and acts on that brush's art,
|
||||
/// never on the image in the layers whose id the art repeated: Link Info, Go To (which clears the
|
||||
/// selection and names the brush), Placement Options, Relink, Update and Embed. The ids each change
|
||||
/// gives the art are kept in the library, so the next list names the same rows.
|
||||
#[test]
|
||||
fn links_commands_act_on_images_in_stored_brush_art_by_id() {
|
||||
let dir = Folder::new("brush-act");
|
||||
let (mut s, kept) = brush_links(&dir);
|
||||
let band = row_id_owned(&mut s, "art.png", "brush");
|
||||
assert_ne!(band, kept.0);
|
||||
let info = s.execute("links.info", &json!({"id": band})).unwrap();
|
||||
assert_eq!((info["fileName"].as_str(), info["status"].as_str(), info["id"].as_u64()), (Some("art.png"), Some("ok"), Some(band)), "{info}");
|
||||
s.execute("select.set", &json!({"ids": [kept.0]})).unwrap();
|
||||
let go = s.execute("links.goTo", &json!({"id": band})).unwrap();
|
||||
assert_eq!(
|
||||
(go["onCanvas"].clone(), go["owner"]["kind"].clone(), go["owner"]["name"].clone()),
|
||||
(json!(false), json!("brush"), json!("Band")),
|
||||
"{go}"
|
||||
);
|
||||
assert!(s.doc().unwrap().selection.objects.is_empty(), "nothing on the canvas is selected");
|
||||
s.execute("links.placementOptions", &json!({"ids": [band], "preserve": "fit"})).unwrap();
|
||||
assert_eq!(s.execute("links.placementOptions", &json!({"ids": [band]})).unwrap()["placement"]["preserve"], "fit");
|
||||
assert_eq!(brush_image(&s, "Band").placement.preserve, vectorcraft_doc::links::Preserve::Fit);
|
||||
assert_eq!(brush_image_id(&s, "Band"), band, "the brush keeps the id it is listed by");
|
||||
assert_ne!(image(&s, kept).placement.preserve, vectorcraft_doc::links::Preserve::Fit, "the image in the layers is left as it was");
|
||||
// Relink: the brush's image shows the new file; the image in the layers keeps its file.
|
||||
let other = dir.file("other.png");
|
||||
write(&other, &png(40, 20, BLUE));
|
||||
let r = s.execute("links.relink", &json!({"ids": [band], "path": other})).unwrap();
|
||||
assert_eq!(r["relinked"], json!([band]), "{r}");
|
||||
assert_eq!(brush_image(&s, "Band").link.unwrap().path, other);
|
||||
assert_eq!(image(&s, kept).link.unwrap().path, dir.file("art.png"));
|
||||
assert_eq!(row_id_owned(&mut s, "other.png", "brush"), band, "{}", s.execute("links.list", &json!({})).unwrap());
|
||||
// Update: a changed file is read again for the scatter brush's image.
|
||||
let dots = row_id_owned(&mut s, "scatter.png", "brush");
|
||||
let changed = png(30, 30, RED);
|
||||
write(&dir.file("scatter.png"), &changed);
|
||||
let r = s.execute("links.update", &json!({"ids": [dots]})).unwrap();
|
||||
assert_eq!(r["updated"], json!([dots]), "{r}");
|
||||
assert_eq!(brush_image(&s, "Specks").link.unwrap().size, Some(changed.len() as u64));
|
||||
// Embed: the pattern brush's side tile keeps its pixels and no longer links.
|
||||
let frame = row_id_owned(&mut s, "side.png", "brush");
|
||||
let r = s.execute("links.embed", &json!({"ids": [frame]})).unwrap();
|
||||
assert_eq!(r["embedded"], json!([frame]), "{r}");
|
||||
assert!(brush_image(&s, "Frame").link.is_none());
|
||||
let embedded = s.execute("links.list", &json!({"show": "embedded"})).unwrap();
|
||||
assert_eq!(embedded["links"][0]["id"], json!(frame), "{embedded}");
|
||||
// Relink All Instances takes the image in the layers and the brush's image of the same file.
|
||||
let r = s.execute("links.relink", &json!({"ids": [kept.0], "path": dir.file("scatter.png"), "scope": "all"})).unwrap();
|
||||
assert_eq!(r["relinked"], json!([kept.0]), "{r}");
|
||||
let r = s.execute("links.relink", &json!({"ids": [dots], "path": other, "scope": "all"})).unwrap();
|
||||
let mut relinked: Vec<u64> = r["relinked"].as_array().unwrap().iter().map(|v| v.as_u64().unwrap()).collect();
|
||||
relinked.sort();
|
||||
let mut both = vec![kept.0, dots];
|
||||
both.sort();
|
||||
assert_eq!(relinked, both, "{r}");
|
||||
assert_eq!(image(&s, kept).link.unwrap().path, other);
|
||||
// One undo step puts the brushes back as they were.
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(brush_image(&s, "Specks").link.unwrap().path, dir.file("scatter.png"));
|
||||
}
|
||||
|
||||
/// The id of the `links.list` row of file `name` whose owner is of `kind`.
|
||||
fn row_id_owned(s: &mut Session, name: &str, kind: &str) -> u64 {
|
||||
let list = s.execute("links.list", &json!({})).unwrap();
|
||||
let row = list["links"].as_array().unwrap().iter().find(|r| r["name"] == name && r["owner"]["kind"] == kind);
|
||||
row.unwrap_or_else(|| panic!("{name} in {kind}: {list}"))["id"].as_u64().unwrap()
|
||||
}
|
||||
|
||||
/// A document saved with links in its stored brushes opens with them reported by the ids the list
|
||||
/// gives them, and a library entry that does not read as a brush is kept as it was.
|
||||
#[test]
|
||||
fn links_in_stored_brush_art_open_by_id_and_unreadable_entries_stay() {
|
||||
let dir = Folder::new("brush-open");
|
||||
let (mut s, _) = brush_links(&dir);
|
||||
let odd = json!({"name": "Odd", "type": "unknown", "art": {"id": 3}});
|
||||
s.edit("Odd brush", |d, _| {
|
||||
if let Some(list) = d.unknown.get_mut(vectorcraft_brush::DOC_KEY).and_then(Value::as_array_mut) {
|
||||
list.push(odd.clone());
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
let path = dir.file("poster.vectorcraft");
|
||||
save(&mut s, &path);
|
||||
std::fs::remove_file(dir.file("scatter.png")).unwrap();
|
||||
let r = open(&mut s, &path);
|
||||
let dots = row_id_owned(&mut s, "scatter.png", "brush");
|
||||
assert_eq!(r["missingLinks"][0]["ids"], json!([dots]), "{r}");
|
||||
assert_eq!(brush_image_id(&s, "Specks"), dots, "the library keeps the ids opening gave");
|
||||
let band = row_id_owned(&mut s, "art.png", "brush");
|
||||
s.execute("links.embed", &json!({"ids": [band]})).unwrap();
|
||||
let doc = &s.doc().unwrap().doc;
|
||||
let stored = doc.unknown[vectorcraft_brush::DOC_KEY].as_array().unwrap();
|
||||
assert_eq!(stored.last(), Some(&odd), "the unreadable entry is left as it was");
|
||||
assert!(doc.mounted_art.is_empty());
|
||||
}
|
||||
|
||||
/// The Color Mask of an image in a stored brush's art is made again for the new pixels when Update
|
||||
/// reads the image's changed file.
|
||||
#[test]
|
||||
fn updating_an_image_in_stored_brush_art_makes_its_color_mask_again() {
|
||||
let dir = Folder::new("brush-mask");
|
||||
let path = dir.file("logo.png");
|
||||
write(&path, &png(12, 10, RED));
|
||||
let mut s = session();
|
||||
let id = place(&mut s, &path);
|
||||
s.execute("select.set", &json!({"ids": [id.0]})).unwrap();
|
||||
let hide_white = json!({"background": "colors", "colors": "#ffffff", "contiguous": false, "tolerance": 0, "softness": 0});
|
||||
s.execute("addon.image.colorMask", &hide_white).unwrap();
|
||||
s.execute("brush.new", &json!({"type": "scatter", "name": "Masked", "ids": [id.0]})).unwrap();
|
||||
s.execute("edit.clear", &json!({"ids": [id.0]})).unwrap();
|
||||
write(&path, &png(14, 10, BLUE));
|
||||
let row = row_id_owned(&mut s, "logo.png", "brush");
|
||||
let r = s.execute("links.update", &json!({"ids": [row]})).unwrap();
|
||||
assert_eq!(r["updated"], json!([row]), "{r}");
|
||||
let doc = &s.doc().unwrap().doc;
|
||||
let brush = vectorcraft_brush::find(doc, "Masked").unwrap();
|
||||
let mut masks = vec![];
|
||||
brush.art()[0].walk(&mut |n| {
|
||||
if let (NodeKind::Image(im), Some(m)) = (&n.kind, Document::color_mask_of(n)) {
|
||||
masks.push((m.made_for(im), doc.images.contains_key(&m.key)));
|
||||
}
|
||||
});
|
||||
assert_eq!(masks, [(true, true)], "the mask is made for the new pixels");
|
||||
}
|
||||
|
||||
/// An image that Placement Options clip, alone in a group the user made, goes into a new clip group
|
||||
/// inside that group when its file grows, in the layers and in a symbol's art; the user's group
|
||||
/// stays a plain group.
|
||||
|
||||
@@ -90,6 +90,38 @@ fn two_links_and_a_font_are_collected_and_relinked() {
|
||||
assert!(s.doc().unwrap().doc.images.values().all(|b| !b.is_proxy()), "the copies' pixels");
|
||||
}
|
||||
|
||||
/// A file linked only from a stored brush's art is copied, and the packaged document's brush
|
||||
/// links to the copy.
|
||||
#[test]
|
||||
fn a_file_linked_from_a_stored_brush_is_collected_and_relinked() {
|
||||
let dir = Folder::new("package-brush");
|
||||
let file = dir.file("art/leaf.png");
|
||||
write(&file, &png(40, 20, RED));
|
||||
let mut s = session();
|
||||
let id = place(&mut s, &file);
|
||||
run(&mut s, "brush.new", json!({"type": "scatter", "name": "Leaves Here", "ids": [id.0]}));
|
||||
run(&mut s, "edit.clear", json!({"ids": [id.0]}));
|
||||
save(&mut s, &dir.file("poster.vectorcraft"));
|
||||
let out = dir.file("out");
|
||||
let r = run(&mut s, "file.package", json!({"folder": out, "copyFonts": false}));
|
||||
let files: Vec<&str> = r["files"].as_array().unwrap().iter().map(|f| f.as_str().unwrap()).collect();
|
||||
assert!(files.contains(&"Links/leaf.png"), "{r}");
|
||||
let root = std::path::Path::new(&out).join("poster Folder");
|
||||
std::fs::remove_dir_all(dir.0.join("art")).unwrap();
|
||||
let r = open(&mut s, &root.join("poster.vectorcraft").to_string_lossy());
|
||||
assert_eq!(r["missingLinks"], json!([]), "{r}");
|
||||
let brush = vectorcraft_brush::find(&s.doc().unwrap().doc, "Leaves Here").unwrap();
|
||||
let mut paths = vec![];
|
||||
brush.art()[0].walk(&mut |n| {
|
||||
if let vectorcraft_doc::NodeKind::Image(im) = &n.kind {
|
||||
paths.push(im.link.as_ref().map(|l| (l.path.clone(), l.relative.clone())));
|
||||
}
|
||||
});
|
||||
let [Some((path, relative))] = paths.as_slice() else { panic!("{paths:?}") };
|
||||
assert!(std::path::Path::new(path).starts_with(root.join("Links")), "{path}");
|
||||
assert_eq!(relative.as_deref(), Some("Links/leaf.png"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_unsaved_document_cant_be_packaged() {
|
||||
let mut s = session();
|
||||
|
||||
@@ -1042,6 +1042,57 @@ fn saving_a_document_refreshes_its_placed_copies_inside_symbols_and_patterns() {
|
||||
assert_eq!(hashes, [hash.clone(), hash], "the symbol's, then the pattern's");
|
||||
}
|
||||
|
||||
/// Saving a document reads it again for its placed copy in the art of a stored brush of another
|
||||
/// open document, the only place that document shows it.
|
||||
#[test]
|
||||
fn saving_a_document_refreshes_its_placed_copy_in_a_stored_brush() {
|
||||
let dir = Folder::new("placed-refresh-brush");
|
||||
let src = dir.file("logo.vectorcraft");
|
||||
source(&src, 100.0, 50.0, RED, 0);
|
||||
let mut s = session();
|
||||
let id = place(&mut s, &src);
|
||||
s.execute("brush.new", &json!({"type": "scatter", "name": "Logos", "ids": [id.0]})).unwrap();
|
||||
s.execute("edit.clear", &json!({"ids": [id.0]})).unwrap();
|
||||
let parent = s.active_index().unwrap();
|
||||
s.execute("document.open", &json!({"path": src})).unwrap();
|
||||
s.execute("select.all", &json!({})).unwrap();
|
||||
s.execute("paint.setFill", &json!({"color": "#1414e6"})).unwrap();
|
||||
s.execute("document.save", &json!({})).unwrap();
|
||||
let hash = Some(vectorcraft_doc::links::hash_bytes(&std::fs::read(&src).unwrap()));
|
||||
let doc = &s.documents()[parent];
|
||||
assert_eq!(doc.history.undo.last().unwrap().label, "Update Links");
|
||||
let brush = vectorcraft_brush::find(&doc.doc, "Logos").unwrap();
|
||||
let mut hashes = vec![];
|
||||
brush.art()[0].walk(&mut |n| {
|
||||
if let NodeKind::PlacedDocument(p) = &n.kind {
|
||||
hashes.push(p.link.hash.clone());
|
||||
}
|
||||
});
|
||||
assert_eq!(hashes, [hash]);
|
||||
}
|
||||
|
||||
/// A placed document kept only in a stored brush's art, whose file is gone and of which the
|
||||
/// document holds only the preview, is output as its preview with the warning that says so.
|
||||
#[test]
|
||||
fn a_placed_document_in_a_stored_brush_without_its_file_is_output_as_its_preview() {
|
||||
let dir = Folder::new("placed-brush-preview");
|
||||
let src = dir.file("logo.vectorcraft");
|
||||
// Content of its own: files read are cached by content, and this one is removed.
|
||||
source(&src, 100.0, 50.0, RED, 3);
|
||||
let mut s = session();
|
||||
let id = place(&mut s, &src);
|
||||
s.execute("brush.new", &json!({"type": "scatter", "name": "Logos", "ids": [id.0]})).unwrap();
|
||||
// The copy in the layers has the save keep only the preview.
|
||||
let doc_path = dir.file("poster.vectorcraft");
|
||||
save(&mut s, &doc_path);
|
||||
std::fs::remove_file(&src).unwrap();
|
||||
let r = open(&mut s, &doc_path);
|
||||
assert_eq!(r["missingLinks"].as_array().map(Vec::len), Some(1), "{r}");
|
||||
s.execute("edit.clear", &json!({"ids": [id.0]})).unwrap();
|
||||
let svg = s.execute("document.serialize", &json!({"format": "svg"})).unwrap();
|
||||
assert!(svg["warnings"].to_string().contains("1 placed document output as its preview"), "{svg}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn link_info_gives_the_artboards_size_not_pixels() {
|
||||
let dir = Folder::new("placed-info");
|
||||
|
||||
@@ -360,8 +360,8 @@ fn badge(ui: &Ui, r: egui::Rect, status: &str) {
|
||||
}
|
||||
|
||||
/// The tooltip of row `r`. A row outside the layer tree also names where it is stored
|
||||
/// (`links.list`'s `owner`): the symbol, graph design or pattern whose art holds it, and the object
|
||||
/// whose opacity mask holds it, by `mask` (that object's name) or else by its id.
|
||||
/// (`links.list`'s `owner`): the symbol, graph design, pattern or brush whose art holds it, and the
|
||||
/// object whose opacity mask holds it, by `mask` (that object's name) or else by its id.
|
||||
pub(super) fn status_tip(r: &Value, mask: Option<String>) -> String {
|
||||
let tip = file_tip(r);
|
||||
let owner = &r["owner"];
|
||||
@@ -370,6 +370,7 @@ pub(super) fn status_tip(r: &Value, mask: Option<String>) -> String {
|
||||
Some("symbol") => Some(tl!("Symbols")),
|
||||
Some("graphDesign") => Some(tl!("Graph Design")),
|
||||
Some("pattern") => Some(tl!("Pattern")),
|
||||
Some("brush") => Some(tl!("Brushes")),
|
||||
_ => None,
|
||||
};
|
||||
let tip = match art {
|
||||
|
||||
@@ -189,6 +189,7 @@ fn a_nested_rows_tooltip_names_what_holds_it() {
|
||||
assert_eq!(tip(json!({"kind": "symbol", "name": "Mark"}), None), "/x/a.png\nSymbols: Mark");
|
||||
assert_eq!(tip(json!({"kind": "pattern", "name": "Dots"}), None), "/x/a.png\nPattern: Dots");
|
||||
assert_eq!(tip(json!({"kind": "graphDesign", "name": "Bar"}), None), "/x/a.png\nGraph Design: Bar");
|
||||
assert_eq!(tip(json!({"kind": "brush", "name": "Leaf", "index": 2}), None), "/x/a.png\nBrushes: Leaf");
|
||||
assert_eq!(tip(json!({"kind": "layers", "maskOf": 7}), Some("<Ellipse>")), "/x/a.png\nOpacity Mask: <Ellipse>");
|
||||
assert_eq!(tip(json!({"kind": "symbol", "name": "Mark", "maskOf": 7}), None), "/x/a.png\nSymbols: Mark\nOpacity Mask: 7");
|
||||
// A row in the layers says only its file.
|
||||
|
||||
Reference in New Issue
Block a user