mirror of
https://github.com/storytold/photocraft.git
synced 2026-11-10 20:32:31 +00:00
* Layers: Merge Group for a selected group (#3832) With a lone group selected, Layer > Merge Layers (Cmd+E) reads Merge Group and the Layers panel's right-click menu offers Merge Group instead of Merge Down, as in Photoshop. The new layer.mergeGroup command flattens the group's visible contents into one pixel layer in its place, named after the group, keeping its opacity, blend mode (Pass Through becomes Normal), clipping and locks. Before, Cmd+E on a group ran Merge Down and merged the whole group into the layer below it. * i18n: Vietnamese row for Merge Group Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Brandon Thomas <echelon@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
22 files changed
+135
No files matched your search
@@ -782,6 +782,13 @@ fn build() -> Vec<CommandSpec> {
|
||||
})?;
|
||||
Ok(Value::Null)
|
||||
}),
|
||||
cmd!("layer.mergeGroup", "Merge Group", ["Layer"], None, r##"{"layer":id?}"##, has_layer, |s, p| {
|
||||
let id = layer_param(s, p)?;
|
||||
if !s.active().and_then(|d| d.doc.layer(id)).is_some_and(|l| matches!(l.content, LayerContent::Group(_))) {
|
||||
return Err(EngineError::Other("Merge Group needs a group".into()));
|
||||
}
|
||||
merge_group(s, id)
|
||||
}),
|
||||
cmd!("layer.flattenImage", "Flatten Image", ["Layer"], None, "{}", has_doc, |s, _| {
|
||||
s.edit("Flatten Image", |doc, active| {
|
||||
let fmt = doc.pixel_format();
|
||||
@@ -1427,6 +1434,37 @@ pub(crate) fn layer_copy(doc: &Document, id: LayerId) -> Result<Layer> {
|
||||
Ok(dup)
|
||||
}
|
||||
|
||||
/// Layer › Merge Group (⌘E with a group selected, #3832): the group's visible contents, as they
|
||||
/// show, become one pixel layer in its place, named after it. Like Photoshop, the layer keeps the
|
||||
/// group's opacity, blend mode (Pass Through becomes Normal), clipping and locks; the group's mask
|
||||
/// and style are applied to the pixels.
|
||||
fn merge_group(s: &mut Session, id: LayerId) -> Result<Value> {
|
||||
s.edit("Merge Group", |doc, active| {
|
||||
let group = doc.layer(id).ok_or(EngineError::NoLayer(id))?.clone();
|
||||
if !group.visible {
|
||||
return Err(EngineError::Other("the group is hidden".into()));
|
||||
}
|
||||
let fmt = doc.pixel_format();
|
||||
let fmt = photocraft_color::PixelFormat::new(fmt.mode, fmt.sample, true);
|
||||
let mut solo = group.clone();
|
||||
solo.opacity = 1.0;
|
||||
solo.blend = photocraft_doc::BlendMode::Normal;
|
||||
solo.clipped = false;
|
||||
let mut tmp = doc.clone();
|
||||
tmp.layers = vec![solo];
|
||||
let mut merged = Layer::new(group.name.clone(), LayerContent::Raster(pixels::composite_layers(&tmp, fmt, None)));
|
||||
merged.id = group.id;
|
||||
merged.opacity = group.opacity;
|
||||
merged.blend = if group.blend == photocraft_doc::BlendMode::PassThrough { photocraft_doc::BlendMode::Normal } else { group.blend };
|
||||
merged.clipped = group.clipped;
|
||||
merged.locks = group.locks;
|
||||
*doc.layer_mut(id).ok_or(EngineError::NoLayer(id))? = merged;
|
||||
*active = Some(id);
|
||||
Ok(())
|
||||
})?;
|
||||
Ok(json!({ "layer": id.0 }))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod registry_tests {
|
||||
use super::*;
|
||||
|
||||
@@ -1217,6 +1217,12 @@ pub fn group_layers(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
fn merge_layers(s: &mut Session) -> Result<Value> {
|
||||
let sel = selected(s);
|
||||
if sel.len() < 2 {
|
||||
// ⌘E on a lone group is Photoshop's Merge Group (#3832); on a layer, Merge Down.
|
||||
if let Some(&id) = sel.first()
|
||||
&& s.active().and_then(|d| d.doc.layer(id)).is_some_and(|l| matches!(l.content, LayerContent::Group(_)))
|
||||
{
|
||||
return s.execute("layer.mergeGroup", json!({ "layer": id.0 }));
|
||||
}
|
||||
return s.execute("layer.mergeDown", json!({}));
|
||||
}
|
||||
let mid = s.edit("Merge Layers", |doc, active| {
|
||||
|
||||
@@ -1469,3 +1469,41 @@ fn delete_layer_preserves_an_unaffected_active_layer_and_last_layer_guard() {
|
||||
assert_eq!(s.active().unwrap().active_layer, Some(other));
|
||||
assert_eq!(s.active().unwrap().history.past_len(), before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_down_on_a_group_merges_the_group() {
|
||||
// #3832: with a group selected, Merge Down is Photoshop's Merge Group (⌘E): the group's
|
||||
// contents become one layer in its place, named after it, and the layer below is untouched.
|
||||
let mut s = session_with_doc();
|
||||
let below = s.execute("layer.new.layer", json!({"name": "Below"})).unwrap()["layer"].as_u64().unwrap();
|
||||
s.execute("edit.fill", json!({"color": "#00ff00"})).unwrap();
|
||||
let a = s.execute("layer.new.layer", json!({"name": "A"})).unwrap()["layer"].as_u64().unwrap();
|
||||
s.execute("select.rect", json!({"x": 0, "y": 0, "width": 10, "height": 48})).unwrap();
|
||||
s.execute("edit.fill", json!({"color": "#ff0000"})).unwrap();
|
||||
s.execute("select.deselect", json!({})).unwrap();
|
||||
let b = s.execute("layer.new.layer", json!({"name": "B"})).unwrap()["layer"].as_u64().unwrap();
|
||||
s.execute("select.rect", json!({"x": 20, "y": 0, "width": 10, "height": 48})).unwrap();
|
||||
s.execute("edit.fill", json!({"color": "#0000ff"})).unwrap();
|
||||
s.execute("select.deselect", json!({})).unwrap();
|
||||
s.execute("layer.select", json!({"layer": b})).unwrap();
|
||||
s.execute("layer.select", json!({"layer": a, "mode": "add"})).unwrap();
|
||||
let group = s.execute("layer.groupLayers", json!({"name": "Badge"})).unwrap()["layer"].as_u64().unwrap();
|
||||
s.execute("layer.setProps", json!({"layer": group, "opacity": 0.5})).ok();
|
||||
let before = [px(&mut s, 5, 5), px(&mut s, 25, 5), px(&mut s, 50, 5)];
|
||||
let layers = s.active().unwrap().doc.layer_count();
|
||||
s.execute("layer.select", json!({"layer": group})).unwrap();
|
||||
s.execute("layer.mergeLayers", json!({})).unwrap();
|
||||
let st = s.active().unwrap();
|
||||
let merged = st.doc.layer(st.active_layer.unwrap()).unwrap().clone();
|
||||
assert_eq!(merged.name, "Badge");
|
||||
assert!(matches!(merged.content, photocraft_doc::LayerContent::Raster(_)), "one pixel layer");
|
||||
assert!(st.doc.layer(photocraft_doc::LayerId(below)).is_some(), "the layer below stays");
|
||||
assert_eq!(st.doc.layer_count(), layers - 2, "group and its two layers became one");
|
||||
let after = [px(&mut s, 5, 5), px(&mut s, 25, 5), px(&mut s, 50, 5)];
|
||||
for (a, b) in before.iter().zip(&after) {
|
||||
assert!(a.iter().zip(b).all(|(x, y)| (x - y).abs() < 1.0 / 255.0), "{before:?} -> {after:?}");
|
||||
}
|
||||
// Undo brings the group back.
|
||||
s.execute("edit.undo", json!({})).unwrap();
|
||||
assert!(s.active().unwrap().doc.layer(photocraft_doc::LayerId(group)).is_some());
|
||||
}
|
||||
@@ -398,6 +398,7 @@
|
||||
Select Linked Layers Vybrat propojené vrstvy
|
||||
Merge Layers Sloučit vrstvy
|
||||
Merge Down Sloučit dolů
|
||||
Merge Group Sloučit skupinu
|
||||
Merge Visible Sloučit viditelné
|
||||
Stamp Visible Otisknout viditelné
|
||||
Select Layer Above Vybrat vrstvu nad
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -394,6 +394,7 @@
|
||||
Select Linked Layers Verknüpfte Ebenen auswählen
|
||||
Merge Layers Ebenen zusammenfügen
|
||||
Merge Down Nach unten zusammenfügen
|
||||
Merge Group Gruppe zusammenfügen
|
||||
Merge Visible Sichtbare zusammenfügen
|
||||
Stamp Visible Sichtbare Ebenen stempeln
|
||||
Select Layer Above Ebene darüber auswählen
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -1838,6 +1838,7 @@
|
||||
New Guide Layout… Νέα διάταξη οδηγών…
|
||||
New Guides From Shape Νέοι οδηγοί από σχήμα
|
||||
Merge Down Συγχώνευση με το υποκείμενο επίπεδο
|
||||
Merge Group Συγχώνευση ομάδας
|
||||
Stamp Visible Συνένωση ορατών σε νέο επίπεδο
|
||||
Select Layer Above Επιλογή επάνω επιπέδου
|
||||
Select Layer Below Επιλογή κάτω επιπέδου
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 1000 and column 18.
|
@@ -391,6 +391,7 @@
|
||||
Select Linked Layers Seleccionar capas vinculadas
|
||||
Merge Layers Combinar capas
|
||||
Merge Down Combinar hacia abajo
|
||||
Merge Group Combinar grupo
|
||||
Merge Visible Combinar visibles
|
||||
Stamp Visible Sellar visibles
|
||||
Select Layer Above Seleccionar la capa de arriba
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 4 and column 47.
|
@@ -387,6 +387,7 @@
|
||||
Select Linked Layers Sélectionner les calques liés
|
||||
Merge Layers Fusionner les calques
|
||||
Merge Down Fusionner avec le calque inférieur
|
||||
Merge Group Fusionner le groupe
|
||||
Merge Visible Fusionner les calques visibles
|
||||
Stamp Visible Tamponner les calques visibles
|
||||
Stamp Down Tamponner sur le calque inférieur
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -380,6 +380,7 @@
|
||||
Select Linked Layers Pilih Layer Tertaut
|
||||
Merge Layers Gabungkan Layer
|
||||
Merge Down Gabung ke Bawah
|
||||
Merge Group Gabungkan Grup
|
||||
Merge Visible Gabung yang Terlihat
|
||||
Stamp Visible Stamp yang Terlihat
|
||||
Stamp Down Stamp ke Bawah
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 4 and column 47.
|
@@ -390,6 +390,7 @@
|
||||
Select Linked Layers Seleziona livelli collegati
|
||||
Merge Layers Unisci livelli
|
||||
Merge Down Unisci sotto
|
||||
Merge Group Unisci gruppo
|
||||
Merge Visible Unisci visibili
|
||||
Stamp Visible Unisci visibili in nuovo
|
||||
Select Layer Above Seleziona livello sopra
|
||||
|
||||
|
@@ -391,6 +391,7 @@
|
||||
Select Linked Layers リンクしたレイヤーを選択
|
||||
Merge Layers レイヤーを結合
|
||||
Merge Down 下のレイヤーと結合
|
||||
Merge Group グループを結合
|
||||
Merge Visible 表示レイヤーを結合
|
||||
Stamp Visible 表示レイヤーをスタンプ
|
||||
Select Layer Above 上のレイヤーを選択
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -375,6 +375,7 @@
|
||||
Select Linked Layers 연결된 레이어 선택
|
||||
Merge Layers 레이어 병합
|
||||
Merge Down 아래 레이어와 병합
|
||||
Merge Group 그룹 병합
|
||||
Merge Visible 보이는 레이어 병합
|
||||
Stamp Visible 보이는 레이어의 병합 사본 만들기
|
||||
Select Layer Above 위 레이어 선택
|
||||
|
||||
|
@@ -383,6 +383,7 @@
|
||||
Select Linked Layers Gekoppelde lagen selecteren
|
||||
Merge Layers Lagen samenvoegen
|
||||
Merge Down Omlaag samenvoegen
|
||||
Merge Group Groep samenvoegen
|
||||
Merge Visible Zichtbare samenvoegen
|
||||
Stamp Visible Zichtbare stempelen
|
||||
Select Layer Above Laag erboven selecteren
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 8 and column 34.
|
@@ -389,6 +389,7 @@
|
||||
Select Linked Layers Zaznacz połączone warstwy
|
||||
Merge Layers Scal warstwy
|
||||
Merge Down Scal w dół
|
||||
Merge Group Scal grupę
|
||||
Merge Visible Scal widoczne
|
||||
Stamp Visible Odbij widoczne
|
||||
Select Layer Above Zaznacz warstwę powyżej
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -383,6 +383,7 @@
|
||||
Select Linked Layers Selecionar camadas vinculadas
|
||||
Merge Layers Mesclar camadas
|
||||
Merge Down Mesclar para baixo
|
||||
Merge Group Mesclar grupo
|
||||
Merge Visible Mesclar visíveis
|
||||
Stamp Visible Carimbar visíveis
|
||||
Stamp Down Carimbar para baixo
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -1897,6 +1897,7 @@
|
||||
Lock Layers… Заблокировать слои…
|
||||
Mean Среднее
|
||||
Merge Down Выполнить сведение
|
||||
Merge Group Объединить группу
|
||||
Merge Layers Объединить слои
|
||||
Merge Shape Components Объединить компоненты фигуры
|
||||
Merge Visible Свести видимые
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -1876,6 +1876,7 @@
|
||||
Lock Layers… Заблокувати шари…
|
||||
Mean Середнє значення
|
||||
Merge Down Об'єднати з нижнім
|
||||
Merge Group Об'єднати групу
|
||||
Merge Layers Об'єднати шари
|
||||
Merge Shape Components Об'єднати компоненти фігури
|
||||
Merge Visible Об'єднати видимі
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 1356 and column 18.
|
@@ -936,6 +936,7 @@
|
||||
Disable Layer Mask Tắt mặt nạ lớp
|
||||
Release Clipping Mask Nhả mặt nạ cắt
|
||||
Merge Down Gộp xuống
|
||||
Merge Group Gộp nhóm
|
||||
Quick Export As PNG Xuất nhanh dạng PNG
|
||||
Rename Layer… Đổi tên lớp…
|
||||
Frame from Layers… Frame từ các lớp…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -393,6 +393,7 @@
|
||||
Select Linked Layers 选择链接的图层
|
||||
Merge Layers 合并图层
|
||||
Merge Down 向下合并
|
||||
Merge Group 合并组
|
||||
Merge Visible 合并可见图层
|
||||
Stamp Visible 盖印可见图层
|
||||
Select Layer Above 选择上方图层
|
||||
|
||||
|
@@ -398,6 +398,7 @@
|
||||
Select Linked Layers 選取連結的圖層
|
||||
Merge Layers 合併圖層
|
||||
Merge Down 向下合併圖層
|
||||
Merge Group 合併群組
|
||||
Merge Visible 合併可見圖層
|
||||
Stamp Visible 蓋印可見圖層
|
||||
Select Layer Above 選取上方圖層
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -111,6 +111,9 @@ pub fn entries(l: &Layer, multi: bool, has_selection: bool) -> Vec<Entry> {
|
||||
v.push(None);
|
||||
if multi {
|
||||
v.push(Some((tl!("Merge Layers"), "layer.mergeLayers")));
|
||||
} else if matches!(l.content, LayerContent::Group(_)) {
|
||||
// Photoshop's Merge Group: a group merges itself, not into the layer below (#3832).
|
||||
v.push(Some((tl!("Merge Group"), "layer.mergeGroup")));
|
||||
} else {
|
||||
v.push(Some((tl!("Merge Down"), "layer.mergeDown")));
|
||||
}
|
||||
@@ -654,6 +657,31 @@ mod tests {
|
||||
assert!(ids.contains(&"layer.mergeLayers"));
|
||||
}
|
||||
|
||||
/// #3832: a group's right-click menu and the Layer menu offer Photoshop's Merge Group, which
|
||||
/// merges the group itself (not into the layer below), and it is enabled.
|
||||
#[test]
|
||||
fn a_group_offers_merge_group() {
|
||||
let mut s = photocraft_engine::Session::new();
|
||||
s.execute("file.new", json!({"width": 10, "height": 10})).unwrap();
|
||||
s.execute("layer.new.layer", json!({})).unwrap();
|
||||
s.execute("layer.groupLayers", json!({"name": "G"})).unwrap();
|
||||
let st = s.active().unwrap();
|
||||
let g = st.doc.layer(st.active_layer.unwrap()).unwrap().clone();
|
||||
let e: Vec<(&str, &str)> = entries(&g, false, false).into_iter().flatten().collect();
|
||||
assert!(e.contains(&("Merge Group", "layer.mergeGroup")), "{e:?}");
|
||||
assert!(!e.iter().any(|x| x.0 == "Merge Down"));
|
||||
let mut app = crate::PhotocraftApp::new(s, Default::default());
|
||||
app.sync_views();
|
||||
let items = crate::menus::menu_items(&app);
|
||||
let item = items.iter().find(|i| i.id == "layer.mergeLayers").unwrap();
|
||||
assert_eq!(item.label, "Merge Group");
|
||||
assert!(item.enabled, "Merge Group runs on a group");
|
||||
// A plain layer keeps Merge Layers.
|
||||
app.run("layer.new.layer", json!({})).unwrap();
|
||||
let items = crate::menus::menu_items(&app);
|
||||
assert_eq!(items.iter().find(|i| i.id == "layer.mergeLayers").unwrap().label, "Merge Layers");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_mask_uses_the_selection() {
|
||||
assert_eq!(add_mask_command(false, false), "layer.layerMask.revealAll");
|
||||
|
||||
@@ -966,6 +966,14 @@ pub fn menu_items(app: &PhotocraftApp) -> Vec<MenuItem> {
|
||||
{
|
||||
item.label = crate::layer_menu_ui::mask_toggle_label(mask.enabled).into();
|
||||
}
|
||||
// Layer › Merge Layers reads Merge Group with just a group selected, as in Photoshop (#3832).
|
||||
if let Some(st) = app.session.active()
|
||||
&& st.selected_layers().len() <= 1
|
||||
&& st.active_layer.and_then(|id| st.doc.layer(id)).is_some_and(|l| matches!(l.content, photocraft_doc::LayerContent::Group(_)))
|
||||
&& let Some(item) = items.iter_mut().find(|i| i.id == "layer.mergeLayers")
|
||||
{
|
||||
item.label = "Merge Group".into();
|
||||
}
|
||||
// The File quick-export command uses the format selected in Export Preferences.
|
||||
// On the web it intentionally downloads PNG until the quick-export service supports
|
||||
// the other formats; the Layer quick-export command also always produces PNG.
|
||||
|
||||
Reference in new issue
Block a user