Layer › Smart Objects › Convert to Layers (#535) (#829)

* Layer > Smart Objects > Convert to Layers (#535)

Unpacks a smart object's contents back into layers at its placement:
several layers land in a group named after the smart object (carrying
its opacity, blend mode, masks and style); a single layer replaces it
directly when those fold in exactly. Contents are conformed to the
document's mode, profile and depth; smart filters are discarded.
Also in the Layers panel context menu and the Properties quick actions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Parity: Convert to Layers is live (627 of 627); raise FLOOR

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Convert to Layers: pass no inherited group locks to transform_layer (#847 added the parameter)

The layers are transformed inside released(), which lifts every lock in their tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* it: translate Convert to Layers (Italian landed after this PR was written)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
BFlat 2026-10-08 10:44:21 +07:00 committed by GitHub
parent 20fb1903dd
commit 01cd6fffa2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 358 additions and 10 deletions

View File

@ -48,7 +48,15 @@ pub(crate) fn for_each_surface(layers: &mut [Layer], masks: bool, f: &mut dyn Fn
}
}
fn for_each_layer(layers: &mut [Layer], f: &mut dyn FnMut(&mut Layer)) {
/// Converts every pixel surface of a layer tree (masks included) to `depth`.
pub(crate) fn convert_layers_depth(layers: &mut [Layer], depth: SampleType) {
for_each_surface(layers, true, &mut |surf, _| {
let f = surf.format().with_sample(depth);
*surf = surf.convert(f);
});
}
pub(crate) fn for_each_layer(layers: &mut [Layer], f: &mut dyn FnMut(&mut Layer)) {
for l in layers {
f(l);
if let Some(ch) = l.children_mut() {
@ -329,10 +337,7 @@ fn convert_depth(s: &mut Session, depth: SampleType) -> Result<Value> {
return Ok(Value::Null);
}
s.edit("Bit Depth", |doc, _| {
for_each_surface(&mut doc.layers, true, &mut |surf, _| {
let f = surf.format().with_sample(depth);
*surf = surf.convert(f);
});
convert_layers_depth(&mut doc.layers, depth);
for ch in doc.channels.iter_mut().chain(doc.quick_mask.as_mut()) {
let f = ch.surface.format().with_sample(depth);
ch.surface = ch.surface.convert(f);

View File

@ -912,6 +912,14 @@ pub fn specs() -> Vec<CommandSpec> {
|s| s.active().map(|_| ()).ok_or_else(|| "no document open".into()),
|s, _| update_all(s)
),
spec!(
"layer.smartObjects.convertToLayers",
"Convert to Layers",
SO,
r##"{"layer":id?} (contents unpacked at the placement: one layer, or a group named after the smart object; smart filters are discarded)"##,
has_smart,
unpack::convert_to_layers
),
spec!("layer.smartObjects.convertToEmbedded", "Convert to Embedded", SO, r##"{"layer":id?}"##, has_linked, |s, p| {
set_source(s, p, "Convert to Embedded", true, |meta, src| {
let (file_name, bytes) = source_bytes(meta, src).ok_or_else(|| other("the linked file can't be read"))?;
@ -1004,5 +1012,7 @@ pub fn specs() -> Vec<CommandSpec> {
]
}
mod unpack;
#[cfg(test)]
mod tests;

View File

@ -589,3 +589,143 @@ fn warp_on_a_distorted_smart_object_follows_the_corners() {
assert!(after.warp.is_some());
assert_eq!(after.perspective, before);
}
fn convert_to_layers(s: &mut Session) -> Value {
s.execute("layer.smartObjects.convertToLayers", json!({})).unwrap()
}
fn active_layer(s: &Session) -> Layer {
let d = s.active().unwrap();
d.doc.layer(d.active_layer.unwrap()).unwrap().clone()
}
#[test]
fn convert_to_layers_round_trips_a_layer_at_every_depth() {
for depth in DEPTHS {
let mut s = session(depth);
paint(&mut s);
let id = s.active().unwrap().active_layer.unwrap();
s.edit("props", |doc, _| {
let l = doc.layer_mut(id).unwrap();
l.name = "Sky".into();
l.opacity = 0.6;
l.blend = BlendMode::Multiply;
Ok(())
})
.unwrap();
let before = flat(&s);
convert(&mut s);
let smart = flat(&s);
let r = convert_to_layers(&mut s);
assert_eq!(r["group"], false, "one layer replaces the smart object");
let l = active_layer(&s);
assert_eq!(l.id.0, r["layer"].as_u64().unwrap());
assert!(matches!(&l.content, LayerContent::Raster(px) if px.format() == s.active().unwrap().doc.pixel_format()), "depth {depth}");
assert_eq!((l.name.as_str(), l.blend), ("Sky", BlendMode::Multiply));
assert!((l.opacity - 0.6).abs() < 1e-6);
assert_eq!(max_diff(&flat(&s), &before), 0.0, "depth {depth}: whole-pixel placement is exact");
s.undo();
assert!(matches!(active_layer(&s).content, LayerContent::Smart(_)), "one undoable step");
assert_eq!(flat(&s), smart);
}
}
#[test]
fn convert_to_layers_unpacks_several_layers_into_a_group_with_the_smart_layers_properties() {
let mut s = session(8);
paint(&mut s);
let id = convert(&mut s);
s.execute("layer.setProps", json!({"layer": id, "name": "Placed", "opacity": 0.5})).unwrap();
// A second layer inside the contents (Edit Contents, then save).
let child = s.execute("layer.smartObjects.editContents", json!({})).unwrap()["document"].as_u64().unwrap() as usize;
s.execute("layer.new.layer", json!({"name": "Top"})).unwrap();
s.edit("paint", |doc, active| {
doc.layer_mut(active.unwrap()).unwrap().surface_mut().unwrap().fill_rect(Rect::new(2, 2, 8, 8), &[0.0, 0.0, 1.0, 1.0]);
Ok(())
})
.unwrap();
s.execute("layer.smartObjects.saveContents", json!({})).unwrap();
s.execute("file.close", json!({})).unwrap();
assert_ne!(s.active_index(), Some(child));
s.execute("filter.blur.gaussianBlur", json!({"radius": 2})).unwrap();
s.execute("filter.blur.gaussianBlur", json!({"radius": 1})).unwrap();
// Without the filters, the smart object looks exactly like the unpacked group.
s.execute("layer.smartFilter.disableSmartFilters", json!({})).unwrap();
let before = flat(&s);
let r = convert_to_layers(&mut s);
assert_eq!((r["group"].as_bool(), r["discardedSmartFilters"].as_u64()), (Some(true), Some(2)));
let g = active_layer(&s);
assert_eq!((g.name.as_str(), g.blend), ("Placed", BlendMode::Normal), "named after the smart object, isolated");
assert!((g.opacity - 0.5).abs() < 1e-6);
let names: Vec<_> = g.children().unwrap().iter().map(|c| c.name.as_str()).collect();
assert_eq!(names.last(), Some(&"Top"));
assert_eq!(names.len(), 2);
assert!(max_diff(&flat(&s), &before) <= 1.0 / 255.0 + 1e-6);
assert!(!s.is_enabled("layer.smartObjects.convertToLayers"), "a group isn't a smart object");
}
#[test]
fn convert_to_layers_keeps_a_smart_layer_mask_on_a_group() {
let mut s = session(16);
paint(&mut s);
convert(&mut s);
s.execute("layer.layerMask.hideAll", json!({})).unwrap();
let before = flat(&s);
assert!(convert_to_layers(&mut s)["group"].as_bool().unwrap(), "the mask can't fold into the layer");
let g = active_layer(&s);
assert!(g.mask.is_some() && g.children().unwrap()[0].mask.is_none());
assert_eq!(max_diff(&flat(&s), &before), 0.0);
}
#[test]
fn convert_to_layers_applies_the_placement() {
let mut s = session(8);
paint(&mut s);
let id = convert(&mut s);
s.execute("edit.transform", json!({"layer": id, "matrix": [0.5, 0, 0, 0.5, 10.25, 3]})).unwrap();
let before = flat(&s);
convert_to_layers(&mut s);
let l = active_layer(&s);
assert!(matches!(l.content, LayerContent::Raster(_)));
assert!(mean_diff(&flat(&s), &before) < 2e-3, "{}", mean_diff(&flat(&s), &before));
// Distort keeps the projective placement for pixels.
let mut s = session(8);
paint(&mut s);
let id = convert(&mut s);
s.execute("edit.transform", json!({"layer": id, "quad": [[0, 0], [30, 0], [36, 30], [-4, 30]]})).unwrap();
let before = flat(&s);
convert_to_layers(&mut s);
assert!(mean_diff(&flat(&s), &before) < 2e-3);
}
#[test]
fn convert_to_layers_rejects_what_it_cannot_unpack() {
let mut s = session(8);
assert!(!s.is_enabled("layer.smartObjects.convertToLayers"));
assert!(s.execute("layer.smartObjects.convertToLayers", json!({})).is_err());
paint(&mut s);
let raster = s.active().unwrap().active_layer.unwrap().0;
let id = convert(&mut s);
// Bad targets fail without panicking: a pixel layer, a missing layer.
for p in [json!({"layer": raster}), json!({"layer": u64::MAX})] {
assert!(s.execute("layer.smartObjects.convertToLayers", p).is_err());
}
// A warp can't be applied to the unpacked layers: an error, and the document is unchanged.
s.execute("edit.transform.warp", json!({"layer": id, "style": "arc", "bend": 30})).unwrap();
let steps = s.active().unwrap().history.entries().len();
assert!(s.execute("layer.smartObjects.convertToLayers", json!({})).is_err());
assert_eq!(s.active().unwrap().history.entries().len(), steps);
}
#[test]
fn convert_to_layers_conforms_the_contents_to_the_document_mode_and_depth() {
let mut s = session(8);
paint(&mut s);
convert(&mut s);
// The contents stay 8-bit RGB while the document becomes 16-bit grayscale.
s.execute("image.mode.bits16", json!({})).unwrap();
s.execute("image.mode.grayscale", json!({})).unwrap();
convert_to_layers(&mut s);
let fmt = s.active().unwrap().doc.pixel_format();
assert!(matches!(&active_layer(&s).content, LayerContent::Raster(px) if px.format().mode == fmt.mode && px.format().sample == fmt.sample));
}

View File

@ -0,0 +1,175 @@
//! Layer › Smart Objects › Convert to Layers: unpacks a smart object's contents back into layers
//! at its placement, as Photoshop does. Several layers land in a group named after the smart
//! object, which carries its opacity, blend mode, masks and style; a single layer replaces the
//! smart object directly when those fold into it without changing the look. Smart filters are
//! discarded (the result reports how many).
use std::sync::Arc;
use photocraft_algo::transform::Interp;
use photocraft_cms::Intent;
use photocraft_color::BlendMode;
use photocraft_doc::{Document, Group, Layer, LayerContent, LayerId, Metadata, SmartObject, SmartSource};
use serde_json::{Value, json};
use super::{decode_source, detach_psd, other, placement, shift_layer, smart, source_bytes};
use crate::commands::layer_param;
use crate::{EngineError, Result, Session};
/// The smart object's contents as layers in `doc`'s colour model, profile and depth, still in
/// source pixel coordinates. A Background becomes a normal layer (it can't sit above others).
fn contents(doc: &Document, sm: &SmartObject, intent: Intent, bpc: bool) -> Result<Vec<Layer>> {
let (name, bytes) = source_bytes(&doc.metadata, &sm.source).ok_or_else(|| other("the smart object's contents are unavailable (missing linked file?)"))?;
let mut src = decode_source(&name, &bytes)?;
let mode = doc.pixel_format().mode;
if src.pixel_format().mode != mode || src.icc_profile != doc.icc_profile {
crate::color_cmds::convert_document(&mut src, &crate::color_cmds::document_profile(doc), intent, bpc)?;
if src.pixel_format().mode != mode {
return Err(other(format!("can't convert the smart object's contents to the document's {:?} mode", doc.mode)));
}
}
if src.depth != doc.depth {
crate::image_cmds::convert_layers_depth(&mut src.layers, doc.depth);
}
let mut layers = std::mem::take(&mut src.layers);
for l in &mut layers {
if crate::extra_cmds::is_background(l) {
l.name = "Layer 0".into();
l.locks = Default::default();
}
embed_nested(&src.metadata, l);
}
Ok(layers)
}
/// Nested PSD placed layers find their files in the contents' own global blocks, which stay
/// behind: embed those sources so the nested smart objects keep rendering in the document.
fn embed_nested(meta: &Metadata, l: &mut Layer) {
let found = match &l.content {
LayerContent::Smart(SmartObject { source: SmartSource::Linked { path }, .. }) => photocraft_io::linked::find_linked_file(meta, path),
_ => None,
};
if let Some(f) = found {
if let LayerContent::Smart(sm) = &mut l.content {
sm.source = SmartSource::Embedded { file_name: f.file_name, bytes: Arc::new(f.bytes) };
}
detach_psd(l);
}
for c in l.children_mut().into_iter().flatten() {
embed_nested(meta, c);
}
}
/// Moves unpacked layers from source pixels to where the smart object placed them: an exact
/// shift for whole-pixel moves, else its transform (type, shapes and nested smart objects stay
/// live; Distort and Perspective need them rasterized, as Free Transform does).
fn place(doc: &Document, sm: &SmartObject, layers: &mut [Layer]) -> Result<()> {
if sm.warp.as_ref().is_some_and(|w| !w.is_identity()) {
return Err(other("a warped smart object can't be converted to layers; rasterize it instead (Layer › Smart Objects › Rasterize)"));
}
let [a, b, c, d, e, f] = sm.transform.m;
let whole = |v: f64| (v - v.round()).abs() < 1e-9;
if sm.perspective.is_none() && [a - 1.0, b, c, d - 1.0].iter().all(|v| v.abs() < 1e-9) && whole(e) && whole(f) {
// Saturating casts: a placement that far out moves the layers off any canvas either way.
layers.iter_mut().for_each(|l| shift_layer(l, e.round() as i32, f.round() as i32));
return Ok(());
}
let h = placement(sm);
let affine = sm.perspective.is_none().then_some(sm.transform);
for l in layers {
released(l, |l| crate::transform_cmds::transform_layer(None, photocraft_doc::Locks::default(), l, &h, affine, Interp::Bicubic))?;
crate::transform_cmds::refresh_text(doc, l);
}
Ok(())
}
/// Runs `f` with every lock and mask link in `l`'s tree released, so a transform moves all of
/// it (the contents move as one, whatever their locks), then restores them.
fn released(l: &mut Layer, f: impl FnOnce(&mut Layer) -> Result<()>) -> Result<()> {
let mut saved = Vec::new();
crate::image_cmds::for_each_layer(std::slice::from_mut(l), &mut |x| {
saved.push((x.locks, x.mask.as_ref().map(|m| m.linked), x.vector_mask.as_ref().map(|m| m.linked)));
x.locks = Default::default();
x.mask.iter_mut().for_each(|m| m.linked = true);
x.vector_mask.iter_mut().for_each(|m| m.linked = true);
});
let r = f(l);
// A transform keeps the tree's shape, so the walk meets the layers in the same order.
let mut saved = saved.into_iter();
crate::image_cmds::for_each_layer(std::slice::from_mut(l), &mut |x| {
let Some((locks, mask, vector)) = saved.next() else { return };
x.locks = locks;
if let (Some(m), Some(linked)) = (x.mask.as_mut(), mask) {
m.linked = linked;
}
if let (Some(m), Some(linked)) = (x.vector_mask.as_mut(), vector) {
m.linked = linked;
}
});
r
}
/// Whether the smart layer's own properties fold into its one unpacked layer `c` without
/// changing the look: nothing on the smart layer acts on `c`'s composite (masks, style, fill
/// opacity, Blend If, channel exclusions), and `c` blends normally and doesn't look at what's
/// beneath it (alone in the smart object, it lands on transparency).
fn foldable(so: &Layer, c: &Layer) -> bool {
so.mask.is_none()
&& so.vector_mask.is_none()
&& (!so.effects.enabled || so.effects.items.is_empty())
&& so.fill_opacity >= 1.0
&& so.blend_if.is_default()
&& so.excluded_channels == 0
&& !c.clipped
&& matches!(c.blend, BlendMode::Normal | BlendMode::PassThrough)
&& c.blend_if.is_default()
&& c.excluded_channels == 0
}
/// What replaces smart layer `so`: its one unpacked layer with `so`'s properties folded in, or a
/// group named after it that carries them (isolated like the smart object, never pass-through).
fn unpacked(so: &Layer, mut layers: Vec<Layer>) -> Layer {
let blend = if so.blend == BlendMode::PassThrough { BlendMode::Normal } else { so.blend };
if let [c] = layers.as_slice()
&& foldable(so, c)
&& let Some(mut l) = layers.pop()
{
l.name = so.name.clone();
l.visible &= so.visible;
l.opacity *= so.opacity;
l.blend = blend;
l.clipped = so.clipped;
l.locks = so.locks;
l.label = so.label;
l.link_group = so.link_group;
return l;
}
let mut g = so.clone();
g.id = LayerId::fresh();
g.psd_id = None;
g.blend = blend;
g.content = LayerContent::Group(Group { children: layers, expanded: true, artboard: None });
detach_psd(&mut g);
g
}
pub(super) fn convert_to_layers(s: &mut Session, p: &Value) -> Result<Value> {
let id = layer_param(s, p)?;
let (intent, bpc) = (s.color.settings.intent(), s.color.settings.bpc);
let st = s.active().ok_or(EngineError::NoDocument)?;
let doc = &st.doc;
let so = doc.layer(id).ok_or(EngineError::NoLayer(id))?;
let sm = smart(doc, id)?;
let mut layers = contents(doc, sm, intent, bpc)?;
place(doc, sm, &mut layers)?;
let discarded = sm.smart_filters.len();
let out = unpacked(so, layers);
let (new_id, group) = (out.id, out.is_group());
s.edit("Convert to Layers", |doc, active| {
let path = doc.path_of(id).ok_or(EngineError::NoLayer(id))?;
*doc.layer_at_mut(&path).ok_or(EngineError::NoLayer(id))? = out;
*active = Some(new_id);
Ok(())
})?;
Ok(json!({"layer": new_id.0, "group": group, "discardedSmartFilters": discarded}))
}

View File

@ -318,6 +318,7 @@
Smart Object Chytrý objekt
Convert to Smart Object Převést na chytrý objekt
Convert to Embedded Převést na vložený
Convert to Layers Převést na vrstvy
Convert to Linked… Převést na propojený…
Edit Contents Upravit obsah
Replace Contents… Nahradit obsah…

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -314,6 +314,7 @@
Smart Object Smart-Objekt
Convert to Smart Object In Smart-Objekt konvertieren
Convert to Embedded In eingebettetes Smart-Objekt konvertieren
Convert to Layers In Ebenen konvertieren
Convert to Linked… In verknüpftes Smart-Objekt konvertieren…
Edit Contents Inhalt bearbeiten
Replace Contents… Inhalt ersetzen…

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -310,6 +310,7 @@
Smart Object Objeto inteligente
Convert to Smart Object Convertir en objeto inteligente
Convert to Embedded Convertir en incrustado
Convert to Layers Convertir en capas
Convert to Linked… Convertir en vinculado…
Edit Contents Editar contenido
Replace Contents… Reemplazar contenido…

Can't render this file because it contains an unexpected character in line 4 and column 47.

View File

@ -308,6 +308,7 @@
Smart Object Objet intelligent
Convert to Smart Object Convertir en objet intelligent
Convert to Embedded Convertir en objet incorporé
Convert to Layers Convertir en calques
Convert to Linked… Convertir en objet lié…
Edit Contents Modifier le contenu
Replace Contents… Remplacer le contenu…

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -301,6 +301,7 @@
Smart Object Smart Object
Convert to Smart Object Konversi ke Smart Object
Convert to Embedded Konversi ke Tertanam
Convert to Layers Konversi ke Layer
Convert to Linked… Konversi ke Tertaut…
Edit Contents Edit Isi
Replace Contents… Ganti Isi…

Can't render this file because it contains an unexpected character in line 4 and column 47.

View File

@ -2205,3 +2205,4 @@ cameraRaw Amount Quantità
% of all commits % di tutti i commit
Lines +/− Righe +/−
@{login}: {prs} PRs, {commits} commits, +{added} / −{deleted} lines (Δ {delta}), +{binAdded} / −{binDeleted} binary assets, {first} – {last} @{login}: {prs} PR, {commits} commit, +{added} / −{deleted} righe (Δ {delta}), +{binAdded} / −{binDeleted} risorse binarie, {first} – {last}
Convert to Layers Converti in livelli

Can't render this file because it has a wrong number of fields in line 14.

View File

@ -314,6 +314,7 @@
Smart Object スマートオブジェクト
Convert to Smart Object スマートオブジェクトに変換
Convert to Embedded 埋め込みに変換
Convert to Layers レイヤーに変換
Convert to Linked… リンクに変換…
Edit Contents コンテンツを編集
Replace Contents… コンテンツを置き換え…

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -300,6 +300,7 @@
Smart Object 스마트 오브젝트
Convert to Smart Object 스마트 오브젝트로 변환
Convert to Embedded 포함된 오브젝트로 변환
Convert to Layers 레이어로 변환
Convert to Linked… 연결된 오브젝트로 변환…
Edit Contents 내용 편집
Replace Contents… 내용 대체…

Can't render this file because it has a wrong number of fields in line 10.

View File

@ -305,6 +305,7 @@
Smart Object Objeto inteligente
Convert to Smart Object Converter em objeto inteligente
Convert to Embedded Converter em incorporado
Convert to Layers Converter em camadas
Convert to Linked… Converter em vinculado…
Edit Contents Editar conteúdo
Replace Contents… Substituir conteúdo…

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -1726,6 +1726,7 @@
Combine Shapes Комбинировать фигуры
Contract… Сжать…
Convert to Embedded Преобразовать во встроенный
Convert to Layers Преобразовать в слои
Convert to Linked… Преобразовать в связанный…
Count Счёт
Create Layer Создать слой

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -314,6 +314,7 @@
Smart Object 智能对象
Convert to Smart Object 转换为智能对象
Convert to Embedded 转换为嵌入对象
Convert to Layers 转换为图层
Convert to Linked… 转换为链接对象…
Edit Contents 编辑内容
Replace Contents… 替换内容…

Can't render this file because it has a wrong number of fields in line 14.

View File

@ -318,6 +318,7 @@
Smart Object 智慧型物件
Convert to Smart Object 轉換為智慧型物件
Convert to Embedded 轉換為嵌入
Convert to Layers 轉換為圖層
Convert to Linked… 轉換為連結…
Edit Contents 編輯內容
Replace Contents… 取代內容…

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -41,6 +41,7 @@ pub fn entries(l: &Layer, multi: bool, has_selection: bool) -> Vec<Entry> {
LayerContent::Shape(_) => v.push(Some(("Rasterize Layer", "layer.rasterize.shape"))),
LayerContent::Smart(_) => {
v.push(Some((tl!("Edit Contents"), "layer.smartObjects.editContents")));
v.push(Some((tl!("Convert to Layers"), "layer.smartObjects.convertToLayers")));
v.push(Some(("Rasterize Layer", "layer.rasterize.smartObject")));
}
LayerContent::Fill(_) => v.push(Some(("Rasterize Layer", "layer.rasterize.fillContent"))),
@ -223,7 +224,7 @@ mod tests {
s.execute("layer.smartObjects.convertToSmartObject", json!({})).unwrap();
let ids: Vec<_> = entries(&layer(&s), false, false).into_iter().flatten().map(|e| e.1).collect();
let edit = ids.iter().position(|id| *id == "layer.smartObjects.editContents").expect("Edit Contents entry");
assert_eq!(ids[edit + 1], "layer.rasterize.smartObject");
assert_eq!(ids[edit + 1..edit + 3], ["layer.smartObjects.convertToLayers", "layer.rasterize.smartObject"]);
}
#[test]

View File

@ -350,6 +350,7 @@ pub static CATALOG: &[(&[&str], &str, Option<&str>, &str)] = &[
(&["Layer", "Smart Objects"], "---", None, "---"),
(&["Layer", "Smart Objects"], "Convert to Linked…", None, "layer.smartObjects.convertToLinked"),
(&["Layer", "Smart Objects"], "Convert to Embedded", None, "layer.smartObjects.convertToEmbedded"),
(&["Layer", "Smart Objects"], "Convert to Layers", None, "layer.smartObjects.convertToLayers"),
(&["Layer", "Smart Objects"], "---", None, "---"),
(&["Layer", "Smart Objects", "Stack Mode"], "Entropy", None, "layer.smartObjects.stackMode.entropy"),
(&["Layer", "Smart Objects", "Stack Mode"], "Kurtosis", None, "layer.smartObjects.stackMode.kurtosis"),

View File

@ -7,7 +7,7 @@ use crate::menu_catalog::CATALOG;
use crate::menus::is_live;
/// Minimum number of live catalog items. Raise it when parity grows; never lower it.
pub const FLOOR: usize = 620;
pub const FLOOR: usize = 627;
/// One top-level menu's coverage.
#[derive(Clone, Debug, serde::Serialize)]

View File

@ -131,7 +131,11 @@ pub fn quick_actions(content: &LayerContent) -> &'static [(&'static str, &'stati
("Rasterize Type", "type.rasterizeTypeLayer"),
],
LayerContent::Shape(_) => &[("Rasterize Shape", "layer.rasterize.shape"), ("Convert to Smart Object", "layer.smartObjects.convertToSmartObject")],
LayerContent::Smart(_) => &[("Edit Contents", "layer.smartObjects.editContents"), ("Rasterize Smart Object", "layer.rasterize.smartObject")],
LayerContent::Smart(_) => &[
("Edit Contents", "layer.smartObjects.editContents"),
("Convert to Layers", "layer.smartObjects.convertToLayers"),
("Rasterize Smart Object", "layer.rasterize.smartObject"),
],
LayerContent::Group(_) => &[("Ungroup Layers", "layer.ungroupLayers"), ("Convert to Smart Object", "layer.smartObjects.convertToSmartObject")],
LayerContent::Fill(_) => &[("Rasterize Fill Content", "layer.rasterize.fillContent")],
LayerContent::Adjustment(_) => &[],

View File

@ -2,14 +2,14 @@
Generated by `cargo xtask parity` from `crates/ui-egui/src/menu_catalog.rs` (Photoshop's menu tree) and the live command registry. Do not edit by hand.
**626 of 626 menu items live (100.0%).**
**627 of 627 menu items live (100.0%).**
| Menu | Live | Total | % |
|---|---:|---:|---:|
| File | 51 | 51 | 100% |
| Edit | 75 | 75 | 100% |
| Image | 61 | 61 | 100% |
| Layer | 160 | 160 | 100% |
| Layer | 161 | 161 | 100% |
| Type | 44 | 44 | 100% |
| Select | 25 | 25 | 100% |
| Filter | 75 | 75 | 100% |