mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-08 02:30:09 +00:00
Keep a symbol's options when pasting it into another document
Fixes #5902 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
committed by
bflatastic
co-authored by
Claude Opus 5.5
parent
e83bfa5fa1
commit
a76e157de7
@@ -37,6 +37,7 @@ use dynamic::{symbol_instance_reset, symbol_instance_set_appearance};
|
||||
use symbols::selected_instances;
|
||||
pub(crate) use symbols::{
|
||||
expand_instances_under, expandable_instances, instance_in_place, natural_size, normalize, place_xf, set_natural_size, store_symbol,
|
||||
stored_natural_size,
|
||||
};
|
||||
use symbols::{
|
||||
symbol_adjust, symbol_break_link, symbol_delete, symbol_duplicate, symbol_list, symbol_new, symbol_options, symbol_place, symbol_replace,
|
||||
|
||||
@@ -8,11 +8,12 @@ use super::{
|
||||
// ---------- symbols ----------
|
||||
|
||||
pub(crate) fn natural_size(d: &Document, name: &str) -> (f64, f64) {
|
||||
d.unknown
|
||||
.get(SIZES_KEY)
|
||||
.and_then(|m| m.get(name))
|
||||
.and_then(|v| Some((v.get(0)?.as_f64()?, v.get(1)?.as_f64()?)))
|
||||
.unwrap_or((2.0 * HALF, 2.0 * HALF))
|
||||
stored_natural_size(d, name).unwrap_or((2.0 * HALF, 2.0 * HALF))
|
||||
}
|
||||
|
||||
/// The natural size `d` records for symbol `name` (`None` when it records none).
|
||||
pub(crate) fn stored_natural_size(d: &Document, name: &str) -> Option<(f64, f64)> {
|
||||
d.unknown.get(SIZES_KEY).and_then(|m| m.get(name)).and_then(|v| Some((v.get(0)?.as_f64()?, v.get(1)?.as_f64()?)))
|
||||
}
|
||||
|
||||
pub(crate) fn set_natural_size(d: &mut Document, name: &str, size: Option<(f64, f64)>) {
|
||||
|
||||
@@ -24,7 +24,7 @@ use vectorcraft_geom::{PathData, Rect};
|
||||
pub use flavours::{AI, BITMAP, EMF, EPS, FILE_HEAD, Flavour, PASTE_ORDER, PDF, PNG, SVG, TEXT, file_flavour, is_address, names_a_picture};
|
||||
pub use flavours::{ImageRead, read_image};
|
||||
pub(crate) use resources::SwatchChoices;
|
||||
pub use resources::{ArtResources, art_resources};
|
||||
pub use resources::{ArtResources, SymbolOptions, art_resources};
|
||||
|
||||
use super::{CommandSpec, Result, Session, always, bad, cmd, f64_or, has_doc, point_param, str_param};
|
||||
use crate::DocState;
|
||||
@@ -70,6 +70,8 @@ pub struct Clipboard {
|
||||
pub images: BTreeMap<String, ImageBlob>,
|
||||
/// Symbols the objects place (also inside other symbols and patterns).
|
||||
pub symbols: Vec<Symbol>,
|
||||
/// The Symbol Options of [`Self::symbols`] (which the document keeps by name), by symbol name.
|
||||
pub symbol_options: BTreeMap<String, SymbolOptions>,
|
||||
/// Patterns the objects paint with (pasting makes their swatches again).
|
||||
pub patterns: Vec<PatternDef>,
|
||||
/// Global and spot swatches the objects' colours link to, the tint swatches of the tints they
|
||||
|
||||
@@ -20,7 +20,12 @@ use std::sync::Arc;
|
||||
use serde_json::{Map, Value, json};
|
||||
use vectorcraft_brush::Brush;
|
||||
use vectorcraft_color::{Color, Paint, Swatch};
|
||||
use vectorcraft_doc::{Appearance, AppearanceItem, Document, GraphicStyle, Node, NodeKind, PatternDef, Symbol, TextStyleDef};
|
||||
use vectorcraft_doc::{
|
||||
Appearance, AppearanceItem, Document, GraphicStyle, Node, NodeKind, PatternDef, Symbol, SymbolExportType, SymbolNineSlice, SymbolRegistration,
|
||||
TextStyleDef,
|
||||
};
|
||||
|
||||
use super::super::brushsym::{set_natural_size, stored_natural_size};
|
||||
|
||||
use super::super::patterncmds::add_pattern;
|
||||
use super::super::{Result, bad, unique_name};
|
||||
@@ -628,6 +633,38 @@ fn shows_definitions(b: &Brush) -> bool {
|
||||
found
|
||||
}
|
||||
|
||||
/// A symbol's Symbol Options, which its document keeps by symbol name: copied with the symbol and
|
||||
/// given to it under the name pasting adds it as.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct SymbolOptions {
|
||||
/// Natural size `(w, h)` (`None`: the document records none).
|
||||
pub size: Option<(f64, f64)>,
|
||||
pub registration: SymbolRegistration,
|
||||
pub export_type: SymbolExportType,
|
||||
/// 9-slice guides (`None`: 9-slice scaling off).
|
||||
pub nine_slice: Option<SymbolNineSlice>,
|
||||
}
|
||||
|
||||
impl SymbolOptions {
|
||||
/// The options `d` keeps for symbol `name`.
|
||||
fn of(d: &Document, name: &str) -> Self {
|
||||
Self {
|
||||
size: stored_natural_size(d, name),
|
||||
registration: d.symbol_registration(name),
|
||||
export_type: d.symbol_export_type(name),
|
||||
nine_slice: d.symbol_nine_slice(name),
|
||||
}
|
||||
}
|
||||
|
||||
/// Make them the options of `d`'s symbol `name`.
|
||||
fn give(&self, d: &mut Document, name: &str) {
|
||||
set_natural_size(d, name, self.size);
|
||||
d.set_symbol_registration(name, self.registration);
|
||||
d.set_symbol_export_type(name, self.export_type);
|
||||
d.set_symbol_nine_slice(name, self.nine_slice);
|
||||
}
|
||||
}
|
||||
|
||||
/// What pasting did to the document's resources (part of the paste result).
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct Imported {
|
||||
@@ -671,6 +708,12 @@ impl Clipboard {
|
||||
Self {
|
||||
images: src.images.iter().filter(|(k, _)| used.has(Res::Image, k)).map(|(k, b)| (k.clone(), b.clone())).collect(),
|
||||
symbols: src.symbols.iter().filter(|s| used.has(Res::Symbol, &s.name)).cloned().collect(),
|
||||
symbol_options: src
|
||||
.symbols
|
||||
.iter()
|
||||
.filter(|s| used.has(Res::Symbol, &s.name))
|
||||
.map(|s| (s.name.clone(), SymbolOptions::of(src, &s.name)))
|
||||
.collect(),
|
||||
patterns: src.patterns.iter().filter(|p| used.has(Res::Pattern, &p.name)).cloned().collect(),
|
||||
swatches: src.swatches_iter().filter(|w| used.carries(w)).cloned().collect(),
|
||||
graphic_styles: src.graphic_styles.iter().filter(|g| g.id != 0 && used.styles.contains(&g.id)).cloned().collect(),
|
||||
@@ -725,13 +768,27 @@ impl Clipboard {
|
||||
out.placed(r, &def.name, &p);
|
||||
}
|
||||
}
|
||||
// Symbols and patterns, compared once relinked. Those added are relinked at the end, when
|
||||
// every new name and graphic style id is known.
|
||||
// Symbols and patterns, compared once relinked (symbols with their options too). Those
|
||||
// added are relinked at the end, when every new name and graphic style id is known.
|
||||
let mut added_symbols = HashSet::new();
|
||||
for def in self.symbols.iter().filter(|s| used.has(Res::Symbol, &s.name)) {
|
||||
let options = self.symbol_options.get(&def.name);
|
||||
let theirs: HashMap<String, SymbolOptions> = match options {
|
||||
Some(_) => {
|
||||
d.symbols.iter().filter(|s| copy_of(&def.name, &s.name)).map(|s| (s.name.clone(), SymbolOptions::of(d, &s.name))).collect()
|
||||
}
|
||||
None => HashMap::new(),
|
||||
};
|
||||
let r = &out.relink;
|
||||
let p = place(&mut d.symbols, def, same_doc, |a, b| same_art(r, std::slice::from_ref(&a.art), std::slice::from_ref(&b.art)));
|
||||
added_symbols.extend(p.new_name(&def.name));
|
||||
let p = place(&mut d.symbols, def, same_doc, |a, b| {
|
||||
options.is_none_or(|o| theirs.get(&a.name) == Some(o)) && same_art(r, std::slice::from_ref(&a.art), std::slice::from_ref(&b.art))
|
||||
});
|
||||
if let Some(name) = p.new_name(&def.name) {
|
||||
if let Some(o) = options {
|
||||
o.give(d, &name);
|
||||
}
|
||||
added_symbols.insert(name);
|
||||
}
|
||||
out.placed(Res::Symbol, &def.name, &p);
|
||||
}
|
||||
let mut added_patterns = HashSet::new();
|
||||
|
||||
@@ -850,3 +850,94 @@ fn artwork_drop_uses_the_current_layer_even_when_paste_remembers_hidden_or_locke
|
||||
assert!(doc(&s).paste_remembers_layers, "the preference still applies to ordinary Paste");
|
||||
}
|
||||
}
|
||||
|
||||
/// Two touching rectangles made into 9-slice symbol `Audit`, its instance scaled 150% × 75% and
|
||||
/// copied (#5902).
|
||||
fn copied_nine_slice_instance(s: &mut Session) -> Document {
|
||||
let red = rect(s, 30.0, 30.0, 80.0, 60.0);
|
||||
s.execute("paint.setFill", &json!({"ids": [red.0], "color": "#ff0000"})).unwrap();
|
||||
let blue = rect(s, 110.0, 90.0, 40.0, 30.0);
|
||||
s.execute("paint.setFill", &json!({"ids": [blue.0], "color": "#0000ff"})).unwrap();
|
||||
s.execute("select.all", &json!({})).unwrap();
|
||||
s.execute("symbol.new", &json!({"name": "Audit", "nineSlice": true, "registration": 0, "exportType": "graphic"})).unwrap();
|
||||
s.execute("object.scale", &json!({"sx": 150, "sy": 75})).unwrap();
|
||||
s.execute("edit.copy", &json!({})).unwrap();
|
||||
doc(s).clone()
|
||||
}
|
||||
|
||||
/// The symbol options pasted symbol `to` should have: those of `from` in `src`.
|
||||
fn assert_same_symbol_options(src: &Document, from: &str, d: &Document, to: &str) {
|
||||
assert!(src.symbol_nine_slice(from).is_some());
|
||||
assert_eq!(d.symbol_nine_slice(to), src.symbol_nine_slice(from));
|
||||
assert_eq!(d.symbol_registration(to), src.symbol_registration(from));
|
||||
assert_eq!(d.symbol_export_type(to), src.symbol_export_type(from));
|
||||
assert_eq!(d.symbol_natural_xf(to), src.symbol_natural_xf(from));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_nine_slice_symbol_pasted_into_another_document_keeps_its_options_and_look() {
|
||||
let mut s = session();
|
||||
let src = copied_nine_slice_instance(&mut s);
|
||||
let before = render(&s);
|
||||
new_doc(&mut s);
|
||||
s.execute("edit.pasteInPlace", &json!({})).unwrap();
|
||||
assert_same_symbol_options(&src, "Audit", doc(&s), "Audit");
|
||||
assert!(before == render(&s), "the pasted instance draws as in the source");
|
||||
// Pasting again uses the symbol the first paste added.
|
||||
let v = s.execute("edit.pasteInPlace", &json!({})).unwrap();
|
||||
assert_eq!(v["added"], 0, "{v}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_renamed_nine_slice_symbol_keeps_its_options_under_the_new_name() {
|
||||
let mut s = session();
|
||||
let src = copied_nine_slice_instance(&mut s);
|
||||
new_doc(&mut s);
|
||||
let other = ellipse(&mut s);
|
||||
sel(&mut s, &[other]);
|
||||
s.execute("symbol.new", &json!({"name": "Audit"})).unwrap();
|
||||
let own = doc(&s).clone();
|
||||
let v = s.execute("edit.pasteInPlace", &json!({})).unwrap();
|
||||
assert!(renamed(&v).contains(&("symbol".into(), "Audit".into(), "Audit 2".into())), "{v}");
|
||||
assert_same_symbol_options(&src, "Audit", doc(&s), "Audit 2");
|
||||
// The document's own symbol keeps its options.
|
||||
assert_eq!(doc(&s).symbol_nine_slice("Audit"), None);
|
||||
assert_eq!(doc(&s).symbol_natural_xf("Audit"), own.symbol_natural_xf("Audit"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_same_named_symbol_with_other_options_is_not_reused() {
|
||||
let mut s = session();
|
||||
let src = copied_nine_slice_instance(&mut s);
|
||||
new_doc(&mut s);
|
||||
s.execute("edit.pasteInPlace", &json!({})).unwrap();
|
||||
// The document's `Audit` has the same art but 9-slice scaling off now.
|
||||
s.execute("symbol.options", &json!({"name": "Audit", "nineSlice": false})).unwrap();
|
||||
let v = s.execute("edit.pasteInPlace", &json!({})).unwrap();
|
||||
assert!(renamed(&v).contains(&("symbol".into(), "Audit".into(), "Audit 2".into())), "{v}");
|
||||
assert_same_symbol_options(&src, "Audit", doc(&s), "Audit 2");
|
||||
assert_eq!(doc(&s).symbol_nine_slice("Audit"), None, "the document's own symbol keeps its options");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_symbol_without_a_recorded_natural_size_is_pasted_without_one() {
|
||||
let mut s = session();
|
||||
let r = rect(&mut s, 30.0, 30.0, 80.0, 60.0);
|
||||
sel(&mut s, &[r]);
|
||||
s.execute("symbol.new", &json!({"name": "Plain"})).unwrap();
|
||||
s.edit("Forget size", |d, _| {
|
||||
if let Some(m) = d.unknown.get_mut(vectorcraft_doc::SYMBOL_SIZES).and_then(Value::as_object_mut) {
|
||||
m.remove("Plain");
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(doc(&s).unknown.get(vectorcraft_doc::SYMBOL_SIZES).and_then(|m| m.get("Plain")), None);
|
||||
s.execute("edit.copy", &json!({})).unwrap();
|
||||
let before = render(&s);
|
||||
new_doc(&mut s);
|
||||
s.execute("edit.pasteInPlace", &json!({})).unwrap();
|
||||
assert!(doc(&s).symbols.iter().any(|x| x.name == "Plain"));
|
||||
assert_eq!(doc(&s).unknown.get(vectorcraft_doc::SYMBOL_SIZES).and_then(|m| m.get("Plain")), None, "no size is made up for it");
|
||||
assert!(before == render(&s), "the pasted instance draws as in the source");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user