This commit is contained in:
Brandon Thomas committed 2026-11-03 09:04:45 -05:00
commit 708de30891
3 files changed
+48 -12

No files matched your search

+4 -6
View File
@@ -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() }
}
}
+17 -6
View File
@@ -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.
+27
View File
@@ -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();