mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-14 19:27:05 -04:00
file.importProject: import an EffectCraft project into the open project
File > Import > EffectCraft Project (and .ecproj/.ecprojx given to File > Import)
merges the project into a folder named after the file, with every id shifted past
the open project's, in one undo step. Relative footage resolves next to the file;
the result lists comps, root comps, primaryComp and missing media.
offset_ids now shifts tracker Motion Targets and saturates instead of overflowing;
fix_next_id saturates too, so a damaged file can't overflow on open or import.
Menu entry translated in all ten menu catalogues.
Rebased onto current upstream main for the split of #858 (test module list in
engine lib.rs; es/pt-br/ru/fr/de/cs menu translations).
(cherry picked from commit 19c653fccc95133fb35e978a7f7f20a18c0e8c89)
(cherry picked from commit 4e751ad2)
(cherry picked from commit ceed706cc3b5f3755769738a1ddb3d965a94ab6b)
This commit is contained in:
1 parent
470ea7a3af
commit
376f5757fc
11 files changed
+384
-30
No files matched your search
@@ -245,6 +245,8 @@ struct PendingImport {
|
||||
comps: Vec<u64>,
|
||||
items: Vec<u64>,
|
||||
errors: Vec<String>,
|
||||
/// EffectCraft projects merged in (`file.importProject` results); never added to a comp.
|
||||
projects: Vec<Value>,
|
||||
}
|
||||
|
||||
/// Everything probing a file needs, apart from the session (it can run on a background thread).
|
||||
@@ -370,10 +372,18 @@ fn prepare_import(s: &mut Session, p: &Value) -> Result<PendingImport> {
|
||||
_ => return Err(bad("file.import", "page: a page number from 1")),
|
||||
},
|
||||
};
|
||||
let mut paths = paths;
|
||||
let mut comps = vec![];
|
||||
let mut items = vec![];
|
||||
let mut errors = vec![];
|
||||
// EffectCraft projects import as projects, as After Effects imports an .aep.
|
||||
let (project_paths, mut paths): (Vec<String>, Vec<String>) = paths.into_iter().partition(|x| super::import_project::is_project_path(x));
|
||||
let mut projects = vec![];
|
||||
for path in project_paths {
|
||||
match s.execute("file.importProject", json!({"path": path})) {
|
||||
Ok(r) => projects.push(r),
|
||||
Err(e) => errors.push(format!("{path}: {e}")),
|
||||
}
|
||||
}
|
||||
if let Some(retain) = retain {
|
||||
let mut rest = vec![];
|
||||
for path in paths {
|
||||
@@ -424,13 +434,13 @@ fn prepare_import(s: &mut Session, p: &Value) -> Result<PendingImport> {
|
||||
page,
|
||||
psd_layer: p.get("layer").cloned(),
|
||||
};
|
||||
Ok(PendingImport { sources, prober, comps, items, errors })
|
||||
Ok(PendingImport { sources, prober, comps, items, errors, projects })
|
||||
}
|
||||
|
||||
/// The last part of an import: add the probed files to the project (one undo step) and select
|
||||
/// everything the import made, so the Project panel shows it.
|
||||
fn add_footage(s: &mut Session, pending: PendingImport, probed: Vec<std::result::Result<Probed, String>>) -> Result<Value> {
|
||||
let PendingImport { comps, mut items, mut errors, .. } = pending;
|
||||
let PendingImport { comps, mut items, mut errors, projects, .. } = pending;
|
||||
let mut found = vec![];
|
||||
let mut warnings = vec![];
|
||||
for r in probed {
|
||||
@@ -482,7 +492,11 @@ fn add_footage(s: &mut Session, pending: PendingImport, probed: Vec<std::result:
|
||||
if let Some(first) = asked.first() {
|
||||
s.events.push(crate::Event::Frontend { command: "file.interpretFootage".into(), params: json!({"item": first, "reason": "unlabeledAlpha"}) });
|
||||
}
|
||||
Ok(json!({"items": items, "comps": comps, "errors": errors, "warnings": warnings, "unlabeledAlpha": asked}))
|
||||
let mut out = json!({"items": items, "comps": comps, "errors": errors, "warnings": warnings, "unlabeledAlpha": asked});
|
||||
if !projects.is_empty() {
|
||||
out["projects"] = Value::Array(projects);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Whether a file format states how its alpha is stored (PNG, GIF, WebP and PSD are straight by
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
//! File ▸ Import ▸ EffectCraft Project: merge another `.ecproj` (or its XML copy) into the
|
||||
//! current project, as After Effects imports an `.aep` into the open project. The imported
|
||||
//! items land in a folder named after the file; every id is shifted past the current project's
|
||||
//! so nothing collides, and the whole import is one undo step.
|
||||
|
||||
use effectcraft_color::Label;
|
||||
use effectcraft_project::{ItemId, ItemKind, LayerSource, Project, essential};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::{CommandSpec, always, bad, str_p};
|
||||
use crate::{EngineError, Result, Session, cmd};
|
||||
|
||||
const C: &str = "file.importProject";
|
||||
|
||||
/// Whether `path` names a project EffectCraft imports as a project (not as footage).
|
||||
pub(crate) fn is_project_path(path: &str) -> bool {
|
||||
let lower = path.to_ascii_lowercase();
|
||||
lower.ends_with(".ecproj") || lower.ends_with(".ecprojx")
|
||||
}
|
||||
|
||||
/// Read a project file (JSON or its XML copy), as File ▸ Open does.
|
||||
fn read_project(s: &Session, path: &str) -> Result<(Project, Option<String>)> {
|
||||
let bytes = s.services.read_file(path).map_err(|e| EngineError::Other(format!("cannot read {path}: {e}")))?;
|
||||
let text = String::from_utf8(bytes).map_err(|_| bad(C, format!("{path} is not a text project file")))?;
|
||||
let text = if crate::xml_project::is_xml(&text) { crate::xml_project::from_xml(&text).map_err(EngineError::Other)? } else { text };
|
||||
let project = Project::from_json(&text).map_err(|e| bad(C, format!("{path} is not a project EffectCraft can open: {e}")))?;
|
||||
Ok((project, effectcraft_project::saved_by(&text)))
|
||||
}
|
||||
|
||||
/// Footage paths relative to the imported project's folder become absolute when the file is
|
||||
/// there; returns the footage paths that are missing.
|
||||
fn resolve_footage(s: &Session, project: &mut Project, dir: &std::path::Path) -> Vec<String> {
|
||||
let services = s.services.clone();
|
||||
let resolve = |raw: &mut String| -> bool {
|
||||
if raw.is_empty() || services.exists(raw) {
|
||||
return true;
|
||||
}
|
||||
if std::path::Path::new(raw.as_str()).is_absolute() {
|
||||
return false;
|
||||
}
|
||||
let joined = dir.join(raw.as_str()).to_string_lossy().to_string();
|
||||
if services.exists(&joined) {
|
||||
*raw = joined;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
};
|
||||
let mut missing = vec![];
|
||||
for it in project.items.values_mut() {
|
||||
if let ItemKind::Footage(f) = &mut it.kind {
|
||||
if f.sequence.is_empty() {
|
||||
if !resolve(&mut f.path) {
|
||||
missing.push(f.path.clone());
|
||||
}
|
||||
} else {
|
||||
let mut all = true;
|
||||
for frame in &mut f.sequence {
|
||||
all &= resolve(frame);
|
||||
}
|
||||
if !all {
|
||||
missing.push(f.path.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
missing
|
||||
}
|
||||
|
||||
/// The comps no other comp of `project` nests: the ones to open or place.
|
||||
fn root_comps(project: &Project) -> Vec<ItemId> {
|
||||
let nested: std::collections::HashSet<ItemId> =
|
||||
project.comps().flat_map(|(_, c)| c.layers.iter().filter_map(|l| if let LayerSource::Comp { item } = l.source { Some(item) } else { None })).collect();
|
||||
project.comps().map(|(id, _)| *id).filter(|id| !nested.contains(id)).collect()
|
||||
}
|
||||
|
||||
fn import_project(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let path = str_p(p, "path").ok_or_else(|| bad(C, "missing `path` (.ecproj)"))?.to_string();
|
||||
let (mut sub, saved_by) = read_project(s, &path)?;
|
||||
crate::upgrade_effects(&mut sub);
|
||||
let file = std::path::Path::new(&path);
|
||||
let dir = file.parent().map(|d| d.to_path_buf()).unwrap_or_default();
|
||||
let missing = resolve_footage(s, &mut sub, &dir);
|
||||
// The imported project's own render queue stays behind (as for templates).
|
||||
let base = s.project.next_id;
|
||||
if sub.next_id.checked_add(base).is_none() {
|
||||
return Err(bad(C, format!("{path} has ids out of range")));
|
||||
}
|
||||
essential::offset_ids(&mut sub, base);
|
||||
let roots: Vec<ItemId> = root_comps(&sub);
|
||||
let folder_name = file.file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_else(|| "Imported Project".into());
|
||||
let (folder, items) = s.edit("Import Project", None, |proj, st| {
|
||||
let folder = proj.add_item(&folder_name, Label::Yellow, None, ItemKind::Folder);
|
||||
let mut items = vec![];
|
||||
for (id, mut it) in std::mem::take(&mut sub.items) {
|
||||
if it.parent.is_none() {
|
||||
it.parent = Some(folder);
|
||||
}
|
||||
items.push(id.0);
|
||||
proj.items.insert(id, it);
|
||||
}
|
||||
proj.next_id = proj.next_id.max(sub.next_id);
|
||||
proj.fix_next_id();
|
||||
st.project_selection = vec![folder];
|
||||
Ok((folder, items))
|
||||
})?;
|
||||
let comps: Vec<Value> = s
|
||||
.project
|
||||
.comps()
|
||||
.filter(|(id, _)| items.contains(&id.0))
|
||||
.map(|(id, _)| json!({"id": id.0, "name": s.project.item(*id).map(|i| i.name.clone()).unwrap_or_default()}))
|
||||
.collect();
|
||||
let primary = if let [one] = roots.as_slice() { Some(one.0) } else { None };
|
||||
let mut warnings = vec![];
|
||||
if let Some(v) = saved_by.as_deref().filter(|v| effectcraft_project::is_newer_version(v, effectcraft_project::APP_VERSION)) {
|
||||
warnings.push(format!("saved by EffectCraft {v}, newer than this version ({})", effectcraft_project::APP_VERSION));
|
||||
}
|
||||
if !missing.is_empty() {
|
||||
warnings.push(format!("{} footage file{} missing", missing.len(), if missing.len() == 1 { " is" } else { "s are" }));
|
||||
}
|
||||
s.toast(if warnings.is_empty() { format!("Imported {folder_name}") } else { format!("Imported {folder_name}: {}", warnings.join("; ")) });
|
||||
Ok(json!({
|
||||
"folder": folder.0,
|
||||
"items": items,
|
||||
"comps": comps,
|
||||
"rootComps": roots.iter().map(|id| id.0).collect::<Vec<_>>(),
|
||||
"primaryComp": primary,
|
||||
"missingMedia": missing,
|
||||
"warnings": warnings,
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![cmd!(
|
||||
"file.importProject",
|
||||
"EffectCraft Project...",
|
||||
["File", "Import"],
|
||||
None,
|
||||
"{path (.ecproj or .ecprojx)} — merges the project into a folder named after the file; returns {folder, items, comps, rootComps, primaryComp (the only root comp, else null), missingMedia, warnings}",
|
||||
always,
|
||||
import_project
|
||||
)]
|
||||
}
|
||||
@@ -26,6 +26,7 @@ pub mod footage_panel;
|
||||
pub(crate) mod frame_export;
|
||||
mod frontend;
|
||||
mod help;
|
||||
mod import_project;
|
||||
mod key_labels;
|
||||
mod key_transform;
|
||||
mod keys_more;
|
||||
@@ -193,6 +194,7 @@ pub fn command_specs() -> &'static [CommandSpec] {
|
||||
v.extend(file_more::specs());
|
||||
v.extend(relink::specs());
|
||||
v.extend(lottie::specs());
|
||||
v.extend(import_project::specs());
|
||||
v.extend(timeline::specs());
|
||||
v.extend(comp_more::specs());
|
||||
v.extend(frame_export::specs());
|
||||
|
||||
@@ -1132,6 +1132,8 @@ mod tests_frame_export;
|
||||
#[cfg(test)]
|
||||
mod tests_fuzz_open;
|
||||
#[cfg(test)]
|
||||
mod tests_import_project;
|
||||
#[cfg(test)]
|
||||
mod tests_key_metadata_order;
|
||||
#[cfg(test)]
|
||||
mod tests_keylight;
|
||||
|
||||
@@ -528,6 +528,7 @@ File
|
||||
File... | file.import
|
||||
Multiple Files... | file.importMultiple
|
||||
Adobe Premiere Pro Project... | file.importTimeline
|
||||
EffectCraft Project... | file.importProject
|
||||
Placeholder... | file.importPlaceholder
|
||||
Solid... | file.importSolid
|
||||
Lottie... | file.importLottie
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
//! File ▸ Import ▸ EffectCraft Project: another project merges into the open one with fresh ids,
|
||||
//! its references intact, in one undo step.
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use effectcraft_project::{Footage, FootageKind, ItemId, ItemKind, LayerSource, Project};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::Session;
|
||||
|
||||
fn out_dir(name: &str) -> std::path::PathBuf {
|
||||
let d = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../target/test-out").join(name);
|
||||
let _ = std::fs::remove_dir_all(&d);
|
||||
std::fs::create_dir_all(&d).unwrap();
|
||||
d
|
||||
}
|
||||
|
||||
/// Every item, layer and property uid of `p` (they share one counter).
|
||||
fn all_ids(p: &Project) -> Vec<u64> {
|
||||
let mut v = vec![];
|
||||
for (id, it) in &p.items {
|
||||
v.push(id.0);
|
||||
if let ItemKind::Comp(c) = &it.kind {
|
||||
for l in &c.layers {
|
||||
v.push(l.id.0);
|
||||
fn uids(g: &effectcraft_project::PropGroup, v: &mut Vec<u64>) {
|
||||
v.push(g.uid);
|
||||
for c in &g.children {
|
||||
match c {
|
||||
effectcraft_project::Node::Prop(p) => v.push(p.uid),
|
||||
effectcraft_project::Node::Group(g) => uids(g, v),
|
||||
}
|
||||
}
|
||||
}
|
||||
uids(&l.props, &mut v);
|
||||
}
|
||||
}
|
||||
}
|
||||
v
|
||||
}
|
||||
|
||||
/// A project with "Main" nesting "Inner" under a parented null, a data file next to it by a
|
||||
/// relative path and a footage file that's gone. Saved as `<dir>/Library Card.ecproj`.
|
||||
fn source_project(dir: &std::path::Path) -> String {
|
||||
let mut s = Session::default();
|
||||
s.execute("comp.new", json!({"name": "Inner", "width": 64, "height": 64, "frameRate": 30, "duration": 2})).unwrap();
|
||||
let inner = s.active_comp_id().unwrap();
|
||||
s.execute("layer.newSolid", json!({"name": "Fill", "color": "#ff0000"})).unwrap();
|
||||
s.execute("comp.new", json!({"name": "Main", "width": 64, "height": 64, "frameRate": 30, "duration": 2})).unwrap();
|
||||
let main = s.active_comp_id().unwrap();
|
||||
let nest = s.execute("layer.addItem", json!({"comp": main.0, "item": inner.0, "time": 0.0})).unwrap()["layer"].as_u64().unwrap();
|
||||
let rig = s.execute("layer.newNull", json!({"name": "Rig"})).unwrap()["layer"].as_u64().unwrap();
|
||||
s.execute("layer.setParent", json!({"layers": [nest], "parent": rig})).unwrap();
|
||||
std::fs::write(dir.join("data.json"), b"{\"v\": 1}").unwrap();
|
||||
let p = std::sync::Arc::make_mut(&mut s.project);
|
||||
p.add_item("data.json", Default::default(), None, ItemKind::Footage(Footage { path: "data.json".into(), kind: FootageKind::Data, ..Default::default() }));
|
||||
p.add_item(
|
||||
"gone.mov",
|
||||
Default::default(),
|
||||
None,
|
||||
ItemKind::Footage(Footage { path: "/offline/gone.mov".into(), kind: FootageKind::Video, ..Default::default() }),
|
||||
);
|
||||
let path = dir.join("Library Card.ecproj").to_string_lossy().to_string();
|
||||
s.execute("file.saveAs", json!({"path": path})).unwrap();
|
||||
path
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_project_merges_with_fresh_ids_and_one_undo() {
|
||||
let dir = out_dir("import-project");
|
||||
let path = source_project(&dir);
|
||||
|
||||
let mut t = Session::default();
|
||||
t.execute("comp.new", json!({"name": "Show", "width": 64, "height": 64, "frameRate": 30, "duration": 2})).unwrap();
|
||||
t.execute("layer.newSolid", json!({"name": "Back", "color": "#00ff00"})).unwrap();
|
||||
let before = t.project.items.len();
|
||||
let before_ids: HashSet<u64> = all_ids(&t.project).into_iter().collect();
|
||||
|
||||
let r = t.execute("file.importProject", json!({"path": path})).unwrap();
|
||||
let folder = ItemId(r["folder"].as_u64().unwrap());
|
||||
assert_eq!(t.project.item(folder).unwrap().name, "Library Card.ecproj");
|
||||
// Inner, Main, the Solids folder and its solid, two footage items, and the new folder.
|
||||
assert_eq!(t.project.items.len(), before + 7);
|
||||
let imported: Vec<ItemId> = r["items"].as_array().unwrap().iter().map(|v| ItemId(v.as_u64().unwrap())).collect();
|
||||
// Everything lands in the folder (the Solids folder keeps its solid).
|
||||
let top = |mut id: ItemId| {
|
||||
while let Some(up) = t.project.item(id).and_then(|i| i.parent).filter(|p| *p != folder) {
|
||||
id = up;
|
||||
}
|
||||
t.project.item(id).and_then(|i| i.parent)
|
||||
};
|
||||
assert!(imported.iter().all(|id| top(*id) == Some(folder)), "everything lands in the folder");
|
||||
|
||||
// No id is taken twice, and none of the open project's ids moved.
|
||||
let ids = all_ids(&t.project);
|
||||
let unique: HashSet<u64> = ids.iter().copied().collect();
|
||||
assert_eq!(ids.len(), unique.len(), "ids collide");
|
||||
assert!(before_ids.is_subset(&unique));
|
||||
assert!(t.project.next_id > *ids.iter().max().unwrap());
|
||||
|
||||
// References follow the new ids: Main nests the imported Inner, under the imported null.
|
||||
let main = ItemId(r["primaryComp"].as_u64().unwrap());
|
||||
assert_eq!(t.project.item(main).unwrap().name, "Main");
|
||||
assert_eq!(r["rootComps"], json!([main.0]));
|
||||
let comp = t.project.comp(main).unwrap();
|
||||
let nest = comp.layers.iter().find(|l| matches!(l.source, LayerSource::Comp { .. })).unwrap();
|
||||
let LayerSource::Comp { item } = nest.source else { panic!("a precomp layer") };
|
||||
assert_eq!(t.project.item(item).unwrap().name, "Inner");
|
||||
assert!(imported.contains(&item));
|
||||
let parent = nest.parent.unwrap();
|
||||
assert_eq!(comp.layer(parent).unwrap().name, "Rig");
|
||||
|
||||
// A relative footage path resolves next to the imported project; a missing one is reported.
|
||||
let data = t.project.items.values().find(|i| i.name == "data.json").unwrap();
|
||||
let ItemKind::Footage(f) = &data.kind else { panic!("footage") };
|
||||
assert_eq!(std::path::Path::new(&f.path), dir.join("data.json"));
|
||||
assert_eq!(r["missingMedia"], json!(["/offline/gone.mov"]));
|
||||
|
||||
// One undo step removes all of it; redo brings it back.
|
||||
t.execute("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(t.project.items.len(), before);
|
||||
t.execute("edit.redo", json!({})).unwrap();
|
||||
assert_eq!(t.project.items.len(), before + 7);
|
||||
|
||||
// Importing again keeps the copies apart, and File ▸ Import routes .ecproj here.
|
||||
let r2 = t.execute("file.import", json!({"paths": [path]})).unwrap();
|
||||
let again = &r2["projects"][0];
|
||||
assert_ne!(again["primaryComp"], r["primaryComp"]);
|
||||
assert!(r2["items"].as_array().unwrap().is_empty(), "a project is not footage");
|
||||
let ids = all_ids(&t.project);
|
||||
assert_eq!(ids.len(), ids.iter().copied().collect::<HashSet<_>>().len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_project_rejects_bad_files_and_leaves_the_project() {
|
||||
let dir = out_dir("import-project-bad");
|
||||
let mut t = Session::default();
|
||||
t.execute("comp.new", json!({"name": "Show", "width": 64, "height": 64, "frameRate": 30, "duration": 2})).unwrap();
|
||||
let before = t.project.items.len();
|
||||
|
||||
std::fs::write(dir.join("junk.ecproj"), b"not a project").unwrap();
|
||||
assert!(t.execute("file.importProject", json!({"path": dir.join("junk.ecproj").to_string_lossy()})).is_err());
|
||||
assert!(t.execute("file.importProject", json!({"path": dir.join("absent.ecproj").to_string_lossy()})).is_err());
|
||||
|
||||
// Regression: an id at the top of the range saturated instead of overflowing (opening and
|
||||
// importing must not panic), and the import refuses it.
|
||||
let mut p = Project::default();
|
||||
let id = p.add_item("Huge", Default::default(), None, ItemKind::Folder);
|
||||
let mut it = p.items.remove(&id).unwrap();
|
||||
it.id = ItemId(u64::MAX);
|
||||
p.items.insert(it.id, it);
|
||||
let huge = dir.join("huge.ecproj");
|
||||
std::fs::write(&huge, p.to_json()).unwrap();
|
||||
assert_eq!(Project::from_json(&p.to_json()).unwrap().next_id, u64::MAX);
|
||||
let e = t.execute("file.importProject", json!({"path": huge.to_string_lossy()})).unwrap_err();
|
||||
assert!(e.to_string().contains("out of range"), "{e}");
|
||||
assert_eq!(t.project.items.len(), before);
|
||||
}
|
||||
@@ -701,13 +701,13 @@ pub fn with_overrides(project: &Project, comp: ItemId, overrides: &[Override]) -
|
||||
// ------------------------------------------------------------------------------ id remapping
|
||||
|
||||
/// Shift every id (items, layers, property uids, Essential Graphics controls) of `p` by
|
||||
/// `offset`, keeping all references consistent: used when merging a template project into
|
||||
/// another project.
|
||||
/// `offset`, keeping all references consistent: used when merging a template or another project
|
||||
/// into a project. Ids from a damaged file saturate instead of overflowing.
|
||||
pub fn offset_ids(p: &mut Project, offset: u64) {
|
||||
let items = std::mem::take(&mut p.items);
|
||||
for (_, mut it) in items {
|
||||
it.id = ItemId(it.id.0 + offset);
|
||||
it.parent = it.parent.map(|f| ItemId(f.0 + offset));
|
||||
it.id = ItemId(it.id.0.saturating_add(offset));
|
||||
it.parent = it.parent.map(|f| ItemId(f.0.saturating_add(offset)));
|
||||
if let ItemKind::Comp(c) = &mut it.kind {
|
||||
let c = std::sync::Arc::make_mut(c);
|
||||
for l in &mut c.layers {
|
||||
@@ -716,18 +716,18 @@ pub fn offset_ids(p: &mut Project, offset: u64) {
|
||||
if let Some(eg) = &mut c.essential {
|
||||
fn go(v: &mut [EgControl], o: u64) {
|
||||
for c in v {
|
||||
c.id += o;
|
||||
c.id = c.id.saturating_add(o);
|
||||
match &mut c.kind {
|
||||
EgKind::Property { layer, prop, links } => {
|
||||
*layer = LayerId(layer.0 + o);
|
||||
*prop += o;
|
||||
*layer = LayerId(layer.0.saturating_add(o));
|
||||
*prop = prop.saturating_add(o);
|
||||
for k in links {
|
||||
k.layer = LayerId(k.layer.0 + o);
|
||||
k.prop += o;
|
||||
k.layer = LayerId(k.layer.0.saturating_add(o));
|
||||
k.prop = k.prop.saturating_add(o);
|
||||
}
|
||||
}
|
||||
EgKind::Mirror { of } => *of += o,
|
||||
EgKind::Media { layer } => *layer = LayerId(layer.0 + o),
|
||||
EgKind::Mirror { of } => *of = of.saturating_add(o),
|
||||
EgKind::Media { layer } => *layer = LayerId(layer.0.saturating_add(o)),
|
||||
EgKind::Group { children } => go(children, o),
|
||||
EgKind::Comment { .. } => {}
|
||||
}
|
||||
@@ -739,34 +739,43 @@ pub fn offset_ids(p: &mut Project, offset: u64) {
|
||||
p.items.insert(it.id, it);
|
||||
}
|
||||
p.render_queue.clear();
|
||||
p.next_id += offset;
|
||||
p.next_id = p.next_id.saturating_add(offset);
|
||||
}
|
||||
|
||||
fn offset_layer(l: &mut Layer, o: u64) {
|
||||
l.id = LayerId(l.id.0 + o);
|
||||
l.parent = l.parent.map(|x| LayerId(x.0 + o));
|
||||
l.id = LayerId(l.id.0.saturating_add(o));
|
||||
l.parent = l.parent.map(|x| LayerId(x.0.saturating_add(o)));
|
||||
if let Some(tm) = &mut l.track_matte {
|
||||
tm.layer = LayerId(tm.layer.0 + o);
|
||||
tm.layer = LayerId(tm.layer.0.saturating_add(o));
|
||||
}
|
||||
match &mut l.source {
|
||||
LayerSource::Footage { item } | LayerSource::Comp { item } | LayerSource::Solid { item } | LayerSource::Model { item } => *item = ItemId(item.0 + o),
|
||||
LayerSource::Footage { item } | LayerSource::Comp { item } | LayerSource::Solid { item } | LayerSource::Model { item } => {
|
||||
*item = ItemId(item.0.saturating_add(o))
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
fn group(g: &mut PropGroup, o: u64, essential_parent: bool) {
|
||||
g.uid += o;
|
||||
if let GroupKind::Essential { overridden } = &mut g.kind {
|
||||
for u in overridden.iter_mut() {
|
||||
*u += o;
|
||||
g.uid = g.uid.saturating_add(o);
|
||||
match &mut g.kind {
|
||||
GroupKind::Essential { overridden } => {
|
||||
for u in overridden.iter_mut() {
|
||||
*u = u.saturating_add(o);
|
||||
}
|
||||
}
|
||||
// A tracker's Motion Target is a layer of the same comp.
|
||||
GroupKind::Tracker { settings } => {
|
||||
settings.target = settings.target.map(|t| LayerId(t.0.saturating_add(o)));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
let is_essential = matches!(g.kind, GroupKind::Essential { .. }) || essential_parent;
|
||||
for c in &mut g.children {
|
||||
match c {
|
||||
Node::Prop(p) => {
|
||||
p.uid += o;
|
||||
p.uid = p.uid.saturating_add(o);
|
||||
let fix = |v: &mut Value| {
|
||||
if let Value::Layer(Some(id)) = v {
|
||||
*id += o;
|
||||
*id = id.saturating_add(o);
|
||||
}
|
||||
};
|
||||
fix(&mut p.value);
|
||||
@@ -774,7 +783,7 @@ fn offset_layer(l: &mut Layer, o: u64) {
|
||||
fix(&mut k.value);
|
||||
}
|
||||
if is_essential && control_of(&p.match_id).is_some() {
|
||||
p.match_id = match_id(control_of(&p.match_id).unwrap_or(0) + o);
|
||||
p.match_id = match_id(control_of(&p.match_id).unwrap_or(0).saturating_add(o));
|
||||
// Media replacement values are item ids.
|
||||
if matches!(p.ui, ParamUi::Hidden)
|
||||
&& let Value::Scalar(x) = &mut p.value
|
||||
@@ -785,7 +794,7 @@ fn offset_layer(l: &mut Layer, o: u64) {
|
||||
}
|
||||
Node::Group(sub) => {
|
||||
if is_essential && let Some(id) = control_of(&sub.match_id) {
|
||||
sub.match_id = match_id(id + o);
|
||||
sub.match_id = match_id(id.saturating_add(o));
|
||||
}
|
||||
group(sub, o, is_essential);
|
||||
}
|
||||
|
||||
@@ -1394,10 +1394,11 @@ impl Project {
|
||||
pub fn fix_next_id(&mut self) {
|
||||
let mut m = self.next_id;
|
||||
for (id, it) in &self.items {
|
||||
m = m.max(id.0 + 1);
|
||||
// Saturating: a damaged file's ids must not overflow (the import then refuses it).
|
||||
m = m.max(id.0.saturating_add(1));
|
||||
if let ItemKind::Comp(c) = &it.kind {
|
||||
for l in &c.layers {
|
||||
m = m.max(l.id.0 + 1).max(l.props.max_uid() + 1);
|
||||
m = m.max(l.id.0.saturating_add(1)).max(l.props.max_uid().saturating_add(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -303,6 +303,7 @@ const SIMPLIFIED_CHINESE: &[(&str, &str, &str)] = &[
|
||||
("file.import", "File...", "文件..."),
|
||||
("file.importMultiple", "Multiple Files...", "多个文件..."),
|
||||
("file.importTimeline", "Adobe Premiere Pro Project...", "Adobe Premiere Pro 项目..."),
|
||||
("file.importProject", "EffectCraft Project...", "EffectCraft 项目..."),
|
||||
("file.importPlaceholder", "Placeholder...", "占位符..."),
|
||||
("file.importSolid", "Solid...", "纯色..."),
|
||||
("file.importLottie", "Lottie...", "Lottie..."),
|
||||
@@ -933,6 +934,7 @@ const TRADITIONAL_CHINESE: &[(&str, &str, &str)] = &[
|
||||
("file.import", "File...", "檔案..."),
|
||||
("file.importMultiple", "Multiple Files...", "多個檔案..."),
|
||||
("file.importTimeline", "Adobe Premiere Pro Project...", "Adobe Premiere Pro 專案..."),
|
||||
("file.importProject", "EffectCraft Project...", "EffectCraft 專案..."),
|
||||
("file.importPlaceholder", "Placeholder...", "預留位置..."),
|
||||
("file.importSolid", "Solid...", "純色..."),
|
||||
("file.importLottie", "Lottie...", "Lottie..."),
|
||||
@@ -1563,6 +1565,7 @@ const JAPANESE: &[(&str, &str, &str)] = &[
|
||||
("file.importPlaceholder", "Placeholder...", "プレースホルダー..."),
|
||||
("file.importSolid", "Solid...", "平面..."),
|
||||
("file.importLottie", "Lottie...", "Lottie..."),
|
||||
("file.importProject", "EffectCraft Project...", "EffectCraftプロジェクト..."),
|
||||
("file.importVanishingPoint", "Vanishing Point (.vpe)...", "Vanishing Point (.vpe)..."),
|
||||
("essential.importTemplate", "Essential Graphics Template...", "エッセンシャルグラフィックステンプレート..."),
|
||||
("", "Import Recent Footage", "最近使用したフッテージを読み込み"),
|
||||
@@ -2190,6 +2193,7 @@ const UKRAINIAN: &[(&str, &str, &str)] = &[
|
||||
("file.import", "File...", "Файл..."),
|
||||
("file.importMultiple", "Multiple Files...", "Кілька файлів..."),
|
||||
("file.importTimeline", "Adobe Premiere Pro Project...", "Проєкт Adobe Premiere Pro..."),
|
||||
("file.importProject", "EffectCraft Project...", "Проєкт EffectCraft..."),
|
||||
("file.importPlaceholder", "Placeholder...", "Заповнювач..."),
|
||||
("file.importSolid", "Solid...", "Суцільний колір..."),
|
||||
("file.importLottie", "Lottie...", "Lottie..."),
|
||||
@@ -2821,6 +2825,7 @@ const SPANISH: &[(&str, &str, &str)] = &[
|
||||
("file.import", "File...", "Archivo..."),
|
||||
("file.importMultiple", "Multiple Files...", "Varios archivos..."),
|
||||
("file.importTimeline", "Adobe Premiere Pro Project...", "Proyecto de Adobe Premiere Pro..."),
|
||||
("file.importProject", "EffectCraft Project...", "Proyecto de EffectCraft..."),
|
||||
("file.importPlaceholder", "Placeholder...", "Marcador de posición..."),
|
||||
("file.importSolid", "Solid...", "Sólido..."),
|
||||
("file.importLottie", "Lottie...", "Lottie..."),
|
||||
@@ -3450,6 +3455,7 @@ const PORTUGUESE: &[(&str, &str, &str)] = &[
|
||||
("file.import", "File...", "Arquivo..."),
|
||||
("file.importMultiple", "Multiple Files...", "Vários arquivos..."),
|
||||
("file.importTimeline", "Adobe Premiere Pro Project...", "Projeto do Adobe Premiere Pro..."),
|
||||
("file.importProject", "EffectCraft Project...", "Projeto do EffectCraft..."),
|
||||
("file.importPlaceholder", "Placeholder...", "Espaço reservado..."),
|
||||
("file.importSolid", "Solid...", "Sólido..."),
|
||||
("file.importLottie", "Lottie...", "Lottie..."),
|
||||
@@ -4087,6 +4093,7 @@ const RUSSIAN: &[(&str, &str, &str)] = &[
|
||||
("file.import", "File...", "Файл..."),
|
||||
("file.importMultiple", "Multiple Files...", "Несколько файлов..."),
|
||||
("file.importTimeline", "Adobe Premiere Pro Project...", "Проект Adobe Premiere Pro..."),
|
||||
("file.importProject", "EffectCraft Project...", "Проект EffectCraft..."),
|
||||
("file.importPlaceholder", "Placeholder...", "Заполнитель..."),
|
||||
("file.importSolid", "Solid...", "Сплошной цвет..."),
|
||||
("file.importLottie", "Lottie...", "Lottie..."),
|
||||
@@ -4716,6 +4723,7 @@ const FRENCH: &[(&str, &str, &str)] = &[
|
||||
("file.import", "File...", "Fichier..."),
|
||||
("file.importMultiple", "Multiple Files...", "Plusieurs fichiers..."),
|
||||
("file.importTimeline", "Adobe Premiere Pro Project...", "Projet Adobe Premiere Pro..."),
|
||||
("file.importProject", "EffectCraft Project...", "Projet EffectCraft..."),
|
||||
("file.importPlaceholder", "Placeholder...", "Espace réservé..."),
|
||||
("file.importSolid", "Solid...", "Solide..."),
|
||||
("file.importLottie", "Lottie...", "Lottie..."),
|
||||
@@ -5345,6 +5353,7 @@ const GERMAN: &[(&str, &str, &str)] = &[
|
||||
("file.import", "File...", "Datei..."),
|
||||
("file.importMultiple", "Multiple Files...", "Mehrere Dateien..."),
|
||||
("file.importTimeline", "Adobe Premiere Pro Project...", "Adobe Premiere Pro-Projekt..."),
|
||||
("file.importProject", "EffectCraft Project...", "EffectCraft-Projekt..."),
|
||||
("file.importPlaceholder", "Placeholder...", "Platzhalter..."),
|
||||
("file.importSolid", "Solid...", "Farbfläche..."),
|
||||
("file.importLottie", "Lottie...", "Lottie..."),
|
||||
@@ -5974,6 +5983,7 @@ const CZECH: &[(&str, &str, &str)] = &[
|
||||
("file.import", "File...", "Soubor..."),
|
||||
("file.importMultiple", "Multiple Files...", "Více souborů..."),
|
||||
("file.importTimeline", "Adobe Premiere Pro Project...", "Projekt Adobe Premiere Pro..."),
|
||||
("file.importProject", "EffectCraft Project...", "Projekt EffectCraft..."),
|
||||
("file.importPlaceholder", "Placeholder...", "Zástupný záznam..."),
|
||||
("file.importSolid", "Solid...", "Plná vrstva..."),
|
||||
("file.importLottie", "Lottie...", "Lottie..."),
|
||||
|
||||
@@ -1239,6 +1239,7 @@ fn file_dialog(app: &mut EffectcraftApp, id: &str, params: &Value) -> Option<Res
|
||||
"view.importGuides" => ("path", Ask::Open(&["json"])),
|
||||
"file.exportLottie" => ("path", Ask::Save("Animation.json")),
|
||||
"file.importLottie" => ("path", Ask::Open(&["json", "lottie"])),
|
||||
"file.importProject" => ("path", Ask::Open(&["ecproj", "ecprojx"])),
|
||||
"file.importTimeline" => ("path", Ask::Open(&["xml", "fcpxml", "otio", "edl", "aaf", "omf"])),
|
||||
"render.saveCurrentPreview" => ("path", Ask::Save("Preview.mp4")),
|
||||
"file.runScript" if params.get("name").is_none() => ("path", Ask::Open(&["jsx", "js", "jsonl", "json", "txt"])),
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
|
||||
## Update: File ▸ Import ▸ EffectCraft Project
|
||||
|
||||
- **Import a project into a project**, as After Effects imports an `.aep`:
|
||||
`file.importProject {path}` (File ▸ Import ▸ EffectCraft Project..., and `.ecproj` /
|
||||
`.ecprojx` files given to File ▸ Import) merges the project into a folder named after the
|
||||
file. Every item, layer and property id moves past the open project's, so references
|
||||
(precomps, parents, track mattes, Layer Control values, Essential Graphics, tracker
|
||||
targets) stay intact. It's one undo step. Footage paths relative to the imported project
|
||||
resolve next to it. The result lists the comps, the root comps, `primaryComp` (the only
|
||||
root comp) and missing media.
|
||||
- Id shifting saturates instead of overflowing on damaged files, for both templates and
|
||||
projects.
|
||||
Reference in new issue
Block a user