Files
figcraft/crates/engine/src/property_copy_tests.rs
T
Nicek 4c0eb71a4c fix: 粘贴图片填充时避开目标文件已占用的图片键
目标文件已有同名键但数据不同时,粘贴的 paint 改用复制数据的内容哈希键,避免渲染成目标文件里的图片;目标文件原图片保留给已有图层。
2026-10-29 09:33:55 +08:00

174 lines
8.1 KiB
Rust

//! Copying one fill, stroke or effect row (`fill.copy`, `stroke.copy`, `effect.copy`) and pasting
//! it onto other layers (`fill.paste`, `stroke.paste`, `effect.paste`): the Design panel's row
//! menus.
use figcraft_color::{Color, Paint};
use figcraft_doc::{Effect, EffectKind, NodeId};
use serde_json::{Value, json};
use crate::Session;
fn id(v: Value) -> NodeId {
NodeId(v["id"].as_u64().unwrap())
}
fn ex(s: &mut Session, cmd: &str, p: Value) -> Value {
s.execute(cmd, &p).unwrap_or_else(|e| panic!("{cmd} {p}: {e}"))
}
fn rect(s: &mut Session) -> NodeId {
id(ex(s, "shape.rectangle", json!({"x": 0, "y": 0, "width": 10, "height": 10})))
}
fn node(s: &Session, id: NodeId) -> figcraft_doc::Node {
s.doc().unwrap().node(id).unwrap().clone()
}
#[test]
fn a_copied_fill_replaces_a_fill_row_or_adds_one() {
let mut s = Session::new();
let a = rect(&mut s);
let b = rect(&mut s);
let red = Paint::solid(Color::from_hex("#FF0000").unwrap());
ex(&mut s, "fill.add", json!({"ids": [a], "paint": red}));
let copied = ex(&mut s, "fill.copy", json!({"id": a, "index": 1}));
assert_eq!(copied["type"], "solid", "the copy reports the paint it took");
// Onto b's fill row: it replaces that paint.
ex(&mut s, "fill.paste", json!({"ids": [b], "index": 0}));
assert_eq!(node(&s, b).fills, vec![red.clone()]);
// With no row: it adds one on top.
ex(&mut s, "fill.paste", json!({"ids": [b]}));
assert_eq!(node(&s, b).fills, [red.clone(), red]);
// Each paste is one undo step.
ex(&mut s, "edit.undo", json!({}));
assert_eq!(node(&s, b).fills.len(), 1);
}
#[test]
fn a_copied_stroke_pastes_as_a_stroke_only() {
let mut s = Session::new();
let a = rect(&mut s);
let b = rect(&mut s);
let blue = Paint::solid(Color::from_hex("#0000FF").unwrap());
ex(&mut s, "stroke.add", json!({"ids": [a], "paint": blue}));
ex(&mut s, "node.set", json!({"ids": [a], "props": {"strokeWeight": 3.0, "strokeAlign": "outside"}}));
ex(&mut s, "stroke.copy", json!({"id": a, "index": 0}));
ex(&mut s, "stroke.add", json!({"ids": [b]}));
ex(&mut s, "stroke.paste", json!({"ids": [b], "index": 0}));
assert_eq!(node(&s, b).strokes, [blue]);
assert_eq!(node(&s, b).stroke, node(&s, a).stroke, "the weight and alignment come with the paint");
assert_eq!((node(&s, b).stroke.weight, node(&s, b).stroke.align), (3.0, figcraft_doc::StrokeAlign::Outside));
// A copied fill doesn't paste as a stroke, and the target keeps its stroke.
ex(&mut s, "fill.copy", json!({"id": a}));
assert!(s.execute("stroke.paste", &json!({"ids": [b]})).is_err());
assert_eq!(node(&s, b).strokes.len(), 1);
// A layer with no stroke has nothing to copy.
assert!(s.execute("stroke.copy", &json!({"id": b, "index": 1})).is_err());
}
#[test]
fn a_copied_effect_replaces_an_effect_row_or_adds_one() {
let mut s = Session::new();
let a = rect(&mut s);
let b = rect(&mut s);
ex(&mut s, "effect.add", json!({"ids": [a], "effect": {"type": "dropShadow", "radius": 7, "offset": [2, 3]}}));
ex(&mut s, "effect.copy", json!({"id": a, "index": 0}));
let e = node(&s, a).effects[0].clone();
// Onto b's effect row: it replaces that effect.
ex(&mut s, "effect.add", json!({"ids": [b], "type": "layerBlur"}));
ex(&mut s, "effect.paste", json!({"ids": [b], "index": 0}));
assert_eq!(node(&s, b).effects, vec![e.clone()]);
// With no row: it adds one on top.
ex(&mut s, "effect.paste", json!({"ids": [b]}));
assert_eq!(node(&s, b).effects, [e.clone(), e]);
ex(&mut s, "edit.undo", json!({}));
assert_eq!(node(&s, b).effects.len(), 1);
let Effect { kind: EffectKind::DropShadow(shadow), visible } = node(&s, b).effects[0].clone() else {
panic!("the pasted effect is a drop shadow")
};
assert_eq!((shadow.radius, shadow.offset, visible), (7.0, (2.0, 3.0), true));
}
#[test]
fn pasting_design_layers_keeps_a_copied_row() {
let mut s = Session::new();
let a = rect(&mut s);
ex(&mut s, "fill.copy", json!({"id": a}));
// Layers pasted from another app replace the copied layers, not the copied fill row.
let mut other = Session::new();
other.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 10, "height": 10})).unwrap();
let exported = other.execute("file.export", &json!({"format": "fig"})).unwrap();
ex(&mut s, "edit.pasteDesignLayers", json!({"data": exported["data"]}));
let b = rect(&mut s);
ex(&mut s, "fill.paste", json!({"ids": [b], "index": 0}));
}
#[test]
fn a_copied_image_fill_brings_its_image_to_another_file() {
let mut s = Session::new();
let a = rect(&mut s);
ex(&mut s, "fill.add", json!({"ids": [a], "paint": {"type": "image", "image": "img-key"}}));
let data = figcraft_doc::ImageData { bytes: std::sync::Arc::new(vec![1, 2, 3]), width: 2, height: 2 };
s.doc_mut().unwrap().images.insert("img-key".into(), data.clone());
ex(&mut s, "fill.copy", json!({"id": a, "index": 1}));
// Another file has no image under that key: the copy brings the data with it.
ex(&mut s, "file.new", json!({}));
let b = rect(&mut s);
assert!(!s.doc().unwrap().images.contains_key("img-key"));
ex(&mut s, "fill.paste", json!({"ids": [b], "index": 0}));
assert_eq!(s.doc().unwrap().images.get("img-key"), Some(&data));
assert!(matches!(node(&s, b).fills[0].kind, figcraft_color::PaintKind::Image(_)));
}
#[test]
fn a_copied_image_key_taken_in_another_file_gets_the_copied_data() {
let mut s = Session::new();
let a = rect(&mut s);
let copied = figcraft_doc::ImageData { bytes: std::sync::Arc::new(vec![1, 2, 3]), width: 2, height: 2 };
let taken = figcraft_doc::ImageData { bytes: std::sync::Arc::new(vec![9, 9, 9]), width: 3, height: 3 };
ex(&mut s, "fill.add", json!({"ids": [a], "paint": {"type": "image", "image": "img-key"}}));
s.doc_mut().unwrap().images.insert("img-key".into(), copied.clone());
ex(&mut s, "fill.copy", json!({"id": a, "index": 1}));
// The other file already uses that key for different data: the pasted paint takes a content
// key for the copied data instead, and the destination's image stays as it is.
ex(&mut s, "file.new", json!({}));
let b = rect(&mut s);
s.doc_mut().unwrap().images.insert("img-key".into(), taken.clone());
ex(&mut s, "fill.paste", json!({"ids": [b], "index": 0}));
let doc = s.doc().unwrap();
let fill = &doc.node(b).unwrap().fills[0];
let figcraft_color::PaintKind::Image(img) = &fill.kind else { panic!("the pasted fill is an image") };
assert_ne!(img.image, "img-key");
assert_eq!(doc.images.get(&img.image), Some(&copied));
assert_eq!(doc.images.get("img-key"), Some(&taken), "the destination's image stays");
}
#[test]
fn a_copied_pattern_does_not_paste_into_another_file() {
let mut s = Session::new();
let source = rect(&mut s);
let patterned = rect(&mut s);
ex(&mut s, "fill.pattern", json!({"ids": [patterned], "source": source}));
ex(&mut s, "fill.copy", json!({"id": patterned}));
// Its node id means nothing in another file, where it may name an unrelated layer.
ex(&mut s, "file.new", json!({}));
let b = rect(&mut s);
assert!(s.execute("fill.paste", &json!({"ids": [b], "index": 0})).is_err(), "a pattern stays in its file");
}
#[test]
fn a_copied_pattern_cannot_paste_into_a_layer_it_draws() {
let mut s = Session::new();
let outer = id(ex(&mut s, "frame.create", json!({"x": 0, "y": 0, "width": 100, "height": 100})));
let inner = id(ex(&mut s, "node.create", json!({"type": "rectangle", "x": 10, "y": 10, "width": 20, "height": 20, "parent": outer})));
// A pattern taken from the frame's child, on a layer beside the frame.
let source = rect(&mut s);
ex(&mut s, "fill.pattern", json!({"ids": [source], "source": inner}));
ex(&mut s, "fill.copy", json!({"id": source}));
// Onto the frame that holds the pattern's source it would draw itself, as `fill.pattern` refuses.
assert!(s.execute("fill.paste", &json!({"ids": [outer]})).is_err());
// Onto a layer that doesn't hold the pattern's source it still pastes.
let other = rect(&mut s);
ex(&mut s, "fill.paste", json!({"ids": [other], "index": 0}));
}