use std::collections::{BTreeMap, HashMap, HashSet};
use crate::sdf::{self, element_cmp, Path, PathComponent, RelocateList};
use crate::tf::Token;
use super::layer_graph::LayerGraph;
use super::layer_stack::LayerStackId;
use super::mapping::MapFunction;
use super::prim_graph::ArcType;
use super::prim_index::PrimEntry;
use super::{Error, InvalidRelocateReason, LayerId, RelocateConflictReason};
pub(crate) type LayerRelocates = HashMap<LayerId, RelocateList>;
type AuthoredRelocate = (Path, Path, LayerId, String);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum RelocateOccurrence {
Active,
DroppedStructural,
DroppedDuplicateSource,
DroppedConflict,
}
impl RelocateOccurrence {
pub(crate) fn is_active(self) -> bool {
self == RelocateOccurrence::Active
}
}
pub(crate) fn chain_through_relocates(path: &Path, relocates: &[(Path, Path)], skip_source: Option<&Path>) -> Path {
let mut current = path.clone();
for _ in 0..relocates.len() {
let next = relocates
.iter()
.filter(|(s, t)| !t.is_empty() && Some(s) != skip_source)
.filter_map(|(s, t)| current.replace_prefix(s, t).map(|p| (s.as_str().len(), p)))
.filter(|(_, p)| *p != current)
.max_by_key(|(len, _)| *len);
match next {
Some((_, p)) => current = p,
None => break,
}
}
current
}
fn root_prim_name(path: &Path) -> Option<&str> {
match path.components().next() {
Some(PathComponent::Prim(name)) => Some(name),
_ => None,
}
}
fn shift_through_nearest_ancestor(endpoint: &Path, renames: &[(Path, Path)]) -> Path {
renames
.iter()
.filter(|(src, tgt)| !tgt.is_empty() && src != endpoint)
.filter_map(|(src, tgt)| endpoint.replace_prefix(src, tgt).map(|p| (src.as_str().len(), p)))
.max_by_key(|(len, _)| *len)
.map(|(_, p)| p)
.unwrap_or_else(|| endpoint.clone())
}
pub(crate) fn apply_child_relocates(
parent: &Path,
pairs: &[(Path, Path)],
name_order: &mut Vec<Token>,
name_set: &mut HashSet<Token>,
prohibited: &mut HashSet<Token>,
) {
let mut relocations: HashMap<Token, Option<Token>> = HashMap::new();
let mut adds: Vec<Token> = Vec::new();
for (src, tgt) in pairs {
let src_is_child = src.parent().as_ref() == Some(parent);
let tgt_is_child = !tgt.is_empty() && tgt.parent().as_ref() == Some(parent);
if src_is_child {
if let Some(name) = src.name() {
prohibited.insert(name.into());
let rename = tgt_is_child.then(|| tgt.name()).flatten();
relocations.insert(name.into(), rename.map(Token::from));
}
}
if tgt_is_child && !src_is_child {
if let Some(tgt_name) = tgt.name() {
adds.push(tgt_name.into());
}
}
}
adds.sort_by(|a, b| element_cmp(a.as_str(), b.as_str()));
if !relocations.is_empty() {
let mut retained: Vec<Token> = Vec::with_capacity(name_order.len());
for name in name_order.drain(..) {
match relocations.get(&name) {
Some(Some(new_name)) => {
name_set.remove(&name);
if name_set.insert(new_name.clone()) {
retained.push(new_name.clone());
}
}
Some(None) => {
name_set.remove(&name);
}
None => retained.push(name),
}
}
*name_order = retained;
}
for name in adds {
if name_set.insert(name.clone()) {
name_order.push(name);
}
}
}
pub(crate) fn validate_layer_relocates(graph: &LayerGraph) -> (LayerRelocates, Vec<Error>) {
let mut errors = Vec::new();
let mut all: Vec<AuthoredRelocate> = Vec::new();
for &id in graph.all_ids() {
if graph.is_muted(id) {
continue;
}
let layer = graph.layer(id);
for (source, target) in layer.relocates() {
match relocate_invalid_reason(&source, &target) {
None => all.push((source, target, id, layer.identifier().to_string())),
Some(reason) => errors.push(Error::InvalidRelocate {
source_path: source,
target_path: target,
layer: layer.identifier().to_string(),
reason,
}),
}
}
}
if all.is_empty() {
return (HashMap::new(), errors);
}
let mut by_layer: HashMap<LayerId, Vec<usize>> = HashMap::new();
for (idx, (_, _, layer_id, _)) in all.iter().enumerate() {
by_layer.entry(*layer_id).or_default().push(idx);
}
let scope_indices: Vec<Vec<usize>> = graph
.relocate_conflict_scopes()
.into_iter()
.map(|scope| {
let mut seen = HashSet::new();
scope
.into_iter()
.flat_map(|layer| by_layer.get(&layer).into_iter().flatten().copied())
.filter(|idx| seen.insert(*idx))
.collect()
})
.collect();
detect_relocate_conflicts(&all, &scope_indices, &mut errors);
let mut out: LayerRelocates = HashMap::new();
for (source, target, layer_id, _) in all {
out.entry(layer_id).or_default().push((source, target));
}
(out, errors)
}
fn detect_relocate_conflicts(all: &[AuthoredRelocate], scopes: &[Vec<usize>], errors: &mut Vec<Error>) {
let scopes: Vec<Vec<usize>> = scopes
.iter()
.map(|scope| {
let duplicate = duplicate_source_mask(scope.iter().map(|&idx| (true, &all[idx].0)));
scope
.iter()
.copied()
.zip(duplicate)
.filter_map(|(idx, is_duplicate)| (!is_duplicate).then_some(idx))
.collect()
})
.collect();
let mut same_target_errors: Vec<(Path, Vec<usize>)> = Vec::new();
let mut seen_same_target: HashSet<(Path, Vec<usize>)> = HashSet::new();
for scope in &scopes {
let mut by_target: BTreeMap<Path, Vec<usize>> = BTreeMap::new();
for &idx in scope {
let target = &all[idx].1;
if !target.is_empty() {
by_target.entry(target.clone()).or_default().push(idx);
}
}
for (target, mut group) in by_target {
if group.len() <= 1 {
continue;
}
group.sort_unstable();
if seen_same_target.insert((target.clone(), group.clone())) {
same_target_errors.push((target, group));
}
}
}
same_target_errors.sort_by(|a, b| a.0.cmp(&b.0));
for (target, group) in same_target_errors {
let mut sources: Vec<(Path, String)> = group.iter().map(|&i| (all[i].0.clone(), all[i].3.clone())).collect();
sources.sort_by(|a, b| a.0.cmp(&b.0));
errors.push(Error::SameTargetRelocations { target, sources });
}
let mut pairwise: Vec<(usize, usize, RelocateConflictReason)> = Vec::new();
for scope in &scopes {
for &i in scope {
for &j in scope {
if i == j {
continue;
}
for reason in relocate_pair_conflicts(&all[i].0, &all[i].1, &all[j].0, &all[j].1) {
push_pairwise_conflict(&mut pairwise, i, j, reason);
}
}
}
}
pairwise.sort_by(|a, b| {
all[a.0]
.0
.cmp(&all[b.0].0)
.then(a.2.cmp(&b.2))
.then(all[a.1].0.cmp(&all[b.1].0))
});
for (i, j, reason) in pairwise {
errors.push(Error::ConflictingRelocation {
source_path: all[i].0.clone(),
target_path: all[i].1.clone(),
layer: all[i].3.clone(),
other_source_path: all[j].0.clone(),
other_target_path: all[j].1.clone(),
other_layer: all[j].3.clone(),
reason,
});
}
}
fn push_pairwise_conflict(
pairwise: &mut Vec<(usize, usize, RelocateConflictReason)>,
i: usize,
j: usize,
reason: RelocateConflictReason,
) {
if !pairwise.iter().any(|&(pi, pj, pr)| pi == i && pj == j && pr == reason) {
pairwise.push((i, j, reason));
}
}
pub(crate) struct BatchRelocate {
pub pair: (Path, Path),
pub fresh: bool,
pub dropped_seed: bool,
}
pub(crate) fn first_unrepresentable_relocate(
relocates: &[BatchRelocate],
status: &[RelocateOccurrence],
) -> Option<Path> {
debug_assert_eq!(relocates.len(), status.len());
relocates
.iter()
.zip(status)
.find_map(|(r, status)| (r.fresh && !r.dropped_seed && !status.is_active()).then(|| r.pair.0.clone()))
}
fn relocate_pair_conflicts(sa: &Path, ta: &Path, sb: &Path, tb: &Path) -> Vec<RelocateConflictReason> {
let mut reasons = Vec::new();
if !ta.is_empty() && ta == sb {
reasons.push(RelocateConflictReason::TargetIsSource);
}
if !tb.is_empty() && sa == tb {
reasons.push(RelocateConflictReason::SourceIsTarget);
}
if !ta.is_empty() && ta != sb && ta.has_prefix(sb) {
reasons.push(RelocateConflictReason::TargetDescendant);
}
if sa != sb && sa.has_prefix(sb) {
reasons.push(RelocateConflictReason::SourceDescendant);
}
reasons
}
fn duplicate_source_mask<'a>(sources: impl Iterator<Item = (bool, &'a Path)>) -> Vec<bool> {
let mut seen: HashSet<&Path> = HashSet::new();
sources
.map(|(eligible, source)| eligible && !seen.insert(source))
.collect()
}
pub(crate) fn analyze_relocate_occurrences(pairs: &[(Path, Path)]) -> Vec<RelocateOccurrence> {
let mut status: Vec<RelocateOccurrence> = pairs
.iter()
.map(|(source, target)| {
if relocate_invalid_reason(source, target).is_some() {
RelocateOccurrence::DroppedStructural
} else {
RelocateOccurrence::Active
}
})
.collect();
let duplicate = duplicate_source_mask(
pairs
.iter()
.enumerate()
.map(|(i, (source, _))| (status[i].is_active(), source)),
);
for (i, is_duplicate) in duplicate.into_iter().enumerate() {
if is_duplicate {
status[i] = RelocateOccurrence::DroppedDuplicateSource;
}
}
let can_conflict: Vec<bool> = status.iter().map(|s| s.is_active()).collect();
let mut by_target: HashMap<&Path, Vec<usize>> = HashMap::new();
for (i, (_, target)) in pairs.iter().enumerate() {
if can_conflict[i] && !target.is_empty() {
by_target.entry(target).or_default().push(i);
}
}
for group in by_target.values() {
if group.len() > 1 {
for &i in group {
status[i] = RelocateOccurrence::DroppedConflict;
}
}
}
for i in 0..pairs.len() {
if !can_conflict[i] {
continue;
}
let (si, ti) = (&pairs[i].0, &pairs[i].1);
for j in 0..pairs.len() {
if i == j || !can_conflict[j] {
continue;
}
let (sj, tj) = (&pairs[j].0, &pairs[j].1);
if !relocate_pair_conflicts(si, ti, sj, tj).is_empty() {
status[i] = RelocateOccurrence::DroppedConflict;
break;
}
}
}
status
}
fn relocate_invalid_reason(source: &Path, target: &Path) -> Option<InvalidRelocateReason> {
if target.is_empty() {
return None;
}
if source.is_root_prim() {
return Some(InvalidRelocateReason::RootPrimSource);
}
if source == target {
return Some(InvalidRelocateReason::SourceEqualsTarget);
}
if source.has_prefix(target) {
return Some(InvalidRelocateReason::TargetIsAncestor);
}
if target.has_prefix(source) {
return Some(InvalidRelocateReason::TargetIsDescendant);
}
None
}
pub(crate) fn effective_relocates(
graph: &LayerGraph,
path: &Path,
indices: &sdf::PathTable<PrimEntry>,
) -> RelocateList {
let stack_maps = collect_stack_maps(graph, path, indices);
let mut result: RelocateList = Vec::new();
for (stack, map) in &stack_maps {
for (src, tgt) in graph.combined_relocates(*stack) {
let Some(composed_src) = map.map_source_to_target(&src) else {
continue;
};
let composed_tgt = if tgt.is_empty() {
tgt.clone()
} else {
match map.map_source_to_target(&tgt) {
Some(t) => t,
None => continue,
}
};
let pair = (composed_src, composed_tgt);
if !result.contains(&pair) {
result.push(pair);
}
}
}
result.sort_by(|a, b| b.1.as_str().len().cmp(&a.1.as_str().len()));
let snapshot = result.clone();
for entry in &mut result {
entry.0 = shift_through_nearest_ancestor(&entry.0, &snapshot);
if !entry.1.is_empty() {
entry.1 = shift_through_nearest_ancestor(&entry.1, &snapshot);
}
}
result
}
fn collect_stack_maps(
graph: &LayerGraph,
path: &Path,
indices: &sdf::PathTable<PrimEntry>,
) -> Vec<(LayerStackId, MapFunction)> {
let mut maps: Vec<(LayerStackId, MapFunction)> = Vec::new();
for (p, cached_index) in indices.ancestors(path) {
if p.is_abs_root() {
continue;
}
for node in cached_index.index.nodes() {
if node.arc == ArcType::Relocate {
continue;
}
let stack = node.layer_stack_id();
if !maps.iter().any(|(s, m)| *s == stack && *m == node.map_to_root) {
maps.push((stack, node.map_to_root.clone()));
}
}
}
let relocate_layers: Vec<LayerId> = graph
.all_ids()
.iter()
.copied()
.filter(|&id| graph.get(id).is_some_and(|node| !node.relocates.is_empty()))
.collect();
let root_name = root_prim_name(path);
let mut entries: Vec<(&Path, &PrimEntry)> = indices.iter().collect();
entries.sort_by(|(a, _), (b, _)| a.cmp(b));
for (cached_path, cached_index) in entries {
if root_prim_name(cached_path) != root_name {
continue;
}
for node in cached_index.index.all_nodes() {
if node.arc == ArcType::Relocate {
continue;
}
let stack = node.layer_stack_id();
let has_relocates = graph
.layer_stack(stack)
.iter()
.any(|&(layer, _)| relocate_layers.contains(&layer));
if has_relocates && !maps.iter().any(|(s, m)| *s == stack && *m == node.map_to_root) {
maps.push((stack, node.map_to_root.clone()));
}
}
}
maps
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fresh_value_conflict() {
let p = Path::from;
let pairs = vec![(p("/W/A"), p("/W/B")), (p("/W/A"), p("/W/B"))];
let status = analyze_relocate_occurrences(&pairs);
let batch = |fresh: [bool; 2]| -> Vec<BatchRelocate> {
pairs
.iter()
.zip(fresh)
.map(|(pair, fresh)| BatchRelocate {
pair: pair.clone(),
fresh,
dropped_seed: false,
})
.collect()
};
assert_eq!(
first_unrepresentable_relocate(&batch([false, true]), &status),
Some(p("/W/A"))
);
assert_eq!(first_unrepresentable_relocate(&batch([false, false]), &status), None);
}
#[test]
fn invalid_seed_inactive() {
let p = Path::from;
let pairs = vec![(p("/A"), p("/B")), (p("/World/A"), p("/World/B"))];
assert_eq!(
analyze_relocate_occurrences(&pairs),
vec![RelocateOccurrence::DroppedStructural, RelocateOccurrence::Active]
);
}
#[test]
fn conflicting_seeds_inactive() {
let p = Path::from;
let pairs = vec![
(p("/World/A"), p("/World/C")),
(p("/World/B"), p("/World/D")),
(p("/World/D"), p("/World/C")),
];
assert_eq!(
analyze_relocate_occurrences(&pairs),
vec![
RelocateOccurrence::DroppedConflict,
RelocateOccurrence::DroppedConflict,
RelocateOccurrence::DroppedConflict
]
);
}
#[test]
fn duplicate_source_strength() {
let p = Path::from;
let pairs = vec![(p("/World/A"), p("/World/C")), (p("/World/A"), p("/World/D"))];
assert_eq!(
analyze_relocate_occurrences(&pairs),
vec![RelocateOccurrence::Active, RelocateOccurrence::DroppedDuplicateSource]
);
}
#[test]
fn duplicate_source_skips_conflict() {
let p = Path::from;
let pairs = vec![
(p("/World/A"), p("/World/C")),
(p("/World/A"), p("/World/D")),
(p("/World/B"), p("/World/D")),
];
assert_eq!(
analyze_relocate_occurrences(&pairs),
vec![
RelocateOccurrence::Active,
RelocateOccurrence::DroppedDuplicateSource,
RelocateOccurrence::Active,
]
);
}
fn conflict_scope(pairs: &[(&str, &str)]) -> (Vec<AuthoredRelocate>, Vec<Vec<usize>>) {
let layer = LayerId::from_raw(0);
let all: Vec<AuthoredRelocate> = pairs
.iter()
.map(|(s, t)| (Path::from(*s), Path::from(*t), layer, "root.usda".to_string()))
.collect();
let scope = (0..all.len()).collect();
(all, vec![scope])
}
#[test]
fn duplicate_source_not_conflict() {
let (all, scopes) = conflict_scope(&[("/W/A", "/W/C"), ("/W/A", "/W/D"), ("/W/D", "/W/E")]);
let mut errors = Vec::new();
detect_relocate_conflicts(&all, &scopes, &mut errors);
assert!(
errors.is_empty(),
"dropped duplicate source must not conflict: {errors:?}"
);
}
#[test]
fn survivor_conflict_reported() {
let (all, scopes) = conflict_scope(&[("/W/A", "/W/D"), ("/W/A", "/W/C"), ("/W/D", "/W/E")]);
let mut errors = Vec::new();
detect_relocate_conflicts(&all, &scopes, &mut errors);
assert_eq!(errors.len(), 2, "{errors:?}");
assert!(errors.iter().all(|e| matches!(e, Error::ConflictingRelocation { .. })));
}
#[test]
fn relocate_validity() {
let p = Path::from;
let reason = |s, t| relocate_invalid_reason(&p(s), &p(t));
assert_eq!(
reason("/Rig/Other/A/Instance/A", "/Rig/Other/A"),
Some(InvalidRelocateReason::TargetIsAncestor)
);
assert_eq!(
reason("/Rig/A", "/Rig/A/B"),
Some(InvalidRelocateReason::TargetIsDescendant)
);
assert_eq!(
reason("/Rig/B", "/Rig/B"),
Some(InvalidRelocateReason::SourceEqualsTarget)
);
assert_eq!(reason("/A", "/B"), Some(InvalidRelocateReason::RootPrimSource));
assert_eq!(reason("/Rig/Model", "/Group/Model"), None);
assert_eq!(reason("/Rig/Model", ""), None);
}
#[test]
fn nearest_ancestor_only() {
let renames = vec![
(Path::from("/Rig"), Path::from("/Rig2")),
(Path::from("/Rig2/Sub"), Path::from("/Rig2/SubX")),
];
let shifted = shift_through_nearest_ancestor(&Path::from("/Rig/Sub/Anim"), &renames);
assert_eq!(shifted, Path::from("/Rig2/Sub/Anim"));
}
#[test]
fn no_ancestor_match_unchanged() {
let renames = vec![(Path::from("/A"), Path::from("/B"))];
assert_eq!(
shift_through_nearest_ancestor(&Path::from("/A"), &renames),
Path::from("/A")
);
assert_eq!(
shift_through_nearest_ancestor(&Path::from("/X/Y"), &renames),
Path::from("/X/Y")
);
}
#[test]
fn relocated_in_children_element_ordered() {
let pairs = vec![
(Path::from("/Src/B10"), Path::from("/Dst/B10")),
(Path::from("/Src/B9"), Path::from("/Dst/B9")),
];
let mut name_order = Vec::new();
let mut name_set = HashSet::new();
let mut prohibited = HashSet::new();
apply_child_relocates(
&Path::from("/Dst"),
&pairs,
&mut name_order,
&mut name_set,
&mut prohibited,
);
assert_eq!(name_order.iter().map(|t| t.as_str()).collect::<Vec<_>>(), ["B9", "B10"]);
}
}