mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-15 03:36:48 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b5294c877 |
No files matched your search
@@ -878,8 +878,17 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
),
|
||||
spec!("layer.layerStyle.copyLayerStyle", "Copy Layer Style", &["Layer", "Layer Style"], None, r##"{"layer":id?}"##, has_layer, |s, p| {
|
||||
let id = layer_param(s, p)?;
|
||||
let l = s.active().and_then(|d| d.doc.layer(id)).ok_or(EngineError::NoLayer(id))?;
|
||||
let style = (l.effects.clone(), l.blend, l.fill_opacity, l.advanced);
|
||||
let d = s.active().ok_or(EngineError::NoDocument)?;
|
||||
let l = d.doc.layer(id).ok_or(EngineError::NoLayer(id))?;
|
||||
let style = crate::CopiedStyle {
|
||||
effects: l.effects.clone(),
|
||||
blend: l.blend,
|
||||
fill: l.fill_opacity,
|
||||
advanced: l.advanced,
|
||||
blend_if: l.blend_if.clone(),
|
||||
excluded_channels: l.excluded_channels,
|
||||
mode: d.doc.mode,
|
||||
};
|
||||
s.style_clipboard = Some(style);
|
||||
Ok(Value::Null)
|
||||
}),
|
||||
@@ -895,17 +904,27 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
},
|
||||
|s, p| {
|
||||
let ids = style_targets(s, p, |_| true)?;
|
||||
let (mut fx, blend, fill, advanced) = s.style_clipboard.clone().ok_or(EngineError::Other("no layer style has been copied".into()))?;
|
||||
let style = s.style_clipboard.clone().ok_or(EngineError::Other("no layer style has been copied".into()))?;
|
||||
let mut fx = style.effects;
|
||||
s.edit("Paste Layer Style", |doc, _| {
|
||||
// A style copied from a document in another mode takes this one's colours.
|
||||
let mode = doc.mode;
|
||||
fx.items = std::mem::take(&mut fx.items).into_iter().map(|e| e.in_mode(mode)).collect();
|
||||
// Blend If's Gray range carries over to any mode; its channel ranges and the
|
||||
// channel restrictions only to the mode they were set in.
|
||||
let same_mode = style.mode == mode;
|
||||
let mut blend_if = style.blend_if.clone();
|
||||
if !same_mode {
|
||||
blend_if.ranges.truncate(1);
|
||||
}
|
||||
for id in ids {
|
||||
let l = doc.layer_mut(id).ok_or(EngineError::NoLayer(id))?;
|
||||
l.effects = fx.clone();
|
||||
l.blend = blend;
|
||||
l.fill_opacity = fill;
|
||||
l.advanced = advanced;
|
||||
l.blend = style.blend;
|
||||
l.fill_opacity = style.fill;
|
||||
l.advanced = style.advanced;
|
||||
l.blend_if = blend_if.clone();
|
||||
l.excluded_channels = if same_mode { style.excluded_channels } else { 0 };
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
@@ -961,8 +961,9 @@ fn align(s: &mut Session, p: &Value, kind: &str) -> Result<Value> {
|
||||
"canvas" => doc.bounds(),
|
||||
"selection" => doc.selection.as_ref().map(|m| m.content_bounds()).ok_or_else(|| EngineError::Other("there is no selection to align to".into()))?,
|
||||
"auto" | "layers" => match (&doc.selection, sel.len()) {
|
||||
// Photoshop: one layer plus an active selection aligns to the selection bounds.
|
||||
(Some(m), 1) if to == "auto" => m.content_bounds(),
|
||||
// Photoshop: with an active selection, the layers align to its bounds (Layer › Align
|
||||
// Layers to Selection), however many are selected.
|
||||
(Some(m), _) if to == "auto" && !m.content_bounds().is_empty() => m.content_bounds(),
|
||||
// One layer with no selection aligns to the canvas (otherwise it would align to itself).
|
||||
(None, 1) if to == "auto" => doc.bounds(),
|
||||
_ => items.iter().fold(Rect::EMPTY, |a, (_, b)| a.union(b)),
|
||||
|
||||
@@ -391,6 +391,21 @@ impl Default for ToolState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Layer › Layer Style › Copy Layer Style: the effects and the Blending Options, as Photoshop
|
||||
/// copies them (Paste Layer Style puts them all on each target).
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CopiedStyle {
|
||||
pub effects: photocraft_doc::Effects,
|
||||
pub blend: photocraft_color::BlendMode,
|
||||
pub fill: f32,
|
||||
pub advanced: photocraft_doc::AdvancedBlending,
|
||||
pub blend_if: photocraft_doc::BlendIf,
|
||||
pub excluded_channels: u32,
|
||||
/// The colour mode it was copied in: Blend If's channel ranges and the channel restrictions
|
||||
/// only carry over to a document in the same mode.
|
||||
pub mode: photocraft_color::ColorMode,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Session {
|
||||
docs: Vec<DocState>,
|
||||
@@ -404,7 +419,7 @@ pub struct Session {
|
||||
pub clipboard: Option<edit_cmds::Clip>,
|
||||
/// Layer › Layer Style › Copy Layer Style: effects, blend mode, fill opacity and the Advanced
|
||||
/// Blending switches.
|
||||
pub style_clipboard: Option<(photocraft_doc::Effects, photocraft_color::BlendMode, f32, photocraft_doc::AdvancedBlending)>,
|
||||
pub style_clipboard: Option<CopiedStyle>,
|
||||
/// Shape path context menu: copied vector fill and stroke styles.
|
||||
pub path_fill_clipboard: Option<photocraft_doc::Fill>,
|
||||
pub path_stroke_clipboard: Option<photocraft_doc::ShapeStroke>,
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
//! Copy / Paste Layer Style carries the whole style, Blending Options included (Blend If and the
|
||||
//! channel restrictions); align with an active selection aligns to it however many layers are
|
||||
//! selected (layers audit L2).
|
||||
|
||||
use photocraft_engine::Session;
|
||||
use serde_json::json;
|
||||
|
||||
fn layer(s: &Session, id: u64) -> photocraft_doc::Layer {
|
||||
s.active().unwrap().doc.layer(photocraft_doc::LayerId(id)).unwrap().clone()
|
||||
}
|
||||
|
||||
fn new_doc(s: &mut Session, mode: &str) -> (u64, u64) {
|
||||
s.execute("file.new", json!({"width": 64, "height": 64, "mode": mode, "background": "white"})).unwrap();
|
||||
let a = s.execute("layer.new.layer", json!({})).unwrap()["layer"].as_u64().unwrap();
|
||||
let b = s.execute("layer.new.layer", json!({})).unwrap()["layer"].as_u64().unwrap();
|
||||
(a, b)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn paste_layer_style_carries_blend_if_and_channels() {
|
||||
let mut s = Session::new();
|
||||
let (a, b) = new_doc(&mut s, "rgb");
|
||||
s.execute(
|
||||
"layer.layerStyle.blendingOptions",
|
||||
json!({"layer": a, "blendIf": [{"channel": "gray", "underlying": [0, 128]}, {"channel": "red", "thisLayer": [10, 200]}]}),
|
||||
)
|
||||
.unwrap();
|
||||
s.execute("layer.setProps", json!({"layer": a, "channels": [true, false, true]})).unwrap();
|
||||
s.execute("layer.layerStyle.copyLayerStyle", json!({"layer": a})).unwrap();
|
||||
s.execute("layer.layerStyle.pasteLayerStyle", json!({"layer": b})).unwrap();
|
||||
let (la, lb) = (layer(&s, a), layer(&s, b));
|
||||
assert_eq!(lb.blend_if, la.blend_if, "Blend If, Gray and channel ranges");
|
||||
assert_eq!(lb.excluded_channels, la.excluded_channels, "the unchecked G channel");
|
||||
// Into a CMYK document: only the Gray range carries over; channels reset to all.
|
||||
let (c, _) = new_doc(&mut s, "cmyk");
|
||||
s.execute("layer.layerStyle.pasteLayerStyle", json!({"layer": c})).unwrap();
|
||||
let lc = layer(&s, c);
|
||||
assert_eq!(lc.blend_if.get(0), la.blend_if.get(0));
|
||||
assert_eq!(lc.blend_if.ranges.len(), 1, "no RGB channel range on CMYK");
|
||||
assert_eq!(lc.excluded_channels, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn several_layers_align_to_an_active_selection() {
|
||||
let mut s = Session::new();
|
||||
let (a, b) = new_doc(&mut s, "rgb");
|
||||
for (id, x) in [(a, 4), (b, 30)] {
|
||||
s.execute("layer.select", json!({"layer": id})).unwrap();
|
||||
s.execute("select.rect", json!({"x": x, "y": 10, "width": 8, "height": 8})).unwrap();
|
||||
s.execute("edit.fill", json!({"color": "#ff0000"})).unwrap();
|
||||
}
|
||||
s.execute("select.rect", json!({"x": 40, "y": 40, "width": 20, "height": 20})).unwrap();
|
||||
s.execute("layer.select", json!({"layer": a})).unwrap();
|
||||
s.execute("layer.select", json!({"layer": b, "mode": "add"})).unwrap();
|
||||
s.execute("layer.align.leftEdges", json!({})).unwrap();
|
||||
for id in [a, b] {
|
||||
let x0 = layer(&s, id).surface().unwrap().content_bounds().x0;
|
||||
assert_eq!(x0, 40, "layer {id} aligned to the selection's left edge");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user