mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-15 01:15:25 +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();
|
||||
|
||||
Reference in New Issue
Block a user