Fix #124263: Generate unique names during Alembic and USD export

While object names in Blender are already unique, the names themselves
may be "unsafe" for use in the various file formats. During processing
we make the names "safe". However, we did not guarantee that these new
safe names were themselves unique wrt each other. Consider object names
"Test 1" and "Test-1" which both become "Test_1" after being made safe.
These will collide during export; only 1 object would be exported and
it's undefined which object's data would "win".

To rectify this we add another name map to the hierarchy iterator which
is then used to handle collisions as they happen. The map is per-
hierarchy meaning that a name can appear more than once as long as its
under a different hierarchy. E.g.
- `/root/A/X` and another `/root/B/X` is OK
- `/root/A/X` and another `/root/A/X` is NOT OK

Pull Request: https://projects.blender.org/blender/blender/pulls/135418
This commit is contained in:
Jesse Yurkovich
2025-03-21 21:29:08 +01:00
committed by Jesse Yurkovich
parent 321dbb0115
commit 01ddb320dd
4 changed files with 133 additions and 16 deletions

View File

@@ -1258,15 +1258,11 @@ class USDExportTest(AbstractUSDTest):
("/root/Camera/Camera", "Camera"),
("/root/env_light", "DomeLight")
)
def key(el):
return el[0]
expected = tuple(sorted(expected, key=key))
expected = tuple(sorted(expected, key=lambda pair: pair[0]))
stage = Usd.Stage.Open(str(test_path))
actual = ((str(p.GetPath()), p.GetTypeName()) for p in stage.Traverse())
actual = tuple(sorted(actual, key=key))
actual = tuple(sorted(actual, key=lambda pair: pair[0]))
self.assertTupleEqual(expected, actual)
@@ -1311,14 +1307,11 @@ class USDExportTest(AbstractUSDTest):
("/root/env_light", "DomeLight")
)
def key(el):
return el[0]
expected = tuple(sorted(expected, key=key))
expected = tuple(sorted(expected, key=lambda pair: pair[0]))
stage = Usd.Stage.Open(str(test_path))
actual = ((str(p.GetPath()), p.GetTypeName()) for p in stage.Traverse())
actual = tuple(sorted(actual, key=key))
actual = tuple(sorted(actual, key=lambda pair: pair[0]))
self.assertTupleEqual(expected, actual)
@@ -1490,6 +1483,95 @@ class USDExportTest(AbstractUSDTest):
self.assertTrue(tex_path.exists(),
f"Exported texture {tex_path} doesn't exist")
def test_naming_collision_hierarchy(self):
"""Validate that naming collisions during export are handled correctly"""
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_hierarchy_collision.blend"))
export_path = self.tempdir / "usd_hierarchy_collision.usda"
self.export_and_validate(filepath=str(export_path))
expected = (
('/root', 'Xform'),
('/root/Empty', 'Xform'),
('/root/Empty/Par_002', 'Xform'),
('/root/Empty/Par_002/Par_1', 'Mesh'),
('/root/Empty/Par_003', 'Xform'),
('/root/Empty/Par_003/Par_1', 'Mesh'),
('/root/Empty/Par_004', 'Xform'),
('/root/Empty/Par_004/Par_002', 'Mesh'),
('/root/Empty/Par_1', 'Xform'),
('/root/Empty/Par_1/Par_1', 'Mesh'),
('/root/Level1', 'Xform'),
('/root/Level1/Level2', 'Xform'),
('/root/Level1/Level2/Par2_002', 'Xform'),
('/root/Level1/Level2/Par2_002/Par2_002', 'Mesh'),
('/root/Level1/Level2/Par2_1', 'Xform'),
('/root/Level1/Level2/Par2_1/Par2_1', 'Mesh'),
('/root/Level1/Par2_002', 'Xform'),
('/root/Level1/Par2_002/Par2_1', 'Mesh'),
('/root/Level1/Par2_1', 'Xform'),
('/root/Level1/Par2_1/Par2_1', 'Mesh'),
('/root/Test_002', 'Xform'),
('/root/Test_002/Test_1', 'Mesh'),
('/root/Test_003', 'Xform'),
('/root/Test_003/Test_1', 'Mesh'),
('/root/Test_004', 'Xform'),
('/root/Test_004/Test_002', 'Mesh'),
('/root/Test_1', 'Xform'),
('/root/Test_1/Test_1', 'Mesh'),
('/root/env_light', 'DomeLight'),
('/root/xSource_002', 'Xform'),
('/root/xSource_002/Dup_002', 'Xform'),
('/root/xSource_002/Dup_002/Dup_002', 'Mesh'),
('/root/xSource_002/Dup_002_0', 'Xform'),
('/root/xSource_002/Dup_002_0/Dup_002', 'Mesh'),
('/root/xSource_002/Dup_002_1', 'Xform'),
('/root/xSource_002/Dup_002_1/Dup_002', 'Mesh'),
('/root/xSource_002/Dup_002_2', 'Xform'),
('/root/xSource_002/Dup_002_2/Dup_002', 'Mesh'),
('/root/xSource_002/Dup_002_3', 'Xform'),
('/root/xSource_002/Dup_002_3/Dup_002', 'Mesh'),
('/root/xSource_002/Dup_1', 'Xform'),
('/root/xSource_002/Dup_1/Dup_1', 'Mesh'),
('/root/xSource_002/Dup_1_0', 'Xform'),
('/root/xSource_002/Dup_1_0/Dup_1', 'Mesh'),
('/root/xSource_002/Dup_1_1', 'Xform'),
('/root/xSource_002/Dup_1_1/Dup_1', 'Mesh'),
('/root/xSource_002/Dup_1_2', 'Xform'),
('/root/xSource_002/Dup_1_2/Dup_1', 'Mesh'),
('/root/xSource_002/Dup_1_3', 'Xform'),
('/root/xSource_002/Dup_1_3/Dup_1', 'Mesh'),
('/root/xSource_002/xSource_1', 'Mesh'),
('/root/xSource_1', 'Xform'),
('/root/xSource_1/Dup_002', 'Xform'),
('/root/xSource_1/Dup_002/Dup_1', 'Mesh'),
('/root/xSource_1/Dup_1', 'Xform'),
('/root/xSource_1/Dup_1/Dup_1', 'Mesh'),
('/root/xSource_1/Dup_1_0', 'Xform'),
('/root/xSource_1/Dup_1_0/Dup_1', 'Mesh'),
('/root/xSource_1/Dup_1_001', 'Xform'),
('/root/xSource_1/Dup_1_001/Dup_1', 'Mesh'),
('/root/xSource_1/Dup_1_002', 'Xform'),
('/root/xSource_1/Dup_1_002/Dup_1', 'Mesh'),
('/root/xSource_1/Dup_1_003', 'Xform'),
('/root/xSource_1/Dup_1_003/Dup_1', 'Mesh'),
('/root/xSource_1/Dup_1_004', 'Xform'),
('/root/xSource_1/Dup_1_004/Dup_1', 'Mesh'),
('/root/xSource_1/Dup_1_1', 'Xform'),
('/root/xSource_1/Dup_1_1/Dup_1', 'Mesh'),
('/root/xSource_1/Dup_1_2', 'Xform'),
('/root/xSource_1/Dup_1_2/Dup_1', 'Mesh'),
('/root/xSource_1/Dup_1_3', 'Xform'),
('/root/xSource_1/Dup_1_3/Dup_1', 'Mesh'),
('/root/xSource_1/xSource_1', 'Mesh')
)
expected = tuple(sorted(expected, key=lambda pair: pair[0]))
stage = Usd.Stage.Open(str(export_path))
actual = ((str(p.GetPath()), p.GetTypeName()) for p in stage.Traverse())
actual = tuple(sorted(actual, key=lambda pair: pair[0]))
self.assertTupleEqual(expected, actual)
class USDHookBase:
instructions = {}