mirror of
https://github.com/blender/blender
synced 2026-09-29 04:37:17 +03:00
Fix #149678: Add USD import/export of UsdUIAccessibilityAPI data
This change adds support for importing and exporting accessibility metadata to USD. Details: USD supports authoring accessibility metadata via the UsdUIAccessibilityAPI schema. Two methods for authoring accessibility metadata are supported: 1) An accessibility label and description can be specified directly in the USD export options via two new string export fields This data will be set on the default prim of the exported stage and will have a "standard" priority. 2) Accessibility data can be authored in the Blender object's Custom Properties which will be written to the corresponding USD prim during the export. If creating custom properties, the property name must be defined in the following format: - accessibility:\<namespace\>:label - accessibility:\<namespace\>:description - accessibility:\<namespace\>:priority Since the AccessibilityAPI is a MultipleApply schema, the namespace can specify an intended purpose for the accessibility metadata. Note: Although the AccessibilityAPI schema supports time-samples, Blender STRING properties are not keyframe-able, so this change only writes default attribute values without any time-samples. On import, the accessibility properties are populated back into the object's Custom Properties. Authored by Apple: Dan Knowlton Pull Request: https://projects.blender.org/blender/blender/pulls/149682
This commit is contained in:
parent
17945ce420
commit
54ce53ac4e
7 changed files with 321 additions and 2 deletions
|
|
@ -364,6 +364,9 @@ static wmOperatorStatus wm_usd_export_exec(bContext *C, wmOperator *op)
|
|||
RNA_string_get(op->ptr, "custom_properties_namespace", params.custom_properties_namespace);
|
||||
RNA_string_get(op->ptr, "collection", params.collection);
|
||||
|
||||
params.accessibility_label = RNA_string_get(op->ptr, "accessibility_label");
|
||||
params.accessibility_description = RNA_string_get(op->ptr, "accessibility_description");
|
||||
|
||||
bool ok = USD_export(C, filepath, ¶ms, as_background_job, op->reports);
|
||||
|
||||
return as_background_job || ok ? OPERATOR_FINISHED : OPERATOR_CANCELLED;
|
||||
|
|
@ -497,6 +500,14 @@ static void wm_usd_export_draw(bContext *C, wmOperator *op)
|
|||
}
|
||||
}
|
||||
|
||||
if (blender::ui::Layout *panel = layout.panel(
|
||||
C, "USD_export_accessibility", true, IFACE_("Accessibility")))
|
||||
{
|
||||
blender::ui::Layout &col = panel->column(false);
|
||||
col.prop(ptr, "accessibility_label", UI_ITEM_NONE, std::nullopt, ICON_NONE);
|
||||
col.prop(ptr, "accessibility_description", UI_ITEM_NONE, std::nullopt, ICON_NONE);
|
||||
}
|
||||
|
||||
if (blender::ui::Layout *panel = layout.panel(
|
||||
C, "USD_export_experimental", true, IFACE_("Experimental")))
|
||||
{
|
||||
|
|
@ -732,6 +743,22 @@ void WM_OT_usd_export(wmOperatorType *ot)
|
|||
"to blender object and data names which are always exported in the "
|
||||
"'userProperties:blender' namespace");
|
||||
|
||||
prop = RNA_def_string(ot->srna,
|
||||
"accessibility_label",
|
||||
nullptr,
|
||||
0,
|
||||
"Label",
|
||||
"Set the accessibility label for the exported stage's default prim");
|
||||
RNA_def_property_flag(prop, PROP_SKIP_SAVE);
|
||||
|
||||
prop = RNA_def_string(ot->srna,
|
||||
"accessibility_description",
|
||||
nullptr,
|
||||
0,
|
||||
"Description",
|
||||
"Set the accessibility description for the exported stage's default prim");
|
||||
RNA_def_property_flag(prop, PROP_SKIP_SAVE);
|
||||
|
||||
RNA_def_boolean(ot->srna,
|
||||
"author_blender_name",
|
||||
true,
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@
|
|||
#include <pxr/usd/usdGeom/tokens.h>
|
||||
#include <pxr/usd/usdGeom/xform.h>
|
||||
#include <pxr/usd/usdGeom/xformCommonAPI.h>
|
||||
#include <pxr/usd/usdUI/accessibilityAPI.h>
|
||||
#include <pxr/usd/usdUtils/usdzPackage.h>
|
||||
|
||||
#include "MEM_guardedalloc.h"
|
||||
|
|
@ -195,6 +196,35 @@ static void ensure_root_prim(pxr::UsdStageRefPtr stage, const USDExportParams &p
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If the user has provided an accessibility label and description for the export,
|
||||
* write that information to the exported stage's default prim. This information
|
||||
* will be written with the `UsdUIAccessibilityAPI` under the `default`
|
||||
* namespace. Note: The information will only be added if the label is non-empty.
|
||||
*/
|
||||
static void write_root_accessibility_information(pxr::UsdStageRefPtr stage,
|
||||
const USDExportParams ¶ms)
|
||||
{
|
||||
/* Don't apply the API if both the label and description are empty. */
|
||||
if (params.accessibility_label.empty() && params.accessibility_description.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
pxr::UsdUIAccessibilityAPI accessibility_api = pxr::UsdUIAccessibilityAPI::ApplyDefaultAPI(
|
||||
stage->GetDefaultPrim());
|
||||
if (!accessibility_api) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!params.accessibility_label.empty()) {
|
||||
accessibility_api.CreateLabelAttr().Set(params.accessibility_label);
|
||||
}
|
||||
|
||||
if (!params.accessibility_description.empty()) {
|
||||
accessibility_api.CreateDescriptionAttr().Set(params.accessibility_description);
|
||||
}
|
||||
}
|
||||
|
||||
static void report_job_duration(const ExportJobData *data)
|
||||
{
|
||||
timeit::Nanoseconds duration = timeit::Clock::now() - data->start_time;
|
||||
|
|
@ -587,6 +617,9 @@ pxr::UsdStageRefPtr export_to_stage(const USDExportParams ¶ms,
|
|||
}
|
||||
}
|
||||
|
||||
/* Write accessibility information to the default prim. */
|
||||
write_root_accessibility_information(usd_stage, params);
|
||||
|
||||
if (params.use_instancing) {
|
||||
process_scene_graph_instances(params, usd_stage);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
#include "BKE_idprop.hh"
|
||||
|
||||
#include <pxr/usd/usd/attribute.h>
|
||||
#include <pxr/usd/usdUI/accessibilityAPI.h>
|
||||
|
||||
#include "CLG_log.h"
|
||||
static CLG_LogRef LOG = {"io.usd"};
|
||||
|
|
@ -155,11 +156,49 @@ static void set_double_prop(IDProperty *idgroup, const StringRefNull prop_name,
|
|||
IDP_AddToGroup(idgroup, prop);
|
||||
}
|
||||
|
||||
static void set_accessibility_property(const pxr::UsdAttribute &attr,
|
||||
IDProperty *idgroup,
|
||||
const pxr::UsdTimeCode time_code)
|
||||
{
|
||||
/* Only set the property if the attribute has an authored value. */
|
||||
if (!attr.IsAuthored()) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* Since STRING properties do not support keyframes, if there is already a value written
|
||||
* for this property, don't try to write it again. */
|
||||
std::string property_name = attr.GetName().GetString();
|
||||
if (IDP_GetPropertyFromGroup(idgroup, property_name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
pxr::SdfValueTypeName type_name = attr.GetTypeName();
|
||||
if (type_name == pxr::SdfValueTypeNames->String) {
|
||||
std::string value;
|
||||
if (attr.Get<std::string>(&value, time_code)) {
|
||||
set_string_prop(idgroup, property_name, value);
|
||||
}
|
||||
}
|
||||
else if (type_name == pxr::SdfValueTypeNames->Token) {
|
||||
pxr::TfToken value;
|
||||
if (attr.Get<pxr::TfToken>(&value, time_code)) {
|
||||
set_string_prop(idgroup, property_name, value.GetString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void set_id_props_from_prim(ID *id,
|
||||
const pxr::UsdPrim &prim,
|
||||
const eUSDPropertyImportMode property_import_mode,
|
||||
const pxr::UsdTimeCode time_code)
|
||||
{
|
||||
for (const auto &api : pxr::UsdUIAccessibilityAPI::GetAll(prim)) {
|
||||
IDProperty *idgroup = IDP_EnsureProperties(id);
|
||||
set_accessibility_property(api.GetLabelAttr(), idgroup, time_code);
|
||||
set_accessibility_property(api.GetDescriptionAttr(), idgroup, time_code);
|
||||
set_accessibility_property(api.GetPriorityAttr(), idgroup, time_code);
|
||||
}
|
||||
|
||||
pxr::UsdAttributeVector attribs = prim.GetAuthoredAttributes();
|
||||
if (attribs.empty()) {
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
#include <pxr/base/tf/stringUtils.h>
|
||||
#include <pxr/usd/usdGeom/bboxCache.h>
|
||||
#include <pxr/usd/usdGeom/scope.h>
|
||||
#include <pxr/usd/usdUI/accessibilityAPI.h>
|
||||
|
||||
#include "BKE_customdata.hh"
|
||||
|
||||
|
|
@ -27,6 +28,76 @@ namespace usdtokens {
|
|||
static const pxr::TfToken blender_ns("userProperties:blender", pxr::TfToken::Immortal);
|
||||
} // namespace usdtokens
|
||||
|
||||
namespace {
|
||||
struct AccessibilityPropertyName {
|
||||
pxr::TfToken property_namespace;
|
||||
pxr::TfToken property_base_name;
|
||||
};
|
||||
} // anonymous namespace
|
||||
|
||||
static std::optional<AccessibilityPropertyName> parse_accessibility_property_name(
|
||||
IDProperty *prop, bool allow_unicode)
|
||||
{
|
||||
std::vector<std::string> property_tokens = pxr::TfStringTokenize(prop->name, ":");
|
||||
|
||||
/* First check if the property name matches the UsdUIAccessibility format exactly. */
|
||||
if (property_tokens.size() == 3) {
|
||||
pxr::TfToken accessibility_token(property_tokens[0]);
|
||||
pxr::TfToken basename(property_tokens[2]);
|
||||
if (accessibility_token == pxr::UsdUITokens->accessibility &&
|
||||
pxr::UsdUIAccessibilityAPI::IsSchemaPropertyBaseName(basename))
|
||||
{
|
||||
AccessibilityPropertyName property_name;
|
||||
|
||||
/* Sanitize the namespace since this is user-generated and might need to be conformed
|
||||
* to the `allow_unicode` export setting. */
|
||||
property_name.property_namespace = pxr::TfToken(
|
||||
blender::io::usd::make_safe_name(property_tokens[1], allow_unicode));
|
||||
property_name.property_base_name = basename;
|
||||
return property_name;
|
||||
}
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
static bool is_valid_accessibility_priority(const pxr::TfToken &token)
|
||||
{
|
||||
return token == pxr::UsdUITokens->low || token == pxr::UsdUITokens->standard ||
|
||||
token == pxr::UsdUITokens->high;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the accessibility property on the given prim. Note: although the
|
||||
* UsdUIAccessibilityAPI DOES allow time-sampled data for the `label`
|
||||
* and `description` properties, Blender does not currently support
|
||||
* keyframes on string custom properties so time-sample authoring will
|
||||
* not be done here.
|
||||
*/
|
||||
static void write_accessibility_property(const pxr::UsdPrim &prim,
|
||||
const AccessibilityPropertyName &property_name,
|
||||
const std::string &value)
|
||||
{
|
||||
pxr::UsdUIAccessibilityAPI accessibility_api = pxr::UsdUIAccessibilityAPI::Apply(
|
||||
prim, property_name.property_namespace);
|
||||
if (!accessibility_api) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (property_name.property_base_name == pxr::UsdUITokens->label) {
|
||||
accessibility_api.CreateLabelAttr().Set(value);
|
||||
}
|
||||
else if (property_name.property_base_name == pxr::UsdUITokens->description) {
|
||||
accessibility_api.CreateDescriptionAttr().Set(value);
|
||||
}
|
||||
else if (property_name.property_base_name == pxr::UsdUITokens->priority) {
|
||||
pxr::TfToken priority(value);
|
||||
if (is_valid_accessibility_priority(priority)) {
|
||||
accessibility_api.CreatePriorityAttr().Set(priority);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static std::string get_mesh_active_uvlayer_name(const Object *ob)
|
||||
{
|
||||
if (!ob || ob->type != OB_MESH || !ob->data) {
|
||||
|
|
@ -382,6 +453,16 @@ void USDAbstractWriter::write_user_properties(const pxr::UsdPrim &prim,
|
|||
continue;
|
||||
}
|
||||
|
||||
if (auto accessibility_property_name = parse_accessibility_property_name(
|
||||
prop, usd_export_context_.export_params.allow_unicode))
|
||||
{
|
||||
if (prop->type == IDP_STRING && prop->data.pointer) {
|
||||
write_accessibility_property(
|
||||
prim, *accessibility_property_name, static_cast<char *>(prop->data.pointer));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
std::vector<std::string> path_names = pxr::TfStringTokenize(prop->name, ":");
|
||||
|
||||
/* If the path does not already have a namespace prefix, prepend the default namespace
|
||||
|
|
|
|||
|
|
@ -179,6 +179,9 @@ struct USDExportParams {
|
|||
char collection[MAX_ID_NAME - 2] = "";
|
||||
char custom_properties_namespace[MAX_IDPROP_NAME] = "";
|
||||
|
||||
std::string accessibility_label = "";
|
||||
std::string accessibility_description = "";
|
||||
|
||||
eUSDSceneUnits convert_scene_units = eUSDSceneUnits::USD_SCENE_UNITS_METERS;
|
||||
float custom_meters_per_unit = 1.0f;
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import pprint
|
|||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pxr import Gf, Sdf, Usd, UsdGeom, UsdMtlx, UsdShade, UsdSkel, UsdUtils, UsdVol
|
||||
from pxr import Gf, Sdf, Usd, UsdGeom, UsdMtlx, UsdShade, UsdSkel, UsdUI, UsdUtils, UsdVol
|
||||
|
||||
import bpy
|
||||
|
||||
|
|
@ -1980,6 +1980,86 @@ class USDExportTest(AbstractUSDTest):
|
|||
self.assertLess(len(uv_values), 24,
|
||||
f"Unique UV count ({len(uv_values)}) should be less than face vertex count (24)")
|
||||
|
||||
def test_export_accessibility(self):
|
||||
"""Validate that writing UsdUIAccessibilityAPI metadata exports correctly."""
|
||||
|
||||
def verify_accessibility_api(prim, namespace, label, description=None, priority=None):
|
||||
self.assertTrue(prim.HasAPI(UsdUI.AccessibilityAPI))
|
||||
accessibility_api = UsdUI.AccessibilityAPI(prim, namespace)
|
||||
label_attr = accessibility_api.GetLabelAttr()
|
||||
self.assertTrue(label_attr.HasAuthoredValue())
|
||||
self.assertEqual(label_attr.Get(), label)
|
||||
if description is not None:
|
||||
description_attr = accessibility_api.GetDescriptionAttr()
|
||||
self.assertTrue(description_attr.HasAuthoredValue())
|
||||
self.assertEqual(description_attr.Get(), description)
|
||||
if priority is not None:
|
||||
priority_attr = accessibility_api.GetPriorityAttr()
|
||||
self.assertTrue(priority_attr.HasAuthoredValue())
|
||||
self.assertEqual(priority_attr.Get(), priority)
|
||||
|
||||
# Create a few objects to export.
|
||||
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "empty.blend"))
|
||||
bpy.ops.mesh.primitive_uv_sphere_add()
|
||||
sphere = bpy.context.active_object
|
||||
sphere.name = "Sphere"
|
||||
sphere_label = "a sphere"
|
||||
sphere_description = "a primitive uv sphere"
|
||||
sphere["accessibility:default:label"] = sphere_label
|
||||
sphere["accessibility:default:description"] = sphere_description
|
||||
sphere["accessibility:default:priority"] = UsdUI.Tokens.high
|
||||
|
||||
# Apply a second set of accessibility data.
|
||||
sphere_color_label = "blue"
|
||||
sphere_color_description = "a cool slightly greenish blue"
|
||||
sphere["accessibility:color:label"] = sphere_color_label
|
||||
sphere["accessibility:color:description"] = sphere_color_description
|
||||
|
||||
export_path = self.tempdir / "accessibility_basic.usda"
|
||||
root_label = "Accessibility Test"
|
||||
root_description = "This is an accessibility test from Python!"
|
||||
self.export_and_validate(
|
||||
filepath=str(export_path),
|
||||
export_custom_properties=True,
|
||||
accessibility_label=root_label,
|
||||
accessibility_description=root_description,
|
||||
selected_objects_only=True,
|
||||
)
|
||||
|
||||
stage = Usd.Stage.Open(str(export_path))
|
||||
root_prim = stage.GetPrimAtPath("/root")
|
||||
self.assertTrue(root_prim.IsValid())
|
||||
sphere_prim = stage.GetPrimAtPath(f"/root/{sphere.name}")
|
||||
self.assertTrue(sphere_prim.IsValid())
|
||||
|
||||
# Check the accessibility metadata on the root prim (set via the export args).
|
||||
verify_accessibility_api(root_prim, UsdUI.Tokens.default_, root_label, root_description)
|
||||
|
||||
# Check the accessibility metadata exported from custom properties.
|
||||
verify_accessibility_api(
|
||||
sphere_prim, UsdUI.Tokens.default_, sphere_label, sphere_description, UsdUI.Tokens.high)
|
||||
verify_accessibility_api(
|
||||
sphere_prim, "color", sphere_color_label, sphere_color_description)
|
||||
|
||||
# Test another export, but this time do not have the root prim. Verify
|
||||
# that the export settings take precedence over custom properties.
|
||||
export_path = self.tempdir / "accessibility_basic_no_root.usda"
|
||||
self.export_and_validate(
|
||||
filepath=str(export_path),
|
||||
export_custom_properties=True,
|
||||
accessibility_label=root_label,
|
||||
accessibility_description=root_description,
|
||||
root_prim_path="",
|
||||
selected_objects_only=True,
|
||||
)
|
||||
|
||||
stage = Usd.Stage.Open(str(export_path))
|
||||
root_prim = stage.GetPrimAtPath(f"/{sphere.name}")
|
||||
self.assertTrue(root_prim.IsValid())
|
||||
|
||||
# Check that the accessibility information is pulled from the export args.
|
||||
verify_accessibility_api(root_prim, UsdUI.Tokens.default_, root_label, root_description)
|
||||
|
||||
|
||||
class USDHookBase:
|
||||
instructions = {}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import pathlib
|
|||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pxr import Ar, Gf, Sdf, Usd, UsdGeom, UsdShade
|
||||
from pxr import Ar, Gf, Sdf, Usd, UsdGeom, UsdShade, UsdUI
|
||||
|
||||
import bpy
|
||||
|
||||
|
|
@ -2116,6 +2116,62 @@ class USDImportTest(AbstractUSDTest):
|
|||
|
||||
bpy.utils.unregister_class(ImportMtlxTextureUSDHook)
|
||||
|
||||
def test_import_accessibility(self):
|
||||
"""Test importing accessibility metadata as custom properties."""
|
||||
|
||||
# Create a simple USD file with accessibility metadata
|
||||
usd_path = self.tempdir / "accessibility_test.usda"
|
||||
stage = Usd.Stage.CreateNew(str(usd_path))
|
||||
|
||||
# Create a cube with accessibility metadata
|
||||
prim = stage.DefinePrim("/root", "Xform")
|
||||
stage.SetDefaultPrim(prim)
|
||||
|
||||
# Add default namespace
|
||||
default_api = UsdUI.AccessibilityAPI.Apply(prim, "default")
|
||||
label_attr = default_api.CreateLabelAttr()
|
||||
label_attr.Set("Test Prim")
|
||||
description_attr = default_api.CreateDescriptionAttr()
|
||||
description_attr.Set("A test prim for import")
|
||||
priority_attr = default_api.CreatePriorityAttr()
|
||||
priority_attr.Set(UsdUI.Tokens.high)
|
||||
|
||||
# Add custom namespace
|
||||
custom_api = UsdUI.AccessibilityAPI.Apply(prim, "alternate")
|
||||
alt_label_attr = custom_api.CreateLabelAttr()
|
||||
alt_label_attr.Set("Alternate Label")
|
||||
alt_description_attr = custom_api.CreateDescriptionAttr()
|
||||
alt_description_attr.Set("Alternate description")
|
||||
|
||||
stage.Save()
|
||||
|
||||
# Import the USD file
|
||||
res = bpy.ops.wm.usd_import(filepath=str(usd_path))
|
||||
self.assertEqual({'FINISHED'}, res, f"Unable to import USD file {usd_path}")
|
||||
|
||||
# Verify the imported object has the accessibility custom properties
|
||||
xform = bpy.data.objects.get("root")
|
||||
self.assertIsNotNone(xform, "root prim object should be imported")
|
||||
|
||||
label_key = label_attr.GetName()
|
||||
description_key = description_attr.GetName()
|
||||
priority_key = priority_attr.GetName()
|
||||
|
||||
self.assertIn(label_key, xform, "Default label should be imported")
|
||||
self.assertEqual(xform[label_key], label_attr.Get())
|
||||
self.assertIn(description_key, xform, "Default description should be imported")
|
||||
self.assertEqual(xform[description_key], description_attr.Get())
|
||||
self.assertIn(priority_key, xform, "Default priority should be imported")
|
||||
self.assertEqual(xform[priority_key], priority_attr.Get())
|
||||
|
||||
alt_label_key = alt_label_attr.GetName()
|
||||
alt_description_key = alt_description_attr.GetName()
|
||||
|
||||
self.assertIn(alt_label_key, xform, "Alternate label should be imported")
|
||||
self.assertEqual(xform[alt_label_key], alt_label_attr.Get())
|
||||
self.assertIn(alt_description_key, xform, "Alternate description should be imported")
|
||||
self.assertEqual(xform[alt_description_key], alt_description_attr.Get())
|
||||
|
||||
|
||||
class USDImportComparisonTest(unittest.TestCase):
|
||||
@classmethod
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue