effectcraft/crates/engine/src/commands/view.rs

87 lines
4.0 KiB
Rust

//! View menu commands that change the project (guides) or headless-relevant editor state (the
//! region of interest).
use effectcraft_project::Guide;
use serde_json::{Value, json};
use super::{CommandSpec, always, bad, comp_id, f_p, has_comp, str_p};
use crate::{EngineError, Result, Session, cmd};
fn has_guides(s: &Session) -> std::result::Result<(), String> {
has_comp(s)?;
if s.active_comp().is_some_and(|c| !c.guides.is_empty()) { Ok(()) } else { Err("the composition has no guides".into()) }
}
fn add_guide(s: &mut Session, p: &Value) -> Result<Value> {
let cid = comp_id(s, p)?;
let vertical = match str_p(p, "orientation").unwrap_or("vertical") {
"vertical" | "v" => true,
"horizontal" | "h" => false,
o => return Err(bad("view.addGuide", format!("orientation must be vertical or horizontal, not `{o}`"))),
};
let comp = s.project.comp(cid).ok_or(EngineError::NoComp)?;
let position = f_p(p, "position").unwrap_or(if vertical { comp.width as f64 / 2.0 } else { comp.height as f64 / 2.0 });
let n = s.edit("Add Guide", None, |proj, _| {
let c = proj.comp_mut(cid).ok_or(EngineError::NoComp)?;
c.guides.push(Guide { vertical, position });
Ok(c.guides.len())
})?;
Ok(json!({"guides": n}))
}
fn clear_guides(s: &mut Session, p: &Value) -> Result<Value> {
let cid = comp_id(s, p)?;
s.edit("Clear Guides", None, |proj, _| {
proj.comp_mut(cid).ok_or(EngineError::NoComp)?.guides.clear();
Ok(())
})?;
Ok(Value::Null)
}
fn export_guides(s: &mut Session, p: &Value) -> Result<Value> {
let path = str_p(p, "path").ok_or_else(|| bad("view.exportGuides", "missing `path`"))?;
let comp = s.project.comp(comp_id(s, p)?).ok_or(EngineError::NoComp)?;
let doc = json!({"effectcraftGuides": 1, "width": comp.width, "height": comp.height, "guides": comp.guides});
let text = serde_json::to_string_pretty(&doc).unwrap_or_default();
s.services.write_file(path, text.as_bytes()).map_err(|e| EngineError::Other(format!("cannot write {path}: {e}")))?;
Ok(json!({"path": path, "guides": comp.guides.len()}))
}
fn import_guides(s: &mut Session, p: &Value) -> Result<Value> {
let path = str_p(p, "path").ok_or_else(|| bad("view.importGuides", "missing `path`"))?;
let cid = comp_id(s, p)?;
let bytes = s.services.read_file(path).map_err(|e| EngineError::Other(format!("cannot read {path}: {e}")))?;
let doc: Value = serde_json::from_slice(&bytes).map_err(|e| bad("view.importGuides", format!("not a guides file: {e}")))?;
let guides: Vec<Guide> = serde_json::from_value(doc.get("guides").cloned().unwrap_or(json!([]))).map_err(|e| bad("view.importGuides", e.to_string()))?;
let n = guides.len();
s.edit("Import Guides", None, |proj, _| {
proj.comp_mut(cid).ok_or(EngineError::NoComp)?.guides.extend(guides);
Ok(())
})?;
Ok(json!({"imported": n}))
}
fn region_of_interest(s: &mut Session, p: &Value) -> Result<Value> {
s.state.region_of_interest = match p.get("rect") {
Some(Value::Array(a)) if a.len() == 4 => {
let r = [0, 1, 2, 3].map(|i| a[i].as_f64().unwrap_or(0.0));
if r[2] <= 0.0 || r[3] <= 0.0 {
return Err(bad("view.setRegionOfInterest", "width and height must be positive"));
}
Some(r)
}
_ => None,
};
Ok(json!(s.state.region_of_interest))
}
pub fn specs() -> Vec<CommandSpec> {
vec![
cmd!("view.addGuide", "Add Guide...", ["View"], None, "{orientation?: vertical|horizontal, position? (comp px)}", has_comp, add_guide),
cmd!("view.clearGuides", "Clear Guides", ["View"], None, "{comp?}", has_guides, clear_guides),
cmd!("view.importGuides", "Import Guides...", ["View"], None, "{path}", has_comp, import_guides),
cmd!("view.exportGuides", "Export Guides...", ["View"], None, "{path}", has_guides, export_guides),
cmd!("view.setRegionOfInterest", "Region of Interest", [], None, "{rect?: [x, y, w, h] | null}", always, region_of_interest),
]
}