diff --git a/CMakeLists.txt b/CMakeLists.txt index 0f51488..f9e4dbe 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -192,6 +192,11 @@ if(FREEUSD_BUILD_TESTS) target_compile_definitions( freeusd_usd_vol_volume_test PRIVATE "FREEUSD_TEST_FIXTURES_DIR=\"${CMAKE_SOURCE_DIR}/tests/fixtures\"") + add_executable(freeusd_usd_semantics_labels_test tests/usd_semantics_labels_test.cpp) + target_link_libraries(freeusd_usd_semantics_labels_test PRIVATE freeusd::usd freeusd::usdSemantics) + target_compile_definitions( + freeusd_usd_semantics_labels_test PRIVATE "FREEUSD_TEST_FIXTURES_DIR=\"${CMAKE_SOURCE_DIR}/tests/fixtures\"") + add_executable(freeusd_ar_path_security_test tests/ar_path_security_test.cpp) target_link_libraries(freeusd_ar_path_security_test PRIVATE freeusd::ar) @@ -201,6 +206,11 @@ if(FREEUSD_BUILD_TESTS) target_compile_definitions( freeusd_engine_integration_test PRIVATE "FREEUSD_TEST_FIXTURES_DIR=\"${CMAKE_SOURCE_DIR}/tests/fixtures\"") + add_executable(freeusd_usdutils_spatial_grounding_test tests/usdutils_spatial_grounding_test.cpp) + target_link_libraries(freeusd_usdutils_spatial_grounding_test PRIVATE freeusd::usd freeusd::usdUtils) + target_compile_definitions( + freeusd_usdutils_spatial_grounding_test PRIVATE "FREEUSD_TEST_FIXTURES_DIR=\"${CMAKE_SOURCE_DIR}/tests/fixtures\"") + add_executable(freeusd_schema_tokens_bundle_test tests/schema_tokens_bundle_test.cpp) target_link_libraries(freeusd_schema_tokens_bundle_test PRIVATE freeusd::usd_schemas) @@ -273,8 +283,10 @@ if(FREEUSD_BUILD_TESTS) add_test(NAME freeusd_usd_physics_fixed_joint_test COMMAND freeusd_usd_physics_fixed_joint_test) add_test(NAME freeusd_usd_vol_openvdb_test COMMAND freeusd_usd_vol_openvdb_test) add_test(NAME freeusd_usd_vol_volume_test COMMAND freeusd_usd_vol_volume_test) + add_test(NAME freeusd_usd_semantics_labels_test COMMAND freeusd_usd_semantics_labels_test) add_test(NAME freeusd_ar_path_security_test COMMAND freeusd_ar_path_security_test) add_test(NAME freeusd_engine_integration_test COMMAND freeusd_engine_integration_test) + add_test(NAME freeusd_usdutils_spatial_grounding_test COMMAND freeusd_usdutils_spatial_grounding_test) add_test(NAME freeusd_schema_tokens_bundle_test COMMAND freeusd_schema_tokens_bundle_test) add_test(NAME freeusd_time_code_test COMMAND freeusd_time_code_test) add_test(NAME freeusd_edit_target_test COMMAND freeusd_edit_target_test) @@ -371,6 +383,22 @@ if(FREEUSD_BUILD_TESTS) set_target_properties(freeusd_c_engine_integration PROPERTIES C_STANDARD 11 C_STANDARD_REQUIRED ON LINKER_LANGUAGE CXX) add_test(NAME freeusd_c_engine_integration COMMAND freeusd_c_engine_integration) + add_executable(freeusd_c_usdutils_spatial_grounding tests/c_usdutils_spatial_grounding.c) + target_include_directories(freeusd_c_usdutils_spatial_grounding PRIVATE "${PROJECT_SOURCE_DIR}/include") + target_link_libraries(freeusd_c_usdutils_spatial_grounding PRIVATE freeusd::c) + target_compile_definitions( + freeusd_c_usdutils_spatial_grounding PRIVATE "FREEUSD_TEST_FIXTURES_DIR=\"${CMAKE_SOURCE_DIR}/tests/fixtures\"") + set_target_properties(freeusd_c_usdutils_spatial_grounding PROPERTIES C_STANDARD 11 C_STANDARD_REQUIRED ON LINKER_LANGUAGE CXX) + add_test(NAME freeusd_c_usdutils_spatial_grounding COMMAND freeusd_c_usdutils_spatial_grounding) + + add_executable(freeusd_c_usd_semantics_labels tests/c_usd_semantics_labels.c) + target_include_directories(freeusd_c_usd_semantics_labels PRIVATE "${PROJECT_SOURCE_DIR}/include") + target_link_libraries(freeusd_c_usd_semantics_labels PRIVATE freeusd::c) + target_compile_definitions( + freeusd_c_usd_semantics_labels PRIVATE "FREEUSD_TEST_FIXTURES_DIR=\"${CMAKE_SOURCE_DIR}/tests/fixtures\"") + set_target_properties(freeusd_c_usd_semantics_labels PROPERTIES C_STANDARD 11 C_STANDARD_REQUIRED ON LINKER_LANGUAGE CXX) + add_test(NAME freeusd_c_usd_semantics_labels COMMAND freeusd_c_usd_semantics_labels) + add_executable(freeusd_c_usd_skel tests/c_usd_skel.c) target_include_directories(freeusd_c_usd_skel PRIVATE "${PROJECT_SOURCE_DIR}/include") target_link_libraries(freeusd_c_usd_skel PRIVATE freeusd::c) @@ -470,6 +498,7 @@ if(FREEUSD_BUILD_TESTS) freeusd_usd_physics_fixed_joint_test freeusd_usd_vol_openvdb_test freeusd_usd_vol_volume_test + freeusd_usd_semantics_labels_test freeusd_usd_skel_smoke_test freeusd_usd_skel_skinning_matrices_test freeusd_usd_shade_smoke_test @@ -477,6 +506,7 @@ if(FREEUSD_BUILD_TESTS) freeusd_usd_lux_distant_light_test freeusd_ar_path_security_test freeusd_engine_integration_test + freeusd_usdutils_spatial_grounding_test freeusd_schema_tokens_bundle_test freeusd_time_code_test freeusd_edit_target_test @@ -504,6 +534,8 @@ if(FREEUSD_BUILD_TESTS) freeusd_c_stage_prim_specifier freeusd_c_stage_layer_hints freeusd_c_engine_integration + freeusd_c_usdutils_spatial_grounding + freeusd_c_usd_semantics_labels freeusd_c_usd_skel freeusd_c_usd_shade freeusd_c_usd_lux diff --git a/bindings/README.md b/bindings/README.md index 2c3ae5f..831bd42 100644 --- a/bindings/README.md +++ b/bindings/README.md @@ -66,6 +66,10 @@ Shared fixture: **`tests/fixtures/usd_cross_language.usda`**. Tests: **`tests/c_ Skel fixture: **`tests/fixtures/parity_skel_skinning.usda`**. Tests: **`tests/c_usd_skel.c`**, **`tests/usd_skel_smoke_test.cpp`**, **`bindings/go/freeusd_test.go`** (`TestSkelCrossLanguageContract`), **`bindings/rust`** (`skel_cross_language_contract`). +Spatial grounding fixture: **`tests/fixtures/parity_spatial_grounding.usda`**. Tests: **`tests/c_usdutils_spatial_grounding.c`**, **`tests/usdutils_spatial_grounding_test.cpp`**, **`tests/python/test_engine_integration.py`**, **`bindings/go/freeusd_test.go`**, and **`bindings/rust`**. + +Semantic labels fixture: **`tests/fixtures/parity_semantics_labels.usda`**. Tests: **`tests/c_usd_semantics_labels.c`**, **`tests/usd_semantics_labels_test.cpp`**, **`tests/python/test_engine_integration.py`**, **`bindings/go/freeusd_test.go`**, and **`bindings/rust`**. + | Capability | C ABI | C++ / Python | Go | Rust | D | |------------|:-----:|:------------:|:--:|:----:|:--:| | Typed field reads (double/float/bool/int/string/vec/matrix/quat/token) | yes | yes | yes | yes | double/string | @@ -86,6 +90,8 @@ Skel fixture: **`tests/fixtures/parity_skel_skinning.usda`**. Tests: **`tests/c_ | `usdPhysics` FixedJoint bodies / enabled (C ABI) | yes | yes | yes | yes | yes | | `usdSkel` joint names / skinning palette / CPU deform | yes | yes | yes | yes | no | | `usdUtils` engine runtime assess (skel flags) | yes | yes | yes | yes | no | +| `usdUtils` spatial grounding context records | yes | yes | yes | yes | no | +| `usdSemantics` semantic label reads | yes | yes | yes | yes | no | | `usdUtils` flatten | C++ only | yes | n/a | n/a | n/a | ## Adding more bindings diff --git a/bindings/d/dub.json b/bindings/d/dub.json index bc98f51..aea6578 100644 --- a/bindings/d/dub.json +++ b/bindings/d/dub.json @@ -14,6 +14,7 @@ "-lfreeusd_usdLux", "-lfreeusd_usdPhysics", "-lfreeusd_usdVol", + "-lfreeusd_usdSemantics", "-lfreeusd_usdGeom", "-lfreeusd_usd", "-lfreeusd_ar", @@ -39,6 +40,7 @@ "-lfreeusd_usdLux", "-lfreeusd_usdPhysics", "-lfreeusd_usdVol", + "-lfreeusd_usdSemantics", "-lfreeusd_usdGeom", "-lfreeusd_usd", "-lfreeusd_ar", diff --git a/bindings/go/freeusd.go b/bindings/go/freeusd.go index 8110069..a4e2419 100644 --- a/bindings/go/freeusd.go +++ b/bindings/go/freeusd.go @@ -10,8 +10,8 @@ package freeusd /* #cgo CFLAGS: -I${SRCDIR}/../../include -#cgo linux LDFLAGS: -L${SRCDIR}/../../build/src -lfreeusd_c -lfreeusd_base -lfreeusd_usdUtils -lfreeusd_usdSkel -lfreeusd_usdShade -lfreeusd_usdLux -lfreeusd_usdPhysics -lfreeusd_usdVol -lfreeusd_usdGeom -lfreeusd_usd -lfreeusd_ar -lfreeusd_io -lfreeusd_pcp -lfreeusd_sdf -lfreeusd_vt -lfreeusd_tf -lfreeusd_gf -lfreeusd_plug -lstdc++ -lm -lz -llz4 -#cgo darwin LDFLAGS: -L${SRCDIR}/../../build/src -lfreeusd_c -lfreeusd_base -lfreeusd_usdUtils -lfreeusd_usdSkel -lfreeusd_usdShade -lfreeusd_usdLux -lfreeusd_usdPhysics -lfreeusd_usdVol -lfreeusd_usdGeom -lfreeusd_usd -lfreeusd_ar -lfreeusd_io -lfreeusd_pcp -lfreeusd_sdf -lfreeusd_vt -lfreeusd_tf -lfreeusd_gf -lfreeusd_plug -lc++ -lm -lz -llz4 +#cgo linux LDFLAGS: -L${SRCDIR}/../../build/src -lfreeusd_c -lfreeusd_base -lfreeusd_usdUtils -lfreeusd_usdSkel -lfreeusd_usdShade -lfreeusd_usdLux -lfreeusd_usdPhysics -lfreeusd_usdVol -lfreeusd_usdSemantics -lfreeusd_usdGeom -lfreeusd_usd -lfreeusd_ar -lfreeusd_io -lfreeusd_pcp -lfreeusd_sdf -lfreeusd_vt -lfreeusd_tf -lfreeusd_gf -lfreeusd_plug -lstdc++ -lm -lz -llz4 +#cgo darwin LDFLAGS: -L${SRCDIR}/../../build/src -lfreeusd_c -lfreeusd_base -lfreeusd_usdUtils -lfreeusd_usdSkel -lfreeusd_usdShade -lfreeusd_usdLux -lfreeusd_usdPhysics -lfreeusd_usdVol -lfreeusd_usdSemantics -lfreeusd_usdGeom -lfreeusd_usd -lfreeusd_ar -lfreeusd_io -lfreeusd_pcp -lfreeusd_sdf -lfreeusd_vt -lfreeusd_tf -lfreeusd_gf -lfreeusd_plug -lc++ -lm -lz -llz4 #include #include */ @@ -1834,6 +1834,7 @@ type EngineRuntimeSupport struct { UsesPhysicsFixedJoints bool UsesOpenVdbAssets bool UsesVolumes bool + UsesSemanticLabels bool } // AssessEngineRuntimeSupport reports which engine integration mode fits the composed stage. @@ -1877,9 +1878,153 @@ func (s *Stage) AssessEngineRuntimeSupport() (report EngineRuntimeSupport, rc in report.UsesPhysicsFixedJoints = raw.uses_physics_fixed_joints != 0 report.UsesOpenVdbAssets = raw.uses_open_vdb_assets != 0 report.UsesVolumes = raw.uses_volumes != 0 + report.UsesSemanticLabels = raw.uses_semantic_labels != 0 return report, 0 } +// SemanticLabelSet contains one authored semantics:labels: value. +type SemanticLabelSet struct { + Name string + Labels []string +} + +// SpatialGroundingRecord contains clean-room USD-derived cues for text/spatial diagnostics. +type SpatialGroundingRecord struct { + Path string + Name string + ParentPath string + SiblingNames []string + SemanticLabelSets []SemanticLabelSet + WorldPosition [3]float64 + HasWorldBound bool + WorldBoundDimensions [3]float64 + HasMassKg bool + MassKg float64 +} + +// BuildSpatialGroundingContext extracts spatial/text cue records for every traversed prim. +func (s *Stage) BuildSpatialGroundingContext(time float64) (records []SpatialGroundingRecord, rc int) { + if s == nil || s.ptr == nil { + return nil, 1 + } + var raw *C.FreeusdSpatialGroundingRecord + var count C.size_t + rc = int(C.freeusd_usdutils_build_spatial_grounding_context(s.ptr, C.double(time), &raw, &count)) + if rc != 0 { + return nil, rc + } + defer C.freeusd_usdutils_spatial_grounding_records_free(raw, count) + if raw == nil || count == 0 { + return []SpatialGroundingRecord{}, 0 + } + cRecords := unsafe.Slice(raw, int(count)) + records = make([]SpatialGroundingRecord, 0, int(count)) + for _, item := range cRecords { + rec := SpatialGroundingRecord{ + Path: C.GoString(item.path_utf8), + Name: C.GoString(item.name_utf8), + ParentPath: C.GoString(item.parent_path_utf8), + WorldPosition: [3]float64{ + float64(item.world_position[0]), + float64(item.world_position[1]), + float64(item.world_position[2]), + }, + HasWorldBound: item.has_world_bound != 0, + WorldBoundDimensions: [3]float64{ + float64(item.world_bound_dimensions[0]), + float64(item.world_bound_dimensions[1]), + float64(item.world_bound_dimensions[2]), + }, + HasMassKg: item.has_mass_kg != 0, + MassKg: float64(item.mass_kg), + } + if item.sibling_names_utf8 != nil && item.sibling_name_count > 0 { + cSiblings := unsafe.Slice(item.sibling_names_utf8, int(item.sibling_name_count)) + rec.SiblingNames = make([]string, 0, int(item.sibling_name_count)) + for _, sibling := range cSiblings { + if sibling != nil { + rec.SiblingNames = append(rec.SiblingNames, C.GoString(sibling)) + } + } + } + if item.semantic_label_sets != nil && item.semantic_label_set_count > 0 { + cSets := unsafe.Slice(item.semantic_label_sets, int(item.semantic_label_set_count)) + rec.SemanticLabelSets = make([]SemanticLabelSet, 0, int(item.semantic_label_set_count)) + for _, cSet := range cSets { + set := SemanticLabelSet{Name: C.GoString(cSet.name_utf8)} + if cSet.labels_utf8 != nil && cSet.label_count > 0 { + cLabels := unsafe.Slice(cSet.labels_utf8, int(cSet.label_count)) + set.Labels = make([]string, 0, int(cSet.label_count)) + for _, label := range cLabels { + if label != nil { + set.Labels = append(set.Labels, C.GoString(label)) + } + } + } + rec.SemanticLabelSets = append(rec.SemanticLabelSets, set) + } + } + records = append(records, rec) + } + return records, 0 +} + +// ListSemanticLabelSets returns authored semantics:labels: instance names for a prim. +func (s *Stage) ListSemanticLabelSets(primPath string) (sets []string, rc int) { + if s == nil || s.ptr == nil { + return nil, 1 + } + pp := C.CString(primPath) + defer C.free(unsafe.Pointer(pp)) + var arr **C.char + var count C.size_t + rc = int(C.freeusd_stage_list_semantic_label_sets(s.ptr, pp, &arr, &count)) + if rc != 0 { + return nil, rc + } + if arr == nil || count == 0 { + return []string{}, 0 + } + defer C.freeusd_path_list_free(arr, count) + items := unsafe.Slice(arr, int(count)) + sets = make([]string, 0, int(count)) + for _, item := range items { + if item != nil { + sets = append(sets, C.GoString(item)) + } + } + return sets, 0 +} + +// ReadSemanticLabels returns authored labels for one semantics:labels: instance. +func (s *Stage) ReadSemanticLabels(primPath, instanceName string) (labels []string, rc int) { + if s == nil || s.ptr == nil { + return nil, 1 + } + pp := C.CString(primPath) + defer C.free(unsafe.Pointer(pp)) + in := C.CString(instanceName) + defer C.free(unsafe.Pointer(in)) + var arr **C.char + var count C.size_t + rc = int(C.freeusd_stage_read_semantic_labels(s.ptr, pp, in, &arr, &count)) + if rc != 0 { + return nil, rc + } + if arr == nil || count == 0 { + return []string{}, 0 + } + defer C.freeusd_path_list_free(arr, count) + items := unsafe.Slice(arr, int(count)) + labels = make([]string, 0, int(count)) + for _, item := range items { + if item != nil { + labels = append(labels, C.GoString(item)) + } + } + return labels, 0 +} + // ReadSkelJointNames returns skeleton joint names (rc 0 ok). func (s *Stage) ReadSkelJointNames(skeletonPath string) (names []string, rc int) { if s == nil || s.ptr == nil { diff --git a/bindings/go/freeusd_test.go b/bindings/go/freeusd_test.go index 23e1e1c..1ab3357 100644 --- a/bindings/go/freeusd_test.go +++ b/bindings/go/freeusd_test.go @@ -2,6 +2,7 @@ package freeusd import ( "encoding/binary" + "math" "os" "path/filepath" "strings" @@ -1260,9 +1261,95 @@ func TestUsdGeomEngineSubsetParityImageable(t *testing.T) { t.Fatalf("world bounds (%g,%g,%g)-(%g,%g,%g)", minX, minY, minZ, maxX, maxY, maxZ) } - _, _, _, _, _, _, rc = st.ComputeBoundableWorldBounds("/World", 1.0) - if rc == 0 { - t.Fatal("expected NOT_FOUND for non-boundable /World") + // Xform prims without local bounds aggregate descendant world bounds. + wMinX, wMinY, wMinZ, wMaxX, wMaxY, wMaxZ, rc := st.ComputeBoundableWorldBounds("/World", 1.0) + if rc != 0 || wMinX != 0 || wMinY != 1 || wMinZ != 2 || wMaxX != 2 || wMaxY != 3 || wMaxZ != 4 { + t.Fatalf("aggregated /World bounds (%g,%g,%g)-(%g,%g,%g) rc=%d", wMinX, wMinY, wMinZ, wMaxX, wMaxY, wMaxZ, rc) + } +} + +func hasString(items []string, want string) bool { + for _, item := range items { + if item == want { + return true + } + } + return false +} + +func TestUsdUtilsSpatialGroundingContext(t *testing.T) { + fixture := filepath.Join("..", "..", "tests", "fixtures", "parity_spatial_grounding.usda") + st := OpenStageFromRootFile(fixture, RootSubDepthFirst) + if st == nil { + t.Fatalf("OpenStageFromRootFile: %s", LastErrorMessage()) + } + defer st.Free() + + records, rc := st.BuildSpatialGroundingContext(1.0) + if rc != 0 { + t.Fatalf("BuildSpatialGroundingContext rc=%d %s", rc, LastErrorMessage()) + } + if len(records) != 5 { + t.Fatalf("record count=%d", len(records)) + } + var cup *SpatialGroundingRecord + var kitchen *SpatialGroundingRecord + for i := range records { + switch records[i].Path { + case "/World/Kitchen/CupBlue": + cup = &records[i] + case "/World/Kitchen": + kitchen = &records[i] + } + } + if cup == nil || cup.Name != "CupBlue" || cup.ParentPath != "/World/Kitchen" { + t.Fatalf("cup record=%+v", cup) + } + if len(cup.SiblingNames) != 2 || !hasString(cup.SiblingNames, "PlateGreen") || !hasString(cup.SiblingNames, "Stove") { + t.Fatalf("cup siblings=%v", cup.SiblingNames) + } + if len(cup.SemanticLabelSets) != 2 || cup.SemanticLabelSets[0].Name != "engine" || !hasString(cup.SemanticLabelSets[0].Labels, "pickup") || cup.SemanticLabelSets[1].Name != "somaHome" || !hasString(cup.SemanticLabelSets[1].Labels, "Crockery") { + t.Fatalf("cup semantic label sets=%v", cup.SemanticLabelSets) + } + if cup.WorldPosition != [3]float64{6, 2, 3} || !cup.HasWorldBound || cup.WorldBoundDimensions != [3]float64{0.5, 1.5, 0.25} || !cup.HasMassKg || math.Abs(cup.MassKg-0.35) > 1e-6 { + t.Fatalf("cup spatial values=%+v", cup) + } + // Xform prims without local bounds aggregate descendant world bounds. + if kitchen == nil || kitchen.ParentPath != "/World" || len(kitchen.SiblingNames) != 0 || len(kitchen.SemanticLabelSets) != 0 || kitchen.WorldPosition != [3]float64{10, 0, 0} || !kitchen.HasWorldBound || kitchen.WorldBoundDimensions != [3]float64{9.25, 4.25, 7.5} || kitchen.HasMassKg { + t.Fatalf("kitchen record=%+v", kitchen) + } +} + +func TestUsdSemanticsLabels(t *testing.T) { + fixture := filepath.Join("..", "..", "tests", "fixtures", "parity_semantics_labels.usda") + st := OpenStageFromRootFile(fixture, RootSubDepthFirst) + if st == nil { + t.Fatalf("OpenStageFromRootFile: %s", LastErrorMessage()) + } + defer st.Free() + + sets, rc := st.ListSemanticLabelSets("/World/Kitchen/CupBlue") + if rc != 0 { + t.Fatalf("ListSemanticLabelSets rc=%d %s", rc, LastErrorMessage()) + } + if len(sets) != 2 || !hasString(sets, "engine") || !hasString(sets, "somaHome") { + t.Fatalf("sets=%v", sets) + } + labels, rc := st.ReadSemanticLabels("/World/Kitchen/CupBlue", "somaHome") + if rc != 0 || len(labels) != 2 || labels[0] != "Crockery" || labels[1] != "DesignedContainer" { + t.Fatalf("labels=%v rc=%d", labels, rc) + } + stoveLabels, rc := st.ReadSemanticLabels("/World/Kitchen/Stove", "somaHome") + if rc != 0 || len(stoveLabels) != 1 || stoveLabels[0] != "Appliance" { + t.Fatalf("stoveLabels=%v rc=%d", stoveLabels, rc) + } + missing, rc := st.ReadSemanticLabels("/World/Kitchen/CupBlue", "missing") + if rc != 0 || len(missing) != 0 { + t.Fatalf("missing=%v rc=%d", missing, rc) + } + report, rc := st.AssessEngineRuntimeSupport() + if rc != 0 || !report.UsesSemanticLabels || report.RecommendedMode != EngineRuntimeHybrid { + t.Fatalf("semantic runtime report=%+v rc=%d", report, rc) } } diff --git a/bindings/rust/freeusd-sys/build.rs b/bindings/rust/freeusd-sys/build.rs index 6f0f7e7..46426e0 100644 --- a/bindings/rust/freeusd-sys/build.rs +++ b/bindings/rust/freeusd-sys/build.rs @@ -65,6 +65,7 @@ fn main() { "freeusd_usdLux", "freeusd_usdPhysics", "freeusd_usdVol", + "freeusd_usdSemantics", "freeusd_usdGeom", "freeusd_usd", "freeusd_ar", diff --git a/bindings/rust/freeusd-sys/src/lib.rs b/bindings/rust/freeusd-sys/src/lib.rs index 3473f64..27eb807 100644 --- a/bindings/rust/freeusd-sys/src/lib.rs +++ b/bindings/rust/freeusd-sys/src/lib.rs @@ -767,6 +767,29 @@ extern "C" { stage: *const FreeusdStage, out: *mut FreeusdEngineRuntimeSupport, ) -> c_int; + fn freeusd_usdutils_build_spatial_grounding_context( + stage: *const FreeusdStage, + time: c_double, + out_records: *mut *mut FreeusdSpatialGroundingRecord, + out_count: *mut usize, + ) -> c_int; + fn freeusd_usdutils_spatial_grounding_records_free( + records: *mut FreeusdSpatialGroundingRecord, + count: usize, + ); + fn freeusd_stage_list_semantic_label_sets( + stage: *const FreeusdStage, + prim_path: *const c_char, + out_strings: *mut *mut *mut c_char, + out_count: *mut usize, + ) -> c_int; + fn freeusd_stage_read_semantic_labels( + stage: *const FreeusdStage, + prim_path: *const c_char, + instance_name: *const c_char, + out_strings: *mut *mut *mut c_char, + out_count: *mut usize, + ) -> c_int; } #[repr(C)] @@ -803,10 +826,55 @@ pub struct FreeusdEngineRuntimeSupport { pub uses_physics_fixed_joints: c_int, pub uses_open_vdb_assets: c_int, pub uses_volumes: c_int, + pub uses_semantic_labels: c_int, } pub type EngineRuntimeSupport = FreeusdEngineRuntimeSupport; +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct FreeusdSemanticLabelSet { + pub name_utf8: *mut c_char, + pub labels_utf8: *mut *mut c_char, + pub label_count: usize, +} + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct FreeusdSpatialGroundingRecord { + pub path_utf8: *mut c_char, + pub name_utf8: *mut c_char, + pub parent_path_utf8: *mut c_char, + pub sibling_names_utf8: *mut *mut c_char, + pub sibling_name_count: usize, + pub semantic_label_sets: *mut FreeusdSemanticLabelSet, + pub semantic_label_set_count: usize, + pub world_position: [c_double; 3], + pub has_world_bound: c_int, + pub world_bound_dimensions: [c_double; 3], + pub has_mass_kg: c_int, + pub mass_kg: c_double, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct SemanticLabelSet { + pub name: String, + pub labels: Vec, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct SpatialGroundingRecord { + pub path: String, + pub name: String, + pub parent_path: String, + pub sibling_names: Vec, + pub semantic_label_sets: Vec, + pub world_position: [f64; 3], + pub has_world_bound: bool, + pub world_bound_dimensions: [f64; 3], + pub mass_kg: Option, +} + #[repr(C)] #[derive(Debug, Clone, Copy, Default)] pub struct FreeusdLuxDistantLightSample { @@ -2373,6 +2441,187 @@ impl Stage { } } + pub fn build_spatial_grounding_context( + &self, + time: f64, + ) -> Result, i32> { + fn string_from_ptr(p: *const c_char) -> String { + if p.is_null() { + String::new() + } else { + unsafe { CStr::from_ptr(p).to_string_lossy().into_owned() } + } + } + + let mut raw: *mut FreeusdSpatialGroundingRecord = ptr::null_mut(); + let mut count: usize = 0; + let rc = unsafe { + freeusd_usdutils_build_spatial_grounding_context( + self.ptr as *const FreeusdStage, + time as c_double, + &mut raw, + &mut count, + ) + }; + if rc != 0 { + return Err(rc as i32); + } + if raw.is_null() || count == 0 { + return Ok(Vec::new()); + } + let rows = unsafe { std::slice::from_raw_parts(raw, count) }; + let mut out = Vec::with_capacity(count); + for row in rows { + let siblings = if row.sibling_names_utf8.is_null() || row.sibling_name_count == 0 { + Vec::new() + } else { + let sibling_ptrs = unsafe { + std::slice::from_raw_parts(row.sibling_names_utf8, row.sibling_name_count) + }; + sibling_ptrs + .iter() + .filter_map(|&p| { + if p.is_null() { + None + } else { + Some(unsafe { CStr::from_ptr(p).to_string_lossy().into_owned() }) + } + }) + .collect() + }; + let semantic_label_sets = + if row.semantic_label_sets.is_null() || row.semantic_label_set_count == 0 { + Vec::new() + } else { + let set_rows = unsafe { + std::slice::from_raw_parts( + row.semantic_label_sets, + row.semantic_label_set_count, + ) + }; + set_rows + .iter() + .map(|set| { + let labels = if set.labels_utf8.is_null() || set.label_count == 0 { + Vec::new() + } else { + let label_ptrs = unsafe { + std::slice::from_raw_parts(set.labels_utf8, set.label_count) + }; + label_ptrs + .iter() + .filter_map(|&p| { + if p.is_null() { + None + } else { + Some(unsafe { + CStr::from_ptr(p).to_string_lossy().into_owned() + }) + } + }) + .collect() + }; + SemanticLabelSet { + name: string_from_ptr(set.name_utf8), + labels, + } + }) + .collect() + }; + out.push(SpatialGroundingRecord { + path: string_from_ptr(row.path_utf8), + name: string_from_ptr(row.name_utf8), + parent_path: string_from_ptr(row.parent_path_utf8), + sibling_names: siblings, + semantic_label_sets, + world_position: [ + row.world_position[0], + row.world_position[1], + row.world_position[2], + ], + has_world_bound: row.has_world_bound != 0, + world_bound_dimensions: [ + row.world_bound_dimensions[0], + row.world_bound_dimensions[1], + row.world_bound_dimensions[2], + ], + mass_kg: if row.has_mass_kg != 0 { + Some(row.mass_kg) + } else { + None + }, + }); + } + unsafe { + freeusd_usdutils_spatial_grounding_records_free(raw, count); + } + Ok(out) + } + + pub fn list_semantic_label_sets(&self, prim_path: &str) -> Result, i32> { + let pp = CString::new(prim_path).map_err(|_| 1)?; + let mut arr: *mut *mut c_char = ptr::null_mut(); + let mut n: usize = 0; + let rc = unsafe { + freeusd_stage_list_semantic_label_sets( + self.ptr as *const FreeusdStage, + pp.as_ptr(), + &mut arr, + &mut n, + ) + }; + if rc != 0 { + return Err(rc as i32); + } + if arr.is_null() || n == 0 { + return Ok(Vec::new()); + } + let slice = unsafe { std::slice::from_raw_parts(arr, n) }; + let mut out = Vec::with_capacity(n); + for &p in slice { + if !p.is_null() { + out.push(unsafe { CStr::from_ptr(p).to_string_lossy().into_owned() }); + } + } + unsafe { freeusd_path_list_free(arr, n) }; + Ok(out) + } + + pub fn read_semantic_labels( + &self, + prim_path: &str, + instance_name: &str, + ) -> Result, i32> { + let pp = CString::new(prim_path).map_err(|_| 1)?; + let inst = CString::new(instance_name).map_err(|_| 1)?; + let mut arr: *mut *mut c_char = ptr::null_mut(); + let mut n: usize = 0; + let rc = unsafe { + freeusd_stage_read_semantic_labels( + self.ptr as *const FreeusdStage, + pp.as_ptr(), + inst.as_ptr(), + &mut arr, + &mut n, + ) + }; + if rc != 0 { + return Err(rc as i32); + } + if arr.is_null() || n == 0 { + return Ok(Vec::new()); + } + let slice = unsafe { std::slice::from_raw_parts(arr, n) }; + let mut out = Vec::with_capacity(n); + for &p in slice { + if !p.is_null() { + out.push(unsafe { CStr::from_ptr(p).to_string_lossy().into_owned() }); + } + } + unsafe { freeusd_path_list_free(arr, n) }; + Ok(out) + } + /// Resolve a Material `outputs:surface` connection to the connected shader prim path. pub fn read_material_surface_shader_path(&self, material_path: &str) -> Result { let mp = CString::new(material_path).map_err(|_| 1)?; @@ -3988,12 +4237,101 @@ def Xform "Root" (max_x - 2.0).abs() < 1e-9 && (max_y - 3.0).abs() < 1e-9 && (max_z - 4.0).abs() < 1e-9 ); + // Xform prims without local bounds aggregate descendant world bounds. + let (w_min_x, w_min_y, w_min_z, w_max_x, w_max_y, w_max_z) = stage + .compute_boundable_world_bounds("/World", 1.0) + .expect("aggregated world bounds"); + assert!( + (w_min_x - 0.0).abs() < 1e-9 + && (w_min_y - 1.0).abs() < 1e-9 + && (w_min_z - 2.0).abs() < 1e-9 + ); + assert!( + (w_max_x - 2.0).abs() < 1e-9 + && (w_max_y - 3.0).abs() < 1e-9 + && (w_max_z - 4.0).abs() < 1e-9 + ); + } + + #[test] + fn usdutils_spatial_grounding_context_binding() { + let path = fixture_path("parity_spatial_grounding.usda"); + let stage = + Stage::open_from_root_file(&path.to_string_lossy(), 2).expect("open spatial fixture"); + let records = stage + .build_spatial_grounding_context(1.0) + .expect("spatial context"); + assert_eq!(records.len(), 5); + + let cup = records + .iter() + .find(|r| r.path == "/World/Kitchen/CupBlue") + .expect("cup record"); + assert_eq!(cup.name, "CupBlue"); + assert_eq!(cup.parent_path, "/World/Kitchen"); + assert_eq!(cup.sibling_names.len(), 2); + assert!(cup.sibling_names.contains(&"PlateGreen".to_string())); + assert!(cup.sibling_names.contains(&"Stove".to_string())); + assert_eq!(cup.semantic_label_sets.len(), 2); + assert_eq!(cup.semantic_label_sets[0].name, "engine"); + assert_eq!( + cup.semantic_label_sets[0].labels, + vec!["pickup".to_string(), "container".to_string()] + ); + assert_eq!(cup.semantic_label_sets[1].name, "somaHome"); + assert_eq!( + cup.semantic_label_sets[1].labels, + vec!["Crockery".to_string(), "DesignedContainer".to_string()] + ); + assert_eq!(cup.world_position, [6.0, 2.0, 3.0]); + assert!(cup.has_world_bound); + assert_eq!(cup.world_bound_dimensions, [0.5, 1.5, 0.25]); + assert!((cup.mass_kg.expect("mass") - 0.35).abs() < 1e-6); + + let kitchen = records + .iter() + .find(|r| r.path == "/World/Kitchen") + .expect("kitchen record"); + assert_eq!(kitchen.parent_path, "/World"); + assert!(kitchen.sibling_names.is_empty()); + assert!(kitchen.semantic_label_sets.is_empty()); + assert_eq!(kitchen.world_position, [10.0, 0.0, 0.0]); + // Xform prims without local bounds aggregate descendant world bounds. + assert!(kitchen.has_world_bound); + assert_eq!(kitchen.world_bound_dimensions, [9.25, 4.25, 7.5]); + assert_eq!(kitchen.mass_kg, None); + } + + #[test] + fn usd_semantics_labels_binding() { + let path = fixture_path("parity_semantics_labels.usda"); + let stage = + Stage::open_from_root_file(&path.to_string_lossy(), 2).expect("open semantics fixture"); + let sets = stage + .list_semantic_label_sets("/World/Kitchen/CupBlue") + .expect("label sets"); + assert_eq!(sets, vec!["engine".to_string(), "somaHome".to_string()]); + assert_eq!( + stage + .read_semantic_labels("/World/Kitchen/CupBlue", "somaHome") + .expect("cup labels"), + vec!["Crockery".to_string(), "DesignedContainer".to_string()] + ); assert_eq!( stage - .compute_boundable_world_bounds("/World", 1.0) - .unwrap_err(), - ERR_NOT_FOUND + .read_semantic_labels("/World/Kitchen/Stove", "somaHome") + .expect("stove labels"), + vec!["Appliance".to_string()] ); + assert!(stage + .read_semantic_labels("/World/Kitchen/CupBlue", "missing") + .expect("missing labels") + .is_empty()); + let report = stage + .assess_engine_runtime_support() + .expect("runtime report"); + assert_eq!(report.uses_semantic_labels, 1); + assert_eq!(report.recommended_mode, 1); } #[test] diff --git a/docs/engine-integration.md b/docs/engine-integration.md index de7ca41..39f0a1c 100644 --- a/docs/engine-integration.md +++ b/docs/engine-integration.md @@ -58,8 +58,10 @@ target_link_libraries(engine_runtime_bridge PRIVATE freeusd::c) - `freeusd::usdGeom::Xformable` - `freeusd::usdGeom::Imageable` - `freeusd::usdGeom::Boundable` +- `freeusd::usdSemantics::SemanticLabelsAPI` - `freeusd::usdUtils::FlattenStageAtTime` - `freeusd::usdUtils::BuildEngineSceneSnapshot` +- `freeusd::usdUtils::BuildEngineSpatialGroundingContext` - `freeusd::usdUtils::BuildEnginePrimEditorView` - `freeusd::usdUtils::AssessEngineRuntimeSupport` @@ -75,6 +77,8 @@ target_link_libraries(engine_runtime_bridge PRIVATE freeusd::c) - `freeusd_stage_compute_imageable_purpose_utf8` - `freeusd_stage_compute_boundable_local_bounds` - `freeusd_stage_compute_boundable_world_bounds` +- spatial grounding context (`freeusd_usdutils_build_spatial_grounding_context`) +- semantic label reads (`freeusd_stage_list_semantic_label_sets`, `freeusd_stage_read_semantic_labels`) ## Build Options Relevant To Engines @@ -108,6 +112,8 @@ target_link_libraries(engine_runtime_bridge PRIVATE freeusd::c) - composed prim customData through references / payloads: `tests/fixtures/parity_custom_data_refs.usda` - variants: `tests/fixtures/parity_variants.usda` - transforms / visibility / purpose / bounds: `tests/fixtures/parity_imageable.usda` +- spatial grounding cues for downstream diagnostics: `tests/fixtures/parity_spatial_grounding.usda` +- semantic labels: `tests/fixtures/parity_semantics_labels.usda` - preview materials / bindings: `tests/fixtures/parity_shade_preview.usda` - preview-surface texture assets: `tests/fixtures/parity_shade_pbr_textures.usda` - usdLux light families: `tests/fixtures/parity_lux_sphere.usda` (and sibling `parity_lux_*.usda` fixtures) diff --git a/docs/engine-supported-subset.md b/docs/engine-supported-subset.md index e6457d7..063b79b 100644 --- a/docs/engine-supported-subset.md +++ b/docs/engine-supported-subset.md @@ -19,8 +19,10 @@ This document freezes the exact FreeUSD subset a GPL-compatible engine may rely - `freeusd::usdGeom::Xformable` - `freeusd::usdGeom::Imageable` - `freeusd::usdGeom::Boundable` + - `freeusd::usdSemantics::SemanticLabelsAPI` - `freeusd::usdUtils::FlattenStageAtTime` - `freeusd::usdUtils::BuildEngineSceneSnapshot` + - `freeusd::usdUtils::BuildEngineSpatialGroundingContext` - `freeusd::usdUtils::BuildEnginePrimEditorView` - `freeusd::usdUtils::AssessEngineRuntimeSupport` @@ -37,6 +39,9 @@ This document freezes the exact FreeUSD subset a GPL-compatible engine may rely - `freeusd_stage_compute_imageable_purpose_utf8` - `freeusd_stage_compute_boundable_local_bounds` - `freeusd_stage_compute_boundable_world_bounds` + - `freeusd_usdutils_build_spatial_grounding_context` + - `freeusd_stage_list_semantic_label_sets` + - `freeusd_stage_read_semantic_labels` ## Supported Content Slice @@ -70,6 +75,7 @@ This document freezes the exact FreeUSD subset a GPL-compatible engine may rely | composed prim customData through references / payloads | `tests/fixtures/parity_custom_data_refs.usda` | | selected variant expansion | `tests/fixtures/parity_variants.usda` | | visibility / purpose / bounds / transforms | `tests/fixtures/parity_imageable.usda` | +| spatial grounding cues for downstream diagnostics | `tests/fixtures/parity_spatial_grounding.usda` | | mesh point bounds + descendant xform aggregation | `tests/fixtures/parity_boundable_mesh.usda` | | material bindings / preview surface | `tests/fixtures/parity_shade_preview.usda` | | preview-surface texture assets | `tests/fixtures/parity_shade_pbr_textures.usda` | @@ -85,6 +91,7 @@ This document freezes the exact FreeUSD subset a GPL-compatible engine may rely | composed PhysicsCollisionAPI through inherits | `tests/fixtures/parity_physics_collision_inherit.usda` | | OpenVDBAsset in engine snapshots | `tests/fixtures/parity_vol_openvdb.usda` | | Volume + field rel in engine snapshots | `tests/fixtures/parity_vol_volume.usda` | +| authored semantic labels | `tests/fixtures/parity_semantics_labels.usda` | | low-level crate tables | `tests/fixtures/parity_tables.usdc` | | narrow crate scene-open fallback | `tests/fixtures/parity_embedded_scene.usdc` | @@ -113,6 +120,7 @@ This document freezes the exact FreeUSD subset a GPL-compatible engine may rely ### `hybrid_metadata` - Allowed only when engine assets are already baked and runtime reads are limited to narrow metadata, field queries, or inspection helpers. +- Semantic label reads (`semantics:labels:`) are treated as hybrid metadata and should be validated or baked during import. ### `experimental_live_stage` diff --git a/docs/openusd-map.md b/docs/openusd-map.md index 1e4fd16..1b2e443 100644 --- a/docs/openusd-map.md +++ b/docs/openusd-map.md @@ -26,7 +26,7 @@ For **repository layout**, CMake target naming, and which upstream trees are out | `usdHydra` | `freeusd::usdHydra` | **Generated** schema **tokens** (Hydra-facing schema names only; no Hd engine) | | `usdMtlx` | `freeusd::usdMtlx` | **Generated** schema **tokens** | | `usdProc` | `freeusd::usdProc` | **Generated** schema **tokens** | -| `usdSemantics` | `freeusd::usdSemantics` | **Generated** schema **tokens** | +| `usdSemantics` | `freeusd::usdSemantics` | **Generated** schema **tokens** + authored semantic label reads | | `usdUI` | `freeusd::usdUI` | **Generated** schema **tokens** (includes colon-namespaced field names as `TfToken` strings) | | **Usd schema umbrella (opt-in link)** | `freeusd::usd_schemas` | INTERFACE aggregate of **`usdGeom`** + **`usdShade`**, **`usdLux`**, **`usdPhysics`**, **`usdVol`**, **`usdSkel`**, **`usdMedia`**, **`usdRender`**, **`usdRi`**, **`usdHydra`**, **`usdMtlx`**, **`usdProc`**, **`usdSemantics`**, **`usdUI`** (generated **`tokens.hpp`** each); optional C++ bundle **`freeusd/usd/schemaTokens.hpp`** (also includes **`freeusd/usd/schemaDataTokens.hpp`** from **`pxr/usd/usd/generatedSchema.usda`**) | | `plug` | `freeusd::plug` | `Registry` no-op loader; Python **`freeusd.plug`** (`load_all`) | diff --git a/docs/openusd-parity-matrix.md b/docs/openusd-parity-matrix.md index f6de50e..6ccef24 100644 --- a/docs/openusd-parity-matrix.md +++ b/docs/openusd-parity-matrix.md @@ -26,6 +26,8 @@ Status vocabulary: `variantSets` declarations (including composed set/name lists through references), selected-variant payload execution, and variant child expansion. - `tests/fixtures/parity_imageable.usda` Schema-facing `purpose` / `visibility` behavior for `usdGeom::Imageable` and cube-like bounds via `usdGeom::Boundable`. +- `tests/fixtures/parity_spatial_grounding.usda` + `usdUtils` extraction of text-grounding cues from hierarchy, sibling names, authored semantic labels, world position, scaled bounds, and authored physics mass. - `tests/fixtures/parity_custom_data_inherit.usda` Composed prim `customData` through `inherits` arcs (local strongest-wins override plus inherited keys). - `tests/fixtures/parity_custom_data_specializes.usda` @@ -110,6 +112,8 @@ Status vocabulary: `OpenVDBAsset` with `filePath` and `fieldName`. - `tests/fixtures/parity_vol_volume.usda` `Volume` with composed `field` relationship to a child `OpenVDBAsset`. +- `tests/fixtures/parity_semantics_labels.usda` + `UsdSemantics` authored `semantics:labels:` token-array reads through C++, C ABI, Python, Go, and Rust. ## Current Matrix @@ -128,7 +132,7 @@ Status vocabulary: ### Schema And Runtime Helpers -- `implemented`: `usdGeom::Xformable`, `usdGeom::Imageable`, `usdGeom::Boundable`, `usdUtils::FlattenStageAtTime`, and `usdUtils` engine-scene helpers for importer/editor/runtime subset inspection. +- `implemented`: `usdGeom::Xformable`, `usdGeom::Imageable`, `usdGeom::Boundable`, `usdUtils::FlattenStageAtTime`, and `usdUtils` engine-scene helpers for importer/editor/runtime subset inspection, including `BuildEngineSpatialGroundingContext` for clean-room extraction of scene text-grounding cues. - `partial`: `usdGeom::Mesh` reads composed `points`, `extent`, `subdivisionScheme`, `faceVertexCounts`, `faceVertexIndices`, `normals`, `primvars:st`, `primvars:displayOpacity`, and `primvars:displayColor` (`parity_geom_mesh.usda`); USDA load/save accepts `texCoord2f` / `float2` and `vector3f` tuple literals. - `partial`: flattening now preserves evaluated defaults plus composed sample times, but it does not yet reconstruct full authored layer provenance for every arc source. - `partial`: `usdSkel::Skeleton` and `usdSkel::SkelAnimation` read joints, bind/rest matrices, and sampled TRS arrays from USDA; glTF mapping helpers build parent indices and world bind matrices; `SkelBinding` resolves `skel:skeleton` plus `primvars:skel:jointIndices` / `jointWeights`; `SkelRoot` finds skeleton and `skel:animationSource` under a scope (`parity_skel_binding.usda`); `BlendShape` / `SkelBlendShapes` / `MorphTargets` read morph offsets, remap animation weights, and apply CPU morph accumulation (`parity_skel_blend_shapes.usda`; glTF `mesh.weights` + morph target POSITION deltas); `DeformPointsWithSkeleton` performs CPU LBS from joint world matrices and inverse bind transforms (`parity_skel_skinning.usda`); `SkelRoot::FindBoundGeomPaths` lists descendants with `skel:skeleton` bindings (`parity_skel_binding.usda`); end-to-end glTF pipeline fixture (`parity_skel_gltf_pipeline.usda`: bound mesh, dual morph weights, animation-driven LBS). @@ -136,12 +140,13 @@ Status vocabulary: - `partial`: `usdLux::DistantLight` reads `inputs:intensity`, `inputs:color`, and `inputs:angle` at a time code (`parity_lux_distant.usda`); `usdLux::SphereLight` reads `inputs:intensity`, `inputs:color`, and `inputs:radius` (`parity_lux_sphere.usda`); `usdLux::RectLight` reads `inputs:intensity`, `inputs:color`, `inputs:width`, and `inputs:height` (`parity_lux_rect.usda`); `usdLux::DiskLight` reads `inputs:intensity`, `inputs:color`, and `inputs:radius` (`parity_lux_disk.usda`); `usdLux::CylinderLight` reads `inputs:intensity`, `inputs:color`, `inputs:length`, and `inputs:radius` (`parity_lux_cylinder.usda`); `usdLux::DomeLight` reads `inputs:intensity`, `inputs:color`, `inputs:texture:file`, and `inputs:texture:format` (`parity_lux_dome.usda`). - `partial`: `usdPhysics::PhysicsScene` reads `physics:gravityDirection` and `physics:gravityMagnitude` at a time code (`parity_physics_scene.usda`); `usdPhysics::RigidBodyAPI` reads composed `physics:mass`, `physics:kinematicEnabled`, and detects `PhysicsRigidBodyAPI` via composed `apiSchemas` (`parity_physics_rigid_body.usda`, `parity_physics_rigid_body_inherit.usda`, `parity_physics_rigid_body_kinematic.usda`, `parity_physics_rigid_body_refs.usda`); `usdPhysics::CollisionAPI` reads composed `physics:collisionEnabled` and detects `PhysicsCollisionAPI` via composed `apiSchemas` (`parity_physics_collision.usda`, `parity_physics_collision_inherit.usda`); `usdPhysics::MassAPI` reads `physics:density` and `physics:centerOfMass` and detects `PhysicsMassAPI` via composed `apiSchemas` (`parity_physics_mass.usda`); `usdPhysics::FixedJoint` reads `physics:body0` / `physics:body1` relationship targets and `physics:jointEnabled` (`parity_physics_fixed_joint.usda`). - `partial`: `usdVol::OpenVDBAsset` reads `filePath` and `fieldName` at a time code (`parity_vol_openvdb.usda`); `usdVol::Volume` reads prim kind and composed `field` relationship targets, resolving child `OpenVDBAsset` field prims (`parity_vol_volume.usda`). -- `token-only`: most other non-`usdGeom` / non-`usdSkel` / non-`usdShade` / non-`usdLux` / non-`usdPhysics` / non-`usdVol` schema packages remain generated token surfaces only. +- `partial`: `usdSemantics::SemanticLabelsAPI` lists authored semantic label-set names and reads `semantics:labels:` token arrays (`parity_semantics_labels.usda`). +- `token-only`: most other non-`usdGeom` / non-`usdSkel` / non-`usdShade` / non-`usdLux` / non-`usdPhysics` / non-`usdVol` / non-`usdSemantics` schema packages remain generated token surfaces only. ### ABI And Bindings - `implemented`: the C ABI remains the stable FFI contract for stage queries, typed field reads, crate bootstrap/TOC helpers, raw crate section payload access, and the validated `usdGeom` runtime subset (`transform`, `visibility`, `purpose`, `bounds`). -- `partial`: Python remains the broadest wrapper; the C ABI, Go, and Rust now follow the validated crate bootstrap/TOC/raw-section/table subset, composed prim `kind` / `active`, `usdGeom` imageable/boundable, `usdShade` preview-surface diffuse/texture helpers, `usdLux` DistantLight sampling, `usdVol` OpenVDBAsset reads, `usdPhysics` PhysicsScene gravity, RigidBodyAPI, CollisionAPI, MassAPI, and FixedJoint reads, and `usdSkel` joint/skinning helpers. D now has an initial C ABI-backed smoke slice for stage open, prim validity, composed prim `kind` / `active`, double/string field reads, `usdGeom` transform / visibility / purpose / bounds helpers, `usdShade` preview-surface helpers, `usdLux` DistantLight sampling, `usdVol` OpenVDBAsset reads, and selected `usdPhysics` sample reads. Broader runtime/schema helpers are still not fully mirrored outside Python/C++. +- `partial`: Python remains the broadest wrapper; the C ABI, Go, and Rust now follow the validated crate bootstrap/TOC/raw-section/table subset, composed prim `kind` / `active`, `usdGeom` imageable/boundable, `usdUtils` spatial-grounding context records, `usdSemantics` label reads, `usdShade` preview-surface diffuse/texture helpers, `usdLux` DistantLight sampling, `usdVol` OpenVDBAsset reads, `usdPhysics` PhysicsScene gravity, RigidBodyAPI, CollisionAPI, MassAPI, and FixedJoint reads, and `usdSkel` joint/skinning helpers. D now has an initial C ABI-backed smoke slice for stage open, prim validity, composed prim `kind` / `active`, double/string field reads, `usdGeom` transform / visibility / purpose / bounds helpers, `usdShade` preview-surface helpers, `usdLux` DistantLight sampling, `usdVol` OpenVDBAsset reads, and selected `usdPhysics` sample reads. Broader runtime/schema helpers are still not fully mirrored outside Python/C++. - `partial`: Native D (`native/d`) is a separate clean-room implementation track with an initial USDA reader and in-memory stage for root layer metadata, def hierarchy, prim specifier / `kind` / `active` metadata, basic authored values (including `asset`, `matrix4d`, `quatd`, and `quatf`), field and relationship name inspection, prim `customData`, exact `timeSamples`, relationship targets, simple transform / visibility / purpose / bounds helpers, simple `usdVol` and `usdPhysics` schema helpers, simple relocates and prefix substitutions, simple strongest-to-weakest sublayer reads with authored offsets, selected variants, and simple composed reads through `inherits`, `specializes`, default-prim `references`, and default-prim `payloads`, validated against `usd_cross_language.usda`, `parity_imageable.usda`, `parity_stack_root.usda`, `parity_specializes.usda`, `parity_kind_active_refs.usda`, `parity_kind_active_specializes.usda`, `parity_custom_data_inherit.usda`, `parity_custom_data_refs.usda`, `parity_custom_data_specializes.usda`, `parity_variants.usda`, `parity_variant_selection_refs.usda`, `parity_physics_scene.usda`, `parity_physics_rigid_body_kinematic.usda`, `parity_physics_collision.usda`, `parity_physics_mass.usda`, `parity_physics_fixed_joint.usda`, `parity_vol_openvdb.usda`, `parity_vol_volume.usda`, `parity_physics_rigid_body_refs.usda`, and `parity_namespace.usda`; broader schemas and binary crate decode remain incomplete. - `planned`: milestone-by-milestone expansion for more composed query families and runtime helpers once the C ABI slice is stable. @@ -153,7 +158,7 @@ Status vocabulary: ### Engine Integration Contract - `implemented`: `docs/engine-supported-subset.md`, `docs/engine-integration.md`, clean-room/fixture/claim policy docs, `usdUtils::engineScene`, and focused engine integration tests freeze the USDA-first engine path. -- `partial`: shipping runtime remains intentionally narrow; engine snapshots list material bindings, preview-surface materials, textured preview shaders, supported `usdLux` light families, `PhysicsScene` prims, `PhysicsRigidBodyAPI`-shaped prims with composed `physics:mass` (`rigid_body_api_paths`), `PhysicsCollisionAPI`-shaped prims (`collision_api_paths`), `PhysicsFixedJoint` prims (`physics_fixed_joint_paths`), `OpenVDBAsset` prims (`open_vdb_asset_paths`), `Volume` prims (`volume_paths`), and composed prim `kind` paths; `AssessEngineRuntimeSupport` reports `uses_material_bindings`, `uses_preview_surface`, `uses_preview_surface_textures`, `uses_lux_lights`, `uses_physics_scenes`, `uses_rigid_body_api`, `uses_collision_api`, `uses_physics_fixed_joints`, `uses_open_vdb_assets`, `uses_volumes`, `uses_composed_prim_kind`, `uses_prim_active_opinions`, `uses_specializes`, `uses_kind_active_through_arcs` (including `specializes` arcs; see `parity_kind_active_specializes.usda`), and `uses_custom_data_through_arcs`; arbitrary `.usdc` scene decode and broad live-stage runtime guarantees are still out of scope. +- `partial`: shipping runtime remains intentionally narrow; engine snapshots list material bindings, preview-surface materials, textured preview shaders, supported `usdLux` light families, `PhysicsScene` prims, `PhysicsRigidBodyAPI`-shaped prims with composed `physics:mass` (`rigid_body_api_paths`), `PhysicsCollisionAPI`-shaped prims (`collision_api_paths`), `PhysicsFixedJoint` prims (`physics_fixed_joint_paths`), `OpenVDBAsset` prims (`open_vdb_asset_paths`), `Volume` prims (`volume_paths`), semantic-label prims (`semantic_label_prim_paths`), and composed prim `kind` paths; spatial-grounding context records expose prim names, parent paths, sibling names, authored semantic label sets, world positions, scaled world-bound dimensions, and authored mass; `AssessEngineRuntimeSupport` reports `uses_material_bindings`, `uses_preview_surface`, `uses_preview_surface_textures`, `uses_lux_lights`, `uses_physics_scenes`, `uses_rigid_body_api`, `uses_collision_api`, `uses_physics_fixed_joints`, `uses_open_vdb_assets`, `uses_volumes`, `uses_semantic_labels`, `uses_composed_prim_kind`, `uses_prim_active_opinions`, `uses_specializes`, `uses_kind_active_through_arcs` (including `specializes` arcs; see `parity_kind_active_specializes.usda`), and `uses_custom_data_through_arcs`; arbitrary `.usdc` scene decode and broad live-stage runtime guarantees are still out of scope. ## Acceptance Criteria diff --git a/docs/openusd-repo-alignment.md b/docs/openusd-repo-alignment.md index 9e35304..6fd618d 100644 --- a/docs/openusd-repo-alignment.md +++ b/docs/openusd-repo-alignment.md @@ -57,7 +57,7 @@ Typical OpenUSD trees that **do not** have FreeUSD counterparts yet (documented - **Imaging / Hydra / Hgi / Hd***: render delegate stack; **UsdImaging** scene adapters. - **Hydra integration**: FreeUSD exposes **UsdHydra-shaped schema token strings** only (`freeusd::usdHydra::tokens`); there is no Hd render index, scene delegate, or imaging bridge. -- **MaterialX / UI / procedural semantics logic**: **`usdMtlx`**, **`usdUI`**, **`usdProc`**, **`usdSemantics`** supply **token names** from published schema data only; there is no MaterialX graph compiler, UI layout engine, or procedural execution. +- **MaterialX / UI / procedural logic**: **`usdMtlx`**, **`usdUI`**, and **`usdProc`** supply **token names** from published schema data only; **`usdSemantics`** additionally has a narrow authored semantic-label reader. There is no MaterialX graph compiler, UI layout engine, or procedural execution. - **Ndr / Sdr**: shader discovery and parsing. - **exec** and full **trace** implementations beyond the current lightweight stubs. diff --git a/include/freeusd/c/freeusd.h b/include/freeusd/c/freeusd.h index 8119561..ea4555b 100644 --- a/include/freeusd/c/freeusd.h +++ b/include/freeusd/c/freeusd.h @@ -951,6 +951,7 @@ typedef struct FreeusdEngineRuntimeSupport { int uses_physics_fixed_joints; int uses_open_vdb_assets; int uses_volumes; + int uses_semantic_labels; } FreeusdEngineRuntimeSupport; /** @@ -960,6 +961,58 @@ typedef struct FreeusdEngineRuntimeSupport { FREEUSD_C_API int freeusd_usdutils_assess_engine_runtime_support(const FreeusdStage* stage, FreeusdEngineRuntimeSupport* out); +/** One authored semantic label set owned by @ref FreeusdSpatialGroundingRecord. */ +typedef struct FreeusdSemanticLabelSet { + char* name_utf8; + char** labels_utf8; + size_t label_count; +} FreeusdSemanticLabelSet; + +/** One spatial/text grounding cue record from @c freeusd::usdUtils::BuildEngineSpatialGroundingContext. */ +typedef struct FreeusdSpatialGroundingRecord { + char* path_utf8; + char* name_utf8; + char* parent_path_utf8; + char** sibling_names_utf8; + size_t sibling_name_count; + FreeusdSemanticLabelSet* semantic_label_sets; + size_t semantic_label_set_count; + double world_position[3]; + int has_world_bound; + double world_bound_dimensions[3]; + int has_mass_kg; + double mass_kg; +} FreeusdSpatialGroundingRecord; + +/** + * Extract USD-derived text/spatial grounding cues for every traversed prim at @p time. + * On @ref FREEUSD_OK, @p *out_records is a malloc'd array with @p *out_count records. + * Free with @ref freeusd_usdutils_spatial_grounding_records_free. + */ +FREEUSD_C_API int freeusd_usdutils_build_spatial_grounding_context(const FreeusdStage* stage, double time, + FreeusdSpatialGroundingRecord** out_records, + size_t* out_count); + +/** Frees records returned by @ref freeusd_usdutils_build_spatial_grounding_context (safe on NULL). */ +FREEUSD_C_API void freeusd_usdutils_spatial_grounding_records_free(FreeusdSpatialGroundingRecord* records, + size_t count); + +/** + * Authored semantic label set names on @p prim_path_utf8 (from ``semantics:labels:`` attributes). + * On @ref FREEUSD_OK, @p *out_strings / @p *out_count use @ref freeusd_path_list_free. + */ +FREEUSD_C_API int freeusd_stage_list_semantic_label_sets(const FreeusdStage* stage, const char* prim_path_utf8, + char*** out_strings, size_t* out_count); + +/** + * Authored semantic labels for one label set on @p prim_path_utf8. + * On @ref FREEUSD_OK, @p *out_strings / @p *out_count use @ref freeusd_path_list_free. + * Missing sets return @ref FREEUSD_OK with an empty list. + */ +FREEUSD_C_API int freeusd_stage_read_semantic_labels(const FreeusdStage* stage, const char* prim_path_utf8, + const char* instance_name_utf8, char*** out_strings, + size_t* out_count); + /** * Composed prim active flag (strongest opinion; default true if no opinion). * @p out_active receives 0 or 1. diff --git a/include/freeusd/usdSemantics/labelsAPI.hpp b/include/freeusd/usdSemantics/labelsAPI.hpp new file mode 100644 index 0000000..ac103ee --- /dev/null +++ b/include/freeusd/usdSemantics/labelsAPI.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include +#include +#include + +#include "freeusd/export.hpp" +#include "freeusd/sdf/path.hpp" +#include "freeusd/usd/prim.hpp" + +namespace freeusd::usd { +class Stage; +} + +namespace freeusd::usdSemantics { + +/// Multiple-apply semantic labels helper for authored ``semantics:labels:`` token arrays. +struct FREEUSD_API SemanticLabelsAPI { + freeusd::usd::Prim prim; + + SemanticLabelsAPI() = default; + explicit SemanticLabelsAPI(freeusd::usd::Prim p) : prim(std::move(p)) {} + + static SemanticLabelsAPI ReadFromPrim(const std::shared_ptr& stage, + const freeusd::sdf::Path& path); + + explicit operator bool() const noexcept { return prim.IsValid(); } + + bool HasLabels(const std::string& instance_name) const; + std::vector ListLabelSetNames() const; + std::vector GetLabels(const std::string& instance_name, double time = 1.0) const; +}; + +} // namespace freeusd::usdSemantics diff --git a/include/freeusd/usdUtils/engineScene.hpp b/include/freeusd/usdUtils/engineScene.hpp index 25276fb..a177c64 100644 --- a/include/freeusd/usdUtils/engineScene.hpp +++ b/include/freeusd/usdUtils/engineScene.hpp @@ -92,6 +92,9 @@ struct FREEUSD_API EngineSceneNode { bool has_volume{false}; /// Composed ``field`` relationship targets when @ref has_volume is true. std::vector volume_field_asset_paths; + /// Authored ``semantics:labels:`` set names. + std::vector semantic_label_set_names; + bool has_semantic_labels{false}; }; /// Stage-level snapshot for USDA-first import and engine/editor cache generation. @@ -140,6 +143,27 @@ struct FREEUSD_API EngineSceneSnapshot { std::vector open_vdb_asset_paths; /// ``Volume`` prims with composed ``field`` relationship targets. std::vector volume_paths; + /// Prims with authored ``semantics:labels:`` token arrays. + std::vector semantic_label_prim_paths; +}; + +/// One authored semantic label set on a prim. +struct FREEUSD_API EngineSemanticLabelSet { + std::string name; + std::vector labels; +}; + +/// One prim's text-grounding cues for downstream spatial diagnostics or ontology mapping. +struct FREEUSD_API EngineSpatialGroundingRecord { + freeusd::sdf::Path path; + std::string name; + freeusd::sdf::Path parent_path; + std::vector sibling_names; + std::vector semantic_label_sets; + freeusd::gf::Vec3d world_position{}; + bool has_world_bound{false}; + freeusd::gf::Vec3d world_bound_dimensions{}; + std::optional mass_kg; }; /// Editor-facing inspection view for one prim in the validated subset. @@ -189,12 +213,17 @@ struct FREEUSD_API EngineRuntimeSupportReport { bool uses_physics_fixed_joints{false}; bool uses_open_vdb_assets{false}; bool uses_volumes{false}; + bool uses_semantic_labels{false}; std::vector warnings; }; /// Build one engine-oriented snapshot from the currently validated `Stage` / `usdGeom` subset. FREEUSD_API EngineSceneSnapshot BuildEngineSceneSnapshot(const freeusd::usd::Stage& stage, double time = 1.0); +/// Extract clean-room USD-derived cues useful for text-only spatial diagnostics and ontology grounding. +FREEUSD_API std::vector BuildEngineSpatialGroundingContext( + const freeusd::usd::Stage& stage, double time = 1.0); + /// Inspect one prim for editor-facing tree/details panels without exposing unstable internals. FREEUSD_API EnginePrimEditorView BuildEnginePrimEditorView(const freeusd::usd::Prim& prim, double time = 1.0); diff --git a/python/bindings.cpp b/python/bindings.cpp index 5dd43de..02a4c55 100644 --- a/python/bindings.cpp +++ b/python/bindings.cpp @@ -70,6 +70,7 @@ #include "freeusd/usdProc/tokens.hpp" #include "freeusd/usdRender/tokens.hpp" #include "freeusd/usdRi/tokens.hpp" +#include "freeusd/usdSemantics/labelsAPI.hpp" #include "freeusd/usdSemantics/tokens.hpp" #include "freeusd/usdShade/material.hpp" #include "freeusd/usdShade/previewSurface.hpp" @@ -1888,6 +1889,55 @@ reference; breaks cycles encountered along the DFS stack.)pbdoc"); .def( "list_composed_prefix_substitutions", [](const freeusd::usd::Stage& stage) { return stage.ListComposedPrefixSubstitutions(); }) + .def( + "build_spatial_grounding_context", + [](const freeusd::usd::Stage& stage, double time) { + py::list out; + for (const auto& record : freeusd::usdUtils::BuildEngineSpatialGroundingContext(stage, time)) { + py::dict d; + d["path"] = record.path.GetString(); + d["name"] = record.name; + d["parent_path"] = record.parent_path.GetString(); + d["sibling_names"] = record.sibling_names; + py::list semantic_label_sets; + for (const auto& label_set : record.semantic_label_sets) { + py::dict item; + item["name"] = label_set.name; + item["labels"] = label_set.labels; + semantic_label_sets.append(std::move(item)); + } + d["semantic_label_sets"] = std::move(semantic_label_sets); + d["world_position"] = + py::make_tuple(record.world_position.x(), record.world_position.y(), record.world_position.z()); + d["has_world_bound"] = record.has_world_bound; + d["world_bound_dimensions"] = py::make_tuple(record.world_bound_dimensions.x(), + record.world_bound_dimensions.y(), + record.world_bound_dimensions.z()); + if (record.mass_kg.has_value()) { + d["mass_kg"] = *record.mass_kg; + } else { + d["mass_kg"] = py::none(); + } + out.append(std::move(d)); + } + return out; + }, + py::arg("time") = 1.0) + .def( + "list_semantic_label_sets", + [](const freeusd::usd::Stage& stage, const freeusd::sdf::Path& path) { + return freeusd::usdSemantics::SemanticLabelsAPI::ReadFromPrim(stage.shared_from_this(), path) + .ListLabelSetNames(); + }, + py::arg("path")) + .def( + "read_semantic_labels", + [](const freeusd::usd::Stage& stage, const freeusd::sdf::Path& path, const std::string& instance_name, + double time) { + return freeusd::usdSemantics::SemanticLabelsAPI::ReadFromPrim(stage.shared_from_this(), path) + .GetLabels(instance_name, time); + }, + py::arg("path"), py::arg("instance_name"), py::arg("time") = 1.0) .def("list_composed_field_names", &freeusd::usd::Stage::ListComposedFieldNames) .def("list_composed_field_sample_times", &freeusd::usd::Stage::ListComposedFieldSampleTimes) .def("list_composed_relationship_names", &freeusd::usd::Stage::ListComposedRelationshipNames) @@ -3524,7 +3574,9 @@ reference; breaks cycles encountered along the DFS stack.)pbdoc"); .def_readwrite("open_vdb_file_path", &freeusd::usdUtils::EngineSceneNode::open_vdb_file_path) .def_readwrite("open_vdb_field_name", &freeusd::usdUtils::EngineSceneNode::open_vdb_field_name) .def_readwrite("has_volume", &freeusd::usdUtils::EngineSceneNode::has_volume) - .def_readwrite("volume_field_asset_paths", &freeusd::usdUtils::EngineSceneNode::volume_field_asset_paths); + .def_readwrite("volume_field_asset_paths", &freeusd::usdUtils::EngineSceneNode::volume_field_asset_paths) + .def_readwrite("semantic_label_set_names", &freeusd::usdUtils::EngineSceneNode::semantic_label_set_names) + .def_readwrite("has_semantic_labels", &freeusd::usdUtils::EngineSceneNode::has_semantic_labels); py::class_(usdUtils, "EngineSceneSnapshot") .def(py::init<>()) .def_readwrite("root_identifier", &freeusd::usdUtils::EngineSceneSnapshot::root_identifier) @@ -3558,7 +3610,9 @@ reference; breaks cycles encountered along the DFS stack.)pbdoc"); .def_readwrite("physics_fixed_joint_paths", &freeusd::usdUtils::EngineSceneSnapshot::physics_fixed_joint_paths) .def_readwrite("open_vdb_asset_paths", &freeusd::usdUtils::EngineSceneSnapshot::open_vdb_asset_paths) - .def_readwrite("volume_paths", &freeusd::usdUtils::EngineSceneSnapshot::volume_paths); + .def_readwrite("volume_paths", &freeusd::usdUtils::EngineSceneSnapshot::volume_paths) + .def_readwrite("semantic_label_prim_paths", + &freeusd::usdUtils::EngineSceneSnapshot::semantic_label_prim_paths); py::class_(usdUtils, "EnginePrimEditorView") .def(py::init<>()) .def_readwrite("path", &freeusd::usdUtils::EnginePrimEditorView::path) @@ -3614,6 +3668,7 @@ reference; breaks cycles encountered along the DFS stack.)pbdoc"); &freeusd::usdUtils::EngineRuntimeSupportReport::uses_physics_fixed_joints) .def_readwrite("uses_open_vdb_assets", &freeusd::usdUtils::EngineRuntimeSupportReport::uses_open_vdb_assets) .def_readwrite("uses_volumes", &freeusd::usdUtils::EngineRuntimeSupportReport::uses_volumes) + .def_readwrite("uses_semantic_labels", &freeusd::usdUtils::EngineRuntimeSupportReport::uses_semantic_labels) .def_readwrite("warnings", &freeusd::usdUtils::EngineRuntimeSupportReport::warnings); py::class_(usdUtils, "FlattenOptions") .def(py::init<>()) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index c6d3dd9..3e18c40 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -95,7 +95,7 @@ target_link_libraries(freeusd_usdLux INTERFACE freeusd_usdGeom) set_target_properties(freeusd_usdLux PROPERTIES EXPORT_NAME usdLux) target_link_libraries(freeusd_usdUtils PUBLIC freeusd_usdSkel freeusd_usdShade freeusd_usdLux freeusd_usdPhysics - freeusd_usdVol) + freeusd_usdVol freeusd_usdSemantics) add_library(freeusd_usdPhysics STATIC usdPhysics/physicsScene.cpp usdPhysics/rigidBodyAPI.cpp usdPhysics/collisionAPI.cpp usdPhysics/massAPI.cpp usdPhysics/fixedJoint.cpp) @@ -123,7 +123,6 @@ foreach( usdHydra usdMtlx usdProc - usdSemantics usdUI) add_library(freeusd_${_pkg} INTERFACE) add_library(freeusd::${_pkg} ALIAS freeusd_${_pkg}) @@ -132,6 +131,12 @@ foreach( set_target_properties(freeusd_${_pkg} PROPERTIES EXPORT_NAME ${_pkg}) endforeach() +add_library(freeusd_usdSemantics STATIC usdSemantics/labelsAPI.cpp) +add_library(freeusd::usdSemantics ALIAS freeusd_usdSemantics) +target_include_directories(freeusd_usdSemantics PUBLIC "$" "$") +target_link_libraries(freeusd_usdSemantics PUBLIC freeusd_usd freeusd_tf freeusd_build_settings) +set_target_properties(freeusd_usdSemantics PROPERTIES EXPORT_NAME usdSemantics) + add_library(freeusd_usd_schemas INTERFACE) add_library(freeusd::usd_schemas ALIAS freeusd_usd_schemas) target_link_libraries( @@ -186,6 +191,7 @@ target_link_libraries( freeusd_usdLux freeusd_usdPhysics freeusd_usdVol + freeusd_usdSemantics freeusd_plug freeusd_trace freeusd_work @@ -220,6 +226,7 @@ set_target_properties(freeusd_usdShade PROPERTIES EXPORT_NAME usdShade) set_target_properties(freeusd_usdLux PROPERTIES EXPORT_NAME usdLux) set_target_properties(freeusd_usdPhysics PROPERTIES EXPORT_NAME usdPhysics) set_target_properties(freeusd_usdVol PROPERTIES EXPORT_NAME usdVol) +set_target_properties(freeusd_usdSemantics PROPERTIES EXPORT_NAME usdSemantics) set_target_properties(freeusd_usd_schemas PROPERTIES EXPORT_NAME usd_schemas) set_target_properties(freeusd_plug PROPERTIES EXPORT_NAME plug) set_target_properties(freeusd_trace PROPERTIES EXPORT_NAME trace) diff --git a/src/c/api.cpp b/src/c/api.cpp index dc71717..3cece59 100644 --- a/src/c/api.cpp +++ b/src/c/api.cpp @@ -34,6 +34,7 @@ #include "freeusd/usdPhysics/massAPI.hpp" #include "freeusd/usdPhysics/physicsScene.hpp" #include "freeusd/usdPhysics/rigidBodyAPI.hpp" +#include "freeusd/usdSemantics/labelsAPI.hpp" #include "freeusd/usdSkel/skelAnimation.hpp" #include "freeusd/usdSkel/skelBlendShapes.hpp" #include "freeusd/usdSkel/skinning.hpp" @@ -91,6 +92,40 @@ int malloc_string_list(const std::vector& items, char*** out_arr, s return FREEUSD_OK; } +void free_spatial_grounding_record(FreeusdSpatialGroundingRecord* record) { + if (!record) { + return; + } + std::free(record->path_utf8); + std::free(record->name_utf8); + std::free(record->parent_path_utf8); + if (record->sibling_names_utf8) { + for (size_t i = 0; i < record->sibling_name_count; ++i) { + std::free(record->sibling_names_utf8[i]); + } + std::free(record->sibling_names_utf8); + } + if (record->semantic_label_sets) { + for (size_t i = 0; i < record->semantic_label_set_count; ++i) { + std::free(record->semantic_label_sets[i].name_utf8); + if (record->semantic_label_sets[i].labels_utf8) { + for (size_t j = 0; j < record->semantic_label_sets[i].label_count; ++j) { + std::free(record->semantic_label_sets[i].labels_utf8[j]); + } + std::free(record->semantic_label_sets[i].labels_utf8); + } + } + std::free(record->semantic_label_sets); + } + record->path_utf8 = nullptr; + record->name_utf8 = nullptr; + record->parent_path_utf8 = nullptr; + record->sibling_names_utf8 = nullptr; + record->sibling_name_count = 0; + record->semantic_label_sets = nullptr; + record->semantic_label_set_count = 0; +} + bool value_to_int64(const freeusd::vt::Value& v, std::int64_t* out) { if (!out) { return false; @@ -3210,6 +3245,176 @@ int freeusd_usdutils_assess_engine_runtime_support(const FreeusdStage* stage, Fr out->uses_physics_fixed_joints = report.uses_physics_fixed_joints ? 1 : 0; out->uses_open_vdb_assets = report.uses_open_vdb_assets ? 1 : 0; out->uses_volumes = report.uses_volumes ? 1 : 0; + out->uses_semantic_labels = report.uses_semantic_labels ? 1 : 0; + clear_error(); + return FREEUSD_OK; + } catch (const std::exception& e) { + set_error(e.what()); + return FREEUSD_ERR_INTERNAL; + } catch (...) { + set_error("unknown exception"); + return FREEUSD_ERR_INTERNAL; + } +} + +int freeusd_usdutils_build_spatial_grounding_context(const FreeusdStage* stage, double time, + FreeusdSpatialGroundingRecord** out_records, size_t* out_count) { + if (!stage || !stage->inner || !out_records || !out_count) { + set_error("freeusd_usdutils_build_spatial_grounding_context: null argument"); + return FREEUSD_ERR_INVALID_ARGUMENT; + } + *out_records = nullptr; + *out_count = 0; + try { + const std::vector records = + freeusd::usdUtils::BuildEngineSpatialGroundingContext(*stage->inner, time); + if (records.empty()) { + clear_error(); + return FREEUSD_OK; + } + FreeusdSpatialGroundingRecord* out = static_cast( + std::calloc(records.size(), sizeof(FreeusdSpatialGroundingRecord))); + if (!out) { + set_error("out of memory"); + return FREEUSD_ERR_INTERNAL; + } + for (size_t i = 0; i < records.size(); ++i) { + const auto& src = records[i]; + FreeusdSpatialGroundingRecord& dst = out[i]; + dst.path_utf8 = dup_cstr(src.path.GetString()); + dst.name_utf8 = dup_cstr(src.name); + dst.parent_path_utf8 = dup_cstr(src.parent_path.GetString()); + if (!dst.path_utf8 || !dst.name_utf8 || !dst.parent_path_utf8) { + freeusd_usdutils_spatial_grounding_records_free(out, records.size()); + set_error("out of memory"); + return FREEUSD_ERR_INTERNAL; + } + const int list_rc = malloc_string_list(src.sibling_names, &dst.sibling_names_utf8, &dst.sibling_name_count); + if (list_rc != FREEUSD_OK) { + freeusd_usdutils_spatial_grounding_records_free(out, records.size()); + set_error("out of memory"); + return list_rc; + } + if (!src.semantic_label_sets.empty()) { + dst.semantic_label_sets = static_cast( + std::calloc(src.semantic_label_sets.size(), sizeof(FreeusdSemanticLabelSet))); + if (!dst.semantic_label_sets) { + freeusd_usdutils_spatial_grounding_records_free(out, records.size()); + set_error("out of memory"); + return FREEUSD_ERR_INTERNAL; + } + dst.semantic_label_set_count = src.semantic_label_sets.size(); + for (size_t j = 0; j < src.semantic_label_sets.size(); ++j) { + dst.semantic_label_sets[j].name_utf8 = dup_cstr(src.semantic_label_sets[j].name); + if (!dst.semantic_label_sets[j].name_utf8) { + freeusd_usdutils_spatial_grounding_records_free(out, records.size()); + set_error("out of memory"); + return FREEUSD_ERR_INTERNAL; + } + const int label_rc = malloc_string_list(src.semantic_label_sets[j].labels, + &dst.semantic_label_sets[j].labels_utf8, + &dst.semantic_label_sets[j].label_count); + if (label_rc != FREEUSD_OK) { + freeusd_usdutils_spatial_grounding_records_free(out, records.size()); + set_error("out of memory"); + return label_rc; + } + } + } + dst.world_position[0] = src.world_position.x(); + dst.world_position[1] = src.world_position.y(); + dst.world_position[2] = src.world_position.z(); + dst.has_world_bound = src.has_world_bound ? 1 : 0; + dst.world_bound_dimensions[0] = src.world_bound_dimensions.x(); + dst.world_bound_dimensions[1] = src.world_bound_dimensions.y(); + dst.world_bound_dimensions[2] = src.world_bound_dimensions.z(); + dst.has_mass_kg = src.mass_kg.has_value() ? 1 : 0; + dst.mass_kg = src.mass_kg.value_or(0.0); + } + *out_records = out; + *out_count = records.size(); + clear_error(); + return FREEUSD_OK; + } catch (const std::exception& e) { + set_error(e.what()); + return FREEUSD_ERR_INTERNAL; + } catch (...) { + set_error("unknown exception"); + return FREEUSD_ERR_INTERNAL; + } +} + +void freeusd_usdutils_spatial_grounding_records_free(FreeusdSpatialGroundingRecord* records, size_t count) { + if (!records) { + return; + } + for (size_t i = 0; i < count; ++i) { + free_spatial_grounding_record(&records[i]); + } + std::free(records); +} + +int freeusd_stage_list_semantic_label_sets(const FreeusdStage* stage, const char* prim_path_utf8, char*** out_strings, + size_t* out_count) { + if (!stage || !stage->inner || !prim_path_utf8 || !out_strings || !out_count) { + set_error("freeusd_stage_list_semantic_label_sets: null argument"); + return FREEUSD_ERR_INVALID_ARGUMENT; + } + *out_strings = nullptr; + *out_count = 0; + try { + const freeusd::sdf::Path p = freeusd::sdf::Path::FromString(prim_path_utf8); + if (p.IsEmpty()) { + set_error("invalid prim path"); + return FREEUSD_ERR_INVALID_ARGUMENT; + } + const freeusd::usdSemantics::SemanticLabelsAPI api = + freeusd::usdSemantics::SemanticLabelsAPI::ReadFromPrim(stage->inner, p); + if (!api) { + set_error("prim not found"); + return FREEUSD_ERR_NOT_FOUND; + } + const int rc = malloc_string_list(api.ListLabelSetNames(), out_strings, out_count); + if (rc != FREEUSD_OK) { + set_error("out of memory"); + return rc; + } + clear_error(); + return FREEUSD_OK; + } catch (const std::exception& e) { + set_error(e.what()); + return FREEUSD_ERR_INTERNAL; + } catch (...) { + set_error("unknown exception"); + return FREEUSD_ERR_INTERNAL; + } +} + +int freeusd_stage_read_semantic_labels(const FreeusdStage* stage, const char* prim_path_utf8, + const char* instance_name_utf8, char*** out_strings, size_t* out_count) { + if (!stage || !stage->inner || !prim_path_utf8 || !instance_name_utf8 || !out_strings || !out_count) { + set_error("freeusd_stage_read_semantic_labels: null argument"); + return FREEUSD_ERR_INVALID_ARGUMENT; + } + *out_strings = nullptr; + *out_count = 0; + try { + const freeusd::sdf::Path p = freeusd::sdf::Path::FromString(prim_path_utf8); + if (p.IsEmpty() || std::strlen(instance_name_utf8) == 0) { + set_error("invalid semantic labels argument"); + return FREEUSD_ERR_INVALID_ARGUMENT; + } + const freeusd::usdSemantics::SemanticLabelsAPI api = + freeusd::usdSemantics::SemanticLabelsAPI::ReadFromPrim(stage->inner, p); + if (!api) { + set_error("prim not found"); + return FREEUSD_ERR_NOT_FOUND; + } + const int rc = malloc_string_list(api.GetLabels(instance_name_utf8), out_strings, out_count); + if (rc != FREEUSD_OK) { + set_error("out of memory"); + return rc; + } clear_error(); return FREEUSD_OK; } catch (const std::exception& e) { diff --git a/src/usdSemantics/labelsAPI.cpp b/src/usdSemantics/labelsAPI.cpp new file mode 100644 index 0000000..a2b9bb2 --- /dev/null +++ b/src/usdSemantics/labelsAPI.cpp @@ -0,0 +1,81 @@ +#include "freeusd/usdSemantics/labelsAPI.hpp" + +#include + +#include "freeusd/tf/token.hpp" +#include "freeusd/usd/stage.hpp" + +namespace freeusd::usdSemantics { +namespace { + +static constexpr char kLabelAttrPrefix[] = "semantics:labels:"; + +freeusd::tf::Token labels_attr_token(const std::string& instance_name) { + if (instance_name.empty()) { + return {}; + } + return freeusd::tf::Token(std::string(kLabelAttrPrefix) + instance_name); +} + +bool starts_with_label_prefix(const std::string& name) { + constexpr std::size_t prefix_len = sizeof(kLabelAttrPrefix) - 1; + return name.size() > prefix_len && name.compare(0, prefix_len, kLabelAttrPrefix) == 0; +} + +} // namespace + +SemanticLabelsAPI SemanticLabelsAPI::ReadFromPrim(const std::shared_ptr& stage, + const freeusd::sdf::Path& path) { + if (!stage) { + return {}; + } + return SemanticLabelsAPI(stage->GetPrimAtPath(path)); +} + +bool SemanticLabelsAPI::HasLabels(const std::string& instance_name) const { + if (!prim.IsValid()) { + return false; + } + const freeusd::tf::Token attr = labels_attr_token(instance_name); + return !attr.IsEmpty() && prim.HasAttribute(attr); +} + +std::vector SemanticLabelsAPI::ListLabelSetNames() const { + std::vector out; + if (!prim.IsValid()) { + return out; + } + constexpr std::size_t prefix_len = sizeof(kLabelAttrPrefix) - 1; + for (const std::string& name : prim.ListAttributeNames()) { + if (starts_with_label_prefix(name)) { + out.push_back(name.substr(prefix_len)); + } + } + std::sort(out.begin(), out.end()); + out.erase(std::unique(out.begin(), out.end()), out.end()); + return out; +} + +std::vector SemanticLabelsAPI::GetLabels(const std::string& instance_name, double time) const { + std::vector out; + if (!prim.IsValid()) { + return out; + } + const freeusd::tf::Token attr = labels_attr_token(instance_name); + if (attr.IsEmpty()) { + return out; + } + std::vector labels; + if (!prim.GetAttribute(attr, time).GetTokenArray(&labels)) { + return out; + } + out.reserve(labels.size()); + for (const freeusd::tf::Token& label : labels) { + if (!label.IsEmpty()) { + out.push_back(label.GetText()); + } + } + return out; +} + +} // namespace freeusd::usdSemantics diff --git a/src/usdUtils/engineScene.cpp b/src/usdUtils/engineScene.cpp index ae33245..ed04352 100644 --- a/src/usdUtils/engineScene.cpp +++ b/src/usdUtils/engineScene.cpp @@ -13,8 +13,10 @@ #include "freeusd/usdLux/tokens.hpp" #include "freeusd/usdPhysics/collisionAPI.hpp" #include "freeusd/usdPhysics/fixedJoint.hpp" +#include "freeusd/usdPhysics/massAPI.hpp" #include "freeusd/usdPhysics/rigidBodyAPI.hpp" #include "freeusd/usdPhysics/tokens.hpp" +#include "freeusd/usdSemantics/labelsAPI.hpp" #include "freeusd/usdVol/openVdbAsset.hpp" #include "freeusd/usdVol/volume.hpp" #include "freeusd/usdVol/tokens.hpp" @@ -169,6 +171,10 @@ EngineSceneNode build_scene_node(const std::shared_ptr BuildEngineSpatialGroundingContext(const freeusd::usd::Stage& stage, + double time) { + std::vector records; + const std::shared_ptr stage_ptr = stage.shared_from_this(); + stage.TraversePreorder([&records, &stage, &stage_ptr, time](const freeusd::usd::Prim& prim) { + if (!prim.IsValid()) { + return true; + } + EngineSpatialGroundingRecord record; + record.path = prim.GetPath(); + record.name = prim.GetName(); + record.parent_path = record.path.GetParentPath(); + const freeusd::usd::Prim parent = stage.GetPrimAtPath(record.parent_path); + if (parent.IsValid()) { + for (const freeusd::usd::Prim& sibling : parent.GetChildren()) { + if (sibling.IsValid() && sibling.GetPath() != record.path) { + record.sibling_names.push_back(sibling.GetName()); + } + } + } + const freeusd::usdSemantics::SemanticLabelsAPI semantic_labels = + freeusd::usdSemantics::SemanticLabelsAPI::ReadFromPrim(stage_ptr, record.path); + for (const std::string& label_set_name : semantic_labels.ListLabelSetNames()) { + record.semantic_label_sets.push_back({label_set_name, semantic_labels.GetLabels(label_set_name, time)}); + } + + const freeusd::gf::Matrix4d local_to_world = + freeusd::usdGeom::Xformable(prim).ComputeLocalToWorldTransform(time); + record.world_position.set(local_to_world.m[12], local_to_world.m[13], local_to_world.m[14]); + + const freeusd::gf::BBox3d world_bound = freeusd::usdGeom::Boundable(prim).ComputeWorldBound(time); + if (!world_bound.IsEmpty()) { + record.has_world_bound = true; + record.world_bound_dimensions.set(world_bound.max.x() - world_bound.min.x(), + world_bound.max.y() - world_bound.min.y(), + world_bound.max.z() - world_bound.min.z()); + } + + float mass = 0.0F; + const freeusd::usdPhysics::RigidBodyAPI rigid_body = + freeusd::usdPhysics::RigidBodyAPI::ReadFromPrim(stage_ptr, record.path); + if (rigid_body.GetMass(&mass, time)) { + record.mass_kg = static_cast(mass); + } else { + const freeusd::usdPhysics::MassAPI mass_api = freeusd::usdPhysics::MassAPI::ReadFromPrim(stage_ptr, record.path); + if (mass_api.IsMassAPI() && prim.GetAttribute(freeusd::usdPhysics::tokens::physics_mass(), time).GetFloat(&mass)) { + record.mass_kg = static_cast(mass); + } + } + + records.push_back(std::move(record)); + return true; + }); + return records; +} + EnginePrimEditorView BuildEnginePrimEditorView(const freeusd::usd::Prim& prim, double time) { EnginePrimEditorView view; if (!prim.IsValid()) { @@ -346,6 +411,9 @@ EngineRuntimeSupportReport AssessEngineRuntimeSupport(const freeusd::usd::Stage& if (freeusd::usdVol::Volume(prim).IsVolume()) { report.uses_volumes = true; } + const freeusd::usdSemantics::SemanticLabelsAPI semantic_labels = + freeusd::usdSemantics::SemanticLabelsAPI::ReadFromPrim(stage.shared_from_this(), prim.GetPath()); + report.uses_semantic_labels = report.uses_semantic_labels || !semantic_labels.ListLabelSetNames().empty(); report.uses_composed_prim_kind = report.uses_composed_prim_kind || prim.HasPrimKind(); report.uses_prim_active_opinions = report.uses_prim_active_opinions || prim.HasPrimActiveOpinion(); const bool metadata_arc_prim = @@ -479,6 +547,10 @@ EngineRuntimeSupportReport AssessEngineRuntimeSupport(const freeusd::usd::Stage& append_warning(&report.warnings, &seen_warnings, "Scene uses Volume prims; resolve field assets and VDB paths during offline import."); } + if (report.uses_semantic_labels) { + append_warning(&report.warnings, &seen_warnings, + "Scene uses semantic labels; bake or validate label mappings during import."); + } const bool requires_prebake = report.uses_composed_layer_stack || report.uses_references || report.uses_payloads || report.uses_inherits || report.uses_specializes || report.uses_variant_selection || @@ -486,7 +558,8 @@ EngineRuntimeSupportReport AssessEngineRuntimeSupport(const freeusd::usd::Stage& report.uses_time_samples; if (requires_prebake) { report.recommended_mode = EngineRuntimeMode::PreBakedAssetsOnly; - } else if (report.uses_relationships || report.uses_custom_data || report.uses_attribute_connections) { + } else if (report.uses_relationships || report.uses_custom_data || report.uses_attribute_connections || + report.uses_semantic_labels) { append_warning(&report.warnings, &seen_warnings, "Scene uses composed metadata beyond static transforms; limit runtime usage to hybrid metadata reads."); report.recommended_mode = EngineRuntimeMode::HybridMetadata; diff --git a/tests/c_engine_integration.c b/tests/c_engine_integration.c index ab9ddf9..952e8c6 100644 --- a/tests/c_engine_integration.c +++ b/tests/c_engine_integration.c @@ -68,9 +68,16 @@ int main(void) { return 8; } + /* Xform prims without local bounds aggregate descendant world bounds. */ if (freeusd_stage_compute_boundable_world_bounds(stage, "/World", 1.0, &min_x, &min_y, &min_z, &max_x, &max_y, - &max_z) != FREEUSD_ERR_NOT_FOUND) { - fprintf(stderr, "expected NOT_FOUND for non-boundable world prim\n"); + &max_z) != FREEUSD_OK) { + fprintf(stderr, "expected aggregated world bounds for /World: %s\n", freeusd_last_error_message()); + freeusd_stage_free(stage); + return 9; + } + if (!nearly(min_x, 0.0) || !nearly(min_y, 1.0) || !nearly(min_z, 2.0) || !nearly(max_x, 2.0) || + !nearly(max_y, 3.0) || !nearly(max_z, 4.0)) { + fprintf(stderr, "unexpected aggregated world bounds\n"); freeusd_stage_free(stage); return 9; } @@ -108,5 +115,23 @@ int main(void) { return 14; } freeusd_stage_free(stage); + + if (snprintf(path, sizeof path, "%s/parity_semantics_labels.usda", FREEUSD_TEST_FIXTURES_DIR) >= (int)sizeof path) { + fprintf(stderr, "semantics fixture path too long\n"); + return 15; + } + stage = freeusd_stage_open_from_root_file_utf8(path, 2); + if (!stage) { + fprintf(stderr, "semantics stage open failed: %s\n", freeusd_last_error_message()); + return 16; + } + FreeusdEngineRuntimeSupport report; + if (freeusd_usdutils_assess_engine_runtime_support(stage, &report) != FREEUSD_OK || !report.uses_semantic_labels || + report.recommended_mode != FREEUSD_ENGINE_RUNTIME_HYBRID) { + fprintf(stderr, "semantic runtime report mismatch: %s\n", freeusd_last_error_message()); + freeusd_stage_free(stage); + return 17; + } + freeusd_stage_free(stage); return 0; } diff --git a/tests/c_usd_semantics_labels.c b/tests/c_usd_semantics_labels.c new file mode 100644 index 0000000..000a002 --- /dev/null +++ b/tests/c_usd_semantics_labels.c @@ -0,0 +1,88 @@ +#include + +#include +#include + +#ifndef FREEUSD_TEST_FIXTURES_DIR +#error "FREEUSD_TEST_FIXTURES_DIR must be set by CMake for c_usd_semantics_labels" +#endif + +static int has_string(char** strings, size_t count, const char* want) { + for (size_t i = 0; i < count; ++i) { + if (strings[i] && strcmp(strings[i], want) == 0) { + return 1; + } + } + return 0; +} + +int main(void) { + char path[4096]; + if (snprintf(path, sizeof path, "%s/parity_semantics_labels.usda", FREEUSD_TEST_FIXTURES_DIR) >= (int)sizeof path) { + fprintf(stderr, "fixture path too long\n"); + return 1; + } + + FreeusdStage* stage = freeusd_stage_open_from_root_file_utf8(path, 2); + if (!stage) { + fprintf(stderr, "stage open failed: %s\n", freeusd_last_error_message()); + return 2; + } + + char** sets = NULL; + size_t set_count = 0; + if (freeusd_stage_list_semantic_label_sets(stage, "/World/Kitchen/CupBlue", &sets, &set_count) != FREEUSD_OK) { + fprintf(stderr, "list label sets failed: %s\n", freeusd_last_error_message()); + freeusd_stage_free(stage); + return 3; + } + if (set_count != 2 || !has_string(sets, set_count, "engine") || !has_string(sets, set_count, "somaHome")) { + fprintf(stderr, "unexpected label sets\n"); + freeusd_path_list_free(sets, set_count); + freeusd_stage_free(stage); + return 4; + } + freeusd_path_list_free(sets, set_count); + + char** labels = NULL; + size_t label_count = 0; + if (freeusd_stage_read_semantic_labels(stage, "/World/Kitchen/CupBlue", "somaHome", &labels, &label_count) != + FREEUSD_OK) { + fprintf(stderr, "read labels failed: %s\n", freeusd_last_error_message()); + freeusd_stage_free(stage); + return 5; + } + if (label_count != 2 || strcmp(labels[0], "Crockery") != 0 || strcmp(labels[1], "DesignedContainer") != 0) { + fprintf(stderr, "unexpected somaHome labels\n"); + freeusd_path_list_free(labels, label_count); + freeusd_stage_free(stage); + return 6; + } + freeusd_path_list_free(labels, label_count); + + if (freeusd_stage_read_semantic_labels(stage, "/World/Kitchen/Stove", "somaHome", &labels, &label_count) != + FREEUSD_OK) { + fprintf(stderr, "read stove labels failed: %s\n", freeusd_last_error_message()); + freeusd_stage_free(stage); + return 7; + } + if (label_count != 1 || strcmp(labels[0], "Appliance") != 0) { + fprintf(stderr, "unexpected stove label\n"); + freeusd_path_list_free(labels, label_count); + freeusd_stage_free(stage); + return 8; + } + freeusd_path_list_free(labels, label_count); + + if (freeusd_stage_read_semantic_labels(stage, "/World/Kitchen/CupBlue", "missing", &labels, &label_count) != + FREEUSD_OK || + labels != NULL || label_count != 0) { + fprintf(stderr, "missing label set should be empty\n"); + freeusd_path_list_free(labels, label_count); + freeusd_stage_free(stage); + return 9; + } + + freeusd_stage_free(stage); + return 0; +} diff --git a/tests/c_usdutils_spatial_grounding.c b/tests/c_usdutils_spatial_grounding.c new file mode 100644 index 0000000..6457e2c --- /dev/null +++ b/tests/c_usdutils_spatial_grounding.c @@ -0,0 +1,118 @@ +#include + +#include +#include +#include + +#ifndef FREEUSD_TEST_FIXTURES_DIR +#error "FREEUSD_TEST_FIXTURES_DIR must be set by CMake for c_usdutils_spatial_grounding" +#endif + +static int nearly(double a, double b) { return fabs(a - b) <= 1e-9; } + +static const FreeusdSpatialGroundingRecord* find_record(const FreeusdSpatialGroundingRecord* records, size_t count, + const char* path) { + for (size_t i = 0; i < count; ++i) { + if (records[i].path_utf8 && strcmp(records[i].path_utf8, path) == 0) { + return &records[i]; + } + } + return NULL; +} + +static int has_sibling(const FreeusdSpatialGroundingRecord* record, const char* name) { + for (size_t i = 0; i < record->sibling_name_count; ++i) { + if (record->sibling_names_utf8[i] && strcmp(record->sibling_names_utf8[i], name) == 0) { + return 1; + } + } + return 0; +} + +static const FreeusdSemanticLabelSet* find_label_set(const FreeusdSpatialGroundingRecord* record, const char* name) { + for (size_t i = 0; i < record->semantic_label_set_count; ++i) { + if (record->semantic_label_sets[i].name_utf8 && strcmp(record->semantic_label_sets[i].name_utf8, name) == 0) { + return &record->semantic_label_sets[i]; + } + } + return NULL; +} + +int main(void) { + char path[4096]; + if (snprintf(path, sizeof path, "%s/parity_spatial_grounding.usda", FREEUSD_TEST_FIXTURES_DIR) >= + (int)sizeof path) { + fprintf(stderr, "fixture path too long\n"); + return 1; + } + + FreeusdStage* stage = freeusd_stage_open_from_root_file_utf8(path, 2); + if (!stage) { + fprintf(stderr, "stage open failed: %s\n", freeusd_last_error_message()); + return 2; + } + + FreeusdSpatialGroundingRecord* records = NULL; + size_t count = 0; + if (freeusd_usdutils_build_spatial_grounding_context(stage, 1.0, &records, &count) != FREEUSD_OK) { + fprintf(stderr, "spatial context failed: %s\n", freeusd_last_error_message()); + freeusd_stage_free(stage); + return 3; + } + if (count != 5 || !records) { + fprintf(stderr, "unexpected record count: %zu\n", count); + freeusd_usdutils_spatial_grounding_records_free(records, count); + freeusd_stage_free(stage); + return 4; + } + + const FreeusdSpatialGroundingRecord* cup = find_record(records, count, "/World/Kitchen/CupBlue"); + if (!cup || strcmp(cup->name_utf8, "CupBlue") != 0 || strcmp(cup->parent_path_utf8, "/World/Kitchen") != 0) { + fprintf(stderr, "missing cup record\n"); + freeusd_usdutils_spatial_grounding_records_free(records, count); + freeusd_stage_free(stage); + return 5; + } + if (cup->sibling_name_count != 2 || !has_sibling(cup, "PlateGreen") || !has_sibling(cup, "Stove")) { + fprintf(stderr, "unexpected sibling names\n"); + freeusd_usdutils_spatial_grounding_records_free(records, count); + freeusd_stage_free(stage); + return 6; + } + const FreeusdSemanticLabelSet* soma = find_label_set(cup, "somaHome"); + const FreeusdSemanticLabelSet* engine = find_label_set(cup, "engine"); + if (cup->semantic_label_set_count != 2 || !soma || !engine || soma->label_count != 2 || engine->label_count != 2 || + strcmp(soma->labels_utf8[0], "Crockery") != 0 || strcmp(soma->labels_utf8[1], "DesignedContainer") != 0 || + strcmp(engine->labels_utf8[0], "pickup") != 0 || strcmp(engine->labels_utf8[1], "container") != 0) { + fprintf(stderr, "unexpected semantic label sets\n"); + freeusd_usdutils_spatial_grounding_records_free(records, count); + freeusd_stage_free(stage); + return 7; + } + if (!nearly(cup->world_position[0], 6.0) || !nearly(cup->world_position[1], 2.0) || + !nearly(cup->world_position[2], 3.0) || !cup->has_world_bound || + !nearly(cup->world_bound_dimensions[0], 0.5) || !nearly(cup->world_bound_dimensions[1], 1.5) || + !nearly(cup->world_bound_dimensions[2], 0.25) || !cup->has_mass_kg || fabs(cup->mass_kg - 0.35) > 1e-6) { + fprintf(stderr, "unexpected cup spatial values\n"); + freeusd_usdutils_spatial_grounding_records_free(records, count); + freeusd_stage_free(stage); + return 8; + } + + /* Xform prims without local bounds aggregate descendant world bounds. */ + const FreeusdSpatialGroundingRecord* kitchen = find_record(records, count, "/World/Kitchen"); + if (!kitchen || strcmp(kitchen->parent_path_utf8, "/World") != 0 || kitchen->sibling_name_count != 0 || + kitchen->semantic_label_set_count != 0 || !nearly(kitchen->world_position[0], 10.0) || + !kitchen->has_world_bound || !nearly(kitchen->world_bound_dimensions[0], 9.25) || + !nearly(kitchen->world_bound_dimensions[1], 4.25) || !nearly(kitchen->world_bound_dimensions[2], 7.5) || + kitchen->has_mass_kg) { + fprintf(stderr, "unexpected kitchen record\n"); + freeusd_usdutils_spatial_grounding_records_free(records, count); + freeusd_stage_free(stage); + return 9; + } + + freeusd_usdutils_spatial_grounding_records_free(records, count); + freeusd_stage_free(stage); + return 0; +} diff --git a/tests/engine_integration_test.cpp b/tests/engine_integration_test.cpp index 8e92816..096232c 100644 --- a/tests/engine_integration_test.cpp +++ b/tests/engine_integration_test.cpp @@ -467,5 +467,24 @@ int main() { assert(report.recommended_mode == EngineRuntimeMode::HybridMetadata); } + { + std::string err; + auto stage = + Stage::OpenFromRootFile(fixture("parity_semantics_labels.usda"), RootLayerSublayersPolicy::DepthFirst, &err); + assert(stage && err.empty()); + const auto snapshot = BuildEngineSceneSnapshot(*stage, 1.0); + assert(snapshot.semantic_label_prim_paths.size() == 2u); + assert(std::find(snapshot.semantic_label_prim_paths.begin(), snapshot.semantic_label_prim_paths.end(), + Path::FromString("/World/Kitchen/CupBlue")) != snapshot.semantic_label_prim_paths.end()); + const auto* cup = find_node(snapshot, Path::FromString("/World/Kitchen/CupBlue")); + assert(cup != nullptr); + assert(cup->has_semantic_labels); + assert(cup->semantic_label_set_names == std::vector({"engine", "somaHome"})); + + const auto report = AssessEngineRuntimeSupport(*stage); + assert(report.uses_semantic_labels); + assert(report.recommended_mode == EngineRuntimeMode::HybridMetadata); + } + return 0; } diff --git a/tests/fixtures/parity_semantics_labels.usda b/tests/fixtures/parity_semantics_labels.usda new file mode 100644 index 0000000..a2ff97f --- /dev/null +++ b/tests/fixtures/parity_semantics_labels.usda @@ -0,0 +1,21 @@ +#usda 1.0 +( + defaultPrim = "World" +) + +def Xform "World" +{ + def Xform "Kitchen" + { + def Xform "CupBlue" + { + token[] semantics:labels:somaHome = ["Crockery", "DesignedContainer"] + token[] semantics:labels:engine = ["pickup", "container"] + } + + def Xform "Stove" + { + token[] semantics:labels:somaHome = ["Appliance"] + } + } +} diff --git a/tests/fixtures/parity_spatial_grounding.usda b/tests/fixtures/parity_spatial_grounding.usda new file mode 100644 index 0000000..5e48961 --- /dev/null +++ b/tests/fixtures/parity_spatial_grounding.usda @@ -0,0 +1,43 @@ +#usda 1.0 +( + defaultPrim = "World" + metersPerUnit = 1.0 + upAxis = "Y" +) + +def Xform "World" +{ + def Xform "Kitchen" + { + double3 xformOp:translate = (10, 0, 0) + token[] xformOpOrder = [xformOp:translate] + + def Xform "CupBlue" + ( + prepend apiSchemas = ["PhysicsRigidBodyAPI"] + ) + { + double size = 1.0 + float physics:mass = 0.35 + double3 xformOp:translate = (1, 2, 3) + double3 xformOp:scale = (0.5, 1.5, 0.25) + token[] xformOpOrder = [xformOp:translate, xformOp:scale] + token[] semantics:labels:somaHome = ["Crockery", "DesignedContainer"] + token[] semantics:labels:engine = ["pickup", "container"] + } + + def Xform "PlateGreen" + { + double size = 2.0 + double3 xformOp:translate = (4, 0, 0) + token[] xformOpOrder = [xformOp:translate] + } + + def Xform "Stove" + { + double size = 3.0 + double3 xformOp:translate = (0, 0, 5) + token[] xformOpOrder = [xformOp:translate] + } + } +} diff --git a/tests/python/test_engine_integration.py b/tests/python/test_engine_integration.py index d1035fe..61dc623 100644 --- a/tests/python/test_engine_integration.py +++ b/tests/python/test_engine_integration.py @@ -45,6 +45,57 @@ def test_engine_scene_snapshot_and_runtime_mode_bindings() -> None: assert engine_runtime_mode_name(report.recommended_mode) == "experimental_live_stage" +def test_spatial_grounding_context_binding() -> None: + stage = _open_stage("parity_spatial_grounding.usda") + records = stage.build_spatial_grounding_context(1.0) + assert len(records) == 5 + by_path = {record["path"]: record for record in records} + + cup = by_path["/World/Kitchen/CupBlue"] + assert cup["name"] == "CupBlue" + assert cup["parent_path"] == "/World/Kitchen" + assert sorted(cup["sibling_names"]) == ["PlateGreen", "Stove"] + assert cup["semantic_label_sets"] == [ + {"name": "engine", "labels": ["pickup", "container"]}, + {"name": "somaHome", "labels": ["Crockery", "DesignedContainer"]}, + ] + assert cup["world_position"] == (6.0, 2.0, 3.0) + assert cup["has_world_bound"] is True + assert cup["world_bound_dimensions"] == (0.5, 1.5, 0.25) + assert abs(cup["mass_kg"] - 0.35) < 1e-6 + + kitchen = by_path["/World/Kitchen"] + assert kitchen["parent_path"] == "/World" + assert kitchen["sibling_names"] == [] + assert kitchen["semantic_label_sets"] == [] + assert kitchen["world_position"] == (10.0, 0.0, 0.0) + # Xform prims without local bounds aggregate descendant world bounds. + assert kitchen["has_world_bound"] is True + assert kitchen["world_bound_dimensions"] == (9.25, 4.25, 7.5) + assert kitchen["mass_kg"] is None + + +def test_semantic_labels_binding() -> None: + stage = _open_stage("parity_semantics_labels.usda") + cup = SdfPath.from_string("/World/Kitchen/CupBlue") + assert stage.list_semantic_label_sets(cup) == ["engine", "somaHome"] + assert stage.read_semantic_labels(cup, "somaHome", 1.0) == ["Crockery", "DesignedContainer"] + assert stage.read_semantic_labels(cup, "engine", 1.0) == ["pickup", "container"] + assert stage.read_semantic_labels(cup, "missing", 1.0) == [] + assert stage.read_semantic_labels(SdfPath.from_string("/World/Kitchen/Stove"), "somaHome", 1.0) == ["Appliance"] + snapshot = build_engine_scene_snapshot(stage, 1.0) + assert sorted(path.text() for path in snapshot.semantic_label_prim_paths) == [ + "/World/Kitchen/CupBlue", + "/World/Kitchen/Stove", + ] + cup_node = next(node for node in snapshot.nodes if node.path.text() == "/World/Kitchen/CupBlue") + assert cup_node.has_semantic_labels + assert cup_node.semantic_label_set_names == ["engine", "somaHome"] + report = assess_engine_runtime_support(stage) + assert report.uses_semantic_labels + assert report.recommended_mode == EngineRuntimeMode.hybrid_metadata + + def test_engine_editor_view_and_prebake_reports_bindings() -> None: stack_stage = _open_stage("parity_stack_root.usda") editor = build_engine_prim_editor_view(stack_stage.prim_at(SdfPath.from_string("/World/Model")), 15.0) diff --git a/tests/schema_tokens_bundle_test.cpp b/tests/schema_tokens_bundle_test.cpp index 73007a6..892913a 100644 --- a/tests/schema_tokens_bundle_test.cpp +++ b/tests/schema_tokens_bundle_test.cpp @@ -11,6 +11,7 @@ int main() { assert(freeusd::usdGeom::tokens::points().GetText() == std::string("points")); assert(freeusd::usdShade::tokens::Material().GetText() == std::string("Material")); assert(freeusd::usdPhysics::tokens::PhysicsScene().GetText() == std::string("PhysicsScene")); + assert(freeusd::usdSemantics::tokens::SemanticsLabelsAPI().GetText() == std::string("SemanticsLabelsAPI")); assert(freeusd::usdRender::tokens::RenderSettings().GetText() == std::string("RenderSettings")); assert(freeusd::usdUI::tokens::ui_displayName().GetText() == std::string("ui:displayName")); assert(freeusd::usd::schemaDataTokens::ModelAPI().GetText() == std::string("ModelAPI")); diff --git a/tests/usd_semantics_labels_test.cpp b/tests/usd_semantics_labels_test.cpp new file mode 100644 index 0000000..7a034b0 --- /dev/null +++ b/tests/usd_semantics_labels_test.cpp @@ -0,0 +1,47 @@ +#include +#include +#include + +#include "freeusd/sdf/path.hpp" +#include "freeusd/usd/stage.hpp" +#include "freeusd/usdSemantics/labelsAPI.hpp" + +#ifndef FREEUSD_TEST_FIXTURES_DIR +#error "FREEUSD_TEST_FIXTURES_DIR must be set by CMake for usd_semantics_labels_test" +#endif + +namespace { + +std::string fixture(const char* name) { + return std::string(FREEUSD_TEST_FIXTURES_DIR) + "/" + name; +} + +} // namespace + +int main() { + using freeusd::sdf::Path; + using freeusd::usd::RootLayerSublayersPolicy; + using freeusd::usd::Stage; + using freeusd::usdSemantics::SemanticLabelsAPI; + + std::string err; + auto stage = + Stage::OpenFromRootFile(fixture("parity_semantics_labels.usda"), RootLayerSublayersPolicy::DepthFirst, &err); + assert(stage && err.empty()); + + const auto cup = SemanticLabelsAPI::ReadFromPrim(stage, Path::FromString("/World/Kitchen/CupBlue")); + assert(cup); + assert(cup.HasLabels("somaHome")); + assert(cup.HasLabels("engine")); + assert(!cup.HasLabels("missing")); + assert(cup.ListLabelSetNames() == std::vector({"engine", "somaHome"})); + assert(cup.GetLabels("somaHome") == std::vector({"Crockery", "DesignedContainer"})); + assert(cup.GetLabels("engine") == std::vector({"pickup", "container"})); + assert(cup.GetLabels("missing").empty()); + + const auto stove = SemanticLabelsAPI::ReadFromPrim(stage, Path::FromString("/World/Kitchen/Stove")); + assert(stove.GetLabels("somaHome") == std::vector({"Appliance"})); + assert(stove.ListLabelSetNames() == std::vector({"somaHome"})); + + return 0; +} diff --git a/tests/usdutils_spatial_grounding_test.cpp b/tests/usdutils_spatial_grounding_test.cpp new file mode 100644 index 0000000..26990aa --- /dev/null +++ b/tests/usdutils_spatial_grounding_test.cpp @@ -0,0 +1,94 @@ +#include +#include +#include +#include + +#include "freeusd/sdf/path.hpp" +#include "freeusd/usd/stage.hpp" +#include "freeusd/usdUtils/engineScene.hpp" + +#ifndef FREEUSD_TEST_FIXTURES_DIR +#error "FREEUSD_TEST_FIXTURES_DIR must be set by CMake for usdutils_spatial_grounding_test" +#endif + +namespace { + +std::string fixture(const char* name) { + return std::string(FREEUSD_TEST_FIXTURES_DIR) + "/" + name; +} + +bool near(double a, double b) { + return std::fabs(a - b) < 1e-9; +} + +const freeusd::usdUtils::EngineSpatialGroundingRecord* find_record( + const std::vector& records, const freeusd::sdf::Path& path) { + for (const auto& record : records) { + if (record.path == path) { + return &record; + } + } + return nullptr; +} + +bool has_sibling(const freeusd::usdUtils::EngineSpatialGroundingRecord& record, const std::string& name) { + return std::find(record.sibling_names.begin(), record.sibling_names.end(), name) != record.sibling_names.end(); +} + +} // namespace + +int main() { + using freeusd::sdf::Path; + using freeusd::usd::RootLayerSublayersPolicy; + using freeusd::usd::Stage; + using freeusd::usdUtils::BuildEngineSpatialGroundingContext; + + std::string err; + auto stage = + Stage::OpenFromRootFile(fixture("parity_spatial_grounding.usda"), RootLayerSublayersPolicy::DepthFirst, &err); + assert(stage && err.empty()); + + const auto records = BuildEngineSpatialGroundingContext(*stage, 1.0); + const auto* cup = find_record(records, Path::FromString("/World/Kitchen/CupBlue")); + const auto* kitchen = find_record(records, Path::FromString("/World/Kitchen")); + assert(cup != nullptr); + assert(kitchen != nullptr); + + assert(cup->name == "CupBlue"); + assert(cup->parent_path == Path::FromString("/World/Kitchen")); + assert(cup->sibling_names.size() == 2u); + assert(has_sibling(*cup, "PlateGreen")); + assert(has_sibling(*cup, "Stove")); + assert(cup->semantic_label_sets.size() == 2u); + assert(cup->semantic_label_sets[0].name == "engine"); + assert(cup->semantic_label_sets[0].labels == std::vector({"pickup", "container"})); + assert(cup->semantic_label_sets[1].name == "somaHome"); + assert(cup->semantic_label_sets[1].labels == std::vector({"Crockery", "DesignedContainer"})); + + assert(near(cup->world_position.x(), 6.0)); + assert(near(cup->world_position.y(), 2.0)); + assert(near(cup->world_position.z(), 3.0)); + assert(cup->has_world_bound); + assert(near(cup->world_bound_dimensions.x(), 0.5)); + assert(near(cup->world_bound_dimensions.y(), 1.5)); + assert(near(cup->world_bound_dimensions.z(), 0.25)); + assert(cup->mass_kg.has_value()); + // Authored as float; compare with float-precision tolerance. + assert(std::fabs(*cup->mass_kg - 0.35) < 1e-6); + + assert(kitchen->name == "Kitchen"); + assert(kitchen->parent_path == Path::FromString("/World")); + assert(kitchen->sibling_names.empty()); + assert(kitchen->semantic_label_sets.empty()); + assert(near(kitchen->world_position.x(), 10.0)); + assert(near(kitchen->world_position.y(), 0.0)); + assert(near(kitchen->world_position.z(), 0.0)); + // Xform prims without local bounds now aggregate descendant world bounds. + assert(kitchen->has_world_bound); + assert(near(kitchen->world_bound_dimensions.x(), 9.25)); + assert(near(kitchen->world_bound_dimensions.y(), 4.25)); + assert(near(kitchen->world_bound_dimensions.z(), 7.5)); + assert(!kitchen->mass_kg.has_value()); + + return 0; +}