use std::collections::{HashMap, HashSet};
use super::{EditTarget, Prim, Stage, StageAuthoringError};
use crate::{pcp, sdf};
pub struct NamespaceEditor {
stage: Stage,
edits: Vec<NamespaceEdit>,
}
enum NamespaceEdit {
Delete { path: sdf::Path, kind: ObjectKind },
Move {
src: sdf::Path,
dst: sdf::Path,
kind: ObjectKind,
},
}
#[derive(Clone, Copy, PartialEq)]
enum ObjectKind {
Prim,
Property,
}
impl ObjectKind {
fn matches(self, path: &sdf::Path) -> bool {
path.is_property_path() == (self == ObjectKind::Property)
}
}
impl NamespaceEdit {
fn source(&self) -> &sdf::Path {
match self {
NamespaceEdit::Delete { path, .. } => path,
NamespaceEdit::Move { src, .. } => src,
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum NamespaceEditError {
#[error("no namespace edits staged")]
NoEdits,
#[error("source path {0} is not an absolute prim or property path")]
InvalidSource(sdf::Path),
#[error("destination path {0} is not a valid absolute object path")]
InvalidDestination(sdf::Path),
#[error("cannot namespace-edit the pseudo-root")]
PseudoRoot,
#[error("nothing composed at the source path {0}")]
SourceNotFound(sdf::Path),
#[error("an object already exists at the destination {0}")]
DestinationExists(sdf::Path),
#[error("the requested edits cannot be represented as valid relocates (at {0})")]
UnrepresentableRelocateBatch(sdf::Path),
#[error("destination {dst} is the source or a descendant of {src}")]
DestinationUnderSource {
src: sdf::Path,
dst: sdf::Path,
},
#[error("path is the wrong namespace kind for this edit (prim vs property)")]
KindMismatch,
#[error("the edit at {0} would need a relocate the current edit target cannot author")]
RequiresRelocate(sdf::Path),
#[error(transparent)]
Composition(#[from] anyhow::Error),
#[error(transparent)]
Stage(#[from] StageAuthoringError),
}
impl From<sdf::sink::Error> for NamespaceEditError {
fn from(error: sdf::sink::Error) -> Self {
NamespaceEditError::Stage(StageAuthoringError::Rejected(error))
}
}
impl NamespaceEditor {
pub fn new(stage: &Stage) -> Self {
Self {
stage: stage.clone(),
edits: Vec::new(),
}
}
pub fn move_prim(&mut self, old: impl Into<sdf::Path>, new: impl Into<sdf::Path>) -> &mut Self {
self.push_move(old, new, ObjectKind::Prim)
}
pub fn move_property(&mut self, old: impl Into<sdf::Path>, new: impl Into<sdf::Path>) -> &mut Self {
self.push_move(old, new, ObjectKind::Property)
}
pub fn delete_prim(&mut self, path: impl Into<sdf::Path>) -> &mut Self {
self.push_delete(path, ObjectKind::Prim)
}
pub fn delete_property(&mut self, path: impl Into<sdf::Path>) -> &mut Self {
self.push_delete(path, ObjectKind::Property)
}
fn push_move(&mut self, old: impl Into<sdf::Path>, new: impl Into<sdf::Path>, kind: ObjectKind) -> &mut Self {
self.edits.push(NamespaceEdit::Move {
src: old.into(),
dst: new.into(),
kind,
});
self
}
fn push_delete(&mut self, path: impl Into<sdf::Path>, kind: ObjectKind) -> &mut Self {
self.edits.push(NamespaceEdit::Delete {
path: path.into(),
kind,
});
self
}
pub fn rename_prim(&mut self, prim: &Prim, new_name: &str) -> Result<&mut Self, NamespaceEditError> {
let src = prim.path().clone();
let parent = src.parent().ok_or(NamespaceEditError::PseudoRoot)?;
let dst = parent
.append_path(new_name)
.map_err(|_| NamespaceEditError::InvalidDestination(src.clone()))?;
Ok(self.move_prim(src, dst))
}
pub fn reparent_prim(&mut self, prim: &Prim, new_parent: &Prim) -> Result<&mut Self, NamespaceEditError> {
let name = prim.path().name().ok_or(NamespaceEditError::PseudoRoot)?.to_owned();
self.reparent_prim_with_name(prim, new_parent, &name)
}
pub fn reparent_prim_with_name(
&mut self,
prim: &Prim,
new_parent: &Prim,
new_name: &str,
) -> Result<&mut Self, NamespaceEditError> {
let src = prim.path().clone();
let dst = new_parent
.path()
.append_path(new_name)
.map_err(|_| NamespaceEditError::InvalidDestination(src.clone()))?;
Ok(self.move_prim(src, dst))
}
pub fn can_apply(&self) -> Result<(), NamespaceEditError> {
self.execute(false)
}
pub fn apply(&mut self) -> Result<(), NamespaceEditError> {
self.execute(true)?;
self.edits.clear();
Ok(())
}
fn execute(&self, commit: bool) -> Result<(), NamespaceEditError> {
if self.edits.is_empty() {
return Err(NamespaceEditError::NoEdits);
}
match self.plan()? {
BatchPlan::LocalStack {
layer_ids,
seeds,
relocates,
per_edit,
} => self.execute_local_stack(commit, layer_ids, seeds, relocates, per_edit),
BatchPlan::Mapped {
target,
per_edit,
stack_layer_ids,
seeds,
relocates,
} => self.execute_mapped(commit, &target, &per_edit, stack_layer_ids, seeds, relocates),
}
}
fn execute_local_stack(
&self,
commit: bool,
layer_ids: Vec<pcp::LayerId>,
seeds: HashMap<pcp::LayerId, sdf::RelocateList>,
relocate_plan: RelocateStackPlan,
plan: Vec<EditPlan>,
) -> Result<(), NamespaceEditError> {
let resolved = relocate_plan.resolve(seeds)?;
{
let mut graph = self.stage.layers_mut();
let mut layers: Vec<(pcp::LayerId, &mut sdf::Layer)> = graph.layers_mut(&layer_ids).into_iter().collect();
let ids: Vec<pcp::LayerId> = layers.iter().map(|(id, _)| *id).collect();
let mut batch: Vec<&mut sdf::Layer> = layers.iter_mut().map(|(_, layer)| &mut **layer).collect();
let stage_edits = |edits: &mut [sdf::LayerEdit<'_>]| -> Result<(), NamespaceEditError> {
{
let mut refs: Vec<&mut sdf::LayerEdit<'_>> = edits.iter_mut().collect();
for (edit, plan) in self.edits.iter().zip(&plan) {
apply_edit(&mut refs, edit, plan)?;
}
}
for (id, layer) in ids.iter().zip(edits.iter_mut()) {
fixup_embedded_paths(layer, &self.edits)?;
if let Some(next) = resolved.change_for(*id) {
layer.set_relocates(next).map_err(StageAuthoringError::Layer)?;
}
}
Ok(())
};
if commit {
sdf::edit_layers(&mut batch, stage_edits)?;
} else {
return sdf::dry_run_layers(&mut batch, stage_edits);
}
}
self.stage.process_pending();
Ok(())
}
fn execute_mapped(
&self,
commit: bool,
target: &EditTarget,
per_edit: &[MappedEdit],
stack_layer_ids: Vec<pcp::LayerId>,
seeds: HashMap<pcp::LayerId, sdf::RelocateList>,
relocates: RelocateStackPlan,
) -> Result<(), NamespaceEditError> {
let resolved = relocates.resolve(seeds)?;
self.stage
.author_layers_txn(&stack_layer_ids, Some(target.map_function()), commit, |ids, edits| {
for mapped in per_edit {
apply_mapped_edit(edits, mapped)?;
}
for (id, layer) in ids.iter().zip(edits.iter_mut()) {
fixup_mapped_paths(layer, target, &self.edits)?;
if let Some(next) = resolved.change_for(*id) {
layer.set_relocates(next).map_err(StageAuthoringError::Layer)?;
}
}
Ok(())
})
}
fn plan(&self) -> Result<BatchPlan, NamespaceEditError> {
let edit_target = self.stage.edit_target_layer_id()?;
let layer_ids = self.stage.root_stack_layer_ids();
let target = self.stage.edit_target();
if layer_ids.contains(&edit_target) && target.map_function().is_identity() {
self.plan_local_stack(layer_ids, edit_target)
} else {
self.plan_mapped(edit_target, target)
}
}
fn plan_local_stack(
&self,
layer_ids: Vec<pcp::LayerId>,
edit_target: pcp::LayerId,
) -> Result<BatchPlan, NamespaceEditError> {
let seeds = seed_relocates(&self.stage.layers(), &layer_ids);
let projection = NamespaceProjection::new(&self.stage);
let mut relocates = RelocateStackPlan::new(&layer_ids, &seeds, edit_target);
let mut per_edit: Vec<EditPlan> = Vec::with_capacity(self.edits.len());
for (i, edit) in self.edits.iter().enumerate() {
validate_edit_shape(edit)?;
let earlier = &self.edits[..i];
let entry = match edit {
NamespaceEdit::Move { src, dst, .. } => {
let occupied = projection.occupied(dst, earlier)?;
let (present, masks) = projection.cross_arc_facts(src, earlier)?;
if present && masks {
return Err(NamespaceEditError::UnrepresentableRelocateBatch(src.clone()));
}
relocates.record_move(src, dst, present)?;
EditPlan { present, occupied }
}
NamespaceEdit::Delete { path, .. } => {
let (present, masks) = projection.cross_arc_facts(path, earlier)?;
if present && masks {
return Err(NamespaceEditError::UnrepresentableRelocateBatch(path.clone()));
}
relocates.record_delete(path, present)?;
EditPlan {
present,
occupied: false,
}
}
};
per_edit.push(entry);
}
Ok(BatchPlan::LocalStack {
layer_ids,
seeds,
relocates,
per_edit,
})
}
fn plan_mapped(&self, layer_id: pcp::LayerId, target: EditTarget) -> Result<BatchPlan, NamespaceEditError> {
let map = |path: &sdf::Path| {
target
.map_to_spec_path(path)
.ok_or_else(|| NamespaceEditError::Stage(StageAuthoringError::OutsideEditTarget { path: path.clone() }))
};
let stack_id = self
.stage
.mapped_target_stack_id(layer_id)
.map_err(NamespaceEditError::Stage)?;
let (stack_layer_ids, seeds) = {
let layers = self.stage.layers();
let ids: Vec<pcp::LayerId> = layers.layer_stack(stack_id).iter().map(|&(id, _)| id).collect();
let seeds = seed_relocates(&layers, &ids);
(ids, seeds)
};
let projection = NamespaceProjection::new(&self.stage);
let mut relocates = RelocateStackPlan::new(&stack_layer_ids, &seeds, layer_id);
let mut per_edit: Vec<MappedEdit> = Vec::with_capacity(self.edits.len());
for (i, edit) in self.edits.iter().enumerate() {
validate_edit_shape(edit)?;
let earlier = &self.edits[..i];
let stage_src = edit.source().clone();
let src = map(&stage_src)?;
let composed = self.stage.has_spec(&stage_src)?;
let facts = projection.target_facts(&stage_src, earlier, stack_id, &target)?;
if facts.outside {
return Err(NamespaceEditError::RequiresRelocate(stage_src));
}
if facts.masks {
return Err(NamespaceEditError::UnrepresentableRelocateBatch(stage_src));
}
let relocated = facts.below_target && !stage_src.is_property_path();
let (dst, occupied) = match edit {
NamespaceEdit::Move { dst, .. } => {
let mapped_dst = map(dst)?;
let occupied = projection.occupied(dst, earlier)?;
relocates.record_move(&src, &mapped_dst, relocated)?;
(Some(mapped_dst), occupied)
}
NamespaceEdit::Delete { .. } => {
relocates.record_delete(&src, relocated)?;
(None, false)
}
};
per_edit.push(MappedEdit {
stage_src,
src,
dst,
composed,
occupied,
relocated,
});
}
Ok(BatchPlan::Mapped {
target,
per_edit,
stack_layer_ids,
seeds,
relocates,
})
}
pub fn layers_to_edit(&self) -> Result<Vec<String>, NamespaceEditError> {
if self.edits.is_empty() {
return Ok(Vec::new());
}
match self.plan()? {
BatchPlan::LocalStack {
layer_ids,
seeds,
relocates,
..
} => {
let resolved = relocates.resolve(seeds)?;
let sources: Vec<&sdf::Path> = self.edits.iter().map(NamespaceEdit::source).collect();
self.touched_layers(&layer_ids, &resolved, &sources, |p| Ok(project_path(p, &self.edits)))
}
BatchPlan::Mapped {
target,
stack_layer_ids,
seeds,
relocates,
per_edit,
} => {
let resolved = relocates.resolve(seeds)?;
let sources: Vec<&sdf::Path> = per_edit.iter().map(|mapped| &mapped.src).collect();
self.touched_layers(&stack_layer_ids, &resolved, &sources, |p| {
remap_embedded_path(p, &target, &self.edits)
})
}
}
}
fn touched_layers(
&self,
layer_ids: &[pcp::LayerId],
resolved: &ResolvedRelocates,
sources: &[&sdf::Path],
rewrite: impl Fn(&sdf::Path) -> Result<Option<sdf::Path>, NamespaceEditError>,
) -> Result<Vec<String>, NamespaceEditError> {
let layers = self.stage.layers();
let mut result = Vec::new();
for id in layer_ids {
let Some(node) = layers.get(*id) else { continue };
let touches = sources.iter().any(|src| node.layer.data().has_spec(src))
|| resolved.change_for(*id).is_some()
|| layer_fixup_touches(node.layer.data(), &rewrite)?;
if touches {
result.push(layers.identifier(*id).to_string());
}
}
Ok(result)
}
}
enum BatchPlan {
LocalStack {
layer_ids: Vec<pcp::LayerId>,
seeds: HashMap<pcp::LayerId, sdf::RelocateList>,
relocates: RelocateStackPlan,
per_edit: Vec<EditPlan>,
},
Mapped {
target: EditTarget,
per_edit: Vec<MappedEdit>,
stack_layer_ids: Vec<pcp::LayerId>,
seeds: HashMap<pcp::LayerId, sdf::RelocateList>,
relocates: RelocateStackPlan,
},
}
struct MappedEdit {
stage_src: sdf::Path,
src: sdf::Path,
dst: Option<sdf::Path>,
composed: bool,
occupied: bool,
relocated: bool,
}
impl MappedEdit {
fn unreachable_source(&self) -> NamespaceEditError {
if self.composed {
NamespaceEditError::RequiresRelocate(self.stage_src.clone())
} else {
NamespaceEditError::SourceNotFound(self.stage_src.clone())
}
}
}
struct NamespaceProjection<'a> {
stage: &'a Stage,
}
impl<'a> NamespaceProjection<'a> {
fn new(stage: &'a Stage) -> Self {
Self { stage }
}
fn cross_arc_facts(&self, path: &sdf::Path, earlier: &[NamespaceEdit]) -> Result<(bool, bool), NamespaceEditError> {
if path.is_property_path() {
return Ok((false, false));
}
let Some(origin) = projected_origin(path, earlier) else {
return Ok((false, false));
};
let index = self.stage.prim(origin.clone()).prim_index().graph()?;
let facts = classify_source_nodes(&index, &origin, None);
Ok((facts.realized, facts.masks))
}
fn occupied(&self, path: &sdf::Path, earlier: &[NamespaceEdit]) -> Result<bool, NamespaceEditError> {
let Some(origin) = projected_origin(path, earlier) else {
return Ok(false);
};
Ok(self.stage.has_spec(&origin)?)
}
fn target_facts(
&self,
path: &sdf::Path,
earlier: &[NamespaceEdit],
stack: pcp::LayerStackId,
target: &EditTarget,
) -> Result<TargetFacts, NamespaceEditError> {
let Some(origin) = projected_origin(path, earlier) else {
return Ok(TargetFacts::default());
};
let prim = origin.prim_path();
let index = self.stage.prim(prim.clone()).prim_index().graph()?;
let target_spec = target.map_to_spec_path(&prim);
let target_node = target_spec.as_ref().and_then(|spec| {
index
.nodes_with_ids()
.find(|(_, node)| node.layer_stack_id() == stack && node.path() == spec)
.map(|(id, _)| id)
});
let Some(target_node) = target_node else {
return Ok(TargetFacts {
outside: index.nodes_with_ids().any(|(_, node)| node.has_specs()),
below_target: false,
masks: false,
});
};
let facts = classify_source_nodes(&index, &prim, Some(target_node));
Ok(TargetFacts {
below_target: facts.below_target,
outside: facts.outside,
masks: !path.is_property_path() && facts.masks,
})
}
}
#[derive(Default)]
struct TargetFacts {
below_target: bool,
outside: bool,
masks: bool,
}
fn seed_relocates(layers: &pcp::LayerGraph, layer_ids: &[pcp::LayerId]) -> HashMap<pcp::LayerId, sdf::RelocateList> {
let mut seeds: HashMap<pcp::LayerId, sdf::RelocateList> = HashMap::new();
for id in layer_ids {
if let Some(node) = layers.get(*id) {
let pairs = node.layer.relocates();
if !pairs.is_empty() {
seeds.insert(*id, pairs);
}
}
}
seeds
}
struct SourceNodeFacts {
realized: bool,
masks: bool,
below_target: bool,
outside: bool,
}
fn classify_source_nodes(
index: &pcp::PrimIndex,
origin: &sdf::Path,
target_node: Option<pcp::NodeId>,
) -> SourceNodeFacts {
let mut realized = false;
let mut via_relocate = false;
let mut direct_ancestral = false;
let mut below_target = false;
let mut outside = false;
for (id, node) in index.nodes_with_ids() {
if !node.has_specs() {
continue;
}
let mut intro_path = index.graph().path_at_introduction(id);
if intro_path.contains_prim_variant_selection() {
intro_path = intro_path.strip_all_variant_selections();
}
let introduced_away = node
.map_to_root()
.map_source_to_target(&intro_path)
.is_some_and(|intro| &intro != origin);
if node.arc() != pcp::ArcType::Root && introduced_away {
realized = true;
}
if node_under_relocate(index, id) {
via_relocate = true;
} else if introduced_away {
direct_ancestral = true;
}
match target_node {
Some(target) if id != target => {
if node_in_subtree(index, id, target) {
below_target |= index.graph().is_due_to_ancestor(id);
} else {
outside = true;
}
}
_ => {}
}
}
SourceNodeFacts {
realized,
masks: via_relocate && direct_ancestral,
below_target,
outside,
}
}
fn node_in_subtree(index: &pcp::PrimIndex, node: pcp::NodeId, ancestor: pcp::NodeId) -> bool {
let mut current = Some(node);
while let Some(id) = current {
if id == ancestor {
return true;
}
current = index.parent(id);
}
false
}
fn node_under_relocate(index: &pcp::PrimIndex, node: pcp::NodeId) -> bool {
let mut current = Some(node);
while let Some(id) = current {
if index.node(id).arc() == pcp::ArcType::Relocate {
return true;
}
current = index.parent(id);
}
false
}
struct RelocatedEntry {
source: sdf::Path,
target: sdf::Path,
layer: pcp::LayerId,
original: Option<sdf::Relocate>,
dropped_at_seed: bool,
}
impl RelocatedEntry {
fn is_fresh(&self) -> bool {
self.original
.as_ref()
.is_none_or(|(s, t)| s != &self.source || t != &self.target)
}
fn drops_as_identity(&self) -> bool {
self.source == self.target
&& match &self.original {
None => true,
Some((source, target)) => source != target,
}
}
}
struct RelocateStackPlan {
entries: Vec<RelocatedEntry>,
edit_target: pcp::LayerId,
layer_rank: HashMap<pcp::LayerId, usize>,
}
impl RelocateStackPlan {
fn new(
layer_ids: &[pcp::LayerId],
seeds: &HashMap<pcp::LayerId, sdf::RelocateList>,
edit_target: pcp::LayerId,
) -> Self {
let mut seen_layers: HashSet<pcp::LayerId> = HashSet::new();
let ordered_layers: Vec<pcp::LayerId> = layer_ids
.iter()
.copied()
.filter(|layer| seen_layers.insert(*layer))
.collect();
let layer_rank: HashMap<pcp::LayerId, usize> = ordered_layers
.iter()
.enumerate()
.map(|(rank, layer)| (*layer, rank))
.collect();
let seeded: Vec<(pcp::LayerId, sdf::Relocate)> = ordered_layers
.iter()
.filter_map(|layer| seeds.get(layer).map(|pairs| (*layer, pairs)))
.flat_map(|(layer, pairs)| pairs.iter().map(move |pair| (layer, (pair.0.clone(), pair.1.clone()))))
.collect();
let pairs: sdf::RelocateList = seeded.iter().map(|(_, pair)| pair.clone()).collect();
let status = pcp::analyze_relocate_occurrences(&pairs);
let entries = seeded
.into_iter()
.zip(status)
.map(|((layer, pair), status)| RelocatedEntry {
source: pair.0.clone(),
target: pair.1.clone(),
layer,
dropped_at_seed: !status.is_active(),
original: Some(pair),
})
.collect();
Self {
entries,
edit_target,
layer_rank,
}
}
fn record_move(&mut self, src: &sdf::Path, dst: &sdf::Path, cross_arc: bool) -> Result<(), NamespaceEditError> {
let continues_source = self.continuation_source(src);
if let Some(source) = self.prohibiting_source(dst) {
if continues_source.as_ref() != Some(&source) || dst != &source {
return Err(NamespaceEditError::UnrepresentableRelocateBatch(source));
}
}
let continues = continues_source.is_some();
self.reproject(src, dst);
if cross_arc && !continues {
self.insert_edit_target_entry(RelocatedEntry {
source: src.clone(),
target: dst.clone(),
layer: self.edit_target,
original: None,
dropped_at_seed: false,
});
}
Ok(())
}
fn record_delete(&mut self, path: &sdf::Path, cross_arc: bool) -> Result<(), NamespaceEditError> {
let active = self.active_flags();
let orphans_child = self.entries.iter().zip(&active).any(|(e, &a)| {
a && &e.source != path && e.source.has_prefix(path) && !e.target.is_empty() && !e.target.has_prefix(path)
});
if orphans_child {
return Err(NamespaceEditError::UnrepresentableRelocateBatch(path.clone()));
}
let mut continues = false;
let kept: Vec<RelocatedEntry> = std::mem::take(&mut self.entries)
.into_iter()
.zip(active)
.filter_map(|(mut e, a)| {
if a && &e.source != path && e.source.has_prefix(path) {
return None;
}
if a && &e.target == path {
continues = true;
}
if a && !e.target.is_empty() && e.target.has_prefix(path) {
e.target = sdf::Path::default();
}
Some(e)
})
.collect();
self.entries = kept;
if cross_arc && !continues {
self.insert_edit_target_entry(RelocatedEntry {
source: path.clone(),
target: sdf::Path::default(),
layer: self.edit_target,
original: None,
dropped_at_seed: false,
});
}
Ok(())
}
fn insert_edit_target_entry(&mut self, entry: RelocatedEntry) {
debug_assert_eq!(entry.layer, self.edit_target);
let edit_rank = self.layer_rank[&self.edit_target];
let insert_at = self
.entries
.iter()
.position(|e| self.layer_rank[&e.layer] > edit_rank)
.unwrap_or(self.entries.len());
self.entries.insert(insert_at, entry);
}
fn active_flags(&self) -> Vec<bool> {
let pairs: sdf::RelocateList = self
.entries
.iter()
.map(|e| (e.source.clone(), e.target.clone()))
.collect();
pcp::analyze_relocate_occurrences(&pairs)
.into_iter()
.map(|status| status.is_active())
.collect()
}
fn active_relocates(&self) -> sdf::RelocateList {
self.entries
.iter()
.zip(self.active_flags())
.filter(|(_, active)| *active)
.map(|(e, _)| (e.source.clone(), e.target.clone()))
.collect()
}
fn continuation_source(&self, path: &sdf::Path) -> Option<sdf::Path> {
self.active_relocates()
.into_iter()
.find_map(|(source, target)| (!target.is_empty() && target == *path).then_some(source))
}
fn prohibiting_source(&self, path: &sdf::Path) -> Option<sdf::Path> {
self.active_relocates()
.into_iter()
.filter_map(|(source, _)| path.has_prefix(&source).then_some(source))
.max_by_key(|source| source.element_count())
}
fn reproject(&mut self, old: &sdf::Path, new: &sdf::Path) {
let active = self.active_flags();
for (e, is_active) in self.entries.iter_mut().zip(active) {
if !e.target.is_empty() {
if e.target == *old {
if is_active {
e.target = new.clone();
}
} else {
e.target = rebased(&e.target, old, new);
}
}
if &e.source != old {
e.source = rebased(&e.source, old, new);
}
}
}
fn combined(&self) -> Vec<pcp::BatchRelocate> {
self.entries
.iter()
.filter(|e| e.source != e.target)
.map(|e| pcp::BatchRelocate {
pair: (e.source.clone(), e.target.clone()),
fresh: e.is_fresh(),
dropped_seed: e.dropped_at_seed,
})
.collect()
}
fn validate(&self) -> Result<(), NamespaceEditError> {
let combined = self.combined();
let pairs: sdf::RelocateList = combined.iter().map(|r| r.pair.clone()).collect();
let status = pcp::analyze_relocate_occurrences(&pairs);
if let Some(path) = pcp::first_unrepresentable_relocate(&combined, &status) {
return Err(NamespaceEditError::UnrepresentableRelocateBatch(path));
}
if let Some((r, _)) = combined
.iter()
.zip(&status)
.find(|(r, s)| r.dropped_seed && s.is_active())
{
return Err(NamespaceEditError::UnrepresentableRelocateBatch(r.pair.0.clone()));
}
Ok(())
}
fn into_by_layer(self) -> HashMap<pcp::LayerId, sdf::RelocateList> {
let mut by_layer: HashMap<pcp::LayerId, sdf::RelocateList> = HashMap::new();
for e in self.entries {
if e.drops_as_identity() {
continue;
}
by_layer.entry(e.layer).or_default().push((e.source, e.target));
}
by_layer
}
fn resolve(self, seeds: HashMap<pcp::LayerId, sdf::RelocateList>) -> Result<ResolvedRelocates, NamespaceEditError> {
self.validate()?;
Ok(ResolvedRelocates {
seeds,
final_by_layer: self.into_by_layer(),
})
}
}
struct ResolvedRelocates {
seeds: HashMap<pcp::LayerId, sdf::RelocateList>,
final_by_layer: HashMap<pcp::LayerId, sdf::RelocateList>,
}
impl ResolvedRelocates {
fn change_for(&self, id: pcp::LayerId) -> Option<sdf::RelocateList> {
let next = self.final_by_layer.get(&id).cloned().unwrap_or_default();
let prev = self.seeds.get(&id).cloned().unwrap_or_default();
(next != prev).then_some(next)
}
}
struct EditPlan {
present: bool,
occupied: bool,
}
fn apply_edit(
layers: &mut [&mut sdf::LayerEdit<'_>],
edit: &NamespaceEdit,
plan: &EditPlan,
) -> Result<(), NamespaceEditError> {
validate_edit_shape(edit)?;
match edit {
NamespaceEdit::Move { src, dst, .. } => {
if plan.occupied || layers.iter().any(|layer| layer.data().has_spec(dst)) {
return Err(NamespaceEditError::DestinationExists(dst.clone()));
}
stage_across_layers(layers, src, plan.present, |layer| move_spec(layer, src, dst))?;
}
NamespaceEdit::Delete { path, .. } => {
stage_across_layers(layers, path, plan.present, |layer| layer.remove_spec(path))?;
}
}
Ok(())
}
fn validate_edit_shape(edit: &NamespaceEdit) -> Result<(), NamespaceEditError> {
match edit {
NamespaceEdit::Move { src, dst, kind } => {
check_editable(src, *kind, NamespaceEditError::InvalidSource)?;
check_editable(dst, *kind, NamespaceEditError::InvalidDestination)?;
if dst.has_prefix(src) {
return Err(NamespaceEditError::DestinationUnderSource {
src: src.clone(),
dst: dst.clone(),
});
}
}
NamespaceEdit::Delete { path, kind } => {
check_editable(path, *kind, NamespaceEditError::InvalidSource)?;
}
}
Ok(())
}
fn stage_across_layers(
layers: &mut [&mut sdf::LayerEdit<'_>],
path: &sdf::Path,
present: bool,
mut op: impl FnMut(&mut sdf::LayerEdit<'_>) -> Result<bool, sdf::AuthoringError>,
) -> Result<(), NamespaceEditError> {
let mut authored = false;
for layer in layers.iter_mut() {
if op(layer).map_err(StageAuthoringError::Layer)? {
authored = true;
}
}
if !authored && !present {
return Err(NamespaceEditError::SourceNotFound(path.clone()));
}
Ok(())
}
fn apply_mapped_edit(layers: &mut [sdf::LayerEdit<'_>], edit: &MappedEdit) -> Result<(), NamespaceEditError> {
let mut authored = false;
match &edit.dst {
Some(dst) => {
if edit.occupied || layers.iter().any(|layer| layer.data().has_spec(dst)) {
return Err(NamespaceEditError::DestinationExists(dst.clone()));
}
for layer in layers.iter_mut() {
authored |= move_spec(layer, &edit.src, dst).map_err(StageAuthoringError::Layer)?;
}
}
None => {
for layer in layers.iter_mut() {
authored |= layer.remove_spec(&edit.src).map_err(StageAuthoringError::Layer)?;
}
}
}
if !authored && !edit.relocated {
return Err(edit.unreachable_source());
}
Ok(())
}
fn move_spec(layer: &mut sdf::LayerEdit<'_>, src: &sdf::Path, dst: &sdf::Path) -> Result<bool, sdf::AuthoringError> {
let moved = sdf::copy_spec_within(layer.data_mut(), src, dst)?;
if moved {
layer.remove_spec(src)?;
}
Ok(moved)
}
fn fixup_embedded_paths(layer: &mut sdf::LayerEdit<'_>, edits: &[NamespaceEdit]) -> Result<(), NamespaceEditError> {
rewrite_embedded_paths(layer, |p| Ok(project_path(p, edits)))
}
fn fixup_mapped_paths(
layer: &mut sdf::LayerEdit<'_>,
target: &EditTarget,
edits: &[NamespaceEdit],
) -> Result<(), NamespaceEditError> {
rewrite_embedded_paths(layer, |p| remap_embedded_path(p, target, edits))
}
fn embedded_path_rewrites(
data: &dyn sdf::AbstractData,
rewrite: impl Fn(&sdf::Path) -> Result<Option<sdf::Path>, NamespaceEditError>,
) -> Result<Vec<(sdf::Path, String, sdf::Value)>, NamespaceEditError> {
let failed: std::cell::Cell<Option<NamespaceEditError>> = std::cell::Cell::new(None);
let mut changes = Vec::new();
for path in data.spec_paths() {
let fields = data.list_fields(&path).unwrap_or_default();
for field in &fields {
if field == sdf::FieldKey::LayerRelocates.as_str() {
continue;
}
let Some(value) = data
.try_field(&path, field)
.map_err(|e| StageAuthoringError::Layer(e.into()))?
else {
continue;
};
if !value.has_embedded_paths() {
continue;
}
let value = value.into_owned();
let rewritten = value.filter_map_paths(|p| match rewrite(p) {
Ok(mapped) => mapped,
Err(error) => {
failed.set(Some(error));
Some(p.clone())
}
});
if let Some(error) = failed.take() {
return Err(error);
}
if rewritten != value {
changes.push((path.clone(), field.clone(), rewritten));
}
}
}
Ok(changes)
}
fn rewrite_embedded_paths(
layer: &mut sdf::LayerEdit<'_>,
rewrite: impl Fn(&sdf::Path) -> Result<Option<sdf::Path>, NamespaceEditError>,
) -> Result<(), NamespaceEditError> {
for (path, field, value) in embedded_path_rewrites(layer.data(), rewrite)? {
layer.data_mut().set_field(&path, &field, value);
}
Ok(())
}
fn layer_fixup_touches(
data: &dyn sdf::AbstractData,
rewrite: impl Fn(&sdf::Path) -> Result<Option<sdf::Path>, NamespaceEditError>,
) -> Result<bool, NamespaceEditError> {
Ok(!embedded_path_rewrites(data, rewrite)?.is_empty())
}
fn remap_embedded_path(
path: &sdf::Path,
target: &EditTarget,
edits: &[NamespaceEdit],
) -> Result<Option<sdf::Path>, NamespaceEditError> {
let Some(scene) = target.map_function().map_source_to_target(path) else {
return Ok(Some(path.clone()));
};
match project_path(&scene, edits) {
None => Ok(None),
Some(projected) => target
.map_to_spec_target_path(&projected)
.map(Some)
.ok_or_else(|| NamespaceEditError::Stage(StageAuthoringError::OutsideEditTarget { path: projected })),
}
}
fn rebased(path: &sdf::Path, from: &sdf::Path, to: &sdf::Path) -> sdf::Path {
path.replace_prefix(from, to).unwrap_or_else(|| path.clone())
}
fn project_path(path: &sdf::Path, edits: &[NamespaceEdit]) -> Option<sdf::Path> {
let mut current = path.clone();
for edit in edits {
match edit {
NamespaceEdit::Delete { path: removed, .. } => {
if current.has_prefix(removed) {
return None;
}
}
NamespaceEdit::Move { src, dst, .. } => {
current = rebased(¤t, src, dst);
}
}
}
Some(current)
}
fn premove_path(path: &sdf::Path, earlier: &[NamespaceEdit]) -> sdf::Path {
let mut original = path.clone();
for edit in earlier.iter().rev() {
if let NamespaceEdit::Move { src, dst, .. } = edit {
original = rebased(&original, dst, src);
}
}
original
}
fn projected_origin(path: &sdf::Path, earlier: &[NamespaceEdit]) -> Option<sdf::Path> {
let origin = premove_path(path, earlier);
(project_path(&origin, earlier).as_ref() == Some(path)).then_some(origin)
}
fn check_editable(
path: &sdf::Path,
kind: ObjectKind,
invalid: fn(sdf::Path) -> NamespaceEditError,
) -> Result<(), NamespaceEditError> {
if path.is_abs_root() {
return Err(NamespaceEditError::PseudoRoot);
}
if !path.is_abs() {
return Err(invalid(path.clone()));
}
if !kind.matches(path) {
return Err(NamespaceEditError::KindMismatch);
}
Ok(())
}
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use super::*;
use crate::sdf::{self, path, FieldKey, LayerOffset, Specifier, Variability};
use crate::usd::{EditTarget, EditTargetArc, Stage};
fn edit_layer(layer: &mut sdf::Layer, f: impl FnOnce(&mut sdf::LayerEdit<'_>)) {
layer
.edit(|e| {
f(e);
Ok(())
})
.expect("authored");
}
fn sample() -> Stage {
let stage = Stage::builder().in_memory("root.usda").unwrap();
stage.define_prim("/A").unwrap().set_type_name("Xform").unwrap();
stage.define_prim("/A/Child").unwrap();
stage.create_attribute("/A.out", "double").unwrap();
stage.define_prim("/Keep").unwrap();
stage.define_prim("/Other").unwrap();
stage
.create_relationship("/Other.rel")
.unwrap()
.set_targets([path("/A").unwrap(), path("/Keep").unwrap()])
.unwrap();
stage
.create_attribute("/Other.con", "double")
.unwrap()
.set_connections([path("/A.out").unwrap()])
.unwrap();
stage
}
fn valid(stage: &Stage, p: &str) -> bool {
stage.prim(path(p).unwrap()).is_valid().unwrap()
}
fn rel_targets(stage: &Stage, p: &str) -> Vec<String> {
stage
.relationship(path(p).unwrap())
.targets()
.unwrap()
.iter()
.map(|t| t.as_str().to_owned())
.collect()
}
fn connections(stage: &Stage, p: &str) -> Vec<String> {
stage
.attribute(path(p).unwrap())
.connections()
.unwrap()
.iter()
.map(|t| t.as_str().to_owned())
.collect()
}
#[test]
fn rename_subtree_targets() {
let stage = sample();
NamespaceEditor::new(&stage)
.rename_prim(&stage.prim(path("/A").unwrap()), "B")
.unwrap()
.apply()
.unwrap();
assert!(valid(&stage, "/B"));
assert!(valid(&stage, "/B/Child"));
assert!(!valid(&stage, "/A"));
assert_eq!(rel_targets(&stage, "/Other.rel"), vec!["/B", "/Keep"]);
assert_eq!(connections(&stage, "/Other.con"), vec!["/B.out"]);
}
#[test]
fn reparent_under_parent() {
let stage = sample();
let mut editor = NamespaceEditor::new(&stage);
editor
.reparent_prim(&stage.prim(path("/A").unwrap()), &stage.prim(path("/Keep").unwrap()))
.unwrap();
editor.apply().unwrap();
assert!(valid(&stage, "/Keep/A"));
assert!(valid(&stage, "/Keep/A/Child"));
assert!(!valid(&stage, "/A"));
}
#[test]
fn delete_subtree_targets() {
let stage = sample();
let mut editor = NamespaceEditor::new(&stage);
editor.delete_prim(path("/A").unwrap());
editor.apply().unwrap();
assert!(!valid(&stage, "/A"));
assert!(!valid(&stage, "/A/Child"));
assert_eq!(rel_targets(&stage, "/Other.rel"), vec!["/Keep"]);
assert!(connections(&stage, "/Other.con").is_empty());
}
#[test]
fn rename_fixes_internal_ref() {
let mut root = sdf::Layer::new_in_memory("root.usda");
edit_layer(&mut root, |e| {
sdf::PrimSpec::new(e.data_mut(), "/A", Specifier::Def, "Xform").unwrap();
sdf::PrimSpec::new(e.data_mut(), "/Other", Specifier::Def, "").unwrap();
e.data_mut().set_field(
&path("/Other").unwrap(),
FieldKey::References.as_str(),
sdf::Value::ReferenceListOp(sdf::ReferenceListOp::prepended([sdf::Reference {
prim_path: path("/A").unwrap(),
..Default::default()
}])),
);
});
let stage = Stage::builder().make_stage(vec![root], 0, Vec::new());
NamespaceEditor::new(&stage)
.rename_prim(&stage.prim(path("/A").unwrap()), "B")
.unwrap()
.apply()
.unwrap();
let references = stage
.root_layer()
.data()
.try_field(&path("/Other").unwrap(), FieldKey::References.as_str())
.unwrap()
.unwrap()
.into_owned()
.try_as_reference_list_op()
.unwrap();
assert_eq!(references.prepended_items[0].prim_path.as_str(), "/B");
}
#[test]
fn rename_preserves_listop() {
let mut root = sdf::Layer::new_in_memory("root.usda");
edit_layer(&mut root, |e| {
sdf::PrimSpec::new(e.data_mut(), "/A", Specifier::Def, "").unwrap();
sdf::PrimSpec::new(e.data_mut(), "/Other", Specifier::Def, "").unwrap();
sdf::RelationshipSpec::new(e.data_mut(), "/Other.rel", Variability::Varying, false).unwrap();
e.data_mut().set_field(
&path("/Other.rel").unwrap(),
FieldKey::TargetPaths.as_str(),
sdf::Value::PathListOp(sdf::PathListOp::prepended([path("/A").unwrap()])),
);
});
let stage = Stage::builder().make_stage(vec![root], 0, Vec::new());
NamespaceEditor::new(&stage)
.rename_prim(&stage.prim(path("/A").unwrap()), "B")
.unwrap()
.apply()
.unwrap();
let op = stage
.root_layer()
.data()
.try_field(&path("/Other.rel").unwrap(), FieldKey::TargetPaths.as_str())
.unwrap()
.unwrap()
.into_owned()
.try_as_path_list_op()
.unwrap();
assert!(op.explicit_items.is_empty());
assert_eq!(op.prepended_items, vec![path("/B").unwrap()]);
}
#[test]
fn move_property_renames() {
let stage = sample();
NamespaceEditor::new(&stage)
.move_property(path("/A.out").unwrap(), path("/A.renamed").unwrap())
.apply()
.unwrap();
assert!(stage.has_spec(&path("/A.renamed").unwrap()).unwrap());
assert!(!stage.has_spec(&path("/A.out").unwrap()).unwrap());
}
#[test]
fn delete_property_works() {
let stage = sample();
NamespaceEditor::new(&stage)
.delete_property(path("/A.out").unwrap())
.apply()
.unwrap();
assert!(!stage.has_spec(&path("/A.out").unwrap()).unwrap());
}
#[test]
fn batched_two_moves() {
let stage = sample();
let mut editor = NamespaceEditor::new(&stage);
editor
.move_prim(path("/A").unwrap(), path("/B").unwrap())
.move_prim(path("/Keep").unwrap(), path("/Kept").unwrap());
editor.apply().unwrap();
assert!(valid(&stage, "/B") && valid(&stage, "/Kept"));
assert!(!valid(&stage, "/A") && !valid(&stage, "/Keep"));
}
#[test]
fn batched_delete_then_move_onto() {
let stage = sample();
let mut editor = NamespaceEditor::new(&stage);
editor
.delete_prim(path("/Keep").unwrap())
.move_prim(path("/A").unwrap(), path("/Keep").unwrap());
editor.apply().unwrap();
assert!(valid(&stage, "/Keep") && valid(&stage, "/Keep/Child"));
assert!(!valid(&stage, "/A"));
}
#[test]
fn batched_move_then_delete() {
let stage = sample();
let mut editor = NamespaceEditor::new(&stage);
editor
.move_prim(path("/A").unwrap(), path("/B").unwrap())
.delete_prim(path("/B/Child").unwrap());
editor.apply().unwrap();
assert!(valid(&stage, "/B"));
assert!(!valid(&stage, "/B/Child"));
}
#[test]
fn local_child_no_relocate() {
let stage = sample();
NamespaceEditor::new(&stage)
.move_prim(path("/A/Child").unwrap(), path("/B").unwrap())
.apply()
.unwrap();
assert!(valid(&stage, "/B"));
assert!(!valid(&stage, "/A/Child"));
assert!(
stage.root_layer().relocates().is_empty(),
"local spec move should not author relocates: {:?}",
stage.root_layer().relocates()
);
}
#[test]
fn chained_move_then_move() {
let stage = sample();
let mut editor = NamespaceEditor::new(&stage);
editor
.move_prim(path("/A").unwrap(), path("/B").unwrap())
.move_prim(path("/B").unwrap(), path("/C").unwrap());
editor.apply().unwrap();
assert!(valid(&stage, "/C") && valid(&stage, "/C/Child"));
assert!(!valid(&stage, "/A") && !valid(&stage, "/B"));
assert_eq!(rel_targets(&stage, "/Other.rel"), vec!["/C", "/Keep"]);
}
#[test]
fn fixup_across_sublayers() {
let stage = Stage::builder().in_memory("root.usda").unwrap();
let mut sub = sdf::Layer::new_in_memory("sub.usda");
edit_layer(&mut sub, |e| {
sdf::PrimSpec::new(e.data_mut(), "/A", Specifier::Def, "Xform").unwrap();
});
let root_id = stage.root_layer().identifier().to_string();
stage.insert_layer(&root_id, 0, sub, LayerOffset::IDENTITY).unwrap();
stage.define_prim("/Other").unwrap();
stage
.create_relationship("/Other.rel")
.unwrap()
.set_targets([path("/A").unwrap()])
.unwrap();
NamespaceEditor::new(&stage)
.rename_prim(&stage.prim(path("/A").unwrap()), "B")
.unwrap()
.apply()
.unwrap();
assert!(valid(&stage, "/B"));
assert!(!valid(&stage, "/A"));
assert_eq!(rel_targets(&stage, "/Other.rel"), vec!["/B"]);
}
fn referenced_stage() -> Stage {
let mut root = sdf::Layer::new_in_memory("root.usda");
edit_layer(&mut root, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Ref", Specifier::Def, "").unwrap();
e.data_mut().set_field(
&path("/Ref").unwrap(),
FieldKey::References.as_str(),
sdf::Value::ReferenceListOp(sdf::ReferenceListOp::prepended([sdf::Reference {
asset_path: "model.usda".into(),
prim_path: path("/Model").unwrap(),
..Default::default()
}])),
);
});
let mut model = sdf::Layer::new_in_memory("model.usda");
edit_layer(&mut model, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Model", Specifier::Def, "Xform").unwrap();
sdf::PrimSpec::new(e.data_mut(), "/Model/Geom", Specifier::Def, "").unwrap();
});
Stage::builder().make_stage(vec![root, model], 0, Vec::new())
}
#[test]
fn relocate_moves_cross_arc() {
let stage = referenced_stage();
assert!(valid(&stage, "/Ref/Geom"));
NamespaceEditor::new(&stage)
.move_prim(path("/Ref/Geom").unwrap(), path("/Ref/Renamed").unwrap())
.apply()
.unwrap();
let relocates = stage.root_layer().relocates();
assert!(relocates
.iter()
.any(|(s, t)| s == &path("/Ref/Geom").unwrap() && t == &path("/Ref/Renamed").unwrap()));
assert!(valid(&stage, "/Ref/Renamed"));
assert!(!valid(&stage, "/Ref/Geom"));
}
#[test]
fn relocate_deletes_cross_arc() {
let stage = referenced_stage();
NamespaceEditor::new(&stage)
.delete_prim(path("/Ref/Geom").unwrap())
.apply()
.unwrap();
let relocates = stage.root_layer().relocates();
assert!(relocates
.iter()
.any(|(s, t)| s == &path("/Ref/Geom").unwrap() && t.is_empty()));
assert!(!valid(&stage, "/Ref/Geom"));
}
#[test]
fn rejects_composed_only_dst() {
let stage = referenced_stage();
stage.define_prim("/Src").unwrap();
let mut editor = NamespaceEditor::new(&stage);
editor.move_prim(path("/Src").unwrap(), path("/Ref/Geom").unwrap());
assert!(matches!(
editor.can_apply(),
Err(NamespaceEditError::DestinationExists(_))
));
}
#[test]
fn can_apply_dry_run() {
let stage = sample();
let mut editor = NamespaceEditor::new(&stage);
editor.move_prim(path("/A").unwrap(), path("/B").unwrap());
editor.can_apply().unwrap();
assert!(valid(&stage, "/A"));
assert!(!valid(&stage, "/B"));
editor.apply().unwrap();
assert!(valid(&stage, "/B"));
assert!(!valid(&stage, "/A"));
}
#[test]
fn rejects_no_edits() {
let stage = sample();
assert!(matches!(
NamespaceEditor::new(&stage).can_apply(),
Err(NamespaceEditError::NoEdits)
));
}
#[test]
fn rejects_missing_source() {
let stage = sample();
let mut editor = NamespaceEditor::new(&stage);
editor.move_prim(path("/Nope").unwrap(), path("/B").unwrap());
assert!(matches!(editor.can_apply(), Err(NamespaceEditError::SourceNotFound(_))));
}
#[test]
fn rejects_collision() {
let stage = sample();
let mut editor = NamespaceEditor::new(&stage);
editor.move_prim(path("/A").unwrap(), path("/Keep").unwrap());
assert!(matches!(
editor.can_apply(),
Err(NamespaceEditError::DestinationExists(_))
));
}
#[test]
fn rejects_self_descendant() {
let stage = sample();
let mut editor = NamespaceEditor::new(&stage);
editor.move_prim(path("/A").unwrap(), path("/A/Inside").unwrap());
assert!(matches!(
editor.can_apply(),
Err(NamespaceEditError::DestinationUnderSource { .. })
));
}
#[test]
fn rejects_cross_arc_descendant() {
let stage = referenced_stage();
let mut editor = NamespaceEditor::new(&stage);
editor.move_prim(path("/Ref/Geom").unwrap(), path("/Ref/Geom/Sub").unwrap());
assert!(matches!(
editor.can_apply(),
Err(NamespaceEditError::DestinationUnderSource { .. })
));
}
#[test]
fn rejects_kind_mismatch() {
let stage = sample();
let mut editor = NamespaceEditor::new(&stage);
editor.move_prim(path("/A").unwrap(), path("/Other.con").unwrap());
assert!(matches!(editor.can_apply(), Err(NamespaceEditError::KindMismatch)));
}
#[test]
fn rejects_prim_path_in_property_edit() {
let stage = sample();
let mut editor = NamespaceEditor::new(&stage);
editor.move_property(path("/A").unwrap(), path("/B").unwrap());
assert!(matches!(editor.can_apply(), Err(NamespaceEditError::KindMismatch)));
}
#[test]
fn rejects_property_path_in_prim_edit() {
let stage = sample();
let mut editor = NamespaceEditor::new(&stage);
editor.delete_prim(path("/A.out").unwrap());
assert!(matches!(editor.can_apply(), Err(NamespaceEditError::KindMismatch)));
}
fn variant_stage() -> Stage {
let stage = Stage::builder().in_memory("root.usda").unwrap();
stage.define_prim("/Prim").unwrap();
let root = stage.root_layer().identifier().to_string();
stage
.set_edit_target(EditTarget::for_local_direct_variant(
root,
path("/Prim{set=sel}").unwrap(),
))
.unwrap();
stage.define_prim("/Prim/child").unwrap();
stage
.create_attribute("/Prim/child.out", "double")
.unwrap()
.set_connections([path("/Prim/other.in").unwrap()])
.unwrap();
stage.define_prim("/Prim/sibling").unwrap();
stage
}
#[test]
fn variant_rename() {
let stage = variant_stage();
NamespaceEditor::new(&stage)
.move_prim(path("/Prim/child").unwrap(), path("/Prim/renamed").unwrap())
.apply()
.unwrap();
let layer = stage.root_layer();
let data = layer.data();
assert_eq!(
data.spec_type(&path("/Prim{set=sel}renamed").unwrap()),
Some(sdf::SpecType::Prim)
);
assert_eq!(
data.spec_type(&path("/Prim{set=sel}renamed.out").unwrap()),
Some(sdf::SpecType::Attribute)
);
assert!(!data.has_spec(&path("/Prim{set=sel}child").unwrap()));
assert_eq!(
data.spec_type(&path("/Prim{set=sel}sibling").unwrap()),
Some(sdf::SpecType::Prim)
);
let connections = data
.try_field(
&path("/Prim{set=sel}renamed.out").unwrap(),
FieldKey::ConnectionPaths.as_str(),
)
.unwrap()
.expect("connections authored")
.into_owned()
.try_as_path_list_op()
.expect("connections are a path list op");
assert_eq!(connections.explicit_items, vec![path("/Prim/other.in").unwrap()]);
}
#[test]
fn variant_reparent() {
let stage = variant_stage();
NamespaceEditor::new(&stage)
.move_prim(path("/Prim/child").unwrap(), path("/Prim/sibling/child").unwrap())
.apply()
.unwrap();
let layer = stage.root_layer();
let data = layer.data();
assert_eq!(
data.spec_type(&path("/Prim{set=sel}sibling/child").unwrap()),
Some(sdf::SpecType::Prim)
);
assert!(!data.has_spec(&path("/Prim{set=sel}child").unwrap()));
}
#[test]
fn variant_delete() {
let stage = variant_stage();
NamespaceEditor::new(&stage)
.delete_prim(path("/Prim/child").unwrap())
.apply()
.unwrap();
let layer = stage.root_layer();
let data = layer.data();
assert!(!data.has_spec(&path("/Prim{set=sel}child").unwrap()));
assert!(!data.has_spec(&path("/Prim{set=sel}child.out").unwrap()));
assert_eq!(
data.spec_type(&path("/Prim{set=sel}sibling").unwrap()),
Some(sdf::SpecType::Prim)
);
}
#[test]
fn variant_rejects_direct_opinion() {
let text = r#"#usda 1.0
def "Prim" (
variants = { string set = "sel" }
variantSets = "set"
) {
def "child" {}
variantSet "set" = {
"sel" {
def "child" {}
}
}
}
"#;
let data = crate::usda::parser::Parser::new(text).parse().expect("parse usda");
let layer = sdf::Layer::new("root.usda", Box::new(sdf::Data::from_specs(data)));
let stage = Stage::builder().make_stage(vec![layer], 0, Vec::new());
assert!(valid(&stage, "/Prim/child"));
let root = stage.root_layer().identifier().to_string();
stage
.set_edit_target(EditTarget::for_local_direct_variant(
root,
path("/Prim{set=sel}").unwrap(),
))
.unwrap();
let mut editor = NamespaceEditor::new(&stage);
editor.move_prim(path("/Prim/child").unwrap(), path("/Prim/renamed").unwrap());
assert!(matches!(
editor.can_apply(),
Err(NamespaceEditError::RequiresRelocate(_))
));
}
fn arc_target_stage() -> Stage {
let mut root = sdf::Layer::new_in_memory("root.usda");
edit_layer(&mut root, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Ref", Specifier::Def, "").unwrap();
e.data_mut().set_field(
&path("/Ref").unwrap(),
FieldKey::References.as_str(),
sdf::Value::ReferenceListOp(sdf::ReferenceListOp::prepended([sdf::Reference {
asset_path: "model.usda".into(),
prim_path: path("/Model").unwrap(),
..Default::default()
}])),
);
});
let mut model = sdf::Layer::new_in_memory("model.usda");
edit_layer(&mut model, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Model", Specifier::Def, "Xform").unwrap();
sdf::PrimSpec::new(e.data_mut(), "/Model/A", Specifier::Def, "").unwrap();
sdf::PrimSpec::new(e.data_mut(), "/Model/B", Specifier::Def, "").unwrap();
});
let stage = Stage::builder().make_stage(vec![root, model], 0, Vec::new());
let target = stage
.edit_target_for_node(&path("/Ref").unwrap(), EditTargetArc::Reference)
.unwrap();
stage.set_edit_target(target).unwrap();
stage
}
#[test]
fn arc_move_child() {
let stage = arc_target_stage();
assert!(valid(&stage, "/Ref/A"));
NamespaceEditor::new(&stage)
.move_prim(path("/Ref/A").unwrap(), path("/Ref/Renamed").unwrap())
.apply()
.unwrap();
let model = stage.layer("model.usda").expect("model layer");
assert_eq!(
model.data().spec_type(&path("/Model/Renamed").unwrap()),
Some(sdf::SpecType::Prim)
);
assert!(!model.data().has_spec(&path("/Model/A").unwrap()));
assert!(!stage.root_layer().data().has_spec(&path("/Ref/Renamed").unwrap()));
assert!(valid(&stage, "/Ref/Renamed"));
assert!(!valid(&stage, "/Ref/A"));
assert!(stage.root_layer().relocates().is_empty());
}
#[test]
fn arc_delete_child() {
let stage = arc_target_stage();
NamespaceEditor::new(&stage)
.delete_prim(path("/Ref/A").unwrap())
.apply()
.unwrap();
let model = stage.layer("model.usda").expect("model layer");
assert!(!model.data().has_spec(&path("/Model/A").unwrap()));
assert!(!valid(&stage, "/Ref/A"));
assert!(valid(&stage, "/Ref/B"));
}
#[test]
fn arc_target_referenced_and_sublayered() {
let mut root = sdf::Layer::new_in_memory("root.usda");
edit_layer(&mut root, |e| {
e.data_mut().set_field(
&sdf::Path::abs_root(),
FieldKey::SubLayers.as_str(),
sdf::Value::StringVec(vec!["model.usda".into()]),
);
sdf::PrimSpec::new(e.data_mut(), "/Ref", Specifier::Def, "").unwrap();
e.data_mut().set_field(
&path("/Ref").unwrap(),
FieldKey::References.as_str(),
sdf::Value::ReferenceListOp(sdf::ReferenceListOp::prepended([sdf::Reference {
asset_path: "model.usda".into(),
prim_path: path("/Model").unwrap(),
..Default::default()
}])),
);
});
let mut model = sdf::Layer::new_in_memory("model.usda");
edit_layer(&mut model, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Model", Specifier::Def, "Xform").unwrap();
sdf::PrimSpec::new(e.data_mut(), "/Model/A", Specifier::Def, "").unwrap();
});
let stage = Stage::builder().make_stage(vec![root, model], 0, Vec::new());
assert!(valid(&stage, "/Ref/A"));
let target = stage
.edit_target_for_node(&path("/Ref").unwrap(), EditTargetArc::Reference)
.unwrap();
stage.set_edit_target(target).unwrap();
NamespaceEditor::new(&stage)
.move_prim(path("/Ref/A").unwrap(), path("/Ref/Renamed").unwrap())
.apply()
.unwrap();
let model = stage.layer("model.usda").expect("model layer");
assert_eq!(
model.data().spec_type(&path("/Model/Renamed").unwrap()),
Some(sdf::SpecType::Prim)
);
assert!(valid(&stage, "/Ref/Renamed"));
assert!(!valid(&stage, "/Ref/A"));
assert!(stage.root_layer().relocates().is_empty());
}
#[test]
fn arc_move_fixes_target() {
let mut root = sdf::Layer::new_in_memory("root.usda");
edit_layer(&mut root, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Ref", Specifier::Def, "").unwrap();
e.data_mut().set_field(
&path("/Ref").unwrap(),
FieldKey::References.as_str(),
sdf::Value::ReferenceListOp(sdf::ReferenceListOp::prepended([sdf::Reference {
asset_path: "model.usda".into(),
prim_path: path("/Model").unwrap(),
..Default::default()
}])),
);
});
let mut model = sdf::Layer::new_in_memory("model.usda");
edit_layer(&mut model, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Model", Specifier::Def, "Xform").unwrap();
sdf::PrimSpec::new(e.data_mut(), "/Model/A", Specifier::Def, "").unwrap();
sdf::PrimSpec::new(e.data_mut(), "/Model/B", Specifier::Def, "").unwrap();
sdf::RelationshipSpec::new(e.data_mut(), "/Model/B.rel", Variability::Varying, false).unwrap();
e.data_mut().set_field(
&path("/Model/B.rel").unwrap(),
FieldKey::TargetPaths.as_str(),
sdf::Value::PathListOp(sdf::PathListOp::explicit([path("/Model/A").unwrap()])),
);
});
let stage = Stage::builder().make_stage(vec![root, model], 0, Vec::new());
let target = stage
.edit_target_for_node(&path("/Ref").unwrap(), EditTargetArc::Reference)
.unwrap();
stage.set_edit_target(target).unwrap();
NamespaceEditor::new(&stage)
.move_prim(path("/Ref/A").unwrap(), path("/Ref/Renamed").unwrap())
.apply()
.unwrap();
let model = stage.layer("model.usda").expect("model layer");
let op = model
.data()
.try_field(&path("/Model/B.rel").unwrap(), FieldKey::TargetPaths.as_str())
.unwrap()
.expect("targets authored")
.into_owned()
.try_as_path_list_op()
.expect("targets are a path list op");
assert_eq!(op.explicit_items, vec![path("/Model/Renamed").unwrap()]);
assert_eq!(rel_targets(&stage, "/Ref/B.rel"), vec!["/Ref/Renamed"]);
}
#[test]
fn arc_dest_occupied() {
let stage = arc_target_stage();
let mut editor = NamespaceEditor::new(&stage);
editor.move_prim(path("/Ref/A").unwrap(), path("/Ref/B").unwrap());
assert!(matches!(
editor.can_apply(),
Err(NamespaceEditError::DestinationExists(_))
));
}
#[test]
fn mapped_outside_arc() {
let stage = arc_target_stage();
let mut editor = NamespaceEditor::new(&stage);
editor.move_prim(path("/Ref/A").unwrap(), path("/Elsewhere").unwrap());
assert!(matches!(
editor.can_apply(),
Err(NamespaceEditError::Stage(StageAuthoringError::OutsideEditTarget { .. }))
));
}
fn deep_arc_stage() -> Stage {
let mut root = sdf::Layer::new_in_memory("root.usda");
edit_layer(&mut root, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Ref", Specifier::Def, "").unwrap();
e.data_mut().set_field(
&path("/Ref").unwrap(),
FieldKey::References.as_str(),
sdf::Value::ReferenceListOp(sdf::ReferenceListOp::prepended([sdf::Reference {
asset_path: "model.usda".into(),
prim_path: path("/Model").unwrap(),
..Default::default()
}])),
);
});
let mut model = sdf::Layer::new_in_memory("model.usda");
edit_layer(&mut model, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Model", Specifier::Def, "Xform").unwrap();
e.data_mut().set_field(
&path("/Model").unwrap(),
FieldKey::References.as_str(),
sdf::Value::ReferenceListOp(sdf::ReferenceListOp::prepended([sdf::Reference {
asset_path: "deep.usda".into(),
prim_path: path("/Deep").unwrap(),
..Default::default()
}])),
);
});
let mut deep = sdf::Layer::new_in_memory("deep.usda");
edit_layer(&mut deep, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Deep", Specifier::Def, "Xform").unwrap();
sdf::PrimSpec::new(e.data_mut(), "/Deep/Inner", Specifier::Def, "").unwrap();
});
let stage = Stage::builder().make_stage(vec![root, model, deep], 0, Vec::new());
let target = stage
.edit_target_for_node(&path("/Ref").unwrap(), EditTargetArc::Reference)
.unwrap();
stage.set_edit_target(target).unwrap();
stage
}
#[test]
fn mapped_deep_arc_move() {
let stage = deep_arc_stage();
assert!(valid(&stage, "/Ref/Inner"));
NamespaceEditor::new(&stage)
.move_prim(path("/Ref/Inner").unwrap(), path("/Ref/Moved").unwrap())
.apply()
.unwrap();
let model = stage.layer("model.usda").expect("model layer");
assert!(model
.relocates()
.iter()
.any(|(s, t)| s == &path("/Model/Inner").unwrap() && t == &path("/Model/Moved").unwrap()));
assert!(stage.root_layer().relocates().is_empty());
assert!(valid(&stage, "/Ref/Moved"));
assert!(!valid(&stage, "/Ref/Inner"));
}
#[test]
fn mapped_deep_arc_delete() {
let stage = deep_arc_stage();
assert!(valid(&stage, "/Ref/Inner"));
NamespaceEditor::new(&stage)
.delete_prim(path("/Ref/Inner").unwrap())
.apply()
.unwrap();
let model = stage.layer("model.usda").expect("model layer");
assert!(model
.relocates()
.iter()
.any(|(s, t)| s == &path("/Model/Inner").unwrap() && t.is_empty()));
assert!(stage.root_layer().relocates().is_empty());
assert!(!valid(&stage, "/Ref/Inner"));
}
#[test]
fn mapped_internal_arc_relocates() {
let mut root = sdf::Layer::new_in_memory("root.usda");
edit_layer(&mut root, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Ref", Specifier::Def, "").unwrap();
e.data_mut().set_field(
&path("/Ref").unwrap(),
FieldKey::References.as_str(),
sdf::Value::ReferenceListOp(sdf::ReferenceListOp::prepended([sdf::Reference {
asset_path: "model.usda".into(),
prim_path: path("/Model").unwrap(),
..Default::default()
}])),
);
});
let mut model = sdf::Layer::new_in_memory("model.usda");
edit_layer(&mut model, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Model", Specifier::Def, "Xform").unwrap();
e.data_mut().set_field(
&path("/Model").unwrap(),
FieldKey::References.as_str(),
sdf::Value::ReferenceListOp(sdf::ReferenceListOp::prepended([sdf::Reference {
prim_path: path("/Deep").unwrap(),
..Default::default()
}])),
);
sdf::PrimSpec::new(e.data_mut(), "/Deep", Specifier::Def, "Xform").unwrap();
sdf::PrimSpec::new(e.data_mut(), "/Deep/Inner", Specifier::Def, "").unwrap();
});
let stage = Stage::builder().make_stage(vec![root, model], 0, Vec::new());
assert!(valid(&stage, "/Ref/Inner"));
let target = stage
.edit_target_for_node(&path("/Ref").unwrap(), EditTargetArc::Reference)
.unwrap();
stage.set_edit_target(target).unwrap();
NamespaceEditor::new(&stage)
.move_prim(path("/Ref/Inner").unwrap(), path("/Ref/Moved").unwrap())
.apply()
.unwrap();
let model = stage.layer("model.usda").expect("model layer");
assert!(model
.relocates()
.iter()
.any(|(s, t)| s == &path("/Model/Inner").unwrap() && t == &path("/Model/Moved").unwrap()));
assert!(valid(&stage, "/Ref/Moved"));
assert!(!valid(&stage, "/Ref/Inner"));
}
#[test]
fn mapped_within_stack_residue() {
let mut root = sdf::Layer::new_in_memory("root.usda");
edit_layer(&mut root, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Ref", Specifier::Def, "").unwrap();
e.data_mut().set_field(
&path("/Ref").unwrap(),
FieldKey::References.as_str(),
sdf::Value::ReferenceListOp(sdf::ReferenceListOp::prepended([sdf::Reference {
asset_path: "model.usda".into(),
prim_path: path("/Model").unwrap(),
..Default::default()
}])),
);
});
let mut model = sdf::Layer::new_in_memory("model.usda");
edit_layer(&mut model, |e| {
e.data_mut().set_field(
&sdf::Path::abs_root(),
FieldKey::SubLayers.as_str(),
sdf::Value::StringVec(vec!["model_sub.usda".into()]),
);
sdf::PrimSpec::new(e.data_mut(), "/Model", Specifier::Def, "Xform").unwrap();
sdf::PrimSpec::new(e.data_mut(), "/Model/A", Specifier::Def, "").unwrap();
});
let mut model_sub = sdf::Layer::new_in_memory("model_sub.usda");
edit_layer(&mut model_sub, |e| {
sdf::PrimSpec::over(e.data_mut(), "/Model/A").unwrap();
sdf::AttributeSpec::new(e.data_mut(), "/Model/A.attr", "double", Variability::Varying, false).unwrap();
});
let stage = Stage::builder().make_stage(vec![root, model, model_sub], 0, Vec::new());
let target = stage
.edit_target_for_node(&path("/Ref").unwrap(), EditTargetArc::Reference)
.unwrap();
stage.set_edit_target(target).unwrap();
NamespaceEditor::new(&stage)
.move_prim(path("/Ref/A").unwrap(), path("/Ref/Renamed").unwrap())
.apply()
.unwrap();
let model = stage.layer("model.usda").expect("model layer");
let model_sub = stage.layer("model_sub.usda").expect("model_sub layer");
assert_eq!(
model.data().spec_type(&path("/Model/Renamed").unwrap()),
Some(sdf::SpecType::Prim)
);
assert!(!model.data().has_spec(&path("/Model/A").unwrap()));
assert_eq!(
model_sub.data().spec_type(&path("/Model/Renamed.attr").unwrap()),
Some(sdf::SpecType::Attribute)
);
assert!(!model_sub.data().has_spec(&path("/Model/A").unwrap()));
assert!(model.relocates().is_empty());
assert!(valid(&stage, "/Ref/Renamed"));
assert!(!valid(&stage, "/Ref/A"));
}
#[test]
fn mapped_multi_layer_provenance() {
let mut root = sdf::Layer::new_in_memory("root.usda");
edit_layer(&mut root, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Ref", Specifier::Def, "").unwrap();
e.data_mut().set_field(
&path("/Ref").unwrap(),
FieldKey::References.as_str(),
sdf::Value::ReferenceListOp(sdf::ReferenceListOp::prepended([sdf::Reference {
asset_path: "model.usda".into(),
prim_path: path("/Model").unwrap(),
..Default::default()
}])),
);
});
let mut model = sdf::Layer::new_in_memory("model.usda");
edit_layer(&mut model, |e| {
e.data_mut().set_field(
&sdf::Path::abs_root(),
FieldKey::SubLayers.as_str(),
sdf::Value::StringVec(vec!["model_sub.usda".into()]),
);
sdf::PrimSpec::new(e.data_mut(), "/Model", Specifier::Def, "Xform").unwrap();
sdf::PrimSpec::new(e.data_mut(), "/Model/A", Specifier::Def, "").unwrap();
});
let mut model_sub = sdf::Layer::new_in_memory("model_sub.usda");
edit_layer(&mut model_sub, |e| {
sdf::PrimSpec::over(e.data_mut(), "/Model/A").unwrap();
sdf::AttributeSpec::new(e.data_mut(), "/Model/A.attr", "double", Variability::Varying, false).unwrap();
});
let stage = Stage::builder().make_stage(vec![root, model, model_sub], 0, Vec::new());
let target = stage
.edit_target_for_node(&path("/Ref").unwrap(), EditTargetArc::Reference)
.unwrap();
stage.set_edit_target(target).unwrap();
let seen: std::rc::Rc<std::cell::Cell<Option<&'static str>>> = std::rc::Rc::new(std::cell::Cell::new(None));
{
let seen = seen.clone();
stage.add_sink(move |_: &Stage, change: &crate::usd::CommittedChange<'_>| {
seen.set(Some(match change.provenance {
crate::usd::Provenance::LocalStack => "local",
crate::usd::Provenance::EditTarget(_) => "target",
crate::usd::Provenance::DirectLayerEdit => "direct",
}));
});
}
NamespaceEditor::new(&stage)
.move_prim(path("/Ref/A").unwrap(), path("/Ref/Renamed").unwrap())
.apply()
.unwrap();
assert_eq!(seen.get(), Some("target"));
}
#[test]
fn mapped_relocate_atomic() {
let stage = deep_arc_stage();
let mut editor = NamespaceEditor::new(&stage);
editor
.move_prim(path("/Ref/Inner").unwrap(), path("/Ref/Moved").unwrap())
.move_prim(path("/Ref/Missing").unwrap(), path("/Ref/X").unwrap());
assert!(editor.apply().is_err());
let model = stage.layer("model.usda").expect("model layer");
assert!(model.relocates().is_empty());
assert!(valid(&stage, "/Ref/Inner"));
assert!(!valid(&stage, "/Ref/Moved"));
}
#[test]
fn mapped_relocate_orphans_child() {
let mut root = sdf::Layer::new_in_memory("root.usda");
edit_layer(&mut root, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Ref", Specifier::Def, "").unwrap();
e.data_mut().set_field(
&path("/Ref").unwrap(),
FieldKey::References.as_str(),
sdf::Value::ReferenceListOp(sdf::ReferenceListOp::prepended([sdf::Reference {
asset_path: "model.usda".into(),
prim_path: path("/Model").unwrap(),
..Default::default()
}])),
);
});
let mut model = sdf::Layer::new_in_memory("model.usda");
edit_layer(&mut model, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Model", Specifier::Def, "Xform").unwrap();
e.data_mut().set_field(
&path("/Model").unwrap(),
FieldKey::References.as_str(),
sdf::Value::ReferenceListOp(sdf::ReferenceListOp::prepended([sdf::Reference {
asset_path: "deep.usda".into(),
prim_path: path("/Deep").unwrap(),
..Default::default()
}])),
);
});
let mut deep = sdf::Layer::new_in_memory("deep.usda");
edit_layer(&mut deep, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Deep", Specifier::Def, "Xform").unwrap();
sdf::PrimSpec::new(e.data_mut(), "/Deep/Inner", Specifier::Def, "").unwrap();
sdf::PrimSpec::new(e.data_mut(), "/Deep/Inner/Grand", Specifier::Def, "").unwrap();
});
let stage = Stage::builder().make_stage(vec![root, model, deep], 0, Vec::new());
assert!(valid(&stage, "/Ref/Inner/Grand"));
let target = stage
.edit_target_for_node(&path("/Ref").unwrap(), EditTargetArc::Reference)
.unwrap();
stage.set_edit_target(target).unwrap();
let mut editor = NamespaceEditor::new(&stage);
editor
.move_prim(path("/Ref/Inner/Grand").unwrap(), path("/Ref/Grand").unwrap())
.delete_prim(path("/Ref/Inner").unwrap());
assert!(matches!(
editor.can_apply(),
Err(NamespaceEditError::UnrepresentableRelocateBatch(_))
));
}
#[test]
fn mapped_atomic() {
let stage = arc_target_stage();
let mut editor = NamespaceEditor::new(&stage);
editor
.move_prim(path("/Ref/A").unwrap(), path("/Ref/Moved").unwrap())
.move_prim(path("/Ref/Missing").unwrap(), path("/Ref/X").unwrap());
assert!(editor.apply().is_err());
let model = stage.layer("model.usda").expect("model layer");
assert!(model.data().has_spec(&path("/Model/A").unwrap()));
assert!(!model.data().has_spec(&path("/Model/Moved").unwrap()));
assert!(valid(&stage, "/Ref/A"));
assert!(!valid(&stage, "/Ref/Moved"));
}
#[test]
fn mapped_no_source() {
let stage = arc_target_stage();
let mut editor = NamespaceEditor::new(&stage);
editor.move_prim(path("/Ref/Missing").unwrap(), path("/Ref/X").unwrap());
assert!(matches!(editor.can_apply(), Err(NamespaceEditError::SourceNotFound(_))));
}
#[test]
fn mapped_layers_to_edit() {
let stage = arc_target_stage();
let mut editor = NamespaceEditor::new(&stage);
editor.move_prim(path("/Ref/A").unwrap(), path("/Ref/Renamed").unwrap());
assert_eq!(editor.layers_to_edit().unwrap(), vec!["model.usda".to_string()]);
}
#[test]
fn mapped_layers_to_edit_fixup_only() {
let mut root = sdf::Layer::new_in_memory("root.usda");
edit_layer(&mut root, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Ref", Specifier::Def, "").unwrap();
e.data_mut().set_field(
&path("/Ref").unwrap(),
FieldKey::References.as_str(),
sdf::Value::ReferenceListOp(sdf::ReferenceListOp::prepended([sdf::Reference {
asset_path: "model.usda".into(),
prim_path: path("/Model").unwrap(),
..Default::default()
}])),
);
});
let mut model = sdf::Layer::new_in_memory("model.usda");
edit_layer(&mut model, |e| {
e.data_mut().set_field(
&sdf::Path::abs_root(),
FieldKey::SubLayers.as_str(),
sdf::Value::StringVec(vec!["model_sub.usda".into()]),
);
sdf::PrimSpec::new(e.data_mut(), "/Model", Specifier::Def, "Xform").unwrap();
sdf::PrimSpec::new(e.data_mut(), "/Model/A", Specifier::Def, "").unwrap();
});
let mut model_sub = sdf::Layer::new_in_memory("model_sub.usda");
edit_layer(&mut model_sub, |e| {
sdf::RelationshipSpec::new(e.data_mut(), "/Model/B.rel", Variability::Varying, false).unwrap();
e.data_mut().set_field(
&path("/Model/B.rel").unwrap(),
FieldKey::TargetPaths.as_str(),
sdf::Value::PathListOp(sdf::PathListOp::explicit([path("/Model/A").unwrap()])),
);
});
let stage = Stage::builder().make_stage(vec![root, model, model_sub], 0, Vec::new());
let target = stage
.edit_target_for_node(&path("/Ref").unwrap(), EditTargetArc::Reference)
.unwrap();
stage.set_edit_target(target).unwrap();
let mut editor = NamespaceEditor::new(&stage);
editor.move_prim(path("/Ref/A").unwrap(), path("/Ref/Renamed").unwrap());
let layers = editor.layers_to_edit().unwrap();
assert!(layers.contains(&"model.usda".to_string()), "got {layers:?}");
assert!(layers.contains(&"model_sub.usda".to_string()), "got {layers:?}");
}
fn arc_overridden_stage(overrides: &[&str]) -> Stage {
let mut root = sdf::Layer::new_in_memory("root.usda");
edit_layer(&mut root, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Ref", Specifier::Def, "").unwrap();
e.data_mut().set_field(
&path("/Ref").unwrap(),
FieldKey::References.as_str(),
sdf::Value::ReferenceListOp(sdf::ReferenceListOp::prepended([sdf::Reference {
asset_path: "model.usda".into(),
prim_path: path("/Model").unwrap(),
..Default::default()
}])),
);
for name in overrides {
sdf::PrimSpec::over(e.data_mut(), format!("/Ref/{name}").as_str()).unwrap();
}
});
let mut model = sdf::Layer::new_in_memory("model.usda");
edit_layer(&mut model, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Model", Specifier::Def, "Xform").unwrap();
sdf::PrimSpec::new(e.data_mut(), "/Model/A", Specifier::Def, "").unwrap();
});
let stage = Stage::builder().make_stage(vec![root, model], 0, Vec::new());
let target = stage
.edit_target_for_node(&path("/Ref").unwrap(), EditTargetArc::Reference)
.unwrap();
stage.set_edit_target(target).unwrap();
stage
}
#[test]
fn mapped_dest_composed_elsewhere() {
let stage = arc_overridden_stage(&["B"]);
assert!(valid(&stage, "/Ref/B"));
assert!(!stage
.layer("model.usda")
.unwrap()
.data()
.has_spec(&path("/Model/B").unwrap()));
let mut editor = NamespaceEditor::new(&stage);
editor.move_prim(path("/Ref/A").unwrap(), path("/Ref/B").unwrap());
assert!(matches!(
editor.can_apply(),
Err(NamespaceEditError::DestinationExists(_))
));
}
#[test]
fn mapped_source_multi_layer() {
let stage = arc_overridden_stage(&["A"]);
let mut editor = NamespaceEditor::new(&stage);
editor.move_prim(path("/Ref/A").unwrap(), path("/Ref/Renamed").unwrap());
assert!(matches!(
editor.can_apply(),
Err(NamespaceEditError::RequiresRelocate(_))
));
}
#[test]
fn mapped_delete_multi_layer() {
let stage = arc_overridden_stage(&["A"]);
let mut editor = NamespaceEditor::new(&stage);
editor.delete_prim(path("/Ref/A").unwrap());
assert!(matches!(
editor.can_apply(),
Err(NamespaceEditError::RequiresRelocate(_))
));
}
#[test]
fn rejects_relative_path() {
let stage = sample();
let mut editor = NamespaceEditor::new(&stage);
editor.move_prim(sdf::Path::from("A"), path("/B").unwrap());
assert!(matches!(editor.can_apply(), Err(NamespaceEditError::InvalidSource(_))));
}
#[test]
fn invalid_batch_atomic() {
let stage = sample();
let mut editor = NamespaceEditor::new(&stage);
editor
.move_prim(path("/A").unwrap(), path("/B").unwrap())
.move_prim(path("/Nope").unwrap(), path("/X").unwrap());
assert!(editor.apply().is_err());
assert!(valid(&stage, "/A"));
assert!(!valid(&stage, "/B"));
}
#[test]
fn rejects_pseudo_root() {
let stage = sample();
let mut editor = NamespaceEditor::new(&stage);
editor.delete_prim(sdf::Path::abs_root());
assert!(matches!(editor.can_apply(), Err(NamespaceEditError::PseudoRoot)));
}
#[test]
fn layers_to_edit_lists() {
let stage = sample();
let mut editor = NamespaceEditor::new(&stage);
editor.delete_prim(path("/A").unwrap());
let layers = editor.layers_to_edit().unwrap();
assert_eq!(layers, vec![stage.root_layer().identifier().to_string()]);
}
#[test]
fn layers_to_edit_rejects() {
let mut root = sdf::Layer::new_in_memory("root.usda");
edit_layer(&mut root, |e| {
sdf::PrimSpec::new(e.data_mut(), "/B", Specifier::Def, "").unwrap();
e.set_relocates(vec![
(path("/A/X").unwrap(), path("/B/Y").unwrap()),
(path("/C/Y").unwrap(), path("/D/Y").unwrap()),
])
.unwrap();
});
let stage = Stage::builder().make_stage(vec![root], 0, Vec::new());
let mut editor = NamespaceEditor::new(&stage);
editor.move_prim(path("/B").unwrap(), path("/D").unwrap());
assert!(matches!(
editor.can_apply(),
Err(NamespaceEditError::UnrepresentableRelocateBatch(_))
));
assert!(matches!(
editor.layers_to_edit(),
Err(NamespaceEditError::UnrepresentableRelocateBatch(_))
));
}
#[test]
fn relocate_fold_existing() {
let mut root = sdf::Layer::new_in_memory("root.usda");
edit_layer(&mut root, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Ref", Specifier::Def, "").unwrap();
e.data_mut().set_field(
&path("/Ref").unwrap(),
FieldKey::References.as_str(),
sdf::Value::ReferenceListOp(sdf::ReferenceListOp::prepended([sdf::Reference {
asset_path: "model.usda".into(),
prim_path: path("/Model").unwrap(),
..Default::default()
}])),
);
e.set_relocates(vec![(path("/Ref/Orig").unwrap(), path("/Ref/Geom").unwrap())])
.unwrap();
});
let mut model = sdf::Layer::new_in_memory("model.usda");
edit_layer(&mut model, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Model", Specifier::Def, "Xform").unwrap();
sdf::PrimSpec::new(e.data_mut(), "/Model/Orig", Specifier::Def, "").unwrap();
});
let stage = Stage::builder().make_stage(vec![root, model], 0, Vec::new());
NamespaceEditor::new(&stage)
.move_prim(path("/Ref/Geom").unwrap(), path("/Ref/Final").unwrap())
.apply()
.unwrap();
let relocates = stage.root_layer().relocates();
assert!(
relocates
.iter()
.any(|(s, t)| s == &path("/Ref/Orig").unwrap() && t == &path("/Ref/Final").unwrap()),
"expected (/Ref/Orig, /Ref/Final) in {relocates:?}"
);
assert!(
!relocates.iter().any(|(s, _)| s == &path("/Ref/Geom").unwrap()),
"transient /Ref/Geom source must be dropped: {relocates:?}"
);
let targets: Vec<_> = relocates
.iter()
.filter(|(_, t)| !t.is_empty())
.map(|(_, t)| t)
.collect();
assert_eq!(
targets.len(),
targets.iter().collect::<HashSet<_>>().len(),
"duplicate destinations: {relocates:?}"
);
}
#[test]
fn fold_delete_child() {
let mut root = sdf::Layer::new_in_memory("root.usda");
edit_layer(&mut root, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Ref", Specifier::Def, "").unwrap();
e.data_mut().set_field(
&path("/Ref").unwrap(),
FieldKey::References.as_str(),
sdf::Value::ReferenceListOp(sdf::ReferenceListOp::prepended([sdf::Reference {
asset_path: "model.usda".into(),
prim_path: path("/Model").unwrap(),
..Default::default()
}])),
);
e.set_relocates(vec![(path("/Ref/B").unwrap(), path("/Ref/Geom/Sub").unwrap())])
.unwrap();
});
let mut model = sdf::Layer::new_in_memory("model.usda");
edit_layer(&mut model, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Model", Specifier::Def, "Xform").unwrap();
sdf::PrimSpec::new(e.data_mut(), "/Model/Geom", Specifier::Def, "").unwrap();
sdf::PrimSpec::new(e.data_mut(), "/Model/Geom/Sub", Specifier::Def, "").unwrap();
});
let stage = Stage::builder().make_stage(vec![root, model], 0, Vec::new());
NamespaceEditor::new(&stage)
.delete_prim(path("/Ref/Geom").unwrap())
.apply()
.unwrap();
let relocates = stage.root_layer().relocates();
assert!(
relocates
.iter()
.any(|(s, t)| s == &path("/Ref/B").unwrap() && t.is_empty()),
"expected (/Ref/B, '') in {relocates:?}"
);
assert!(
relocates
.iter()
.any(|(s, t)| s == &path("/Ref/Geom").unwrap() && t.is_empty()),
"expected (/Ref/Geom, '') in {relocates:?}"
);
}
#[test]
fn chain_move_delete() {
let stage = referenced_stage();
let mut editor = NamespaceEditor::new(&stage);
editor
.move_prim(path("/Ref/Geom").unwrap(), path("/Ref/Renamed").unwrap())
.delete_prim(path("/Ref/Renamed").unwrap());
editor.apply().unwrap();
let relocates = stage.root_layer().relocates();
assert!(
relocates
.iter()
.any(|(s, t)| s == &path("/Ref/Geom").unwrap() && t.is_empty()),
"expected (/Ref/Geom, '') in {relocates:?}"
);
assert!(!valid(&stage, "/Ref/Geom"));
assert!(!valid(&stage, "/Ref/Renamed"));
}
#[test]
fn chain_two_moves() {
let stage = referenced_stage();
let mut editor = NamespaceEditor::new(&stage);
editor
.move_prim(path("/Ref/Geom").unwrap(), path("/Ref/Renamed").unwrap())
.move_prim(path("/Ref/Renamed").unwrap(), path("/Ref/Final").unwrap());
editor.apply().unwrap();
let relocates = stage.root_layer().relocates();
assert!(
relocates
.iter()
.any(|(s, t)| s == &path("/Ref/Geom").unwrap() && t == &path("/Ref/Final").unwrap()),
"expected (/Ref/Geom, /Ref/Final) in {relocates:?}"
);
let sources: HashSet<_> = relocates.iter().map(|(s, _)| s).collect();
let targets: HashSet<_> = relocates.iter().map(|(_, t)| t).collect();
assert!(
sources.intersection(&targets).next().is_none(),
"chain found in {relocates:?}"
);
assert!(valid(&stage, "/Ref/Final"));
assert!(!valid(&stage, "/Ref/Geom"));
assert!(!valid(&stage, "/Ref/Renamed"));
}
#[test]
fn rejects_premature_delete() {
let stage = referenced_stage();
let mut editor = NamespaceEditor::new(&stage);
editor
.delete_prim(path("/Ref/Renamed").unwrap())
.move_prim(path("/Ref/Geom").unwrap(), path("/Ref/Renamed").unwrap());
assert!(matches!(editor.can_apply(), Err(NamespaceEditError::SourceNotFound(_))));
}
#[test]
fn relocate_fold_sequential() {
let stage = referenced_stage();
NamespaceEditor::new(&stage)
.move_prim(path("/Ref/Geom").unwrap(), path("/Ref/Renamed").unwrap())
.apply()
.unwrap();
NamespaceEditor::new(&stage)
.move_prim(path("/Ref/Renamed").unwrap(), path("/Ref/Final").unwrap())
.apply()
.unwrap();
let relocates = stage.root_layer().relocates();
assert!(
relocates
.iter()
.any(|(s, t)| s == &path("/Ref/Geom").unwrap() && t == &path("/Ref/Final").unwrap()),
"expected (/Ref/Geom, /Ref/Final) in {relocates:?}"
);
assert!(
!relocates.iter().any(|(s, _)| s == &path("/Ref/Renamed").unwrap()),
"transient /Ref/Renamed source must be dropped: {relocates:?}"
);
assert!(valid(&stage, "/Ref/Final"));
assert!(!valid(&stage, "/Ref/Geom"));
assert!(!valid(&stage, "/Ref/Renamed"));
}
#[test]
fn chain_move_descendant() {
let mut root = sdf::Layer::new_in_memory("root.usda");
edit_layer(&mut root, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Ref", Specifier::Def, "").unwrap();
e.data_mut().set_field(
&path("/Ref").unwrap(),
FieldKey::References.as_str(),
sdf::Value::ReferenceListOp(sdf::ReferenceListOp::prepended([sdf::Reference {
asset_path: "model.usda".into(),
prim_path: path("/Model").unwrap(),
..Default::default()
}])),
);
});
let mut model = sdf::Layer::new_in_memory("model.usda");
edit_layer(&mut model, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Model", Specifier::Def, "Xform").unwrap();
sdf::PrimSpec::new(e.data_mut(), "/Model/Geom", Specifier::Def, "").unwrap();
sdf::PrimSpec::new(e.data_mut(), "/Model/Geom/Sub", Specifier::Def, "").unwrap();
});
let stage = Stage::builder().make_stage(vec![root, model], 0, Vec::new());
NamespaceEditor::new(&stage)
.move_prim(path("/Ref/Geom").unwrap(), path("/Ref/Renamed").unwrap())
.move_prim(path("/Ref/Renamed/Sub").unwrap(), path("/Ref/Renamed/Sub2").unwrap())
.apply()
.unwrap();
let relocates = stage.root_layer().relocates();
assert!(
relocates
.iter()
.any(|(s, t)| s == &path("/Ref/Renamed/Sub").unwrap() && t == &path("/Ref/Renamed/Sub2").unwrap()),
"expected (/Ref/Renamed/Sub, /Ref/Renamed/Sub2) in {relocates:?}"
);
assert!(valid(&stage, "/Ref/Renamed/Sub2"));
assert!(!valid(&stage, "/Ref/Renamed/Sub"));
assert!(!valid(&stage, "/Ref/Geom"));
}
#[test]
fn rejects_missing_descendant() {
let stage = referenced_stage(); let mut editor = NamespaceEditor::new(&stage);
editor
.move_prim(path("/Ref/Geom").unwrap(), path("/Ref/Renamed").unwrap())
.move_prim(
path("/Ref/Renamed/Missing").unwrap(),
path("/Ref/Renamed/Other").unwrap(),
);
assert!(matches!(editor.can_apply(), Err(NamespaceEditError::SourceNotFound(_))));
}
#[test]
fn rejects_property_descendant() {
let mut root = sdf::Layer::new_in_memory("root.usda");
edit_layer(&mut root, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Ref", Specifier::Def, "").unwrap();
e.data_mut().set_field(
&path("/Ref").unwrap(),
FieldKey::References.as_str(),
sdf::Value::ReferenceListOp(sdf::ReferenceListOp::prepended([sdf::Reference {
asset_path: "model.usda".into(),
prim_path: path("/Model").unwrap(),
..Default::default()
}])),
);
});
let mut model = sdf::Layer::new_in_memory("model.usda");
edit_layer(&mut model, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Model", Specifier::Def, "Xform").unwrap();
sdf::PrimSpec::new(e.data_mut(), "/Model/Geom", Specifier::Def, "").unwrap();
sdf::AttributeSpec::new(e.data_mut(), "/Model/Geom.attr", "double", Variability::Varying, false).unwrap();
});
let stage = Stage::builder().make_stage(vec![root, model], 0, Vec::new());
let mut editor = NamespaceEditor::new(&stage);
editor
.move_prim(path("/Ref/Geom").unwrap(), path("/Ref/Renamed").unwrap())
.move_property(path("/Ref/Renamed.attr").unwrap(), path("/Ref/Renamed.attr2").unwrap());
assert!(matches!(editor.can_apply(), Err(NamespaceEditError::SourceNotFound(_))));
assert!(
!stage.root_layer().relocates().iter().any(|(s, _)| s.is_property_path()),
"no property relocate must be authored"
);
}
fn nested_ref_stage(children: &[&str]) -> Stage {
let mut root = sdf::Layer::new_in_memory("root.usda");
edit_layer(&mut root, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Ref", Specifier::Def, "").unwrap();
e.data_mut().set_field(
&path("/Ref").unwrap(),
FieldKey::References.as_str(),
sdf::Value::ReferenceListOp(sdf::ReferenceListOp::prepended([sdf::Reference {
asset_path: "model.usda".into(),
prim_path: path("/Model").unwrap(),
..Default::default()
}])),
);
});
let mut model = sdf::Layer::new_in_memory("model.usda");
edit_layer(&mut model, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Model", Specifier::Def, "Xform").unwrap();
sdf::PrimSpec::new(e.data_mut(), "/Model/Geom", Specifier::Def, "").unwrap();
for c in children {
sdf::PrimSpec::new(e.data_mut(), format!("/Model/Geom/{c}").as_str(), Specifier::Def, "").unwrap();
}
});
Stage::builder().make_stage(vec![root, model], 0, Vec::new())
}
#[test]
fn occupied_in_relocated() {
let stage = nested_ref_stage(&["Sub", "Other"]);
let mut editor = NamespaceEditor::new(&stage);
editor
.move_prim(path("/Ref/Geom").unwrap(), path("/A").unwrap())
.move_prim(path("/A/Sub").unwrap(), path("/A/Other").unwrap());
assert!(matches!(
editor.can_apply(),
Err(NamespaceEditError::DestinationExists(_))
));
}
#[test]
fn child_source_follows_parent() {
let stage = nested_ref_stage(&["Sub"]);
let mut editor = NamespaceEditor::new(&stage);
editor
.move_prim(path("/Ref/Geom").unwrap(), path("/A").unwrap())
.move_prim(path("/A/Sub").unwrap(), path("/B").unwrap())
.move_prim(path("/A").unwrap(), path("/C").unwrap());
editor.apply().unwrap();
let relocates = stage.root_layer().relocates();
assert!(
relocates
.iter()
.any(|(s, t)| s == &path("/C/Sub").unwrap() && t == &path("/B").unwrap()),
"expected (/C/Sub, /B) in {relocates:?}"
);
assert!(valid(&stage, "/C"));
assert!(valid(&stage, "/B"));
assert!(!valid(&stage, "/A"));
assert!(!valid(&stage, "/C/Sub"));
}
#[test]
fn delete_orphans_moved_child() {
let stage = nested_ref_stage(&["Sub"]);
let mut editor = NamespaceEditor::new(&stage);
editor
.move_prim(path("/Ref/Geom").unwrap(), path("/A").unwrap())
.move_prim(path("/A/Sub").unwrap(), path("/B").unwrap())
.delete_prim(path("/A").unwrap());
assert!(matches!(
editor.can_apply(),
Err(NamespaceEditError::UnrepresentableRelocateBatch(_))
));
}
#[test]
fn move_into_vacated_dst() {
let mut root = sdf::Layer::new_in_memory("root.usda");
edit_layer(&mut root, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Ref", Specifier::Def, "").unwrap();
e.data_mut().set_field(
&path("/Ref").unwrap(),
FieldKey::References.as_str(),
sdf::Value::ReferenceListOp(sdf::ReferenceListOp::prepended([sdf::Reference {
asset_path: "model.usda".into(),
prim_path: path("/Model").unwrap(),
..Default::default()
}])),
);
});
let mut model = sdf::Layer::new_in_memory("model.usda");
edit_layer(&mut model, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Model", Specifier::Def, "Xform").unwrap();
sdf::PrimSpec::new(e.data_mut(), "/Model/A", Specifier::Def, "").unwrap();
sdf::PrimSpec::new(e.data_mut(), "/Model/X", Specifier::Def, "").unwrap();
});
let stage = Stage::builder().make_stage(vec![root, model], 0, Vec::new());
let mut editor = NamespaceEditor::new(&stage);
editor
.move_prim(path("/Ref/A").unwrap(), path("/Ref/B").unwrap())
.move_prim(path("/Ref/B").unwrap(), path("/Ref/C").unwrap())
.move_prim(path("/Ref/X").unwrap(), path("/Ref/B").unwrap());
editor.apply().unwrap();
assert!(valid(&stage, "/Ref/C"));
assert!(valid(&stage, "/Ref/B"));
assert!(!valid(&stage, "/Ref/A"));
assert!(!valid(&stage, "/Ref/X"));
}
#[test]
fn rename_referenced_prim() {
let stage = referenced_stage();
NamespaceEditor::new(&stage)
.move_prim(path("/Ref").unwrap(), path("/Ref2").unwrap())
.apply()
.unwrap();
assert!(valid(&stage, "/Ref2/Geom"));
assert!(!valid(&stage, "/Ref"));
assert!(
stage.root_layer().relocates().is_empty(),
"rename should author no relocate"
);
}
#[test]
fn move_onto_composed_property() {
let mut root = sdf::Layer::new_in_memory("root.usda");
edit_layer(&mut root, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Ref", Specifier::Def, "").unwrap();
e.data_mut().set_field(
&path("/Ref").unwrap(),
FieldKey::References.as_str(),
sdf::Value::ReferenceListOp(sdf::ReferenceListOp::prepended([sdf::Reference {
asset_path: "model.usda".into(),
prim_path: path("/Model").unwrap(),
..Default::default()
}])),
);
sdf::PrimSpec::new(e.data_mut(), "/Src", Specifier::Def, "").unwrap();
sdf::AttributeSpec::new(e.data_mut(), "/Src.attr", "double", Variability::Varying, false).unwrap();
});
let mut model = sdf::Layer::new_in_memory("model.usda");
edit_layer(&mut model, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Model", Specifier::Def, "Xform").unwrap();
sdf::AttributeSpec::new(e.data_mut(), "/Model.attr", "double", Variability::Varying, false).unwrap();
});
let stage = Stage::builder().make_stage(vec![root, model], 0, Vec::new());
let mut editor = NamespaceEditor::new(&stage);
editor.move_property(path("/Src.attr").unwrap(), path("/Ref.attr").unwrap());
assert!(matches!(
editor.can_apply(),
Err(NamespaceEditError::DestinationExists(_))
));
}
#[test]
fn fold_relocate_to_empty() {
let mut root = sdf::Layer::new_in_memory("root.usda");
edit_layer(&mut root, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Ref", Specifier::Def, "").unwrap();
e.data_mut().set_field(
&path("/Ref").unwrap(),
FieldKey::References.as_str(),
sdf::Value::ReferenceListOp(sdf::ReferenceListOp::prepended([sdf::Reference {
asset_path: "model.usda".into(),
prim_path: path("/Model").unwrap(),
..Default::default()
}])),
);
e.set_relocates(vec![(path("/Ref/Orig").unwrap(), path("/Ref/Geom").unwrap())])
.unwrap();
});
let mut model = sdf::Layer::new_in_memory("model.usda");
edit_layer(&mut model, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Model", Specifier::Def, "Xform").unwrap();
sdf::PrimSpec::new(e.data_mut(), "/Model/Orig", Specifier::Def, "").unwrap();
});
let stage = Stage::builder().make_stage(vec![root, model], 0, Vec::new());
assert!(valid(&stage, "/Ref/Geom"));
NamespaceEditor::new(&stage)
.move_prim(path("/Ref/Geom").unwrap(), path("/Ref/Orig").unwrap())
.apply()
.unwrap();
let relocates = stage.root_layer().relocates();
assert!(relocates.is_empty(), "stale relocate left authored: {relocates:?}");
assert!(valid(&stage, "/Ref/Orig"), "prim should be back at /Ref/Orig");
assert!(!valid(&stage, "/Ref/Geom"), "prim should no longer be at /Ref/Geom");
}
#[test]
fn local_retarget_relocate() {
let mut root = sdf::Layer::new_in_memory("root.usda");
edit_layer(&mut root, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Ref", Specifier::Def, "").unwrap();
e.data_mut().set_field(
&path("/Ref").unwrap(),
FieldKey::References.as_str(),
sdf::Value::ReferenceListOp(sdf::ReferenceListOp::prepended([sdf::Reference {
asset_path: "model.usda".into(),
prim_path: path("/Model").unwrap(),
..Default::default()
}])),
);
sdf::PrimSpec::new(e.data_mut(), "/Local", Specifier::Def, "").unwrap();
e.set_relocates(vec![(path("/Ref/Geom").unwrap(), path("/Local/Geom").unwrap())])
.unwrap();
});
let mut model = sdf::Layer::new_in_memory("model.usda");
edit_layer(&mut model, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Model", Specifier::Def, "Xform").unwrap();
sdf::PrimSpec::new(e.data_mut(), "/Model/Geom", Specifier::Def, "").unwrap();
});
let stage = Stage::builder().make_stage(vec![root, model], 0, Vec::new());
assert!(valid(&stage, "/Local/Geom"));
NamespaceEditor::new(&stage)
.move_prim(path("/Local").unwrap(), path("/Moved").unwrap())
.apply()
.unwrap();
let relocates = stage.root_layer().relocates();
assert!(
relocates
.iter()
.any(|(s, t)| s == &path("/Ref/Geom").unwrap() && t == &path("/Moved/Geom").unwrap()),
"relocate target should follow the local move: {relocates:?}"
);
assert!(
valid(&stage, "/Moved/Geom"),
"relocated prim should follow to /Moved/Geom"
);
assert!(!valid(&stage, "/Local/Geom"));
}
#[test]
fn local_keeps_invalid() {
let mut root = sdf::Layer::new_in_memory("root.usda");
edit_layer(&mut root, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Local", Specifier::Def, "").unwrap();
e.set_relocates(vec![(path("/B").unwrap(), path("/C").unwrap())])
.unwrap();
});
let stage = Stage::builder().make_stage(vec![root], 0, Vec::new());
NamespaceEditor::new(&stage)
.move_prim(path("/Local").unwrap(), path("/Moved").unwrap())
.apply()
.unwrap();
assert!(valid(&stage, "/Moved"));
assert!(!valid(&stage, "/Local"));
}
#[test]
fn invalid_seed_resurrection() {
let mut root = sdf::Layer::new_in_memory("root.usda");
edit_layer(&mut root, |e| {
sdf::PrimSpec::new(e.data_mut(), "/A", Specifier::Def, "").unwrap();
sdf::PrimSpec::new(e.data_mut(), "/A/X", Specifier::Def, "").unwrap();
e.set_relocates(vec![(path("/A/X").unwrap(), path("/A").unwrap())])
.unwrap();
});
let stage = Stage::builder().make_stage(vec![root], 0, Vec::new());
let mut editor = NamespaceEditor::new(&stage);
editor.move_prim(path("/A").unwrap(), path("/C").unwrap());
assert!(matches!(
editor.can_apply(),
Err(NamespaceEditError::UnrepresentableRelocateBatch(_))
));
assert!(valid(&stage, "/A"));
assert!(!valid(&stage, "/C"));
assert_eq!(
stage.root_layer().relocates(),
vec![(path("/A/X").unwrap(), path("/A").unwrap())]
);
}
#[test]
fn relocate_in_sublayer_folds() {
let mut root = sdf::Layer::new_in_memory("root.usda");
edit_layer(&mut root, |e| {
e.data_mut().set_field(
&sdf::Path::abs_root(),
FieldKey::SubLayers.as_str(),
sdf::Value::StringVec(vec!["sub.usda".into()]),
);
sdf::PrimSpec::new(e.data_mut(), "/Ref", Specifier::Def, "").unwrap();
e.data_mut().set_field(
&path("/Ref").unwrap(),
FieldKey::References.as_str(),
sdf::Value::ReferenceListOp(sdf::ReferenceListOp::prepended([sdf::Reference {
asset_path: "model.usda".into(),
prim_path: path("/Model").unwrap(),
..Default::default()
}])),
);
});
let mut sub = sdf::Layer::new_in_memory("sub.usda");
edit_layer(&mut sub, |e| {
e.set_relocates(vec![(path("/Ref/Orig").unwrap(), path("/Ref/Geom").unwrap())])
.unwrap();
});
let mut model = sdf::Layer::new_in_memory("model.usda");
edit_layer(&mut model, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Model", Specifier::Def, "Xform").unwrap();
sdf::PrimSpec::new(e.data_mut(), "/Model/Orig", Specifier::Def, "").unwrap();
});
let stage = Stage::builder().make_stage(vec![root, sub, model], 0, Vec::new());
assert!(valid(&stage, "/Ref/Geom"), "sublayer relocate should compose");
NamespaceEditor::new(&stage)
.move_prim(path("/Ref/Geom").unwrap(), path("/Ref/Final").unwrap())
.apply()
.unwrap();
assert!(valid(&stage, "/Ref/Final"), "relocated prim should move to /Ref/Final");
assert!(!valid(&stage, "/Ref/Geom"));
assert!(
stage.root_layer().relocates().is_empty(),
"root should author no relocate: {:?}",
stage.root_layer().relocates()
);
}
#[test]
fn rejects_cross_layer_conflict() {
let mut root = sdf::Layer::new_in_memory("root.usda");
edit_layer(&mut root, |e| {
e.data_mut().set_field(
&sdf::Path::abs_root(),
FieldKey::SubLayers.as_str(),
sdf::Value::StringVec(vec!["sub.usda".into()]),
);
sdf::PrimSpec::new(e.data_mut(), "/Ref", Specifier::Def, "").unwrap();
e.data_mut().set_field(
&path("/Ref").unwrap(),
FieldKey::References.as_str(),
sdf::Value::ReferenceListOp(sdf::ReferenceListOp::prepended([sdf::Reference {
asset_path: "model.usda".into(),
prim_path: path("/Model").unwrap(),
..Default::default()
}])),
);
});
let mut sub = sdf::Layer::new_in_memory("sub.usda");
edit_layer(&mut sub, |e| {
e.set_relocates(vec![(path("/Ref/C").unwrap(), path("/Ref/D").unwrap())])
.unwrap();
});
let mut model = sdf::Layer::new_in_memory("model.usda");
edit_layer(&mut model, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Model", Specifier::Def, "Xform").unwrap();
sdf::PrimSpec::new(e.data_mut(), "/Model/X", Specifier::Def, "").unwrap();
});
let stage = Stage::builder().make_stage(vec![root, sub, model], 0, Vec::new());
let mut editor = NamespaceEditor::new(&stage);
editor.move_prim(path("/Ref/X").unwrap(), path("/Ref/C").unwrap());
assert!(matches!(
editor.can_apply(),
Err(NamespaceEditError::UnrepresentableRelocateBatch(_))
));
}
#[test]
fn new_ignores_invalid() {
let mut root = sdf::Layer::new_in_memory("root.usda");
edit_layer(&mut root, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Ref", Specifier::Def, "").unwrap();
e.data_mut().set_field(
&path("/Ref").unwrap(),
FieldKey::References.as_str(),
sdf::Value::ReferenceListOp(sdf::ReferenceListOp::prepended([sdf::Reference {
asset_path: "model.usda".into(),
prim_path: path("/Model").unwrap(),
..Default::default()
}])),
);
e.set_relocates(vec![(path("/B").unwrap(), path("/C").unwrap())])
.unwrap();
});
let mut model = sdf::Layer::new_in_memory("model.usda");
edit_layer(&mut model, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Model", Specifier::Def, "Xform").unwrap();
sdf::PrimSpec::new(e.data_mut(), "/Model/X", Specifier::Def, "").unwrap();
});
let stage = Stage::builder().make_stage(vec![root, model], 0, Vec::new());
NamespaceEditor::new(&stage)
.move_prim(path("/Ref/X").unwrap(), path("/B").unwrap())
.apply()
.unwrap();
assert!(valid(&stage, "/B"));
assert!(!valid(&stage, "/Ref/X"));
}
#[test]
fn invalid_seed_stays_inert() {
let mut root = sdf::Layer::new_in_memory("root.usda");
edit_layer(&mut root, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Ref", Specifier::Def, "").unwrap();
e.data_mut().set_field(
&path("/Ref").unwrap(),
FieldKey::References.as_str(),
sdf::Value::ReferenceListOp(sdf::ReferenceListOp::prepended([sdf::Reference {
asset_path: "model.usda".into(),
prim_path: path("/Model").unwrap(),
..Default::default()
}])),
);
e.set_relocates(vec![(path("/A").unwrap(), path("/Ref/Geom").unwrap())])
.unwrap();
});
let mut model = sdf::Layer::new_in_memory("model.usda");
edit_layer(&mut model, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Model", Specifier::Def, "Xform").unwrap();
sdf::PrimSpec::new(e.data_mut(), "/Model/Geom", Specifier::Def, "").unwrap();
});
let stage = Stage::builder().make_stage(vec![root, model], 0, Vec::new());
NamespaceEditor::new(&stage)
.move_prim(path("/Ref/Geom").unwrap(), path("/Final").unwrap())
.apply()
.unwrap();
let relocates = stage.root_layer().relocates();
assert!(
relocates
.iter()
.any(|(s, t)| s == &path("/A").unwrap() && t == &path("/Ref/Geom").unwrap()),
"invalid old pair should remain unchanged: {relocates:?}"
);
assert!(
relocates
.iter()
.any(|(s, t)| s == &path("/Ref/Geom").unwrap() && t == &path("/Final").unwrap()),
"valid move pair should be authored: {relocates:?}"
);
assert!(valid(&stage, "/Final"));
assert!(!valid(&stage, "/Ref/Geom"));
}
#[test]
fn inactive_conflict_reprojects() {
let mut root = sdf::Layer::new_in_memory("root.usda");
edit_layer(&mut root, |e| {
sdf::PrimSpec::new(e.data_mut(), "/World", Specifier::Def, "").unwrap();
sdf::PrimSpec::new(e.data_mut(), "/Other", Specifier::Def, "").unwrap();
sdf::PrimSpec::new(e.data_mut(), "/Other/X", Specifier::Def, "").unwrap();
e.set_relocates(vec![
(path("/World/A").unwrap(), path("/World/C").unwrap()),
(path("/Other/X").unwrap(), path("/World/A/B").unwrap()),
])
.unwrap();
});
let stage = Stage::builder().make_stage(vec![root], 0, Vec::new());
assert!(valid(&stage, "/Other/X"));
NamespaceEditor::new(&stage)
.move_prim(path("/World").unwrap(), path("/Scene").unwrap())
.apply()
.unwrap();
let relocates = stage.root_layer().relocates();
assert!(
relocates
.iter()
.any(|(s, t)| s == &path("/Scene/A").unwrap() && t == &path("/Scene/C").unwrap()),
"conflicting pair target should follow /World -> /Scene: {relocates:?}"
);
assert!(
relocates
.iter()
.any(|(s, t)| s == &path("/Other/X").unwrap() && t == &path("/Scene/A/B").unwrap()),
"dropped pair target should follow /World -> /Scene: {relocates:?}"
);
assert!(valid(&stage, "/Other/X"));
}
#[test]
fn duplicate_source_inert() {
let mut root = sdf::Layer::new_in_memory("root.usda");
edit_layer(&mut root, |e| {
e.data_mut().set_field(
&sdf::Path::abs_root(),
FieldKey::SubLayers.as_str(),
sdf::Value::StringVec(vec!["sub.usda".into()]),
);
sdf::PrimSpec::new(e.data_mut(), "/Ref", Specifier::Def, "").unwrap();
e.data_mut().set_field(
&path("/Ref").unwrap(),
FieldKey::References.as_str(),
sdf::Value::ReferenceListOp(sdf::ReferenceListOp::prepended([sdf::Reference {
asset_path: "model.usda".into(),
prim_path: path("/Model").unwrap(),
..Default::default()
}])),
);
e.set_relocates(vec![(path("/Ref/Orig").unwrap(), path("/Ref/Strong").unwrap())])
.unwrap();
});
let mut sub = sdf::Layer::new_in_memory("sub.usda");
edit_layer(&mut sub, |e| {
e.set_relocates(vec![(path("/Ref/Orig").unwrap(), path("/Ref/Geom").unwrap())])
.unwrap();
});
let mut model = sdf::Layer::new_in_memory("model.usda");
edit_layer(&mut model, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Model", Specifier::Def, "Xform").unwrap();
sdf::PrimSpec::new(e.data_mut(), "/Model/Orig", Specifier::Def, "").unwrap();
sdf::PrimSpec::new(e.data_mut(), "/Model/Geom", Specifier::Def, "").unwrap();
});
let stage = Stage::builder().make_stage(vec![root, sub, model], 0, Vec::new());
assert!(valid(&stage, "/Ref/Strong"));
assert!(valid(&stage, "/Ref/Geom"));
NamespaceEditor::new(&stage)
.move_prim(path("/Ref/Geom").unwrap(), path("/Final").unwrap())
.apply()
.unwrap();
let root_relocates = stage.root_layer().relocates();
assert!(
root_relocates
.iter()
.any(|(s, t)| s == &path("/Ref/Geom").unwrap() && t == &path("/Final").unwrap()),
"move pair should be authored: {root_relocates:?}"
);
assert!(valid(&stage, "/Final"));
assert!(!valid(&stage, "/Ref/Geom"));
}
#[test]
fn edit_target_strength() {
let root = pcp::LayerId::from_raw(0);
let sub = pcp::LayerId::from_raw(1);
let source = path("/Ref/X").unwrap();
let weak_target = path("/Ref/T").unwrap();
let move_target = path("/Final").unwrap();
let mut seeds = HashMap::new();
seeds.insert(sub, vec![(source.clone(), weak_target.clone())]);
let mut move_plan = RelocateStackPlan::new(&[root, sub], &seeds, root);
move_plan.record_move(&source, &move_target, true).unwrap();
let combined = move_plan.combined();
assert_eq!(combined[0].pair, (source.clone(), move_target));
assert!(combined[0].fresh);
let pairs: sdf::RelocateList = combined.iter().map(|r| r.pair.clone()).collect();
let status = pcp::analyze_relocate_occurrences(&pairs);
assert_eq!(pcp::first_unrepresentable_relocate(&combined, &status), None);
let mut delete_plan = RelocateStackPlan::new(&[root, sub], &seeds, root);
delete_plan.record_delete(&source, true).unwrap();
let combined = delete_plan.combined();
assert_eq!(combined[0].pair, (source, sdf::Path::default()));
assert!(combined[0].fresh);
let pairs: sdf::RelocateList = combined.iter().map(|r| r.pair.clone()).collect();
let status = pcp::analyze_relocate_occurrences(&pairs);
assert_eq!(pcp::first_unrepresentable_relocate(&combined, &status), None);
}
#[test]
fn rejects_deleted_source() {
let mut root = sdf::Layer::new_in_memory("root.usda");
edit_layer(&mut root, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Ref", Specifier::Def, "").unwrap();
e.data_mut().set_field(
&path("/Ref").unwrap(),
FieldKey::References.as_str(),
sdf::Value::ReferenceListOp(sdf::ReferenceListOp::prepended([sdf::Reference {
asset_path: "model.usda".into(),
prim_path: path("/Model").unwrap(),
..Default::default()
}])),
);
sdf::PrimSpec::new(e.data_mut(), "/Local", Specifier::Def, "").unwrap();
});
let mut model = sdf::Layer::new_in_memory("model.usda");
edit_layer(&mut model, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Model", Specifier::Def, "Xform").unwrap();
sdf::PrimSpec::new(e.data_mut(), "/Model/Geom", Specifier::Def, "").unwrap();
sdf::PrimSpec::new(e.data_mut(), "/Model/Geom/Sub", Specifier::Def, "").unwrap();
});
let stage = Stage::builder().make_stage(vec![root, model], 0, Vec::new());
let mut editor = NamespaceEditor::new(&stage);
editor
.move_prim(path("/Ref/Geom").unwrap(), path("/A").unwrap())
.delete_prim(path("/A/Sub").unwrap())
.move_prim(path("/Local").unwrap(), path("/A/Sub").unwrap());
assert!(matches!(
editor.can_apply(),
Err(NamespaceEditError::UnrepresentableRelocateBatch(_))
));
assert!(valid(&stage, "/Local"));
assert!(!valid(&stage, "/A"));
}
#[test]
fn delete_sublayer_relocate_target() {
let mut root = sdf::Layer::new_in_memory("root.usda");
edit_layer(&mut root, |e| {
e.data_mut().set_field(
&sdf::Path::abs_root(),
FieldKey::SubLayers.as_str(),
sdf::Value::StringVec(vec!["sub.usda".into()]),
);
sdf::PrimSpec::new(e.data_mut(), "/Ref", Specifier::Def, "").unwrap();
e.data_mut().set_field(
&path("/Ref").unwrap(),
FieldKey::References.as_str(),
sdf::Value::ReferenceListOp(sdf::ReferenceListOp::prepended([sdf::Reference {
asset_path: "model.usda".into(),
prim_path: path("/Model").unwrap(),
..Default::default()
}])),
);
});
let mut sub = sdf::Layer::new_in_memory("sub.usda");
edit_layer(&mut sub, |e| {
e.set_relocates(vec![(path("/Ref/Orig").unwrap(), path("/Ref/Geom").unwrap())])
.unwrap();
});
let mut model = sdf::Layer::new_in_memory("model.usda");
edit_layer(&mut model, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Model", Specifier::Def, "Xform").unwrap();
sdf::PrimSpec::new(e.data_mut(), "/Model/Orig", Specifier::Def, "").unwrap();
});
let stage = Stage::builder().make_stage(vec![root, sub, model], 0, Vec::new());
assert!(valid(&stage, "/Ref/Geom"));
NamespaceEditor::new(&stage)
.delete_prim(path("/Ref/Geom").unwrap())
.apply()
.unwrap();
assert!(
!valid(&stage, "/Ref/Geom"),
"deleted relocated prim should no longer compose"
);
}
fn sublayer_relocate_stage(relocate: (&str, &str), model_children: &[&str]) -> Stage {
let mut root = sdf::Layer::new_in_memory("root.usda");
edit_layer(&mut root, |e| {
e.data_mut().set_field(
&sdf::Path::abs_root(),
FieldKey::SubLayers.as_str(),
sdf::Value::StringVec(vec!["sub.usda".into()]),
);
sdf::PrimSpec::new(e.data_mut(), "/Ref", Specifier::Def, "").unwrap();
e.data_mut().set_field(
&path("/Ref").unwrap(),
FieldKey::References.as_str(),
sdf::Value::ReferenceListOp(sdf::ReferenceListOp::prepended([sdf::Reference {
asset_path: "model.usda".into(),
prim_path: path("/Model").unwrap(),
..Default::default()
}])),
);
sdf::PrimSpec::new(e.data_mut(), "/Local", Specifier::Def, "").unwrap();
});
let mut sub = sdf::Layer::new_in_memory("sub.usda");
edit_layer(&mut sub, |e| {
e.set_relocates(vec![(path(relocate.0).unwrap(), path(relocate.1).unwrap())])
.unwrap();
});
let mut model = sdf::Layer::new_in_memory("model.usda");
edit_layer(&mut model, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Model", Specifier::Def, "Xform").unwrap();
for c in model_children {
sdf::PrimSpec::new(e.data_mut(), format!("/Model/{c}").as_str(), Specifier::Def, "").unwrap();
}
});
Stage::builder().make_stage(vec![root, sub, model], 0, Vec::new())
}
#[test]
fn delete_orphans_sublayer_child() {
let stage = sublayer_relocate_stage(("/Ref/Orig", "/Ref/Geom"), &["Orig"]);
assert!(valid(&stage, "/Ref/Geom"));
let mut editor = NamespaceEditor::new(&stage);
editor
.move_prim(path("/Ref/Geom").unwrap(), path("/B").unwrap())
.delete_prim(path("/Ref").unwrap());
assert!(matches!(
editor.can_apply(),
Err(NamespaceEditError::UnrepresentableRelocateBatch(_))
));
}
#[test]
fn sublayer_target_reprojects() {
let stage = sublayer_relocate_stage(("/Ref/Orig", "/Local/Geom"), &["Orig"]);
assert!(valid(&stage, "/Local/Geom"));
NamespaceEditor::new(&stage)
.move_prim(path("/Local").unwrap(), path("/Moved").unwrap())
.move_prim(path("/Moved/Geom").unwrap(), path("/Final").unwrap())
.apply()
.unwrap();
assert!(valid(&stage, "/Final"));
assert!(!valid(&stage, "/Moved/Geom"));
}
#[test]
fn rename_subroot_referenced_prim() {
let mut root = sdf::Layer::new_in_memory("root.usda");
edit_layer(&mut root, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Ref", Specifier::Def, "").unwrap();
e.data_mut().set_field(
&path("/Ref").unwrap(),
FieldKey::References.as_str(),
sdf::Value::ReferenceListOp(sdf::ReferenceListOp::prepended([sdf::Reference {
asset_path: "model.usda".into(),
prim_path: path("/Model/Geom").unwrap(),
..Default::default()
}])),
);
});
let mut model = sdf::Layer::new_in_memory("model.usda");
edit_layer(&mut model, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Model", Specifier::Def, "Xform").unwrap();
sdf::PrimSpec::new(e.data_mut(), "/Model/Geom", Specifier::Def, "Xform").unwrap();
});
let stage = Stage::builder().make_stage(vec![root, model], 0, Vec::new());
NamespaceEditor::new(&stage)
.move_prim(path("/Ref").unwrap(), path("/Ref2").unwrap())
.apply()
.unwrap();
assert!(valid(&stage, "/Ref2"));
assert!(!valid(&stage, "/Ref"));
assert!(
stage.root_layer().relocates().is_empty(),
"rename should author no relocate"
);
}
#[test]
fn move_reprojects_spec_relocates() {
let mut root = sdf::Layer::new_in_memory("root.usda");
edit_layer(&mut root, |e| {
sdf::PrimSpec::new(e.data_mut(), "/A", Specifier::Def, "").unwrap();
e.data_mut().set_field(
&path("/A").unwrap(),
FieldKey::Relocates.as_str(),
sdf::Value::Relocates(vec![(path("/A/X").unwrap(), path("/A/Y").unwrap())]),
);
});
let stage = Stage::builder().make_stage(vec![root], 0, Vec::new());
NamespaceEditor::new(&stage)
.move_prim(path("/A").unwrap(), path("/Moved").unwrap())
.apply()
.unwrap();
let value = stage
.root_layer()
.data()
.try_field(&path("/Moved").unwrap(), FieldKey::Relocates.as_str())
.unwrap()
.expect("spec-level relocates should move with the prim")
.into_owned();
let relocates = value.try_as_relocates().expect("relocates value");
assert_eq!(
relocates,
vec![(path("/Moved/X").unwrap(), path("/Moved/Y").unwrap())],
"spec-level relocate paths should reproject onto the moved prim"
);
}
#[test]
fn untouched_noop_relocate_preserved() {
let mut root = sdf::Layer::new_in_memory("root.usda");
edit_layer(&mut root, |e| {
sdf::PrimSpec::new(e.data_mut(), "/A", Specifier::Def, "").unwrap();
e.set_relocates(vec![(path("/X/Keep").unwrap(), path("/X/Keep").unwrap())])
.unwrap();
});
let stage = Stage::builder().make_stage(vec![root], 0, Vec::new());
NamespaceEditor::new(&stage)
.move_prim(path("/A").unwrap(), path("/Moved").unwrap())
.apply()
.unwrap();
assert!(
stage
.root_layer()
.relocates()
.iter()
.any(|(s, t)| s == &path("/X/Keep").unwrap() && t == &path("/X/Keep").unwrap()),
"untouched no-op relocate must be preserved: {:?}",
stage.root_layer().relocates()
);
}
#[test]
fn moved_noop_preserved() {
let mut root = sdf::Layer::new_in_memory("root.usda");
edit_layer(&mut root, |e| {
sdf::PrimSpec::new(e.data_mut(), "/A", Specifier::Def, "").unwrap();
sdf::PrimSpec::new(e.data_mut(), "/A/X", Specifier::Def, "").unwrap();
e.set_relocates(vec![(path("/A/X").unwrap(), path("/A/X").unwrap())])
.unwrap();
});
let stage = Stage::builder().make_stage(vec![root], 0, Vec::new());
NamespaceEditor::new(&stage)
.move_prim(path("/A").unwrap(), path("/B").unwrap())
.apply()
.unwrap();
assert_eq!(
stage.root_layer().relocates(),
vec![(path("/B/X").unwrap(), path("/B/X").unwrap())],
"moved no-op relocate metadata follows the namespace edit"
);
}
#[test]
fn duplicate_sublayer_seed() {
let mut root = sdf::Layer::new_in_memory("root.usda");
edit_layer(&mut root, |e| {
e.data_mut().set_field(
&sdf::Path::abs_root(),
FieldKey::SubLayers.as_str(),
sdf::Value::StringVec(vec!["sub.usda".into(), "sub.usda".into()]),
);
sdf::PrimSpec::new(e.data_mut(), "/A", Specifier::Def, "").unwrap();
});
let mut sub = sdf::Layer::new_in_memory("sub.usda");
edit_layer(&mut sub, |e| {
e.set_relocates(vec![(path("/Ref/Orig").unwrap(), path("/Ref/Geom").unwrap())])
.unwrap();
});
let stage = Stage::builder().make_stage(vec![root, sub], 0, Vec::new());
NamespaceEditor::new(&stage)
.move_prim(path("/A").unwrap(), path("/Moved").unwrap())
.apply()
.unwrap();
let relocates = stage.layer("sub.usda").expect("sub layer").relocates();
assert_eq!(
relocates,
vec![(path("/Ref/Orig").unwrap(), path("/Ref/Geom").unwrap())],
"unrelated edit must not duplicate repeated sublayer relocates"
);
}
#[test]
fn layers_to_edit_relocates() {
let stage = sublayer_relocate_stage(("/Ref/Orig", "/Ref/Geom"), &["Orig"]);
let mut editor = NamespaceEditor::new(&stage);
editor.move_prim(path("/Ref/Geom").unwrap(), path("/Ref/Final").unwrap());
let layers = editor.layers_to_edit().unwrap();
assert!(
layers.iter().any(|id| id == "sub.usda"),
"sublayer whose relocate is rewritten should be reported: {layers:?}"
);
}
fn relocate_target_masking_stage() -> Stage {
let mut root = sdf::Layer::new_in_memory("root.usda");
edit_layer(&mut root, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Ref", Specifier::Def, "").unwrap();
e.data_mut().set_field(
&path("/Ref").unwrap(),
FieldKey::References.as_str(),
sdf::Value::ReferenceListOp(sdf::ReferenceListOp::prepended([sdf::Reference {
asset_path: "model.usda".into(),
prim_path: path("/Model").unwrap(),
..Default::default()
}])),
);
e.set_relocates(vec![(path("/Ref/Orig").unwrap(), path("/Ref/Geom").unwrap())])
.unwrap();
});
let mut model = sdf::Layer::new_in_memory("model.usda");
edit_layer(&mut model, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Model", Specifier::Def, "Xform").unwrap();
sdf::PrimSpec::new(e.data_mut(), "/Model/Orig", Specifier::Def, "").unwrap();
sdf::PrimSpec::new(e.data_mut(), "/Model/Geom", Specifier::Def, "").unwrap();
});
Stage::builder().make_stage(vec![root, model], 0, Vec::new())
}
#[test]
fn rejects_masking_move() {
let stage = relocate_target_masking_stage();
assert!(valid(&stage, "/Ref/Geom"));
let mut editor = NamespaceEditor::new(&stage);
editor.move_prim(path("/Ref/Geom").unwrap(), path("/Final").unwrap());
assert!(matches!(
editor.can_apply(),
Err(NamespaceEditError::UnrepresentableRelocateBatch(_))
));
}
#[test]
fn rejects_masking_delete() {
let stage = relocate_target_masking_stage();
let mut editor = NamespaceEditor::new(&stage);
editor.delete_prim(path("/Ref/Geom").unwrap());
assert!(matches!(
editor.can_apply(),
Err(NamespaceEditError::UnrepresentableRelocateBatch(_))
));
}
#[test]
fn mapped_rejects_masking() {
let mut root = sdf::Layer::new_in_memory("root.usda");
edit_layer(&mut root, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Ref", Specifier::Def, "").unwrap();
e.data_mut().set_field(
&path("/Ref").unwrap(),
FieldKey::References.as_str(),
sdf::Value::ReferenceListOp(sdf::ReferenceListOp::prepended([sdf::Reference {
asset_path: "model.usda".into(),
prim_path: path("/Model").unwrap(),
..Default::default()
}])),
);
});
let mut model = sdf::Layer::new_in_memory("model.usda");
edit_layer(&mut model, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Model", Specifier::Def, "Xform").unwrap();
e.data_mut().set_field(
&path("/Model").unwrap(),
FieldKey::References.as_str(),
sdf::Value::ReferenceListOp(sdf::ReferenceListOp::prepended([sdf::Reference {
asset_path: "deep.usda".into(),
prim_path: path("/Deep").unwrap(),
..Default::default()
}])),
);
e.set_relocates(vec![(path("/Model/Orig").unwrap(), path("/Model/Geom").unwrap())])
.unwrap();
});
let mut deep = sdf::Layer::new_in_memory("deep.usda");
edit_layer(&mut deep, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Deep", Specifier::Def, "Xform").unwrap();
sdf::PrimSpec::new(e.data_mut(), "/Deep/Orig", Specifier::Def, "").unwrap();
sdf::PrimSpec::new(e.data_mut(), "/Deep/Geom", Specifier::Def, "").unwrap();
});
let stage = Stage::builder().make_stage(vec![root, model, deep], 0, Vec::new());
assert!(valid(&stage, "/Ref/Geom"));
let target = stage
.edit_target_for_node(&path("/Ref").unwrap(), EditTargetArc::Reference)
.unwrap();
stage.set_edit_target(target).unwrap();
let mut editor = NamespaceEditor::new(&stage);
editor.move_prim(path("/Ref/Geom").unwrap(), path("/Ref/Final").unwrap());
assert!(matches!(
editor.can_apply(),
Err(NamespaceEditError::UnrepresentableRelocateBatch(_))
));
}
#[test]
fn rejects_dropped_conflict() {
let mut root = sdf::Layer::new_in_memory("root.usda");
edit_layer(&mut root, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Ref", Specifier::Def, "").unwrap();
e.data_mut().set_field(
&path("/Ref").unwrap(),
FieldKey::References.as_str(),
sdf::Value::ReferenceListOp(sdf::ReferenceListOp::prepended([sdf::Reference {
asset_path: "model.usda".into(),
prim_path: path("/Model").unwrap(),
..Default::default()
}])),
);
e.set_relocates(vec![
(path("/Ref/A").unwrap(), path("/Ref/Geom").unwrap()),
(path("/Ref/B").unwrap(), path("/Ref/Geom").unwrap()),
])
.unwrap();
});
let mut model = sdf::Layer::new_in_memory("model.usda");
edit_layer(&mut model, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Model", Specifier::Def, "Xform").unwrap();
sdf::PrimSpec::new(e.data_mut(), "/Model/Geom", Specifier::Def, "").unwrap();
sdf::PrimSpec::new(e.data_mut(), "/Model/A", Specifier::Def, "").unwrap();
sdf::PrimSpec::new(e.data_mut(), "/Model/B", Specifier::Def, "").unwrap();
});
let stage = Stage::builder().make_stage(vec![root, model], 0, Vec::new());
assert!(valid(&stage, "/Ref/Geom"));
let mut editor = NamespaceEditor::new(&stage);
editor.move_prim(path("/Ref/Geom").unwrap(), path("/Dst").unwrap());
assert!(matches!(
editor.can_apply(),
Err(NamespaceEditError::UnrepresentableRelocateBatch(_))
));
}
#[test]
fn delete_over_dropped_chain() {
let mut root = sdf::Layer::new_in_memory("root.usda");
edit_layer(&mut root, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Ref", Specifier::Def, "").unwrap();
e.data_mut().set_field(
&path("/Ref").unwrap(),
FieldKey::References.as_str(),
sdf::Value::ReferenceListOp(sdf::ReferenceListOp::prepended([sdf::Reference {
asset_path: "model.usda".into(),
prim_path: path("/Model").unwrap(),
..Default::default()
}])),
);
e.set_relocates(vec![
(path("/Ref/X").unwrap(), path("/Ref/Geom").unwrap()),
(path("/Ref/Geom").unwrap(), path("/Ref/Y").unwrap()),
])
.unwrap();
});
let mut model = sdf::Layer::new_in_memory("model.usda");
edit_layer(&mut model, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Model", Specifier::Def, "Xform").unwrap();
sdf::PrimSpec::new(e.data_mut(), "/Model/Geom", Specifier::Def, "").unwrap();
sdf::PrimSpec::new(e.data_mut(), "/Model/X", Specifier::Def, "").unwrap();
});
let stage = Stage::builder().make_stage(vec![root, model], 0, Vec::new());
assert!(valid(&stage, "/Ref/Geom"));
let mut editor = NamespaceEditor::new(&stage);
editor.delete_prim(path("/Ref/Geom").unwrap());
assert!(matches!(
editor.can_apply(),
Err(NamespaceEditError::UnrepresentableRelocateBatch(_))
));
}
}