* 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:
parent
20fb1903dd
commit
01cd6fffa2
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}))
|
||||
}
|
||||
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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(_) => &[],
|
||||
|
|
|
|||
|
|
@ -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% |
|
||||
|
|
|
|||
Loading…
Reference in New Issue