mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-05 18:18:49 +00:00
Fix #2904: a second layer or a copy of v-b1.mp4 is v-b1.mp4 2, not v-b1.mp 2
Comp::unique_layer_name and Project::numbered_item_name each stripped every trailing digit before numbering. Both use one helper now, numbered_name, which replaces a trailing number only when a space sets it off (Red Solid 1 -> Red Solid 2); digits that end a word stay.
This commit is contained in:
1 parent
a646bdcc2e
commit
0b60a86bfe
3 files changed
+48
-12
No files matched your search
@@ -265,13 +265,11 @@ impl Comp {
|
||||
self.layers.iter().find(|l| matches!(l.source, LayerSource::Camera) && l.switches.video && l.is_active_at(t))
|
||||
}
|
||||
|
||||
/// Unique layer name with a numeric suffix (`Shape Layer 2`).
|
||||
/// `base`, or the next free numbered name when a layer already has it (`Shape Layer 2`,
|
||||
/// `v-b1.mp4 2`; see `numbered_name`).
|
||||
pub fn unique_layer_name(&self, base: &str) -> String {
|
||||
if !self.layers.iter().any(|l| l.name == base) {
|
||||
return base.to_string();
|
||||
}
|
||||
let stem = base.trim_end_matches(|c: char| c.is_ascii_digit()).trim_end();
|
||||
(2..).map(|i| format!("{stem} {i}")).find(|n| !self.layers.iter().any(|l| &l.name == n)).unwrap_or_default()
|
||||
let taken = |n: &str| self.layers.iter().any(|l| l.name == n);
|
||||
if taken(base) { crate::item::numbered_name(base, taken) } else { base.to_string() }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,19 @@ use super::layer::LayerSource;
|
||||
use super::settings::{APP_VERSION, ProjectError, ProjectSettings, SCHEMA_VERSION, SavedView};
|
||||
use super::{Arc, BTreeMap, Deserialize, FrameRate, Label, Project, Serialize, Tick, file_check, limits};
|
||||
|
||||
/// `base` numbered with the lowest counter from 2 for which `taken` is false: a copy of an item or
|
||||
/// another layer of the same name. A number set off by a space is a counter and is replaced
|
||||
/// (`Red Solid 1` → `Red Solid 2`, `shot 2` → `shot 3`); digits that end a word belong to the
|
||||
/// name and stay (`v-b1.mp4` → `v-b1.mp4 2`, `clip2.mov` → `clip2.mov 2`).
|
||||
pub(crate) fn numbered_name(base: &str, taken: impl Fn(&str) -> bool) -> String {
|
||||
let head = base.trim_end_matches(|c: char| c.is_ascii_digit());
|
||||
let stem = match head.strip_suffix(' ') {
|
||||
Some(stem) if head.len() < base.len() && !stem.trim_end().is_empty() => stem.trim_end(),
|
||||
_ => base,
|
||||
};
|
||||
(2u64..).map(|k| format!("{stem} {k}")).find(|n| !taken(n)).unwrap_or_default()
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||
pub struct ItemId(pub u64);
|
||||
|
||||
@@ -162,13 +175,11 @@ impl Project {
|
||||
pub fn find_by_name(&self, name: &str) -> Option<&Item> {
|
||||
self.items.values().find(|i| i.name == name)
|
||||
}
|
||||
/// A numbered name for a copy of `base` that no project item has (Edit ▸ Duplicate): its
|
||||
/// trailing number is dropped and the lowest free one from 2 is added (`Comp 1` → `Comp 2`,
|
||||
/// `Name` → `Name 2`, then `Name 3`). Items for which `ignore` holds don't count.
|
||||
/// A numbered name for a copy of `base` that no project item has (Edit ▸ Duplicate), as
|
||||
/// `numbered_name` gives it (`Comp 1` → `Comp 2`, `Name` → `Name 2`, then `Name 3`,
|
||||
/// `v-b1.mp4` → `v-b1.mp4 2`). Items for which `ignore` holds don't count.
|
||||
pub fn numbered_item_name(&self, base: &str, ignore: impl Fn(&Item) -> bool) -> String {
|
||||
let stem = base.trim_end_matches(|c: char| c.is_ascii_digit()).trim_end();
|
||||
let taken = |n: &str| self.items.values().any(|i| i.name == n && !ignore(i));
|
||||
(2u64..).map(|k| format!("{stem} {k}")).find(|n| !taken(n)).unwrap_or_default()
|
||||
numbered_name(base, |n| self.items.values().any(|i| i.name == n && !ignore(i)))
|
||||
}
|
||||
/// `base` when no project item has that name, else the next free numbered name as
|
||||
/// [`Project::numbered_item_name`] gives it. Items for which `ignore` holds don't count.
|
||||
|
||||
@@ -113,6 +113,33 @@ fn resolution_factors_and_scales() {
|
||||
assert_eq!(Resolution::Third.scale(0.1, 1.0), 1.0 / 3.0);
|
||||
}
|
||||
|
||||
/// Another layer of the same source, and a duplicated item, get a numbered name. Only a number set
|
||||
/// off by a space is a counter; digits that end a word stay (`v-b1.mp4` gave `v-b1.mp 2`).
|
||||
#[test]
|
||||
fn numbered_names_keep_digits_that_end_a_word() {
|
||||
// (the name in use, the next layer's and the item copy's name)
|
||||
let rows = [
|
||||
("v-b1.mp4", "v-b1.mp4 2"),
|
||||
("clip2.mov", "clip2.mov 2"),
|
||||
("shot 2", "shot 3"),
|
||||
("a.b.c", "a.b.c 2"),
|
||||
("Background", "Background 2"),
|
||||
("Red Solid 1", "Red Solid 2"),
|
||||
];
|
||||
for (name, next) in rows {
|
||||
let mut p = Project::default();
|
||||
let mut comp = Comp::new(64, 36, FrameRate::FPS_29_97, Tick::from_seconds_f64(1.0));
|
||||
let solid = Solid { color: [0.5; 3], width: 64, height: 36, pixel_aspect: 1.0 };
|
||||
let item = p.add_item(name, effectcraft_color::Label::Red, None, ItemKind::Solid(solid));
|
||||
let first = build::layer(&mut p, &comp, name, LayerSource::Solid { item }, (64, 36), None);
|
||||
assert_eq!(first.name, name, "the first layer of {name}");
|
||||
comp.layers.push(first);
|
||||
let second = build::layer(&mut p, &comp, name, LayerSource::Solid { item }, (64, 36), None);
|
||||
assert_eq!(second.name, next, "the second layer of {name}");
|
||||
assert_eq!(p.numbered_item_name(name, |_| false), next, "a copy of the item {name}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unique_names_and_cycles() {
|
||||
let (p, cid) = project_with_layer();
|
||||
|
||||
Reference in new issue
Block a user