diff --git a/crates/engine/src/cmd/create.rs b/crates/engine/src/cmd/create.rs index 71a9b822..934bac5e 100644 --- a/crates/engine/src/cmd/create.rs +++ b/crates/engine/src/cmd/create.rs @@ -83,7 +83,7 @@ pub fn specs() -> Vec { spec!( "image.place", "Place image", - "{data (base64) | path, x?, y?, width?, height?, parent?, target? (an existing shape to fill instead of making a layer), mode? (fill|fit|crop|tile)} → {id, image, width, height, imageProfile (its embedded colour profile; null: taken as sRGB)}", + "{data (base64) | path, x?, y?, width?, height?, parent?, target? (an existing layer: the image is added on top of its fills instead of making a layer), mode? (fill|fit|crop|tile)} → {id, image, width, height, imageProfile (its embedded colour profile; null: taken as sRGB)}", true, place_image ), @@ -378,10 +378,10 @@ pub fn accepts_image_fill(kind: &NodeKind) -> bool { kind.is_shape() || kind.is_frame_like() || matches!(kind, NodeKind::Text(_) | NodeKind::BooleanOp { .. }) } -/// Replace the fill list and detach variable bindings on paints that the image replaces. -pub(crate) fn replace_with_image_fill(node: &mut Node, image: String, mode: ImageScaleMode) { - node.fills = vec![Paint::image(image, mode)]; - node.bound.retain(|key, _| !matches!(super::lists::list_binding(key), Some((super::lists::BoundList::Fills, _, _)))); +/// Add an image fill on top of the layer's fills: the others keep their place and variable +/// bindings; a linked fill style no longer describes the list, so the link goes. +fn add_image_fill(node: &mut Node, image: String, mode: ImageScaleMode) { + node.fills.push(Paint::image(image, mode)); node.styles.fill = None; } @@ -415,7 +415,7 @@ pub fn place_image(s: &mut Session, p: &Value) -> Result { let (hash, _, _) = figcraft_format::add_image(doc, bytes).map_err(|e| bad(cmd, e.to_string()))?; let profile = figcraft_render::images::profile_name(&hash, doc.images.get(&hash)); let node = doc.get_mut(id)?; - replace_with_image_fill(node, hash.clone(), mode); + add_image_fill(node, hash.clone(), mode); return Ok(json!({"id": id, "image": hash, "width": node.width, "height": node.height, "imageProfile": profile})); } let page = s.page()?; diff --git a/crates/ui-egui/src/paste_image_tests.rs b/crates/ui-egui/src/paste_image_tests.rs index c659569c..a76eae79 100644 --- a/crates/ui-egui/src/paste_image_tests.rs +++ b/crates/ui-egui/src/paste_image_tests.rs @@ -196,7 +196,10 @@ fn cmd_v_fills_a_selected_oval_without_adding_a_layer() { let node = doc.node(oval).unwrap(); assert_eq!(doc.node_count(), before); assert!(matches!(node.kind, figcraft_doc::NodeKind::Ellipse { .. })); - assert!(matches!(node.fills[0].kind, figcraft_color::PaintKind::Image(_))); + // Added on top: the oval's own fill stays under it. + assert_eq!(node.fills.len(), original_fill.len() + 1); + assert_eq!(node.fills[..original_fill.len()], original_fill[..]); + assert!(matches!(node.fills.last().unwrap().kind, figcraft_color::PaintKind::Image(_))); assert_eq!((node.width, node.height), (120.0, 80.0)); assert_eq!(w.app.session.selection(), vec![oval]); let png = figcraft_engine::cmd::file::export_bytes(doc, &[oval], figcraft_doc::ExportFormat::Png, 1.0).unwrap(); @@ -209,7 +212,7 @@ fn cmd_v_fills_a_selected_oval_without_adding_a_layer() { } #[test] -fn pasting_an_image_detaches_the_replaced_fill_variable() { +fn pasting_an_image_keeps_the_other_fills_variables() { let mut w = Win::new(); let oval = NodeId(w.app.session.execute("shape.ellipse", &json!({"x": 200, "y": 100, "width": 120, "height": 80})).unwrap()["id"].as_u64().unwrap()); @@ -221,12 +224,14 @@ fn pasting_an_image_detaches_the_replaced_fill_variable() { w.copy_image_elsewhere(); w.cmd_v(CMD); let node = w.app.session.doc().unwrap().node(oval).unwrap(); - assert!(matches!(node.fills[0].kind, figcraft_color::PaintKind::Image(_))); - assert!(node.bound.is_empty(), "the old colour variable must not remain on the image fill"); + assert!(matches!(node.fills.last().unwrap().kind, figcraft_color::PaintKind::Image(_))); + assert!(matches!(node.fills[0].kind, figcraft_color::PaintKind::Solid { .. })); + assert!(!node.bound.is_empty(), "the colour fill under the image keeps its variable"); } +/// An image layer copied in FigCraft pastes as a layer, a selected shape or not. #[test] -fn cmd_v_fills_a_selected_oval_with_an_image_copied_in_figcraft() { +fn an_image_layer_copied_in_figcraft_pastes_as_a_layer() { let mut w = Win::new(); w.copy_image_elsewhere(); w.cmd_v(CMD); @@ -235,14 +240,15 @@ fn cmd_v_fills_a_selected_oval_with_an_image_copied_in_figcraft() { let oval = NodeId(w.app.session.execute("shape.ellipse", &json!({"x": 200, "y": 100, "width": 120, "height": 80})).unwrap()["id"].as_u64().unwrap()); + let fills = w.app.session.doc().unwrap().node(oval).unwrap().fills.clone(); let before = w.app.session.doc().unwrap().node_count(); w.cmd_v(CMD); let doc = w.app.session.doc().unwrap(); - assert_eq!(doc.node_count(), before); + assert_eq!(doc.node_count(), before + 1); assert!(doc.contains(source)); - assert!(matches!(doc.node(oval).unwrap().fills[0].kind, figcraft_color::PaintKind::Image(_))); - assert_eq!(w.app.session.selection(), vec![oval]); + assert_eq!(doc.node(oval).unwrap().fills, fills); + assert_ne!(w.app.session.selection(), vec![oval]); } #[test]