use std::collections::{BTreeMap, HashMap, HashSet};
use crate::sdf::{element_cmp, Path, PathComponent, RelocateList};
use super::layer_graph::LayerGraph;
use super::mapping::MapFunction;
use super::prim_graph::ArcType;
use super::prim_index::PrimIndex;
use super::{Error, InvalidRelocateReason, LayerId, RelocateConflictReason};
pub(crate) type LayerRelocates = HashMap<LayerId, RelocateList>;
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<String>,
name_set: &mut HashSet<String>,
prohibited: &mut HashSet<String>,
) {
let mut relocations: HashMap<String, Option<String>> = HashMap::new();
let mut adds: Vec<String> = 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.to_string());
let rename = tgt_is_child.then(|| tgt.name()).flatten();
relocations.insert(name.to_string(), rename.map(str::to_string));
}
}
if tgt_is_child && !src_is_child {
if let Some(tgt_name) = tgt.name() {
adds.push(tgt_name.to_string());
}
}
}
adds.sort_by(|a, b| element_cmp(a, b));
if !relocations.is_empty() {
let mut retained: Vec<String> = 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<(Path, Path, LayerId, String)> = Vec::new();
for &id in graph.all_ids() {
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,
}),
}
}
}
let scopes = graph.relocate_conflict_scopes();
let same_stack = |i: usize, j: usize| {
let left = all[i].2;
let right = all[j].2;
scopes
.iter()
.any(|scope| scope.contains(&left) && scope.contains(&right))
};
let conflicting = detect_relocate_conflicts(&all, &same_stack, &mut errors);
let mut out: LayerRelocates = HashMap::new();
for (idx, (source, target, layer_id, _)) in all.into_iter().enumerate() {
if !conflicting.contains(&idx) {
out.entry(layer_id).or_default().push((source, target));
}
}
(out, errors)
}
fn detect_relocate_conflicts(
all: &[(Path, Path, LayerId, String)],
same_stack: &dyn Fn(usize, usize) -> bool,
errors: &mut Vec<Error>,
) -> HashSet<usize> {
let mut conflicting = HashSet::new();
let mut by_target: BTreeMap<Path, Vec<usize>> = BTreeMap::new();
for (idx, (_, target, _, _)) in all.iter().enumerate() {
if !target.is_empty() {
by_target.entry(target.clone()).or_default().push(idx);
}
}
for (target, idxs) in &by_target {
let mut remaining = idxs.clone();
while let Some(seed) = remaining.pop() {
let mut group = vec![seed];
let mut changed = true;
while changed {
changed = false;
let mut i = 0;
while i < remaining.len() {
if group.iter().any(|&member| same_stack(member, remaining[i])) {
group.push(remaining.swap_remove(i));
changed = true;
} else {
i += 1;
}
}
}
if group.len() <= 1 {
continue;
}
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: target.clone(),
sources,
});
conflicting.extend(group);
}
}
let mut pairwise: Vec<(usize, usize, RelocateConflictReason)> = Vec::new();
for i in 0..all.len() {
for j in 0..all.len() {
if i == j || !same_stack(i, j) {
continue;
}
let (si, ti) = (&all[i].0, &all[i].1);
let (sj, tj) = (&all[j].0, &all[j].1);
if !ti.is_empty() && ti == sj {
pairwise.push((i, j, RelocateConflictReason::TargetIsSource));
}
if !tj.is_empty() && si == tj {
pairwise.push((i, j, RelocateConflictReason::SourceIsTarget));
}
if !ti.is_empty() && ti != sj && ti.has_prefix(sj) {
pairwise.push((i, j, RelocateConflictReason::TargetDescendant));
}
if si != sj && si.has_prefix(sj) {
pairwise.push((i, j, RelocateConflictReason::SourceDescendant));
}
}
}
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,
});
conflicting.insert(i);
}
conflicting
}
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: &HashMap<Path, PrimIndex>) -> RelocateList {
let layer_maps = collect_layer_maps(graph, path, indices);
let mut result: RelocateList = Vec::new();
for (li, map) in &layer_maps {
let relocates = match graph.get(*li) {
Some(node) if !node.relocates.is_empty() => &node.relocates,
_ => continue,
};
for (src, tgt) in relocates {
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_layer_maps(
graph: &LayerGraph,
path: &Path,
indices: &HashMap<Path, PrimIndex>,
) -> Vec<(LayerId, MapFunction)> {
let mut maps: Vec<(LayerId, MapFunction)> = Vec::new();
let mut current = Some(path.clone());
while let Some(p) = current {
if let Some(cached_index) = indices.get(&p) {
for node in cached_index.nodes() {
if node.arc == ArcType::Relocate {
continue;
}
for (layer, _) in node.layers() {
if !maps.iter().any(|(li, m)| *li == layer && *m == node.map_to_root) {
maps.push((layer, node.map_to_root.clone()));
}
}
}
}
current = p.parent().filter(|pp| *pp != Path::abs_root());
}
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 cached_paths: Vec<&Path> = indices.keys().collect();
cached_paths.sort();
for cached_path in cached_paths {
let cached_index = &indices[cached_path];
if root_prim_name(cached_path) != root_name {
continue;
}
for node in cached_index.all_nodes() {
if node.arc == ArcType::Relocate {
continue;
}
for (layer, _) in node.layers() {
if relocate_layers.contains(&layer)
&& !maps.iter().any(|(li, m)| *li == layer && *m == node.map_to_root)
{
maps.push((layer, node.map_to_root.clone()));
}
}
}
}
maps
}
#[cfg(test)]
mod tests {
use super::*;
#[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, vec!["B9".to_string(), "B10".to_string()]);
}
}