mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-30 21:36:46 -06:00
* M1: Crop image from the Object menu, the right-click menu and the image fill row (#1464) * M1: crop mode shows the image dimmed outside the layer and heavier crop marks (#1464) * M1: the handle overlay draws gradients and crops in their own helpers (#1464)
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
//! Image paints: adjustment presets, built in and saved in the file.
|
||||
//! Image paints: adjustment presets, built in and saved in the file, and Crop image.
|
||||
|
||||
use figcraft_color::{ImageFilters, PaintKind};
|
||||
use figcraft_doc::{Document, ImagePreset};
|
||||
use figcraft_color::{ImageFilters, ImageScaleMode, PaintKind};
|
||||
use figcraft_doc::{Document, ImagePreset, Node};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::{CommandSpec, ok};
|
||||
use crate::params::{f64_of, get, num, req_index, req_string, req_targets, string, targets};
|
||||
use crate::{Result, Session, bad, spec};
|
||||
use crate::{EngineError, Result, Session, bad, spec};
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![
|
||||
@@ -34,6 +34,13 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
}
|
||||
ok()
|
||||
}),
|
||||
spec!(
|
||||
"image.crop",
|
||||
"Crop image",
|
||||
"{ids? (one layer), index? (an image fill; default its top visible one)} → {id, index}: the image fill switches to Crop, keeping the picture where it was, and the canvas enters crop mode on it",
|
||||
true,
|
||||
crop
|
||||
),
|
||||
spec!("image.presets", "Image presets", "{} → {builtIn: [{id, label, filters}], saved: [{name, filters}]}", false, |s, _| {
|
||||
let built_in: Vec<Value> = ImageFilters::PRESETS.iter().map(|(id, label, f)| json!({"id": id, "label": label, "filters": f})).collect();
|
||||
Ok(json!({"builtIn": built_in, "saved": s.doc()?.image_presets}))
|
||||
@@ -135,6 +142,41 @@ fn adjust(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Ok(json!({"changed": changed}))
|
||||
}
|
||||
|
||||
/// The index of a layer's top visible image fill (fills paint bottom to top).
|
||||
pub fn top_image_fill(n: &Node) -> Option<usize> {
|
||||
n.fills.iter().rposition(|f| f.visible && matches!(f.kind, PaintKind::Image(_)))
|
||||
}
|
||||
|
||||
fn crop(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "image.crop";
|
||||
let ids = req_targets(cmd, s, p)?;
|
||||
// A selection with nothing to crop is refused as unavailable, not as bad input.
|
||||
let refuse = |why: &str| EngineError::Disabled(cmd.into(), why.into());
|
||||
let [id] = ids[..] else { return Err(refuse("select one layer to crop its image")) };
|
||||
let doc = s.doc()?;
|
||||
let n = doc.get(id)?;
|
||||
let index = match get(p, "index") {
|
||||
None | Some(Value::Null) => top_image_fill(n).ok_or_else(|| refuse("the layer has no image fill to crop"))?,
|
||||
Some(_) => req_index(cmd, p, "index")?,
|
||||
};
|
||||
let mut paint = n.fills.get(index).cloned().ok_or_else(|| bad(cmd, "no fill at that index"))?;
|
||||
let PaintKind::Image(img) = &mut paint.kind else { return Err(bad(cmd, "that fill is not an image")) };
|
||||
// An image already in Crop mode only enters crop mode: no edit, no undo step.
|
||||
if img.scale_mode != ImageScaleMode::Crop {
|
||||
let data = doc.images.get(&img.image).ok_or_else(|| bad(cmd, "the image's data is missing"))?;
|
||||
img.crop = Some(img.placement_crop(f64::from(data.width), f64::from(data.height), n.width, n.height));
|
||||
img.scale_mode = ImageScaleMode::Crop;
|
||||
let n = s.doc_mut()?.get_mut(id)?;
|
||||
if let Some(f) = n.fills.get_mut(index) {
|
||||
*f = paint;
|
||||
}
|
||||
// Like any paint edit, the layer leaves its paint style.
|
||||
n.styles.fill = None;
|
||||
}
|
||||
s.request_ui(cmd, json!({"id": id, "index": index}));
|
||||
Ok(json!({"id": id, "index": index}))
|
||||
}
|
||||
|
||||
fn save_preset(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "image.savePreset";
|
||||
let name: String = req_string(cmd, p, "name")?.trim().chars().filter(|c| !c.is_control()).take(MAX_NAME).collect();
|
||||
|
||||
Reference in New Issue
Block a user