mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-09 02:34:14 +00:00
fix(engine): name allocators resume each base's suffix instead of retrying from the start
This commit is contained in:
@@ -395,6 +395,9 @@ fn collect_files(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
// reads (other footage, a proxy, the project file itself), and then nothing is written.
|
||||
let protected = s.protected_files();
|
||||
let mut used_names = BTreeSet::new();
|
||||
// Where each file name's numbering resumes: a name repeated many times is numbered from its
|
||||
// own file name, not by prefixing the previous candidate again.
|
||||
let mut next_k: BTreeMap<String, usize> = BTreeMap::new();
|
||||
let mut plan: Vec<(ItemId, Vec<(String, String)>)> = vec![];
|
||||
for it in s.project.items.values() {
|
||||
let ItemKind::Footage(f) = &it.kind else { continue };
|
||||
@@ -404,11 +407,17 @@ fn collect_files(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let files: Vec<&String> = if f.sequence.is_empty() { vec![&f.path] } else { f.sequence.iter().collect() };
|
||||
let mut pairs = vec![];
|
||||
for src in files {
|
||||
let mut name = std::path::Path::new(src).file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_else(|| "footage".into());
|
||||
let mut k = 2;
|
||||
while !used_names.insert(name.clone()) {
|
||||
name = format!("{k}_{name}");
|
||||
k += 1;
|
||||
let file = std::path::Path::new(src).file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_else(|| "footage".into());
|
||||
let mut name = file.clone();
|
||||
if !used_names.insert(name.clone()) {
|
||||
let k = next_k.entry(file.clone()).or_insert(2);
|
||||
loop {
|
||||
name = format!("{k}_{file}");
|
||||
*k += 1;
|
||||
if used_names.insert(name.clone()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
let dst = format!("{folder}/(Footage)/{name}");
|
||||
// A file that is already where its copy goes stays (it is not copied onto itself).
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
//! premultiplied): the composite as `R`, `G`, `B`, `A` plus one `<layer name>.R/G/B/A` channel
|
||||
//! set per layer (the layer-prefixed channel convention multi-layer EXR readers use).
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use effectcraft_color::BlendMode;
|
||||
use effectcraft_project::{ItemId, LayerId, Project};
|
||||
use effectcraft_raster::Image;
|
||||
@@ -142,17 +144,22 @@ fn srgb_to_linear(c: f32) -> f32 {
|
||||
|
||||
/// A unique EXR layer name (no dots: they separate the layer from the channel). Printable ASCII only:
|
||||
/// the `exr` crate panics on characters it cannot store in a channel name (#1702).
|
||||
fn exr_name(name: &str, used: &mut Vec<String>) -> String {
|
||||
/// `used` holds the names given so far, `next` the suffix each base's search resumes at: many
|
||||
/// layers of one name take a name each without retrying every suffix before theirs.
|
||||
fn exr_name(name: &str, used: &mut HashSet<String>, next: &mut HashMap<String, usize>) -> String {
|
||||
let base: String = name.chars().map(|c| if c == '.' || !(c == ' ' || c.is_ascii_graphic()) { '_' } else { c }).collect();
|
||||
let base = if base.trim().is_empty() { "Layer".to_string() } else { base };
|
||||
let mut n = base.clone();
|
||||
let mut k = 2;
|
||||
while used.contains(&n) {
|
||||
n = format!("{base} {k}");
|
||||
k += 1;
|
||||
if used.insert(base.clone()) {
|
||||
return base;
|
||||
}
|
||||
let k = next.entry(base.clone()).or_insert(2);
|
||||
loop {
|
||||
let n = format!("{base} {k}");
|
||||
*k += 1;
|
||||
if used.insert(n.clone()) {
|
||||
return n;
|
||||
}
|
||||
}
|
||||
used.push(n.clone());
|
||||
n
|
||||
}
|
||||
|
||||
/// The multi-layer EXR bytes of a frame and the channel names.
|
||||
@@ -182,9 +189,9 @@ pub fn layered_exr(s: &Session, cid: ItemId, t: Tick, scale: f64) -> Result<(Vec
|
||||
}
|
||||
};
|
||||
push("", &full);
|
||||
let mut used = vec![];
|
||||
let (mut used, mut next) = (HashSet::new(), HashMap::new());
|
||||
for l in &layers {
|
||||
let n = exr_name(&l.name, &mut used);
|
||||
let n = exr_name(&l.name, &mut used, &mut next);
|
||||
push(&n, &l.image);
|
||||
}
|
||||
let layer = Layer::new((w, h), LayerAttributes::default(), Encoding::SMALL_LOSSLESS, AnyChannels::sort(channels.into()));
|
||||
@@ -240,3 +247,23 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use super::exr_name;
|
||||
|
||||
#[test]
|
||||
fn many_layers_of_one_name_take_a_name_each_in_order() {
|
||||
let (mut used, mut next) = (HashSet::new(), HashMap::new());
|
||||
// `A 3` is taken by a layer of its own before the repeats reach it.
|
||||
let names: Vec<String> = ["A", "A", "A 3", "A", "A.b"].iter().map(|n| exr_name(n, &mut used, &mut next)).collect();
|
||||
assert_eq!(names, ["A", "A 2", "A 3", "A 4", "A_b"]);
|
||||
// 20 000 layers of one name: each found without retrying the suffixes before it.
|
||||
for _ in 0..20_000 {
|
||||
exr_name("Same", &mut used, &mut next);
|
||||
}
|
||||
assert_eq!(next["Same"], 20_001);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -751,6 +751,9 @@ fn footage_dir(s: &Session, name: &str, footage_dir: Option<&str>, project_path:
|
||||
fn extract_footage(s: &Session, proj: &mut Project, entries: &[(String, Vec<u8>)], dir: &str) -> Result<Vec<String>> {
|
||||
let mut written = std::collections::HashMap::new();
|
||||
let mut used = std::collections::HashSet::new();
|
||||
// Where each (item, file) pair's numbering resumes, so repeated names don't retry every
|
||||
// number before theirs.
|
||||
let mut next = std::collections::HashMap::new();
|
||||
for (k, v) in entries.iter().filter(|(k, _)| k.starts_with("media/")) {
|
||||
// `media/<item>/<file>` → one folder; a name another item took gets the item's id.
|
||||
let file = safe_file_name(&base_name(k));
|
||||
@@ -763,10 +766,10 @@ fn extract_footage(s: &Session, proj: &mut Project, entries: &[(String, Vec<u8>)
|
||||
// case-insensitive volume (Windows and macOS as they come), and Win32 drops a trailing
|
||||
// dot or space, so `a.png.` is `a.png` there. The name written stays as it is.
|
||||
let mut name = file.clone();
|
||||
let mut n = 0usize;
|
||||
let n = next.entry((item, same_file_key(&file))).or_insert(0usize);
|
||||
while !used.insert(same_file_key(&name)) {
|
||||
n += 1;
|
||||
name = if n == 1 { format!("{item}-{file}") } else { format!("{item}-{n}-{file}") };
|
||||
*n += 1;
|
||||
name = if *n == 1 { format!("{item}-{file}") } else { format!("{item}-{n}-{file}") };
|
||||
}
|
||||
let out = format!("{dir}/{name}");
|
||||
s.services.store_file(&out, v).map_err(|e| EngineError::Other(format!("cannot write {out}: {e}")))?;
|
||||
|
||||
@@ -331,3 +331,17 @@ fn background_pre_render_movies_are_checked_again_before_publication() {
|
||||
assert!(listing(&f.dir).iter().all(|name| !name.starts_with(".effectcraft-prerender-")));
|
||||
}
|
||||
}
|
||||
|
||||
/// Footage files sharing a name are numbered from that name (`3_a.mov`, not `3_2_a.mov`).
|
||||
#[test]
|
||||
fn collect_files_numbers_repeated_names_from_the_name() {
|
||||
let mut f = collecting("collect-repeated", "a.mov");
|
||||
for d in ["in2", "in3"] {
|
||||
std::fs::create_dir_all(f.path(d)).unwrap();
|
||||
f.footage(&format!("{d}/a.mov"));
|
||||
}
|
||||
collect(&mut f).unwrap();
|
||||
let mut names = listing(&f.path("out/(Footage)"));
|
||||
names.sort();
|
||||
assert_eq!(names, ["2_a.mov", "3_a.mov", "a.mov"]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user