//! Menu entries of M14.5: Edit ▸ History, File ▸ Import Recent Footage, Replace Footage ▸ With //! Layered Comp, Save a Copy As XML, Animation ▸ Recent Animation Presets, Composition ▸ VR //! (Create VR Environment, Extract Cubemap), the "Assign Shortcut to …" submenus, Help ▸ Enable //! Logging / Reveal Logging File / System Compatibility Report, and `File.execute()` for scripts. use effectcraft_geom::{Mat4, Vec3, vec3}; use effectcraft_project::{ItemId, ItemKind, LayerSource}; use serde_json::{Value, json}; use super::{CommandSpec, always, b_p, bad, comp_id, has_comp, has_layers, has_project_selection, str_p}; use crate::{EngineError, Event, Result, Session, cmd, query}; /// Run `f` (which may run several undoable commands) as one undo step named `label`; on error /// the project is restored. pub(crate) fn grouped(s: &mut Session, label: &str, f: impl FnOnce(&mut Session) -> Result) -> Result { // The steps `f` records go to an empty stack (so the undo limit can't drop the snapshot // taken before them), then become one step on the real one. let saved = std::mem::take(&mut s.history.undo); let before = s.project.clone(); let r = f(s); let changed = !s.history.undo.is_empty(); s.history.undo = saved; s.history.merge_key = None; match r { Ok(v) => { if changed { s.history.undo.push((label.to_string(), before)); let levels = s.prefs.general.undo_levels.max(1) as usize; if s.history.undo.len() > levels { let extra = s.history.undo.len() - levels; s.history.undo.drain(..extra); } } Ok(v) } Err(e) => { if changed { s.project = before; s.sanitize_state(); s.bump(); } Err(e) } } } fn id_of(v: &Value, key: &str) -> Result { v.get(key).and_then(Value::as_u64).ok_or_else(|| EngineError::Other(format!("expected `{key}` in {v}"))) } // ---------------------------------------------------------------- Edit ▸ History /// Edit ▸ History: `{steps: n}` undoes the last n steps (the entry n−1 of the list), `{redo: n}` /// redoes n; without either, lists the steps (newest first). fn history(s: &mut Session, p: &Value) -> Result { if let Some(n) = p.get("steps").and_then(Value::as_u64) { if n as usize > s.history.undo.len() { return Err(bad("edit.history", format!("only {} undo steps", s.history.undo.len()))); } for _ in 0..n { s.undo(); } } else if let Some(n) = p.get("redo").and_then(Value::as_u64) { if n as usize > s.history.redo.len() { return Err(bad("edit.history", format!("only {} redo steps", s.history.redo.len()))); } for _ in 0..n { s.redo(); } } let undo: Vec<&str> = s.history.undo.iter().rev().map(|(l, _)| l.as_str()).collect(); let redo: Vec<&str> = s.history.redo.iter().rev().map(|(l, _)| l.as_str()).collect(); Ok(json!({"undo": undo, "redo": redo})) } fn has_history(s: &Session) -> std::result::Result<(), String> { if s.history.undo.is_empty() && s.history.redo.is_empty() { Err("nothing to undo".into()) } else { Ok(()) } } // ---------------------------------------------------------------- recent footage / presets fn import_recent(s: &mut Session, p: &Value) -> Result { let path = match (str_p(p, "path"), p.get("index").and_then(Value::as_u64)) { (Some(x), _) => x.to_string(), (None, Some(i)) => s.prefs.recent_footage.get(i as usize).cloned().ok_or_else(|| bad("file.importRecent", format!("no recent footage #{i}")))?, _ => s.prefs.recent_footage.first().cloned().ok_or_else(|| bad("file.importRecent", "no recent footage"))?, }; super::file::import(s, &json!({"paths": [path]})) } fn has_recent_footage(s: &Session) -> std::result::Result<(), String> { if s.prefs.recent_footage.is_empty() { Err("no recent footage".into()) } else { Ok(()) } } fn clear_recent_footage(s: &mut Session, _: &Value) -> Result { s.prefs.recent_footage.clear(); s.save_prefs(); s.prefs_revision += 1; Ok(Value::Null) } fn apply_recent_preset(s: &mut Session, p: &Value) -> Result { let path = match (str_p(p, "path"), p.get("index").and_then(Value::as_u64)) { (Some(x), _) => x.to_string(), (None, Some(i)) => s.prefs.recent_presets.get(i as usize).cloned().ok_or_else(|| bad("anim.applyRecentPreset", format!("no recent preset #{i}")))?, _ => s.prefs.recent_presets.first().cloned().ok_or_else(|| bad("anim.applyRecentPreset", "no recent animation presets"))?, }; let mut q = json!({"path": path}); if let Some(l) = p.get("layers") { q["layers"] = l.clone(); } s.execute("anim.applyPreset", q) } fn has_recent_presets(s: &Session) -> std::result::Result<(), String> { has_layers(s)?; if s.prefs.recent_presets.is_empty() { Err("no recent animation presets".into()) } else { Ok(()) } } fn clear_recent_presets(s: &mut Session, _: &Value) -> Result { s.prefs.recent_presets.clear(); s.save_prefs(); s.prefs_revision += 1; Ok(Value::Null) } // ---------------------------------------------------------------- File menu fn save_copy_xml(s: &mut Session, p: &Value) -> Result { let path = str_p(p, "path").ok_or_else(|| bad("file.saveCopyAsXml", "missing `path` (.ecprojx)"))?; let xml = crate::xml_project::to_xml(&s.project.to_json()).map_err(EngineError::Other)?; s.services.write_file(path, xml.as_bytes()).map_err(|e| EngineError::Other(format!("cannot write {path}: {e}")))?; s.toast(format!("Saved an XML copy to {path}")); Ok(json!({"path": path, "bytes": xml.len()})) } /// File ▸ Replace Footage ▸ With Layered Comp: a layered (Photoshop) footage item becomes a /// composition of its layers; every layer using the footage now uses the comp, and the footage /// item goes. One undo step. fn replace_with_layered_comp(s: &mut Session, p: &Value) -> Result { let item = p.get("item").and_then(Value::as_u64).map(ItemId).or_else(|| s.state.project_selection.first().copied()); let item = item.ok_or_else(|| bad("file.replaceWithLayeredComp", "select a layered footage item"))?; let path = match s.project.item(item).map(|i| &i.kind) { Some(ItemKind::Footage(f)) if f.codec == "PSD" || f.path.to_ascii_lowercase().ends_with(".psd") => f.path.clone(), _ => return Err(bad("file.replaceWithLayeredComp", "the item is not layered footage (a Photoshop file)")), }; let bytes = s.services.read_file(&path).map_err(|e| EngineError::Other(format!("cannot read {path}: {e}")))?; grouped(s, "Replace Footage", |s| { let (comp, _, _) = super::file::import_psd_comp(s, &path, bytes, false)?; let comp = ItemId(comp); s.edit("Replace Footage", None, |proj, st| { let ids: Vec = proj.items.keys().copied().collect(); for cid in ids { if cid == comp { continue; } if let Some(c) = proj.comp_mut(cid) { for l in &mut c.layers { if matches!(&l.source, LayerSource::Footage { item: i } if *i == item) { l.source = LayerSource::Comp { item: comp }; } } } } proj.items.remove(&item); st.project_selection = vec![comp]; Ok(()) })?; Ok(json!({"comp": comp.0})) }) } fn has_footage(s: &Session) -> std::result::Result<(), String> { has_project_selection(s)?; let ok = s.state.project_selection.iter().any(|i| matches!(s.project.item(*i).map(|x| &x.kind), Some(ItemKind::Footage(_)))); if ok { Ok(()) } else { Err("select a footage item".into()) } } /// `File.execute()` from scripts (and agents): open a file with its default application. /// Needs Allow Scripts to Write Files and Access Network; with Warn User When Executing Files on, /// the frontend asks first (`confirmed: true` skips the question). fn execute_file(s: &mut Session, p: &Value) -> Result { let path = str_p(p, "path").ok_or_else(|| bad("file.executeFile", "missing `path`"))?; if !s.prefs.scripting.allow_scripts_write_files { return Err(bad( "file.executeFile", "scripts can't execute files unless Settings ▸ Scripting & Expressions ▸ Allow Scripts to Write Files and Access Network is on", )); } if !std::path::Path::new(path).exists() { return Err(bad("file.executeFile", format!("no file {path}"))); } if s.prefs.scripting.warn_executing_files && !b_p(p, "confirmed").unwrap_or(false) { s.events.push(Event::Frontend { command: "app.confirmExecute".into(), params: json!({"path": path}) }); return Ok(json!({"needsConfirmation": path})); } let url = format!("file://{}", path.replace(' ', "%20")); s.events.push(Event::OpenUrl(url)); Ok(json!({"opened": path})) } // ---------------------------------------------------------------- Assign Shortcut to … /// The F10 / F11 / F12 (3D views) or Shift+F10 / F11 / F12 (workspaces) slots. fn slot_p(p: &Value, cmd: &str, shift: bool) -> Result { let raw = str_p(p, "slot").ok_or_else(|| bad(cmd, "missing `slot` (F10, F11 or F12)"))?; let k = raw.trim().trim_start_matches("Shift+").to_ascii_uppercase(); if !matches!(k.as_str(), "F10" | "F11" | "F12") { return Err(bad(cmd, "slot: F10, F11 or F12")); } Ok(if shift { format!("Shift+{k}") } else { k }) } /// Bind `keys` to the bindable `key`, taking it from whatever had it. fn reassign(s: &mut Session, key: &str, slot: &str, cmd: &str) -> Result { let t = s.shortcuts(); let b = t.find(key).cloned().ok_or_else(|| bad(cmd, format!("`{key}` can't have a shortcut")))?; let holders: Vec<(crate::shortcuts::Bindable, Vec)> = t .bindables .iter() .filter(|x| x.key != key && t.keys.get(&x.key).is_some_and(|k| k.iter().any(|k| k == slot))) .map(|x| (x.clone(), t.keys.get(&x.key).cloned().unwrap_or_default())) .collect(); let mine = t.keys.get(key).cloned().unwrap_or_default(); let replaced: Vec = holders.iter().map(|(h, _)| h.label.clone()).collect(); for (h, keys) in &holders { let rest: Vec = keys.iter().filter(|k| *k != slot).cloned().collect(); s.keymaps.set(h, &rest).map_err(|e| bad(cmd, e))?; } let mut keys = vec![slot.to_string()]; keys.extend(mine.into_iter().filter(|k| k != slot)); let created = s.keymaps.set(&b, &keys).map_err(|e| bad(cmd, e))?; s.shortcuts_changed(); if let Some(n) = &created { s.toast(format!("The default shortcuts can't be changed: created the preset \"{n}\"")); } Ok(json!({"command": key, "keys": keys, "replaced": replaced, "preset": s.keymaps.active})) } /// The command id of a 3D view. pub(crate) fn view_command(v: effectcraft_render::three_d::View3D) -> &'static str { use effectcraft_render::three_d::View3D::*; match v { ActiveCamera => "view.3d.activeCamera", Default => "view.3d.default", Front => "view.3d.front", Left => "view.3d.left", Top => "view.3d.top", Back => "view.3d.back", Right => "view.3d.right", Bottom => "view.3d.bottom", Custom1 => "view.3d.custom1", Custom2 => "view.3d.custom2", Custom3 => "view.3d.custom3", } } /// View ▸ Assign Shortcut to "": F10, F11 or F12 switches to the view. fn assign_view_shortcut(s: &mut Session, p: &Value) -> Result { let slot = slot_p(p, "view.assign3dShortcut", false)?; let cid = comp_id(s, p)?; let cur = s.state.views3d.get(&cid).map(|v| v.current).unwrap_or_default(); reassign(s, view_command(cur), &slot, "view.assign3dShortcut") } /// Window ▸ Assign Shortcut to "" Workspace: Shift+F10, Shift+F11 or Shift+F12. /// The frontend fills in the current workspace. fn assign_workspace_shortcut(s: &mut Session, p: &Value) -> Result { let slot = slot_p(p, "window.assignWorkspaceShortcut", true)?; let Some(ws) = str_p(p, "workspace") else { return super::frontend(s, "window.assignWorkspaceShortcut", p) }; let key = crate::shortcuts::binding_key("window.workspace", &json!({"name": ws})); reassign(s, &key, &slot, "window.assignWorkspaceShortcut") } // ---------------------------------------------------------------- Help fn log_file(s: &Session) -> std::path::PathBuf { crate::logging::log_path(s.config.as_ref().and_then(|c| c.dir()).as_deref()) } fn enable_logging(s: &mut Session, p: &Value) -> Result { let on = b_p(p, "on").unwrap_or(!crate::logging::is_enabled()); let file = log_file(s); crate::logging::set_enabled(on, file.clone()).map_err(|e| EngineError::Other(format!("cannot write {}: {e}", file.display())))?; s.prefs_revision += 1; s.toast(if on { format!("Logging to {}", file.display()) } else { "Logging off".into() }); Ok(json!({"enabled": on, "path": file.to_string_lossy()})) } fn reveal_log(s: &mut Session, _: &Value) -> Result { let file = crate::logging::current_file().unwrap_or_else(|| log_file(s)); let path = file.to_string_lossy().to_string(); let url = super::layer_menu::folder_url(&path); s.events.push(Event::OpenUrl(url.clone())); Ok(json!({"path": path, "url": url})) } /// Media formats File ▸ Import reads (with the desktop host's decoders). pub const IMPORT_FORMATS: &[&str] = &[ "MP4/MOV (H.264, HEVC, ProRes, AV1)", "WebM/MKV (VP8, VP9, AV1)", "PNG", "JPEG", "GIF", "WebP", "TIFF", "BMP", "OpenEXR", "Photoshop (PSD)", "SVG", "WAV", "AIFF", "MP3", "FLAC", "AAC", "Opus", "glTF/GLB", "OBJ", "Lottie JSON", ]; /// Help ▸ System Compatibility Report: OS, CPU, memory, GPU adapter, import/export formats. pub fn system_report(s: &Session) -> Value { let mem = crate::sysinfo::memory(); let gb = |b: u64| format!("{:.1} GB", b as f64 / (1u64 << 30) as f64); let export: Vec<&str> = s.exporter.as_ref().map(|e| e.formats().into_iter().map(|f| f.label()).collect()).unwrap_or_default(); let gpu = s.accel.as_ref().map(|a| a.name()); let budgets = s.prefs.cache_budgets(mem.or(s.sys_memory)); let mut issues = vec![]; if gpu.is_none() { issues.push("No GPU compositor: rendering uses the CPU (all features still work).".to_string()); } if mem.is_some_and(|m| m.total < 8 << 30) { issues.push("Less than 8 GB of memory: use lower preview resolutions for long or large comps.".to_string()); } if s.exporter.is_none() { issues.push("Export is not available in this build.".to_string()); } json!({ "app": format!("EffectCraft {}", env!("CARGO_PKG_VERSION")), "os": crate::sysinfo::os_version(), "arch": std::env::consts::ARCH, "cpu": crate::sysinfo::cpu_name(), "cores": crate::sysinfo::cpu_cores(), "memory": mem.map(|m| json!({"total": gb(m.total), "available": gb(m.available)})), "gpu": gpu, "caches": {"layer": gb(budgets.layer as u64), "footage": gb(budgets.media as u64), "preview": gb(budgets.preview as u64)}, "import": IMPORT_FORMATS, "export": export, "logging": crate::logging::is_enabled(), "recentWarnings": crate::logging::recent().into_iter().rev().take(20).collect::>(), "issues": issues, }) } /// The report as text (the dialog and `--` CLI output). pub fn report_text(r: &Value) -> String { let st = |k: &str| r.get(k).and_then(Value::as_str).unwrap_or("unknown").to_string(); let list = |k: &str| r.get(k).and_then(Value::as_array).map(|a| a.iter().filter_map(Value::as_str).collect::>().join(", ")).unwrap_or_default(); let mut t = format!( "{}\nOperating system: {} ({})\nCPU: {} ({} logical cores)\n", st("app"), st("os"), st("arch"), st("cpu"), r.get("cores").and_then(Value::as_u64).unwrap_or(1) ); match r.get("memory").filter(|m| !m.is_null()) { Some(m) => t.push_str(&format!("Memory: {} ({} available)\n", m["total"].as_str().unwrap_or("?"), m["available"].as_str().unwrap_or("?"))), None => t.push_str("Memory: unknown\n"), } t.push_str(&format!("GPU: {}\n", r.get("gpu").and_then(Value::as_str).unwrap_or("none (CPU compositing)"))); let c = &r["caches"]; t.push_str(&format!( "Cache budgets: layer {}, footage {}, preview {}\n", c["layer"].as_str().unwrap_or("?"), c["footage"].as_str().unwrap_or("?"), c["preview"].as_str().unwrap_or("?") )); t.push_str(&format!("Import: {}\nExport: {}\n", list("import"), list("export"))); let issues = list("issues"); t.push_str(&if issues.is_empty() { "No compatibility issues found.".to_string() } else { format!("Issues: {issues}") }); t } fn report(s: &mut Session, p: &Value) -> Result { let r = system_report(s); if !b_p(p, "quiet").unwrap_or(false) { s.events.push(Event::Frontend { command: "app.showReport".into(), params: json!({"title": "System Compatibility Report", "text": report_text(&r)}) }); } Ok(r) } // ---------------------------------------------------------------- Composition ▸ VR /// Orientation (degrees, AE order) of a camera whose +X, +Y (down) and +Z axes point along the /// given world directions: searched over quarter turns, which is all the cube faces need. fn orientation_for_axes(x: Vec3, y: Vec3, z: Vec3) -> [f64; 3] { let mut best = ([0.0; 3], f64::MAX); for a in 0..4 { for b in 0..4 { for c in 0..4 { let o = vec3(a as f64 * 90.0, b as f64 * 90.0, c as f64 * 90.0); let m = Mat4::orientation(o); let err = (m.apply_vec(vec3(1.0, 0.0, 0.0)) - x).length() + (m.apply_vec(vec3(0.0, 1.0, 0.0)) - y).length() + (m.apply_vec(vec3(0.0, 0.0, 1.0)) - z).length(); if err < best.1 { best = ([o.x, o.y, o.z], err); } } } } best.0 } /// Cube faces in the VR Converter's order (+X, −X, +Y, −Y, +Z, −Z): name, forward, right and up /// in the VR convention (X right, Y up, Z front). const FACES: [(&str, [f64; 3], [f64; 3], [f64; 3]); 6] = [ ("Right", [1.0, 0.0, 0.0], [0.0, 0.0, -1.0], [0.0, 1.0, 0.0]), ("Left", [-1.0, 0.0, 0.0], [0.0, 0.0, 1.0], [0.0, 1.0, 0.0]), ("Top", [0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, -1.0]), ("Bottom", [0.0, -1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]), ("Front", [0.0, 0.0, 1.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]), ("Back", [0.0, 0.0, -1.0], [-1.0, 0.0, 0.0], [0.0, 1.0, 0.0]), ]; /// (column, row) of each face in the 3:2 cube map. const CELLS_3X2: [(u32, u32); 6] = [(0, 0), (1, 0), (2, 0), (0, 1), (1, 1), (2, 1)]; /// VR world direction → After Effects world (Y down). fn ae(v: [f64; 3]) -> Vec3 { vec3(v[0], -v[1], v[2]) } /// Composition ▸ VR ▸ Create VR Environment: renders the active comp's 3D scene in every /// direction. Six face comps (a 90° camera each at `position`, default the comp centre, with a /// copy of the scene's layers), a 3:2 cube map of them, and an equirectangular output comp /// (VR Converter, cube map → equirectangular). One undo step. fn create_vr_environment(s: &mut Session, p: &Value) -> Result { let cid = comp_id(s, p)?; let src = s.project.comp(cid).ok_or(EngineError::NoComp)?.clone(); let name = s.project.item(cid).map(|i| i.name.clone()).unwrap_or_default(); let size = p.get("size").and_then(Value::as_u64).unwrap_or(1024).clamp(64, 8192); let center = match p.get("position").and_then(Value::as_array) { Some(a) => [0, 1, 2].map(|i| a.get(i).and_then(Value::as_f64).unwrap_or(0.0)), None => [src.width as f64 / 2.0, src.height as f64 / 2.0, 0.0], }; let fps = src.frame_rate.as_f64(); let dur = src.duration.seconds(); let r = grouped(s, "Create VR Environment", |s| { let mut faces = vec![]; for (fname, fw, r, u) in FACES { let fc = id_of( &s.execute( "comp.new", json!({"name": format!("{name} VR {fname}"), "width": size, "height": size, "frameRate": fps, "duration": dur, "open": false}), )?, "comp", )?; let o = orientation_for_axes(ae(r), ae([-u[0], -u[1], -u[2]]), ae(fw)); let cam = id_of(&s.execute("layer.newCamera", json!({"comp": fc, "name": format!("{fname} Camera"), "angleOfView": 90.0, "type": "oneNode"}))?, "layer")?; s.execute("prop.set", json!({"comp": fc, "layer": cam, "path": "transform/position", "value": center}))?; s.execute("prop.set", json!({"comp": fc, "layer": cam, "path": "transform/orientation", "value": o}))?; // The scene's layers (not its cameras), in the scene's coordinates, under the face // camera. let layers: Vec = src.layers.iter().filter(|l| !l.is_camera()).cloned().collect(); s.edit("Create VR Environment", None, |proj, _| { let mut next = proj.next_id; let mut map = std::collections::BTreeMap::new(); let mut copies = vec![]; for l in &layers { let mut c = l.clone(); super::edit::reid(&mut c, &mut next); map.insert(l.id, c.id); copies.push(c); } for c in &mut copies { c.parent = c.parent.and_then(|p| map.get(&p).copied()); if let Some(m) = &mut c.track_matte { m.layer = map.get(&m.layer).copied().unwrap_or(m.layer); } } proj.next_id = next; let comp = proj.comp_mut(ItemId(fc)).ok_or(EngineError::NoComp)?; comp.layers.extend(copies); Ok(()) })?; faces.push(fc); } let cube = id_of( &s.execute( "comp.new", json!({"name": format!("{name} VR Cube Map"), "width": size * 3, "height": size * 2, "frameRate": fps, "duration": dur, "open": false}), )?, "comp", )?; for (i, fc) in faces.iter().enumerate() { let l = id_of(&s.execute("layer.addItem", json!({"comp": cube, "item": fc, "time": 0.0}))?, "layer")?; let (c, rr) = CELLS_3X2[i]; let pos = [(c as f64 + 0.5) * size as f64, (rr as f64 + 0.5) * size as f64]; s.execute("prop.set", json!({"comp": cube, "layer": l, "path": "transform/position", "value": pos}))?; } let out = id_of( &s.execute( "comp.new", json!({"name": format!("{name} VR Output"), "width": size * 2, "height": size, "frameRate": fps, "duration": dur, "open": false}), )?, "comp", )?; let l = id_of(&s.execute("layer.addItem", json!({"comp": out, "item": cube, "time": 0.0}))?, "layer")?; s.execute("prop.set", json!({"comp": out, "layer": l, "path": "transform/scale", "value": [200.0 / 3.0, 50.0, 100.0]}))?; s.execute("effect.apply", json!({"comp": out, "layers": [l], "effect": "ec.vr.converter"}))?; s.execute("prop.set", json!({"comp": out, "layer": l, "path": "effects/#1/sourceProjection", "value": 2}))?; s.execute("prop.set", json!({"comp": out, "layer": l, "path": "effects/#1/targetProjection", "value": 0}))?; Ok(json!({"faces": faces, "cubeMap": cube, "output": out})) })?; s.open_comp(ItemId(r["output"].as_u64().unwrap_or(0))); Ok(r) } /// Composition ▸ VR ▸ Extract Cubemap: a 3:2 cube-map comp of an equirectangular comp (VR /// Converter, equirectangular → cube map), for editing the faces. One undo step. fn extract_cubemap(s: &mut Session, p: &Value) -> Result { let cid = comp_id(s, p)?; let src = s.project.comp(cid).ok_or(EngineError::NoComp)?.clone(); let name = s.project.item(cid).map(|i| i.name.clone()).unwrap_or_default(); let face = p.get("faceSize").and_then(Value::as_u64).unwrap_or((src.width / 4).max(16) as u64).clamp(16, 8192); let (w, h) = (face * 3, face * 2); let r = grouped(s, "Extract Cubemap", |s| { let cube = id_of( &s.execute( "comp.new", json!({"name": format!("{name} Cubemap"), "width": w, "height": h, "frameRate": src.frame_rate.as_f64(), "duration": src.duration.seconds(), "open": false}), )?, "comp", )?; let l = id_of(&s.execute("layer.addItem", json!({"comp": cube, "item": cid.0, "time": 0.0}))?, "layer")?; let scale = [w as f64 / src.width.max(1) as f64 * 100.0, h as f64 / src.height.max(1) as f64 * 100.0, 100.0]; s.execute("prop.set", json!({"comp": cube, "layer": l, "path": "transform/scale", "value": scale}))?; s.execute("effect.apply", json!({"comp": cube, "layers": [l], "effect": "ec.vr.converter"}))?; s.execute("prop.set", json!({"comp": cube, "layer": l, "path": "effects/#1/sourceProjection", "value": 0}))?; s.execute("prop.set", json!({"comp": cube, "layer": l, "path": "effects/#1/targetProjection", "value": 2}))?; Ok(json!({"cubeMap": cube, "layer": l, "faceSize": face})) })?; s.open_comp(ItemId(r["cubeMap"].as_u64().unwrap_or(0))); Ok(r) } pub fn specs() -> Vec { vec![ cmd!("edit.history", "History", [], None, "{steps?: n (undo n steps), redo?: n}", has_history, history), cmd!("file.importRecent", "Import Recent Footage", [], None, "{index? | path?}", has_recent_footage, import_recent), cmd!("file.clearRecentFootage", "Clear Recent Footage", ["File", "Import Recent Footage"], None, "{}", has_recent_footage, clear_recent_footage), cmd!("anim.applyRecentPreset", "Recent Animation Presets", [], None, "{index? | path?, layers?}", has_recent_presets, apply_recent_preset), cmd!( "anim.clearRecentPresets", "Clear Recent Presets", ["Animation", "Recent Animation Presets"], None, "{}", |s| if s.prefs.recent_presets.is_empty() { Err("no recent animation presets".into()) } else { Ok(()) }, clear_recent_presets ), cmd!("file.saveCopyAsXml", "Save a Copy As XML...", ["File", "Save As"], None, "{path: .ecprojx}", always, save_copy_xml), cmd!("file.replaceWithLayeredComp", "With Layered Comp", ["File", "Replace Footage"], None, "{item?}", has_footage, replace_with_layered_comp), cmd!("file.executeFile", "Execute File", [], None, "{path, confirmed?}", always, execute_file), cmd!("view.assign3dShortcut", "Assign Shortcut to 3D View", [], None, "{slot: F10|F11|F12, comp?}", has_comp, assign_view_shortcut), cmd!( "window.assignWorkspaceShortcut", "Assign Shortcut to Workspace", [], None, "{slot: Shift+F10|Shift+F11|Shift+F12, workspace?}", always, assign_workspace_shortcut ), cmd!("help.enableLogging", "Enable Logging", ["Help"], None, "{on?}", always, enable_logging), cmd!("help.revealLogFile", "Reveal Logging File", ["Help"], None, "{}", always, reveal_log), cmd!("help.systemReport", "System Compatibility Report...", ["Help"], None, "{quiet?}", always, report), cmd!( "comp.vr.createEnvironment", "Create VR Environment...", ["Composition", "VR"], None, "{comp?, size?: face pixels (1024), position?: [x,y,z]}", has_comp, create_vr_environment ), cmd!("comp.vr.extractCubemap", "Extract Cubemap...", ["Composition", "VR"], None, "{comp?, faceSize?}", has_comp, extract_cubemap), query!("help.systemInfo", "System Information", "{}", |s, _| Ok(system_report(s))), ] }