M7: preserve bindings and design layers when pasting images

This commit is contained in:
YarilaFox
2026-10-20 20:12:47 +05:00
parent 31da698dd5
commit 6165d2ec85
5 changed files with 70 additions and 20 deletions
+8 -2
View File
@@ -377,6 +377,13 @@ 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, _, _))));
node.styles.fill = None;
}
/// Place an image as a rectangle with an image fill (Figma's image layer).
pub fn place_image(s: &mut Session, p: &Value) -> Result<Value> {
let cmd = "image.place";
@@ -407,8 +414,7 @@ pub fn place_image(s: &mut Session, p: &Value) -> Result<Value> {
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)?;
node.fills = vec![Paint::image(hash.clone(), mode)];
node.styles.fill = None;
replace_with_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()?;
+1 -2
View File
@@ -701,8 +701,7 @@ fn paste(s: &mut Session, p: &Value) -> Result<Value> {
let doc = s.doc_mut()?;
doc.images.entry(hash.clone()).or_insert(data);
let node = doc.get_mut(*target)?;
node.fills = vec![figcraft_color::Paint::image(hash, figcraft_color::ImageScaleMode::Fill)];
node.styles.fill = None;
super::create::replace_with_image_fill(node, hash, figcraft_color::ImageScaleMode::Fill);
return Ok(json!({"ids": [target]}));
}
let page = s.page()?;
+28
View File
@@ -45,6 +45,34 @@ fn paste_takes_layers_copied_in_another_design_app() {
assert_eq!(selected_names(&app), ["Copied layer"]);
}
struct HtmlAndImage(String);
impl Services for HtmlAndImage {
fn clipboard_get_html(&mut self) -> Option<String> {
Some(self.0.clone())
}
fn clipboard_get_image(&mut self) -> Option<(u32, u32, Vec<u8>)> {
Some((2, 2, vec![255; 16]))
}
}
#[test]
fn editable_design_layers_take_priority_over_a_png_representation() {
let mut session = Session::new();
let selected = session.execute("shape.ellipse", &json!({"x": 100, "y": 100, "width": 80, "height": 80})).unwrap()["id"].as_u64().unwrap();
let mut app = FigApp::new(session, Box::new(HtmlAndImage(copied_html())));
let before = app.session.doc().unwrap().node_count();
crate::menus::paste(&mut app, None, None);
assert_eq!(selected_names(&app), ["Copied layer"]);
assert_eq!(app.session.doc().unwrap().node_count(), before + 1);
assert!(matches!(
app.session.doc().unwrap().node(figcraft_doc::NodeId(selected)).unwrap().fills[0].kind,
figcraft_color::PaintKind::Solid { .. }
));
}
#[test]
fn paste_here_puts_copied_design_layers_at_the_point() {
let html = copied_html();
+16 -16
View File
@@ -36,22 +36,6 @@ pub const UI_COMMANDS: &[&str] = &[
/// other way to read it). `at`: the page point to paste at (Paste here); `None` pastes in the
/// middle of the view.
pub fn paste(app: &mut FigApp, event_text: Option<&str>, at: Option<(f64, f64)>) {
let selected_layer = match app.session.selection().as_slice() {
[id] if at.is_none()
&& app.session.doc().ok().and_then(|d| d.node(*id)).is_some_and(|n| figcraft_engine::cmd::create::accepts_image_fill(&n.kind)) =>
{
Some(*id)
}
_ => None,
};
if let Some(target) = selected_layer
&& let Some((png, _, _)) = clipboard_png(app)
{
use base64::Engine as _;
let b64 = base64::engine::general_purpose::STANDARD.encode(png);
app.run("image.place", json!({"data": b64, "target": target}));
return;
}
// Only HTML that holds readable design data takes this path; other HTML pastes as before.
if let Some(html) = app.services.clipboard_get_html().filter(|h| figcraft_engine::cmd::edit::design_clipboard_buffer(h).is_some()) {
let mut params = json!({"html": html});
@@ -69,6 +53,22 @@ pub fn paste(app: &mut FigApp, event_text: Option<&str>, at: Option<(f64, f64)>)
}
return;
}
let selected_layer = match app.session.selection().as_slice() {
[id] if at.is_none()
&& app.session.doc().ok().and_then(|d| d.node(*id)).is_some_and(|n| figcraft_engine::cmd::create::accepts_image_fill(&n.kind)) =>
{
Some(*id)
}
_ => None,
};
if let Some(target) = selected_layer
&& let Some((png, _, _)) = clipboard_png(app)
{
use base64::Engine as _;
let b64 = base64::engine::general_purpose::STANDARD.encode(png);
app.run("image.place", json!({"data": b64, "target": target}));
return;
}
let sys_text = app.services.clipboard_get_text().or_else(|| event_text.map(str::to_string)).filter(|t| !t.is_empty());
let ours = sys_text.is_some() && sys_text == app.ui.last_copy;
let (cx, cy) = at.or_else(|| app.session.ds().ok().map(|d| (d.view.cx, d.view.cy))).unwrap_or((0.0, 0.0));
+17
View File
@@ -208,6 +208,23 @@ fn cmd_v_fills_a_selected_oval_without_adding_a_layer() {
assert_eq!(w.app.session.doc().unwrap().node(oval).unwrap().fills, original_fill);
}
#[test]
fn pasting_an_image_detaches_the_replaced_fill_variable() {
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());
w.app.session.execute("variable.createCollection", &json!({"name": "Theme"})).unwrap();
w.app.session.execute("variable.create", &json!({"collection": "Theme", "name": "brand", "type": "color", "value": "#FF0000"})).unwrap();
w.app.session.execute("variable.bind", &json!({"ids": [oval.0], "field": "fill", "variable": "brand"})).unwrap();
assert!(!w.app.session.doc().unwrap().node(oval).unwrap().bound.is_empty());
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");
}
#[test]
fn cmd_v_fills_a_selected_oval_with_an_image_copied_in_figcraft() {
let mut w = Win::new();