diff --git a/build.py b/build.py index 8b0bfd4a22..deb5603649 100755 --- a/build.py +++ b/build.py @@ -37,6 +37,15 @@ import tarfile import time import zipfile +import json + +########################################################### +# mapping for platform string from python builtin str +platform_mapping = { + "darwin": "osx", + "windows": "win", + "linux": "lin" + } ############################################################ # Helpers for printing output @@ -635,6 +644,40 @@ def Package(context): ############################################################ # InstallContext class InstallContext: + + kFilesToExclude = [] + kPluginsToExclude = [] + kPluginsToInclude = [] + kPlatformToInclude = [] + kPlatformToExclude = [] + allLabelsToInclude = [] + + ctestArgs = list() + + # get list of labels to pass to ctest-args + def get_ctest_labels(self): + try: + with open("tests-to-run.json", 'r') as file: + ctest_labels_config = json.load(file) + except json.JSONDecodeError as e: + print(f"Failed to decode JSON: {e}") + except FileNotFoundError as e: + print(f"File not found: {e}") + except Exception as e: + print(f"An error occurred: {e}") + + for key, values in ctest_labels_config.items(): + if key == "plugins_to_include": + self.kPluginsToInclude = values + if key == "plugins_to_exclude": + self.kPluginsToExclude = values + if key == "platforms_to_include": + self.kPlatformToInclude = values + if key == "platforms_to_exclude": + self.kPlatformToExclude = values + if key == "files_to_exclude": + self.kFilesToExclude = values + def __init__(self, args): # Assume the project's top level cmake is in the current source directory self.mayaHydraSrcDir = os.path.normpath( @@ -716,12 +759,33 @@ def __init__(self, args): self.stagesArgs.append(arg) # CTest arguments - self.ctestArgs = list() for argList in args.ctest_args: for arg in argList.split(","): self.ctestArgs.append(arg) - # Redirect output stream to file + # get labels to be passed for ctest + self.get_ctest_labels() + + # add -E args for test file to be excluded by name + if self.kFilesToExclude: + self.ctestArgs.append(f'{"-E"} {"|".join(self.kFilesToExclude)}') + + # add -L args, test with following labels to run + if self.kPluginsToInclude: + self.ctestArgs.append(f'{"-L"} {"|".join(self.kPluginsToInclude)}') + + # Determine the current platform + current_platform = platform.system().lower() + exclPlatform = [] + for a in self.kPlatformToExclude: + if platform_mapping.get(current_platform) in a: + exclPlatform.append(a) + + allExcludeLabels = self.kPluginsToExclude + exclPlatform + # add -LE args, test with following labels to be skipped + if allExcludeLabels: + self.ctestArgs.append(f'{"-LE"} {"|".join(allExcludeLabels)}') + self.redirectOutstreamFile = args.redirect_outstream_file try: diff --git a/cmake/test.cmake b/cmake/test.cmake index c5bf6b674d..52abe999ec 100644 --- a/cmake/test.cmake +++ b/cmake/test.cmake @@ -32,6 +32,54 @@ if(MayaUsd_FOUND) endif() endif() +function(find_labels label_set label_list) + string(REPLACE ":" ";" split_labels ${label_set}) + list(LENGTH split_labels len) + if(len GREATER 0) + list(GET split_labels 1 labels_value) + # we expect comma separated labels + string(REPLACE "," ";" all_labels ${labels_value}) + set(local_label_list "") + foreach(label ${all_labels}) + list(APPEND local_label_list ${label}) + endforeach() + set(${label_list} ${local_label_list} PARENT_SCOPE) + endif() +endfunction() + +function(get_testfile_and_labels all_labels test_filename test_script) + # fetch labels for each test file + string(REPLACE "|" ";" tests_with_tags ${test_script}) + list(GET tests_with_tags 0 filename) + # set the test file to input as no labels were passed + set(${test_filename} ${filename} PARENT_SCOPE) + list(LENGTH tests_with_tags length) + math(EXPR one_less_length "${length} - 1") + if(length GREATER 1) + set(collect_labels "") + foreach(i RANGE 1 ${one_less_length}) + list(GET tests_with_tags ${i} item) + find_labels(${item} label_list) + list(APPEND collect_labels ${label_list}) + endforeach() + set(${all_labels} ${collect_labels} PARENT_SCOPE) + else() + set(${all_labels} "" PARENT_SCOPE) + endif() +endfunction() + +function(apply_labels_to_test test_labels test_file) + set_property(TEST ${test_file} APPEND PROPERTY LABELS "default") + list(LENGTH test_labels list_length) + if(${list_length} GREATER 0) + # if(NOT ${test_labels} STREQUAL "") + foreach(label ${test_labels}) + set_property(TEST ${test_file} APPEND PROPERTY LABELS ${label}) + message(STATUS "Added test label \"${label}\" for ${test_file}") + endforeach() + endif() +endfunction() + function(mayaUsd_get_unittest_target unittest_target unittest_basename) get_filename_component(unittest_name ${unittest_basename} NAME_WE) set(${unittest_target} "${unittest_name}" PARENT_SCOPE) @@ -94,7 +142,7 @@ endif() # separate_argument_list before passing to this func # if you start with a cmake-style list. # -function(mayaUsd_add_test test_name) +function(mayaUsd_add_test test_name) # ----------------- # 1) Arg processing # ----------------- diff --git a/doc/images/instanceSelectionHighlight.png b/doc/images/instanceSelectionHighlight.png new file mode 100644 index 0000000000..7c0faa96dc Binary files /dev/null and b/doc/images/instanceSelectionHighlight.png differ diff --git a/doc/images/instanceTranslations.png b/doc/images/instanceTranslations.png new file mode 100644 index 0000000000..e154d700c4 Binary files /dev/null and b/doc/images/instanceTranslations.png differ diff --git a/doc/images/instancedBy.png b/doc/images/instancedBy.png new file mode 100644 index 0000000000..c1071e4e18 Binary files /dev/null and b/doc/images/instancedBy.png differ diff --git a/doc/images/instancerTopology.png b/doc/images/instancerTopology.png new file mode 100644 index 0000000000..c81cab5008 Binary files /dev/null and b/doc/images/instancerTopology.png differ diff --git a/doc/images/pointInstancerSelectionHighlight.png b/doc/images/pointInstancerSelectionHighlight.png new file mode 100644 index 0000000000..1097cf5668 Binary files /dev/null and b/doc/images/pointInstancerSelectionHighlight.png differ diff --git a/doc/images/prototypeSelectionHighlight.png b/doc/images/prototypeSelectionHighlight.png new file mode 100644 index 0000000000..06c9baf5c5 Binary files /dev/null and b/doc/images/prototypeSelectionHighlight.png differ diff --git a/doc/selectionHighlightingArchitecture.md b/doc/selectionHighlightingArchitecture.md index 4168f08252..0e6796bbdb 100644 --- a/doc/selectionHighlightingArchitecture.md +++ b/doc/selectionHighlightingArchitecture.md @@ -8,7 +8,7 @@ supports Hydra rendering in this repository as the Flow Viewport Toolkit (name subject to change). This document will describe the state of Flow Viewport Toolkit selection -highlighting as of 21-Sep-2023. +highlighting as of 31-May-2024. ## Behavior @@ -18,8 +18,9 @@ are shown differently in the viewport for ease of understanding. An application will provide a way to select an object, or to select components of an object. For example, for a mesh object, these components may be points, -edges, or faces. At time of writing, only object selection highlighting is -supported, and selection highlighting of components is unimplemented. +edges, or faces. Currently, only object selection highlighting and point +instancing highlighting of meshes are supported. Selection highlighting of +components is unimplemented. ## Selection: Application versus Hydra @@ -27,7 +28,8 @@ The application maintains an edit-friendly version of the scene. This scene is translated into a Hydra scene by scene indices. Correspondingly, there are two versions of the selection, one in the application, with objects and their paths described with application-specific classes, and a version of the selection in -the Hydra scene, described as prims and their `SdfPath`s +the Hydra scene, described as prims and their `SdfPath`s, as well as their +associated selection data sources. ## Requirements @@ -45,8 +47,8 @@ Requirements for selection highlighting are: highlight appearance. - It must be possible to let a data injecting data model provide prims to Hydra - that already contain selection highlighting. At time of writing - (19-Sep-2023), this is true of Maya native Dag data, where selection + that already contain selection highlighting. Currently, + this is true of Maya native Dag data, where selection highlighting is done by OGS. ## Selection Highlighting Styles @@ -59,16 +61,16 @@ special way, e.g. object contour, modified object color, or object overlay. The former approach is handled by having a plugin provide a selection highlighting filtering scene index to the Flow Viewport Toolkit, and is the -topic of this document at time of writing (20-Sep-2023). The -latter is handled by having a plugin provide a selection highlighting -task to the Flow Viewport Toolkit, and is currently unimplemented. +topic of this document at time of writing. The latter is handled by having +a plugin provide a selection highlighting task to the Flow Viewport Toolkit, +and is currently unimplemented. ## Added Geometry Plugin Software Architecture Requirements A selection highlighting plugin that provides added geometry to scene must provide the following services: -- A way to translate the application's selection path(s) into Hydra paths: +- A way to translate the application's selection path(s) into Hydra paths and data sources: - So that the appropriate prims in Hydra can be dirtied on selection change. - So that selected prims in Hydra can have a data source added. This is embodied in a **Path interface**. @@ -83,8 +85,8 @@ provide the following services: ### Selection Change This -[selection change code](../lib/flowViewport/sceneIndex/fvpSelectionSceneIndex.cpp#L150-L167) -shows the use of the *Path Interface*, through the *SceneIndexPath()* method, +[selection change code](../lib/flowViewport/sceneIndex/fvpSelectionSceneIndex.cpp#L152-L173) +shows the use of the *Path Interface*, through the *UfePathToPrimSelections()* method, called on the input scene index. The path interface allows the selection scene index to translate selected application paths to selected Hydra scene index paths. @@ -92,7 +94,7 @@ paths. ### Wireframe Selection Highlighting This -[wireframe selection highlighting code](../lib/flowViewport/sceneIndex/fvpWireframeSelectionHighlightSceneIndex.cpp#L76-L97) +[wireframe selection highlighting code](../lib/flowViewport/sceneIndex/fvpWireframeSelectionHighlightSceneIndex.cpp#L462-L465) shows the use of the *Selection*, through the *HasFullySelectedAncestorInclusive()* method, called on the input selection. The selection allows a selection highlighting filtering scene index to query @@ -178,7 +180,7 @@ index is optional. The object modeling is the following: - **Selection**: builtin provided by the Flow Viewport Toolkit. - - Encapsulates the Hydra selection as scene index paths. + - Encapsulates the Hydra selection as scene index paths and selection data sources. - Is shared by the selection scene index and all selection highlighting scene indices. - **Selection scene index**: builtin provided by the Flow Viewport Toolkit. @@ -232,7 +234,7 @@ class Selection{ } class PathInterface{ -+SceneIndexPath(Ufe::Path) SdfPath ++UfePathToPrimSelections(Ufe::Path) SdfPath } class SelectionSceneIndex @@ -287,3 +289,213 @@ WireframeSelectionHighlightSceneIndex o-- Selection : Read propagate across scene index inputs, so that if a Maya Dag ancestor is selected, a USD descendant's appearance can change. This is the same situation as global transformation and visibility. + +## Mesh point instancing wireframe selection highlighting + +We currently support wireframe selection highlighting for point instancing of meshes +for the three different point instancing selection modes : + +- Point Instancer + +![Point instancer selection highlight](images/pointInstancerSelectionHighlight.png) + +- Instance + +![Instance selection highlight](images/instanceSelectionHighlight.png) + +- Prototype + +![Prototype selection highlight](images/prototypeSelectionHighlight.png) + +Here is an overview of how point instancing works in Hydra, and how we implement +wireframe selection highlighting for it. + +### Scene index structure + +In Hydra, a point instancer is represented as a prim of type `instancer`, +with an `instancerTopology` data source. + +![instancerTopology data source](images/instancerTopology.png) + +This data source contains three relevant inner data sources : +- The `prototypes` data source, of type `VtArray`, lists the paths +to each prototype this point instancer instances. +- The `instanceIndices` data source, a vector data source where each element +data source (`i0, i1, i2, etc.`) is of type `VtArray` and contains +which instances correspond to which prototype. For example, if `i1` contains +`0, 3`, then the first and fourth instances will be using the second prototype. +- The `mask` data source, of type `VtArray`, which can optionally be used +to show/hide specific instances (e.g. if the 3rd element of the mask is `false`, +then the 3rd instance will be hidden). If this array is empty, all instances will be +shown. + +--- + +Per-instance data is specified using primvar data sources, namely : +- hydra:instanceTranslations +- hydra:instanceRotations +- hydra:instanceScales +- hydra:instanceTransforms + +![instanceTranslations primvar data source](images/instanceTranslations.png) + +Where the corresponding primvarValue data source lists the instance-specific data. +Note that while the first three are 3-dimensional vectors and `hydra:instanceTransforms` +is a 4x4 matrix, they can all be used simultaneously (internally, they will all be +converted to 4x4 matrices, and then multiplied together). + +--- + +On the other end of instancing, prototype prims have an `instancedBy` data source. + +![instancedBy data source](images/instancedBy.png) + +This data source contains up to two inner data sources : +- (required) : The `paths` data source, of type `VtArray`, lists the paths +to each instancer that instances this prototype. +- (optional) : When a sub-hierarchy is prototyped, the `prototypeRoots`, of type +`VtArray`, lists the paths to the roots of the sub-hierarchies that are being +prototyped. For example, if we are instancing an xform that has a child mesh, +then the prototype xform and mesh prims will each have the same `instancedBy` data source, +where the `paths` data source will point to the instancers that use this prototype, and +where the `prototypeRoots` will point to the xform prim. + +--- + +Some notes about the behavioral impacts of the hierarchical location of prims : +- Prims that are rooted under an instancer will not be drawn unless instanced +- Prototypes that are instanced will still be drawn as if they were not instanced +(i.e. the instances will be drawn in addition to the base prim itself), unless as +mentioned they are rooted under an instancer. + +### Nested/Composed instancers + +It is possible for an instancer itself to be instanced by another, and thus have both the +`instancerTopology` and the `instancedBy` data sources. Note that this does not preclude +such a prototyped instancer from also drawing geometry itself. If the prototyped instancer +is a child of the instancing instancer, then yes, such a nested instancer will not draw by +itself, and will be instance-drawn through the parent instancer. However, if the prototyped +instancer has no parent instancer, but it is instanced by another instancer somewhere else +in the hierarchy, then both the prototyped instancer will draw as if it were by itself, but +also be instance-drawn by the other instancer. + +This nesting and composition of instancers is what leads to most of the complexity of point +instancing selection highlighting. We can view such nesting and composition of instancers as +graphs, with the vertices being the instancer prims, and the edges being the paths contained +in the `instancerTopology/prototypes`, `instancedBy/paths` and `instancedBy/prototypeRoots` +data sources, as well as parent-child relationships. + +For example, given the following scene structure : +``` +Root +|__TopInstancer +| |__NestedInstancer +| | |__LeafInstancer +| | |__PrototypePrim +| | |__PrototypeSubPrim +|__AnotherPrototypePrim + |__ChildInstancer +``` + +```mermaid +graph LR + TopInstancer -->|instancerTopology/prototypes| NestedInstancer + NestedInstancer -->|instancerTopology/prototypes| LeafInstancer + LeafInstancer -->|instancedBy/paths| NestedInstancer + NestedInstancer -->|instancedBy/paths| TopInstancer + LeafInstancer -->|instancerTopology/prototypes| PrototypePrim + PrototypePrim -->|instancedBy/paths| LeafInstancer + PrototypeSubPrim -->|instancedBy/prototypeRoots| PrototypePrim + + TopInstancer -->|instancerTopology/prototypes| AnotherPrototypePrim + AnotherPrototypePrim -->|parent-child relationship| ChildInstancer + ChildInstancer -->|instancedBy/prototypeRoots| AnotherPrototypePrim + ChildInstancer -->|instancedBy/paths| TopInstancer +``` + +### Implementation for point instancer and instance selection + +This section will focus on selection highlighting when trying to highlight point instancers +as a whole or specific instances, as these require a more complicated workflow. Unlike standard +selection, we cannot simply override instanced meshes to use the `refinedWireOnSurf`/`wireOnSurf` +HdReprs, as that would lead to highlighting all instances of the prototype all the time. Instead, +we opt for the following approach : when an instancer is selected (entirely or only certain instances), +we will create a mirror of the instancing graph it is a part of, and make the mirror copies of the +instanced meshes draw with a wireframe representation. This mirror graph includes everything from +the most deeply buried prims to the topmost instancers; anything that this instancer affects or is +affected by, including itself. In practice, this means that each prototype and each instancer will +have a corresponding mirror prim for selection highlighting, that will be located alongside it as +a sibling. This way, any parent transforms affecting the original prim will also affect the selection +highlight mirror prim. + +For example, given the following scene structure : +``` +Root +|__TopInstancer + |__NestedInstancer + |__Prototype +``` +the resulting scene structure with selection highlighting would become : +``` +Root +|__TopInstancer +| |__NestedInstancer +| | |__Prototype +| | |__Prototype_SelectionHighlight +| |__NestedInstancer_SelectionHighlight +|__TopInstancer_SelectionHighlight +``` +where `TopInstancer_SelectionHighlight` would instance `NestedInstancer_SelectionHighlight`, which would in turn instance `Prototype_SelectionHighlight`. + +Note that in the case where a prototype is not a single prim but a sub-hierarchy, we only need to +create a single *explicit* selection highlight mirror prim for the whole prototype sub-hierarchy; the +child prims of the selection highlight mirror will simply be pulled from the corresponding original +prim, and thus implicitly be selection highlight mirrors as well. + +Another thing to be aware of is that a nested/composed instancer is not necessarily directly selected, +as it is not necessarily a prototype root itself. If an instancer is a child prim of another prim +that is itself selected or instanced by another instancer, these instancers are still composed +together, but will not point to each other directly. Such cases are an example of when we need to +use the `instancedBy/prototypeRoots` data source to properly construct the mirror graph of instancers. + +An example of this is the following : +``` +Root +|__TopInstancer + |__Prototype + |__ChildInstancer +``` +for which we end up with : +``` +Root +|__TopInstancer +| |__Prototype +| | |__ChildInstancer +| |__Prototype_SelectionHighlight +| |__ChildInstancer +|__TopInstancer_SelectionHighlight +``` +where `TopInstancer_SelectionHighlight` instances `Prototype_SelectionHighlight`, which implicitly draws the selection highlight version of `ChildInstancer`. + +Of note are the following selection highlighting scenarios and their corresponding behaviors : +- Selecting a point instancer in its entirety + - If the instancer is a top-level instancer, all instances it draws WILL be highlighted. + - If the instancer is a prototype, instances of itself drawn by other instancers will NOT be highlighted. + This is an intentional workflow decision from the Hydra for Maya team. +- Selecting specific instances of point instancer + - If the instancer is a top-level instancer, the selected instances it draws WILL be highlighted. + - If the instancer is a prototype, the instances it would indirectly draw through instances of itself drawn by other instancers will NOT be highlighted. + This is an intentional workflow decision from the Hydra for Maya team. +- Selecting a parent prim of a point instancer + - (same as selecting a point instancer in its entirety) + +### Implementation for prototype selection + +We simply do as for a regular selection, and override the original prim's display style +to draw as a wireframe-on-surface representation. + +### Current limitations + +- The wireframe colors for point instancer & instance selections might not always be correct (with + respect to the lead/active selection colors). However, prototype selections should be using the + correct colors. \ No newline at end of file diff --git a/lib/flowViewport/sceneIndex/fvpBlockPrimRemovalPropagationSceneIndex.h b/lib/flowViewport/sceneIndex/fvpBlockPrimRemovalPropagationSceneIndex.h index a8e5f7f1f4..2ba8311f45 100644 --- a/lib/flowViewport/sceneIndex/fvpBlockPrimRemovalPropagationSceneIndex.h +++ b/lib/flowViewport/sceneIndex/fvpBlockPrimRemovalPropagationSceneIndex.h @@ -70,8 +70,8 @@ class BlockPrimRemovalPropagationSceneIndex : public PXR_NS::HdSingleInputFilter //from PathInterface FVP_API - PXR_NS::SdfPath SceneIndexPath(const Ufe::Path& appPath) const override{ - return _pathInterface->SceneIndexPath(appPath); + PrimSelections UfePathToPrimSelections(const Ufe::Path& appPath) const override{ + return _pathInterface->UfePathToPrimSelections(appPath); } protected: diff --git a/lib/flowViewport/sceneIndex/fvpMergingSceneIndex.cpp b/lib/flowViewport/sceneIndex/fvpMergingSceneIndex.cpp index 6a46bfbe80..912d51bc10 100644 --- a/lib/flowViewport/sceneIndex/fvpMergingSceneIndex.cpp +++ b/lib/flowViewport/sceneIndex/fvpMergingSceneIndex.cpp @@ -34,7 +34,7 @@ MergingSceneIndex::MergingSceneIndex() : HdMergingSceneIndex() .Msg("MergingSceneIndex::MergingSceneIndex() called.\n"); } -SdfPath MergingSceneIndex::SceneIndexPath(const Ufe::Path& appPath) const +PrimSelections MergingSceneIndex::UfePathToPrimSelections(const Ufe::Path& appPath) const { // FLOW_VIEWPORT_TODO May be able to use a caching scheme for app path to // scene index path conversion using the run-time ID of the UFE path, as it @@ -50,13 +50,13 @@ SdfPath MergingSceneIndex::SceneIndexPath(const Ufe::Path& appPath) const // scene we know whether it supports the PathInterface or not. auto pathInterface = dynamic_cast(&*inputScene); if (pathInterface) { - auto sceneIndexPath = pathInterface->SceneIndexPath(appPath); - if (!sceneIndexPath.IsEmpty()) { - return sceneIndexPath; + auto primSelections = pathInterface->UfePathToPrimSelections(appPath); + if (!primSelections.empty()) { + return primSelections; } } } - return SdfPath(); + return PrimSelections(); } } diff --git a/lib/flowViewport/sceneIndex/fvpMergingSceneIndex.h b/lib/flowViewport/sceneIndex/fvpMergingSceneIndex.h index d4b98b26cb..6945ca3d34 100644 --- a/lib/flowViewport/sceneIndex/fvpMergingSceneIndex.h +++ b/lib/flowViewport/sceneIndex/fvpMergingSceneIndex.h @@ -41,7 +41,7 @@ class MergingSceneIndex static MergingSceneIndexRefPtr New(); FVP_API - PXR_NS::SdfPath SceneIndexPath(const Ufe::Path& appPath) const override; + PrimSelections UfePathToPrimSelections(const Ufe::Path& appPath) const override; private: MergingSceneIndex(); diff --git a/lib/flowViewport/sceneIndex/fvpPathInterface.cpp b/lib/flowViewport/sceneIndex/fvpPathInterface.cpp index 28445061d3..eaf4df7926 100644 --- a/lib/flowViewport/sceneIndex/fvpPathInterface.cpp +++ b/lib/flowViewport/sceneIndex/fvpPathInterface.cpp @@ -15,8 +15,42 @@ #include "flowViewport/sceneIndex/fvpPathInterface.h" +#include + +#include + +PXR_NAMESPACE_USING_DIRECTIVE + namespace FVP_NS_DEF { PathInterface::~PathInterface() {} +SdfPath PathInterface::SceneIndexPath(const Ufe::Path& appPath) const +{ + auto primSelections = UfePathToPrimSelections(appPath); + if (!TF_VERIFY(primSelections.size() <= 1u)) { + throw PrimPathsCountOutOfRangeException(0, 1, primSelections.size()); + } + return primSelections.empty() ? SdfPath() : primSelections.front().primPath; +} + +SdfPathVector PathInterface::SceneIndexPaths(const Ufe::Path& appPath) const +{ + auto primSelections = UfePathToPrimSelections(appPath); + + SdfPathVector outVector; + outVector.reserve(primSelections.size()); + for (const auto& primSelection : primSelections) { + outVector.emplace_back(primSelection.primPath); + } + return outVector; +} + +PrimPathsCountOutOfRangeException::PrimPathsCountOutOfRangeException(size_t min, size_t max, size_t actual) + : std::out_of_range("Prim paths count out of range, expected [" + + std::to_string(min) + "," + std::to_string(max) + + "] but got " + std::to_string(actual)) +{ +} + } diff --git a/lib/flowViewport/sceneIndex/fvpPathInterface.h b/lib/flowViewport/sceneIndex/fvpPathInterface.h index 55d16c944e..1a4615eba1 100644 --- a/lib/flowViewport/sceneIndex/fvpPathInterface.h +++ b/lib/flowViewport/sceneIndex/fvpPathInterface.h @@ -20,32 +20,58 @@ #include #include +#include +#include +#include + +#include UFE_NS_DEF { class Path; } -PXR_NAMESPACE_OPEN_SCOPE -class SdfPath; -PXR_NAMESPACE_CLOSE_SCOPE - namespace FVP_NS_DEF { +struct PrimSelection +{ + PXR_NS::SdfPath primPath; + PXR_NS::HdDataSourceBaseHandle selectionDataSource; +}; + +// Using TfSmallVector to optimize for selections that map to a few prims, +// which is likely going to be the bulk of use cases. +using PrimSelections = PXR_NS::TfSmallVector; + /// \class PathInterface /// /// A pure interface class to allow for conversion between an application's -/// path, expressed as a Ufe::Path, into an SdfPath valid for a scene index. +/// path, expressed as a Ufe::Path, into SdfPaths valid for a scene index +/// and selection data sources. /// To be used as a mix-in class for scene indices. /// class PathInterface { public: - //! Return the prim path corresponding to the argument application path. + //! Return the prim path(s) corresponding to the argument application path, + //! as well as their associated selection data source(s). + //! If no such selected path exists, an empty container should be returned. + //! \return Selected prim paths and their associated selection data sources. + FVP_API + virtual PrimSelections UfePathToPrimSelections(const Ufe::Path& appPath) const = 0; + + //! Return the prim path corresponding to the argument application path, + //! for when an application path maps to at most a single prim path. //! If no such path exists, an empty SdfPath should be returned. - //! \return scene index path. + //! \return Scene index path. + FVP_API + PXR_NS::SdfPath SceneIndexPath(const Ufe::Path& appPath) const; + + //! Return the prim paths corresponding to the argument application path. + //! If no such paths exist, an empty SdfPathVector should be returned. + //! \return Scene index paths. FVP_API - virtual PXR_NS::SdfPath SceneIndexPath(const Ufe::Path& appPath) const = 0; + PXR_NS::SdfPathVector SceneIndexPaths(const Ufe::Path& appPath) const; protected: @@ -56,6 +82,12 @@ class PathInterface virtual ~PathInterface(); }; +class PrimPathsCountOutOfRangeException : public std::out_of_range +{ +public: + PrimPathsCountOutOfRangeException(size_t min, size_t max, size_t actual); +}; + } #endif diff --git a/lib/flowViewport/sceneIndex/fvpPathInterfaceSceneIndex.h b/lib/flowViewport/sceneIndex/fvpPathInterfaceSceneIndex.h index 9ea5a62db2..5765943d5c 100644 --- a/lib/flowViewport/sceneIndex/fvpPathInterfaceSceneIndex.h +++ b/lib/flowViewport/sceneIndex/fvpPathInterfaceSceneIndex.h @@ -27,7 +27,7 @@ namespace FVP_NS_DEF { /// /// A simple pass-through filtering scene index that adds support for the path /// interface. Derived classes need only implement the -/// PathInterface::SceneIndexPath() virtual. +/// PathInterface::UfePathToPrimSelections() virtual. /// class PathInterfaceSceneIndexBase : public PXR_NS::HdSingleInputFilteringSceneIndexBase diff --git a/lib/flowViewport/sceneIndex/fvpPruneTexturesSceneIndex.cpp b/lib/flowViewport/sceneIndex/fvpPruneTexturesSceneIndex.cpp index b9a0929e54..20d827a694 100644 --- a/lib/flowViewport/sceneIndex/fvpPruneTexturesSceneIndex.cpp +++ b/lib/flowViewport/sceneIndex/fvpPruneTexturesSceneIndex.cpp @@ -78,7 +78,8 @@ PruneTexturesSceneIndex::MarkTexturesDirty(bool isTextured) PruneTexturesSceneIndex::PruneTexturesSceneIndex( HdSceneIndexBaseRefPtr const &inputSceneIndex) - : HdMaterialFilteringSceneIndexBase(inputSceneIndex) + : HdMaterialFilteringSceneIndexBase(inputSceneIndex), + InputSceneIndexUtils(inputSceneIndex) { } @@ -95,10 +96,10 @@ PruneTexturesSceneIndex::_DirtyAllPrims( const HdDataSourceLocatorSet locators) { HdSceneIndexObserver::DirtiedPrimEntries entries; - for (const SdfPath &path : HdSceneIndexPrimView(_GetInputSceneIndex())) { + for (const SdfPath &path : HdSceneIndexPrimView(GetInputSceneIndex())) { entries.push_back({path, locators}); } _SendPrimsDirtied(entries); } -} //end of namespace FVP_NS_DEF \ No newline at end of file +} //end of namespace FVP_NS_DEF diff --git a/lib/flowViewport/sceneIndex/fvpPruneTexturesSceneIndex.h b/lib/flowViewport/sceneIndex/fvpPruneTexturesSceneIndex.h index 336dbbf8c1..1287b2f7b5 100644 --- a/lib/flowViewport/sceneIndex/fvpPruneTexturesSceneIndex.h +++ b/lib/flowViewport/sceneIndex/fvpPruneTexturesSceneIndex.h @@ -30,8 +30,11 @@ typedef PXR_NS::TfRefPtr PruneTexturesSceneIndexC class PruneTexturesSceneIndex : public PXR_NS::HdMaterialFilteringSceneIndexBase + , public Fvp::InputSceneIndexUtils { public: + using PXR_NS::HdMaterialFilteringSceneIndexBase::_GetInputSceneIndex; + FVP_API static PruneTexturesSceneIndexRefPtr New( const PXR_NS::HdSceneIndexBaseRefPtr &inputScene); diff --git a/lib/flowViewport/sceneIndex/fvpSelectionSceneIndex.cpp b/lib/flowViewport/sceneIndex/fvpSelectionSceneIndex.cpp index 5503d72a3a..195246054a 100644 --- a/lib/flowViewport/sceneIndex/fvpSelectionSceneIndex.cpp +++ b/lib/flowViewport/sceneIndex/fvpSelectionSceneIndex.cpp @@ -43,6 +43,7 @@ #include "flowViewport/debugCodes.h" #include "pxr/imaging/hd/retainedDataSource.h" +#include #include "pxr/imaging/hd/selectionSchema.h" #include "pxr/imaging/hd/selectionsSchema.h" @@ -154,16 +155,21 @@ SelectionSceneIndex::AddSelection(const Ufe::Path& appPath) TF_DEBUG(FVP_SELECTION_SCENE_INDEX) .Msg("SelectionSceneIndex::AddSelection(const Ufe::Path& %s) called.\n", Ufe::PathString::string(appPath).c_str()); - // Call our input scene index to convert the application path to a scene - // index path. - auto sceneIndexPath = SceneIndexPath(appPath); + // Call our input scene index to convert the application path to scene index paths and selection data sources. + auto primSelections = UfePathToPrimSelections(appPath); - TF_DEBUG(FVP_SELECTION_SCENE_INDEX) - .Msg(" Adding %s to the Hydra selection.\n", sceneIndexPath.GetText()); + for (const auto& primSelection : primSelections) { + TF_DEBUG(FVP_SELECTION_SCENE_INDEX) + .Msg(" Adding %s to the Hydra selection.\n", primSelection.primPath.GetText()); + } - if (_selection->Add(sceneIndexPath)) { - _SendPrimsDirtied({{sceneIndexPath, selectionsSchemaDefaultLocator}}); + HdSceneIndexObserver::DirtiedPrimEntries dirtiedPrims; + for (const auto& primSelection : primSelections) { + if (_selection->Add(primSelection)) { + dirtiedPrims.emplace_back(primSelection.primPath, selectionsSchemaDefaultLocator); + } } + _SendPrimsDirtied(dirtiedPrims); } void SelectionSceneIndex::RemoveSelection(const Ufe::Path& appPath) @@ -171,13 +177,16 @@ void SelectionSceneIndex::RemoveSelection(const Ufe::Path& appPath) TF_DEBUG(FVP_SELECTION_SCENE_INDEX) .Msg("SelectionSceneIndex::RemoveSelection(const Ufe::Path& %s) called.\n", Ufe::PathString::string(appPath).c_str()); - // Call our input scene index to convert the application path to a scene - // index path. - auto sceneIndexPath = SceneIndexPath(appPath); + // Call our input scene index to convert the application path to scene index paths and selection data sources. + auto primSelections = UfePathToPrimSelections(appPath); - if (_selection->Remove(sceneIndexPath)) { - _SendPrimsDirtied({{sceneIndexPath, selectionsSchemaDefaultLocator}}); + HdSceneIndexObserver::DirtiedPrimEntries dirtiedPrims; + for (const auto& primSelection : primSelections) { + if (_selection->Remove(primSelection.primPath)) { + dirtiedPrims.emplace_back(primSelection.primPath, selectionsSchemaDefaultLocator); + } } + _SendPrimsDirtied(dirtiedPrims); } void @@ -219,21 +228,22 @@ void SelectionSceneIndex::ReplaceSelection(const Ufe::Selection& selection) _selection->Clear(); - SdfPathVector sceneIndexSn; + PrimSelections sceneIndexSn; sceneIndexSn.reserve(selection.size()); for (const auto& snItem : selection) { - // Call our input scene index to convert the application path to a scene - // index path. - auto sceneIndexPath = SceneIndexPath(snItem->path()); + // Call our input scene index to convert the application path to scene index paths and selection data sources. + auto primSelections = UfePathToPrimSelections(snItem->path()); - if (sceneIndexPath.IsEmpty()) { + if (primSelections.empty()) { continue; } - sceneIndexSn.emplace_back(sceneIndexPath); - TF_DEBUG(FVP_SELECTION_SCENE_INDEX) - .Msg(" Adding %s to the Hydra selection.\n", sceneIndexPath.GetText()); - entries.emplace_back(sceneIndexPath, selectionsSchemaDefaultLocator); + for (const auto& primSelection : primSelections) { + sceneIndexSn.emplace_back(primSelection); + TF_DEBUG(FVP_SELECTION_SCENE_INDEX) + .Msg(" Adding %s to the Hydra selection.\n", primSelection.primPath.GetText()); + entries.emplace_back(primSelection.primPath, selectionsSchemaDefaultLocator); + } } _selection->Replace(sceneIndexSn); @@ -250,15 +260,15 @@ bool SelectionSceneIndex::HasFullySelectedAncestorInclusive(const SdfPath& primP return _selection->HasFullySelectedAncestorInclusive(primPath); } -SdfPath SelectionSceneIndex::SceneIndexPath(const Ufe::Path& appPath) const +PrimSelections SelectionSceneIndex::UfePathToPrimSelections(const Ufe::Path& appPath) const { - auto sceneIndexPath = _inputSceneIndexPathInterface->SceneIndexPath(appPath); + auto primSelections = _inputSceneIndexPathInterface->UfePathToPrimSelections(appPath); - if (sceneIndexPath.IsEmpty()) { - TF_WARN("SelectionSceneIndex::SceneIndexPath(%s) returned an empty path, Hydra selection will be incorrect", Ufe::PathString::string(appPath).c_str()); + if (primSelections.empty()) { + TF_WARN("SelectionSceneIndex::UfePathToPrimSelections(%s) returned no path, Hydra selection will be incorrect", Ufe::PathString::string(appPath).c_str()); } - return sceneIndexPath; + return primSelections; } SdfPathVector SelectionSceneIndex::GetFullySelectedPaths() const diff --git a/lib/flowViewport/sceneIndex/fvpSelectionSceneIndex.h b/lib/flowViewport/sceneIndex/fvpSelectionSceneIndex.h index f9f806a444..be4ce573a8 100644 --- a/lib/flowViewport/sceneIndex/fvpSelectionSceneIndex.h +++ b/lib/flowViewport/sceneIndex/fvpSelectionSceneIndex.h @@ -117,7 +117,7 @@ class SelectionSceneIndex final //! and warns about empty return paths. //@{ FVP_API - PXR_NS::SdfPath SceneIndexPath(const Ufe::Path& appPath) const override; + PrimSelections UfePathToPrimSelections(const Ufe::Path& appPath) const override; //@} FVP_API diff --git a/lib/flowViewport/sceneIndex/fvpWireframeSelectionHighlightSceneIndex.cpp b/lib/flowViewport/sceneIndex/fvpWireframeSelectionHighlightSceneIndex.cpp index dfaf66b3ce..9b8fa82bdb 100644 --- a/lib/flowViewport/sceneIndex/fvpWireframeSelectionHighlightSceneIndex.cpp +++ b/lib/flowViewport/sceneIndex/fvpWireframeSelectionHighlightSceneIndex.cpp @@ -20,11 +20,18 @@ #include "flowViewport/debugCodes.h" +#include +#include +#include #include #include #include -#include +#include +#include #include +#include + +#include PXR_NAMESPACE_USING_DIRECTIVE @@ -36,6 +43,14 @@ TF_DEFINE_PRIVATE_TOKENS( (overrideWireframeColor) // Works in HdStorm to override the wireframe color ); +const HdRetainedContainerDataSourceHandle sRefinedWireDisplayStyleDataSource + = HdRetainedContainerDataSource::New( + HdLegacyDisplayStyleSchemaTokens->displayStyle, + HdRetainedContainerDataSource::New( + HdLegacyDisplayStyleSchemaTokens->reprSelector, + HdRetainedTypedSampledDataSource>::New( + { HdReprTokens->refinedWire, TfToken(), TfToken() }))); + const HdRetainedContainerDataSourceHandle sRefinedWireOnSurfaceDisplayStyleDataSource = HdRetainedContainerDataSource::New( HdLegacyDisplayStyleSchemaTokens->displayStyle, @@ -50,6 +65,317 @@ const HdDataSourceLocator reprSelectorLocator( const HdDataSourceLocator primvarsOverrideWireframeColorLocator( HdPrimvarsSchema::GetDefaultLocator().Append(_primVarsTokens->overrideWireframeColor)); + +const std::string selectionHighlightMirrorTag = "_SelectionHighlight"; + +SdfPath _GetSelectionHighlightMirrorPathFromOriginal(const SdfPath& originalPath) +{ + if (originalPath == SdfPath::AbsoluteRootPath()) { + return originalPath; //Avoid a warning in Hydra + } + return originalPath.ReplaceName(TfToken(originalPath.GetName() + selectionHighlightMirrorTag)); +} + +SdfPath _GetOriginalPathFromSelectionHighlightMirror(const SdfPath& mirrorPath) +{ + const std::string primName = mirrorPath.GetName(); + return mirrorPath.ReplaceName(TfToken(primName.substr(0, primName.size() - selectionHighlightMirrorTag.size()))); +} + +// Computes the mask to use for an instancer's selection highlight mirror +// based on the instancer's topology and its selections. This allows +// highlighting only specific instances in the case of instance selections. +VtBoolArray _GetSelectionHighlightMask(const HdInstancerTopologySchema& originalInstancerTopology, const HdSelectionsSchema& selections) +{ + // Schema getters were made const in USD 24.05 (specifically Hydra API version 66). + // We work around this for previous versions by const casting. + VtBoolArray originalMask = +#if HD_API_VERSION < 66 + const_cast(originalInstancerTopology).GetMask()->GetTypedValue(0); +#else + originalInstancerTopology.GetMask()->GetTypedValue(0); +#endif + + if (!selections.IsDefined()) { + return originalMask; + } + + size_t nbInstances = 0; + auto instanceIndices = +#if HD_API_VERSION < 66 + const_cast(originalInstancerTopology).GetInstanceIndices(); +#else + originalInstancerTopology.GetInstanceIndices(); +#endif + for (size_t iInstanceIndex = 0; iInstanceIndex < instanceIndices.GetNumElements(); iInstanceIndex++) { + auto protoInstances = instanceIndices.GetElement(iInstanceIndex)->GetTypedValue(0); + nbInstances += protoInstances.size(); + } + if (!TF_VERIFY(originalMask.empty() || originalMask.size() == nbInstances, "Instancer mask has incorrect size.")) { + return originalMask; + } + VtBoolArray selectionHighlightMask(nbInstances, false); + + for (size_t iSelection = 0; iSelection < selections.GetNumElements(); iSelection++) { + HdSelectionSchema selection = selections.GetElement(iSelection); + // Instancer is expected to be marked "fully selected" even if only certain instances are selected, + // based on USD's _AddToSelection function in selectionSceneIndexObserver.cpp : + // https://github.com/PixarAnimationStudios/OpenUSD/blob/f7b8a021ce3d13f91a0211acf8a64a8b780524df/pxr/imaging/hdx/selectionSceneIndexObserver.cpp#L212-L251 + if (!selection.GetFullySelected() || !selection.GetFullySelected()->GetTypedValue(0)) { + continue; + } + if (!selection.GetNestedInstanceIndices()) { + // We have a selection that has no instances, which means the whole instancer is selected : + // this overrides any instances selection. + return originalMask; + } + HdInstanceIndicesVectorSchema nestedInstanceIndices = selection.GetNestedInstanceIndices(); + for (size_t iInstanceIndices = 0; iInstanceIndices < nestedInstanceIndices.GetNumElements(); iInstanceIndices++) { + HdInstanceIndicesSchema instanceIndices = nestedInstanceIndices.GetElement(0); + for (const auto& instanceIndex : instanceIndices.GetInstanceIndices()->GetTypedValue(0)) { + selectionHighlightMask[instanceIndex] = originalMask.empty() ? true : originalMask[instanceIndex]; + } + } + } + + return selectionHighlightMask; +} + +// Returns the overall data source for an instancer's selection highlight mirror. +// This replaces the mask data source and blocks the selections data source. +HdContainerDataSourceHandle _GetSelectionHighlightInstancerDataSource(const HdContainerDataSourceHandle& originalDataSource) +{ + HdInstancerTopologySchema instancerTopology = HdInstancerTopologySchema::GetFromParent(originalDataSource); + HdSelectionsSchema selections = HdSelectionsSchema::GetFromParent(originalDataSource); + + HdContainerDataSourceEditor editedDataSource = HdContainerDataSourceEditor(originalDataSource); + + if (selections.IsDefined()) { + HdDataSourceLocator maskLocator = HdInstancerTopologySchema::GetDefaultLocator().Append(HdInstancerTopologySchemaTokens->mask); + VtBoolArray selectionHighlightMask = _GetSelectionHighlightMask(instancerTopology, selections); + auto selectionHighlightMaskDataSource = HdRetainedTypedSampledDataSource::New(selectionHighlightMask); + editedDataSource.Set(maskLocator, selectionHighlightMaskDataSource); + } + + editedDataSource.Set(HdSelectionsSchema::GetDefaultLocator(), HdBlockDataSource::New()); + + return editedDataSource.Finish(); +} + +// Returns all paths related to instancing for this prim; this is analogous to getting the edges +// connected to the given vertex (in this case a prim) of an instancing graph. +SdfPathVector _GetInstancingRelatedPaths(const HdSceneIndexPrim& prim) +{ + HdInstancerTopologySchema instancerTopology = HdInstancerTopologySchema::GetFromParent(prim.dataSource); + HdInstancedBySchema instancedBy = HdInstancedBySchema::GetFromParent(prim.dataSource); + + SdfPathVector instancingRelatedPaths; + + if (instancerTopology.IsDefined()) { + auto protoPaths = instancerTopology.GetPrototypes()->GetTypedValue(0); + for (const auto& protoPath : protoPaths) { + instancingRelatedPaths.push_back(protoPath); + } + } + + if (instancedBy.IsDefined()) { + auto instancerPaths = instancedBy.GetPaths()->GetTypedValue(0); + for (const auto& instancerPath : instancerPaths) { + instancingRelatedPaths.push_back(instancerPath); + } + + auto protoRootPaths = instancedBy.GetPrototypeRoots()->GetTypedValue(0); + for (const auto& protoRootPath : protoRootPaths) { + instancingRelatedPaths.push_back(protoRootPath); + } + } + + return instancingRelatedPaths; +} + +// We consider prototypes that have child prims to be different hierarchies, +// separate from each other and from the "root" hierarchy. +VtArray _GetHierarchyRoots(const HdSceneIndexPrim& prim) +{ + HdInstancedBySchema instancedBy = HdInstancedBySchema::GetFromParent(prim.dataSource); + return instancedBy.IsDefined() && instancedBy.GetPrototypeRoots() + ? instancedBy.GetPrototypeRoots()->GetTypedValue(0) + : VtArray({SdfPath::AbsoluteRootPath()}); +} + +bool _IsPrototype(const HdSceneIndexPrim& prim) +{ + HdInstancedBySchema instancedBy = HdInstancedBySchema::GetFromParent(prim.dataSource); + return instancedBy.IsDefined(); +} + +bool _IsPrototypeSubPrim(const HdSceneIndexPrim& prim, const SdfPath& primPath) +{ + HdInstancedBySchema instancedBy = HdInstancedBySchema::GetFromParent(prim.dataSource); + if (!instancedBy.IsDefined()) { + return false; + } + if (!instancedBy.GetPrototypeRoots()) { + return false; + } + auto protoRootPaths = instancedBy.GetPrototypeRoots()->GetTypedValue(0); + for (const auto& protoRootPath : protoRootPaths) { + if (protoRootPath == primPath) { + return false; + } + } + return true; +} + +// Similar to USD's _RerootingSceneIndexPathDataSource : +// https://github.com/PixarAnimationStudios/OpenUSD/blob/f7b8a021ce3d13f91a0211acf8a64a8b780524df/pxr/usdImaging/usdImaging/rerootingSceneIndex.cpp#L35 +class _SelectionHighlightRepathingPathDataSource : public HdPathDataSource +{ +public: + HD_DECLARE_DATASOURCE(_SelectionHighlightRepathingPathDataSource) + + _SelectionHighlightRepathingPathDataSource( + HdPathDataSourceHandle const &inputDataSource, + Fvp::WireframeSelectionHighlightSceneIndex const * const inputSceneIndex) + : _inputDataSource(inputDataSource), + _inputSceneIndex(inputSceneIndex) + { + } + + VtValue GetValue(const Time shutterOffset) override + { + return VtValue(GetTypedValue(shutterOffset)); + } + + bool GetContributingSampleTimesForInterval( + const Time startTime, + const Time endTime, + std::vector