diff --git a/tests/test_data/_mini_graph.dot b/tests/test_data/_mini_graph.dot index e69de29b..246d3c70 100644 --- a/tests/test_data/_mini_graph.dot +++ b/tests/test_data/_mini_graph.dot @@ -0,0 +1,15 @@ +digraph { +rankdir=LR; +edge [color=crimson]; +1 [label="{<0>x:y:z|<1>z}", style="rounded,filled", shape=record]; +2 [label="{<0>a|<1>b}", style="rounded,filled", shape=record]; +3 [label="{<0>c|<1>d}", style="rounded,filled", shape=record, plugs="(0, 1)", active_plugs="(0,)"]; +ancestor [plugs="{'': 0, 'cycle_in': 1, 'roughness': 2, 'cycle_out': 3, 'surface': 4}", active_plugs="{'cycle_in', 'roughness', 'surface', 'cycle_out'}", shape=none, connections="{'surface': [('successor', 'surface')], 'cycle_out': [('ancestor', 'cycle_in')]}", label=<
ancestor
cycle_in
roughness
cycle_out
surface
>]; +successor [plugs="{'': 0, 'surface': 1}", active_plugs="{'surface'}", shape=none, connections="{}", label=<
successor
surface
>]; +1 -> 1 [key=0, color="sienna:crimson:orange"]; +1 -> 2 [key=0, color=crimson]; +2 -> 1 [key=0, color=green]; +3 -> 2 [key=0, color=blue, tailport=0]; +ancestor -> ancestor [key=0, tailport="cycle_in", headport="cycle_out", tooltip="ancestor.cycle_in -> ancestor.cycle_out"]; +successor -> ancestor [key=0, tailport=surface, headport=surface, tooltip="successor.surface -> ancestor.surface"]; +} diff --git a/tests/test_data/_mini_graph.svg b/tests/test_data/_mini_graph.svg new file mode 100644 index 00000000..a6740bd7 --- /dev/null +++ b/tests/test_data/_mini_graph.svg @@ -0,0 +1,106 @@ + + + + + + + + + +1 + +x:y:z + +z + + + +1->1 + + + + + + + +2 + +a + +b + + + +1->2 + + + + + +2->1 + + + + + +3 + +c + +d + + + +3:0->2 + + + + + +ancestor + + +ancestor + +cycle_in + +roughness + +cycle_out + +surface + + + + +ancestor:cycle_in->ancestor:cycle_out + + + + + + + + +successor + + +successor + +surface + + + + +successor:surface->ancestor:surface + + + + + + + + diff --git a/tests/test_views.py b/tests/test_views.py index 1e433a96..22023f41 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -21,29 +21,30 @@ # but don't want to use that since that needs to be set prior to an application initialization (which grill can't control as in USDView, Maya, Houdini...) # https://stackoverflow.com/questions/56159475/qt-webengine-seems-to-be-initialized +# There's about ~0.4s overhead from creating a QApplication for the tests. + # 2024-11-09 - Python-3.13 & USD-24.11 # python -m unittest --durations 0 test_views # Slowest test durations # ---------------------------------------------------------------------- -# 0.755s test_taxonomy_editor (test_views.TestViews.test_taxonomy_editor) -# 0.445s test_scenegraph_composition (test_views.TestViews.test_scenegraph_composition) -# 0.408s test_horizontal_scroll (test_views.TestGraphicsViewport.test_horizontal_scroll) -# 0.390s test_layer_stack_hovers (test_views.TestViews.test_layer_stack_hovers) -# 0.389s test_connection_view (test_views.TestViews.test_connection_view) -# 0.292s test_content_browser (test_views.TestViews.test_content_browser) -# 0.143s test_spreadsheet_editor (test_views.TestViews.test_spreadsheet_editor) -# 0.112s test_prim_composition (test_views.TestViews.test_prim_composition) -# 0.098s test_prim_filter_data (test_views.TestViews.test_prim_filter_data) -# 0.062s test_create_assets (test_views.TestViews.test_create_assets) -# 0.061s test_dot_call (test_views.TestViews.test_dot_call) -# 0.047s test_display_color_editor (test_views.TestViews.test_display_color_editor) -# 0.040s test_stats (test_views.TestViews.test_stats) -# 0.016s test_pan (test_views.TestGraphicsViewport.test_pan) -# 0.002s test_vertical_scroll (test_views.TestGraphicsViewport.test_vertical_scroll) +# 0.354s test_spreadsheet_editor (tests.test_views.TestViews.test_spreadsheet_editor) +# 0.288s test_connection_view (tests.test_views.TestViews.test_connection_view) +# 0.237s test_taxonomy_editor (tests.test_views.TestViews.test_taxonomy_editor) +# 0.236s test_content_browser (tests.test_views.TestViews.test_content_browser) +# 0.217s test_scenegraph_composition (tests.test_views.TestViews.test_scenegraph_composition) +# 0.180s test_dot_call (tests.test_views.TestViews.test_dot_call) +# 0.051s test_prim_filter_data (tests.test_views.TestViews.test_prim_filter_data) +# 0.050s test_create_assets (tests.test_views.TestViews.test_create_assets) +# 0.038s test_stats (tests.test_views.TestViews.test_stats) +# 0.037s test_graph_views (tests.test_views.TestViews.test_graph_views) +# 0.032s test_prim_composition (tests.test_views.TestViews.test_prim_composition) +# 0.029s test_display_color_editor (tests.test_views.TestViews.test_display_color_editor) +# 0.004s test_pan (tests.test_views.TestViews.test_pan) +# 0.001s test_horizontal_scroll (tests.test_views.TestViews.test_horizontal_scroll) # # (durations < 0.001s were hidden; use -v to show these durations) # ---------------------------------------------------------------------- -# Ran 18 tests in 3.267s +# Ran 18 tests in 2.141s class TestPrivate(unittest.TestCase): @@ -87,71 +88,71 @@ def test_core(self): _core._ensure_dot() +_test_bed = Path(__file__).parent / "mini_test_bed" / "main-world-test.1.usda" + + class TestViews(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls._app = QtWidgets.QApplication.instance() or QtWidgets.QApplication([]) + def setUp(self): + ... # return - self._app = QtWidgets.QApplication.instance() or QtWidgets.QApplication([]) - root_path = "/root" - - sphere_stage = Usd.Stage.CreateInMemory() - UsdGeom.Sphere.Define(sphere_stage, "/sph") - sphere_root = sphere_stage.DefinePrim(root_path) - sphere_root.CreateAttribute("greet", Sdf.ValueTypeNames.String).Set("hello") - sphere_stage.SetDefaultPrim(sphere_root) - - capsule_stage = Usd.Stage.CreateInMemory() - UsdGeom.Capsule.Define(capsule_stage, "/cap") - capsule_root = capsule_stage.DefinePrim(root_path) - capsule_root.CreateAttribute("who", Sdf.ValueTypeNames.String).Set("world") - capsule_stage.SetDefaultPrim(capsule_root) - - merged_stage = Usd.Stage.CreateInMemory() - with Sdf.ChangeBlock(): - for i in (capsule_stage, sphere_stage): - merged_stage.GetRootLayer().subLayerPaths.append(i.GetRootLayer().identifier) - merged_stage.SetDefaultPrim(merged_stage.GetPrimAtPath(root_path)) - - world = Usd.Stage.CreateInMemory() - self.nested = world.DefinePrim("/nested/child") - self.sibling = world.DefinePrim("/nested/sibling") - self.nested.GetReferences().AddReference(merged_stage.GetRootLayer().identifier) - - self.capsule = capsule_stage - self.sphere = sphere_stage - self.merge = merged_stage - self.world = world + # self.grill_world = Usd.Stage.Open(str(_test_bed)) + + # root_path = "/root" + # + # sphere_stage = Usd.Stage.CreateInMemory() + # UsdGeom.Sphere.Define(sphere_stage, "/sph") + # sphere_root = sphere_stage.DefinePrim(root_path) + # sphere_root.CreateAttribute("greet", Sdf.ValueTypeNames.String).Set("hello") + # sphere_stage.SetDefaultPrim(sphere_root) + # + # capsule_stage = Usd.Stage.CreateInMemory() + # UsdGeom.Capsule.Define(capsule_stage, "/cap") + # capsule_root = capsule_stage.DefinePrim(root_path) + # capsule_root.CreateAttribute("who", Sdf.ValueTypeNames.String).Set("world") + # capsule_stage.SetDefaultPrim(capsule_root) + # + # merged_stage = Usd.Stage.CreateInMemory() + # with Sdf.ChangeBlock(): + # for i in (capsule_stage, sphere_stage): + # merged_stage.GetRootLayer().subLayerPaths.append(i.GetRootLayer().identifier) + # merged_stage.SetDefaultPrim(merged_stage.GetPrimAtPath(root_path)) + # + # world = Usd.Stage.CreateInMemory() + # self.nested = world.DefinePrim("/nested/child") + # self.sibling = world.DefinePrim("/nested/sibling") + # self.nested.GetReferences().AddReference(merged_stage.GetRootLayer().identifier) + # + # self.capsule = capsule_stage + # self.sphere = sphere_stage + # self.merge = merged_stage + # self.world = world + # self._tmpf = tempfile.mkdtemp() self._token = cook.Repository.set(cook.Path(self._tmpf) / "repo") - self.grill_root_asset = names.UsdAsset.get_anonymous() - self.grill_world = gworld = cook.fetch_stage(self.grill_root_asset.name) - self.taxon_a = cook.define_taxon(gworld, "a") - self.taxon_b = cook.define_taxon(gworld, "b", references=(self.taxon_a,)) - self.unit_b = cook.create_unit(self.taxon_b, "GenericAgent") + # self.grill_root_asset = names.UsdAsset.get_anonymous() + # self.grill_world = gworld = cook.fetch_stage(self.grill_root_asset.name) + # self.taxon_a = cook.define_taxon(gworld, "a") + # self.taxon_b = cook.define_taxon(gworld, "b", references=(self.taxon_a,)) + # self.unit_b = cook.create_unit(self.taxon_b, "GenericAgent") def tearDown(self) -> None: cook.Repository.reset(self._token) # Reset all members to USD objects to ensure the used layers are cleared # (otherwise in Windows this can cause failure to remove the temporary files) - self.grill_world = None - # shutil.rmtree(self._tmpf) - self._app.quit() + # self.grill_world = None + shutil.rmtree(self._tmpf) + + @classmethod + def tearDownClass(cls): + cls._app.quit() def test_connection_view(self): - # return - for graph_viewer in _graph.GraphView, _graph._GraphSVGViewer: - with self.subTest(graph_viewer=graph_viewer): - _graph._GraphViewer = graph_viewer - if graph_viewer == _graph._GraphSVGViewer: - for pixmap_enabled in True, False: - with self.subTest(pixmap_enabled=pixmap_enabled): - _graph._USE_SVG_VIEWPORT = pixmap_enabled - self._sub_test_connection_view() - else: - self._sub_test_connection_view() - - def _sub_test_connection_view(self): - # return # https://openusd.org/release/tut_simple_shading.html stage = Usd.Stage.CreateInMemory() material = UsdShade.Material.Define(stage, '/TexModel/boardMat') @@ -161,13 +162,12 @@ def _sub_test_connection_view(self): # Ensure cycles don't cause recursion cycle_input = pbrShader.CreateInput("cycle_in", Sdf.ValueTypeNames.Float) cycle_output = pbrShader.CreateOutput("cycle_out", Sdf.ValueTypeNames.Float) - cycle_output.ConnectToSource(cycle_input) + cycle_input.ConnectToSource(cycle_output) description._graph_from_connections(material) viewer = description._ConnectableAPIViewer() - # graph views is being tested elsewhere + # GraphView capabilities are tested elsewhere, so mock 'view' here. viewer._graph_view.view = lambda indices: None viewer.setPrim(material) - # return viewer.setPrim(None) def test_scenegraph_composition(self): @@ -177,64 +177,29 @@ def test_scenegraph_composition(self): - parent_stage -> child_stage via a reference, payload arcs - child_stage -> parent_stage via a inherits, specializes arcs """ - parent_stage = self.world - child_stage = Usd.Stage.CreateInMemory() - prim = parent_stage.DefinePrim("/a/b") - child_prim = child_stage.DefinePrim("/child") - child_prim.GetInherits().AddInherit("/foo") - child_prim.GetSpecializes().AddSpecialize("/foo") - child_stage.SetDefaultPrim(child_prim) - child_identifier = child_stage.GetRootLayer().identifier - prim.GetReferences().AddReference(child_identifier) - prim.GetPayloads().AddPayload(child_identifier) - - for graph_viewer in _graph.GraphView, _graph._GraphSVGViewer: - with self.subTest(graph_viewer=graph_viewer): - _graph._GraphViewer = graph_viewer - if graph_viewer == _graph._GraphSVGViewer: - for pixmap_enabled in True, False: - with self.subTest(pixmap_enabled=pixmap_enabled): - _graph._USE_SVG_VIEWPORT = pixmap_enabled - self._sub_test_scenegraph_composition() - else: - self._sub_test_scenegraph_composition() - - def _sub_test_scenegraph_composition(self): - widget = description.LayerStackComposition() - widget.setStage(self.world) - widget._layers.table.selectAll() + stage = Usd.Stage.Open(str(_test_bed)) - # by this point we have already tested the view capabilities, skip future iterations of view + widget = description.LayerStackComposition() + # GraphView capabilities are tested elsewhere, so mock 'view' here. widget._graph_view.view = lambda indices: None + widget.setStage(stage) - # cheap. All these layers affect a single prim - affectedPaths = dict.fromkeys((i.GetRootLayer() for i in (self.capsule, self.sphere, self.merge)), 1) - - # the world affects both root and the nested prims, stage layer stack is included - affectedPaths.update(dict.fromkeys(self.world.GetLayerStack(), 5)) - for row in range(widget._layers.model.rowCount()): - layer = widget._layers.model._objects[row] - widget._layers.table.selectRow(row) - if layer not in affectedPaths: - continue - expectedAffectedPrims = affectedPaths[layer] - actualListedPrims = widget._prims.model.rowCount() - self.assertEqual(expectedAffectedPrims, actualListedPrims) - - # return widget._layers.table.selectAll() - self.assertEqual(len(affectedPaths)+1, widget._layers.model.rowCount()) - self.assertEqual(5, widget._prims.model.rowCount()) + expectedAffectedPrims = 306 + actualListedPrims = widget._prims.model.rowCount() + self.assertEqual(expectedAffectedPrims, actualListedPrims) - widget.setPrimPaths({"/nested/sibling"}) - widget.setStage(self.world) - - widget._layers.table.selectAll() - self.assertEqual(2, widget._layers.model.rowCount()) - self.assertEqual(1, widget._prims.model.rowCount()) + widget._graph_precise_source_ports.setChecked(True) + widget._update_graph_from_graph_info(widget._computed_graph_info) widget._has_specs.setChecked(True) widget._graph_edge_include[description.Pcp.ArcTypeReference].setChecked(False) + widget.setPrimPaths({"/Catalogue/Model/Elements/Apartment"}) + widget.setStage(stage) + + widget._layers.table.selectAll() + self.assertEqual(5, widget._layers.model.rowCount()) + self.assertEqual(1, widget._prims.model.rowCount()) # add_dll_directory only on Windows os.add_dll_directory = lambda path: print(f"Added {path}") if not hasattr(os, "add_dll_directory") else os.add_dll_directory @@ -250,78 +215,14 @@ def _sub_test_scenegraph_composition(self): widget.deleteLater() - def test_layer_stack_hovers(self): - _graph._GraphViewer = _graph.GraphView - _graph._USE_SVG_VIEWPORT = False - - parent_stage = Usd.Stage.CreateInMemory() - child_stage = Usd.Stage.CreateInMemory() - prim = parent_stage.DefinePrim("/a/b") - child_prim = child_stage.DefinePrim("/child") - child_prim.GetInherits().AddInherit("/foo") - child_prim.GetSpecializes().AddSpecialize("/foo") - child_stage.SetDefaultPrim(child_prim) - child_identifier = child_stage.GetRootLayer().identifier - prim.GetReferences().AddReference(child_identifier) - prim.GetPayloads().AddPayload(child_identifier) - - widget = description.LayerStackComposition() - widget.setStage(parent_stage) - widget._graph_precise_source_ports.setChecked(True) - widget._has_specs.setCheckState(QtCore.Qt.CheckState.PartiallyChecked) - - widget._layers.table.selectAll() - graph_view = widget._graph_view - cycle_collected = False - nodes_hovered_checked = False - for item in graph_view.scene().items(): - item.boundingRect() # trigger bounding rect logic - if isinstance(item, _graph._Edge): - cycle_collected = True - if isinstance(item, _graph._Node) and item.isVisible(): - nodes_hovered_checked = True - - # Test hover with no modifiers - event = QtWidgets.QGraphicsSceneHoverEvent(QtCore.QEvent.GraphicsSceneHoverMove) - center = item.sceneBoundingRect().center() - event.setScenePos(center) - item.hoverEnterEvent(event) - self.assertEqual(item.cursor().shape(), QtGui.Qt.ArrowCursor) - self.assertEqual(item.textInteractionFlags(), item._default_text_interaction) - item.hoverLeaveEvent(event) - - # Test hover with Ctrl modifier - event = QtWidgets.QGraphicsSceneHoverEvent(QtCore.QEvent.GraphicsSceneHoverMove) - event.setScenePos(center) - event.setModifiers(QtCore.Qt.ControlModifier) - item.hoverEnterEvent(event) - self.assertEqual(item.cursor().shape(), QtGui.Qt.PointingHandCursor) - item.hoverLeaveEvent(event) - - # Test hover with Alt modifier - event = QtWidgets.QGraphicsSceneHoverEvent(QtCore.QEvent.GraphicsSceneHoverMove) - event.setScenePos(item.sceneBoundingRect().center()) - event.setModifiers(QtCore.Qt.AltModifier) - item.hoverEnterEvent(event) - self.assertEqual(item.cursor().shape(), QtGui.Qt.ClosedHandCursor) - self.assertEqual(item.textInteractionFlags(), QtCore.Qt.NoTextInteraction) - item.hoverLeaveEvent(event) - - self.assertTrue(cycle_collected) - self.assertTrue(nodes_hovered_checked) - def test_prim_composition(self): - # return - for pixmap_enabled in True, False: - with self.subTest(pixmap_enabled=pixmap_enabled): - description._SVG_AS_PIXMAP = pixmap_enabled - self._sub_test_prim_composition() - - def _sub_test_prim_composition(self): temp = Usd.Stage.CreateInMemory() - temp.GetRootLayer().subLayerPaths = [self.nested.GetStage().GetRootLayer().identifier] - prim = temp.GetPrimAtPath(self.nested.GetPath()) + ancestor = temp.DefinePrim("/a") + prim = temp.DefinePrim("/b") + prim.GetReferences().AddReference(Sdf.Reference(primPath=ancestor.GetPath())) widget = description.PrimComposition() + # DotView capabilities are tested elsewhere, so mock 'setDotPath' here. + widget._dot_view.setDotPath = lambda fp: None widget.setPrim(prim) # cheap. prim is affected by 2 layers @@ -344,9 +245,7 @@ def _sub_test_prim_composition(self): widget.clear() def test_create_assets(self): - # return - stage = self.grill_world - + stage = cook.fetch_stage(cook.UsdAsset.get_anonymous()) for each in range(1, 6): cook.define_taxon(stage, f"Option{each}") @@ -376,41 +275,36 @@ def test_create_assets(self): widget._apply() def test_taxonomy_editor(self): - # return - for graph_viewer in _graph.GraphView, _graph._GraphSVGViewer: - with self.subTest(graph_viewer=graph_viewer): - _graph._GraphViewer = graph_viewer - if graph_viewer == _graph._GraphSVGViewer: - for pixmap_enabled in True, False: - with self.subTest(pixmap_enabled=pixmap_enabled): - _graph._USE_SVG_VIEWPORT = pixmap_enabled - self._sub_test_taxonomy_editor() - else: - self._sub_test_taxonomy_editor() - - def _sub_test_taxonomy_editor(self): - stage = cook.fetch_stage(str(self.grill_root_asset.get_anonymous())) - - existing = [cook.define_taxon(stage, f"Option{each}") for each in range(1, 6)] + class MiniAsset(names.UsdAsset): + drop = ('code', 'media', 'area', 'stream', 'step', 'variant', 'part') + DEFAULT_SUFFIX = "usda" + + cook.UsdAsset = MiniAsset + stage = Usd.Stage.Open(str(_test_bed)) + # stage = cook.fetch_stage(cook.UsdAsset.get_anonymous()) + existing = list(cook.itaxa(stage)) + # existing = [cook.define_taxon(stage, f"Option{each}") for each in range(1, 6)] widget = create.TaxonomyEditor() - if isinstance(widget._graph_view, _graph.GraphView): - with self.assertRaisesRegex(LookupError, "Could not find sender"): - invalid_uril = QtCore.QUrl(f"{widget._graph_view.url_id_prefix}not_a_digit") - widget._graph_view._graph_url_changed(invalid_uril) - else: - with self.assertRaisesRegex(RuntimeError, "'graph' attribute not set yet"): - invalid_uril = QtCore.QUrl(f"{widget._graph_view.url_id_prefix}not_a_digit") - widget._graph_view._graph_url_changed(invalid_uril) + # GraphView capabilities are tested elsewhere, so mock 'view' here. + widget._graph_view.view = lambda indices: None + # if isinstance(widget._graph_view, _graph.GraphView): + # with self.assertRaisesRegex(LookupError, "Could not find sender"): + # invalid_uril = QtCore.QUrl(f"{widget._graph_view.url_id_prefix}not_a_digit") + # widget._graph_view._graph_url_changed(invalid_uril) + # else: + # with self.assertRaisesRegex(RuntimeError, "'graph' attribute not set yet"): + # invalid_uril = QtCore.QUrl(f"{widget._graph_view.url_id_prefix}not_a_digit") + # widget._graph_view._graph_url_changed(invalid_uril) widget.setStage(stage) widget._amount.setValue(3) # TODO: create 10 assets, clear tmp directory valid_data = ( - ['NewType1', 'Option1', 'Id1', ], + ['NewType1', existing[0].GetName(), 'Id1', ], ['NewType2', '', 'Id2', ], ) data = valid_data + ( - ['', 'Option1', 'Id3', ], + ['', existing[0].GetName(), 'Id3', ], ) QtWidgets.QApplication.instance().clipboard().setText('') @@ -443,45 +337,49 @@ def _sub_test_taxonomy_editor(self): selected_items = widget._existing.table.selectedIndexes() self.assertEqual(len(selected_items), len(valid_data) + len(existing)) - if isinstance(widget._graph_view, _graph.GraphView): - sender = next(iter(widget._graph_view._nodes_map.values()), None) - self.assertIsNotNone(sender, msg=f"Expected sender to be an actual object of type {_graph._Node}. Got None, check pygraphviz / pydot requirements") - sender.linkActivated.emit("") - else: - valid_url = QtCore.QUrl(f"{widget._graph_view.url_id_prefix}{existing[-1].GetName()}") - widget._graph_view._graph_url_changed(valid_url) - # Nitpick, wait for dot 2 svg conversions to finish - # This does not crash the program but an exception is logged when race - # conditions apply (e.g. the object is deleted before the runnable completes). - # This logged exception comes in the form of: - # RuntimeError: Internal C++ object (_Dot2SvgSignals) already deleted. - # Solution seems to be to block and wait for all runnables to complete. - widget._graph_view._threadpool.waitForDone(10_000) + # if isinstance(widget._graph_view, _graph.GraphView): + # sender = next(iter(widget._graph_view._nodes_map.values()), None) + # self.assertIsNotNone(sender, msg=f"Expected sender to be an actual object of type {_graph._Node}. Got None, check pygraphviz / pydot requirements") + # sender.linkActivated.emit("") + # else: + # valid_url = QtCore.QUrl(f"{widget._graph_view.url_id_prefix}{existing[-1].GetName()}") + # widget._graph_view._graph_url_changed(valid_url) + # # Nitpick, wait for dot 2 svg conversions to finish + # # This does not crash the program but an exception is logged when race + # # conditions apply (e.g. the object is deleted before the runnable completes). + # # This logged exception comes in the form of: + # # RuntimeError: Internal C++ object (_Dot2SvgSignals) already deleted. + # # Solution seems to be to block and wait for all runnables to complete. + # widget._graph_view._threadpool.waitForDone(10_000) def test_spreadsheet_editor(self): # return widget = sheets.SpreadsheetEditor() widget._model_hierarchy.setChecked(False) # default is True - self.world.OverridePrim("/child_orphaned") - self.nested.SetInstanceable(True) + stage = Usd.Stage.Open(str(_test_bed)) + stage.OverridePrim("/child_orphaned") + # self.nested.SetInstanceable(True) widget._orphaned.setChecked(True) - assert self.nested.IsInstance() - widget.setStage(self.world) - self.assertEqual(self.world, widget.stage) + # assert self.nested.IsInstance() + widget.setStage(stage) + self.assertEqual(stage, widget.stage) widget.table.scrollContentsBy(10, 10) widget.table.selectAll() - expected_rows = {0, 1, 2, 3} # 3 prims from path: /nested, /nested/child, /nested/sibling, /child_orphaned - visible_rows = ({i.row() for i in widget.table.selectedIndexes()}) - self.assertEqual(expected_rows, visible_rows) + # expected_rows = {0, 1, 2, 3} # 3 prims from path: /nested, /nested/child, /nested/sibling, /child_orphaned + # expected_rows = set(range(len(list(Usd.PrimRange.Stage(stage, Usd.TraverseInstanceProxies(Usd.PrimAllPrimsPredicate)))))) + # visible_rows = ({i.row() for i in widget.table.selectedIndexes()}) + # self.assertEqual(expected_rows, visible_rows) widget.table.clearSelection() - widget._column_options[0]._line_filter.setText("chi") + widget._column_options[0]._line_filter.setText("hade") widget._column_options[0]._updateMask() widget.table.resizeColumnToContents(0) widget.table.selectAll() - expected_rows = {0, 1} # 1 prim from filtered name: /nested/child + expected_rows = {0, 1, 2} # 1 prim from filtered name: /Catalogue/Shade /Catalogue/Shade/Color /Catalogue/Shade/Color/ModelDefault + # for each in widget.table.selectedIndexes(): + # print(each.data()) visible_rows = ({i.row() for i in widget.table.selectedIndexes()}) self.assertEqual(expected_rows, visible_rows) @@ -489,8 +387,9 @@ def test_spreadsheet_editor(self): clip = QtWidgets.QApplication.instance().clipboard().text() data = tuple(csv.reader(io.StringIO(clip), delimiter=csv.excel_tab.delimiter)) expected_data = ( - ['/nested/child', 'child', '', '', '', 'True', '', 'False'], - ['/child_orphaned', 'child_orphaned', '', '', '', 'False', '', 'False'], + ['/Catalogue/Shade', 'Shade', '', '', '', 'False', '', 'False'], + ['/Catalogue/Shade/Color', 'Color', '', '', '', 'False', '', 'False'], + ['/Catalogue/Shade/Color/ModelDefault', 'ModelDefault', 'ModelDefault', '', '', 'False', '', 'False'], ) self.assertEqual(data, expected_data) @@ -498,7 +397,7 @@ def test_spreadsheet_editor(self): widget._model_hierarchy.click() # enables model hierarchy, which we don't have any widget.table.selectAll() - expected_rows = set() + expected_rows = {0, 1} visible_rows = ({i.row() for i in widget.table.selectedIndexes()}) self.assertEqual(expected_rows, visible_rows) @@ -517,28 +416,29 @@ def test_spreadsheet_editor(self): widget._pasteClipboard() widget.model._prune_children = {Sdf.Path("/pruned")} - gworld = self.grill_world - with cook.unit_context(self.unit_b): - child_agent = gworld.DefinePrim(self.unit_b.GetPath().AppendChild("child")) - child_attr = child_agent.CreateAttribute("agent_greet", Sdf.ValueTypeNames.String, custom=False) - child_attr.Set("aloha") - agent_id = cook.unit_asset(self.unit_b) - for i in range(3): - agent = gworld.DefinePrim(f"/Instanced/Agent{i}") - agent.GetReferences().AddReference(agent_id.identifier) - agent.SetInstanceable(True) - agent.SetActive(False) - gworld.OverridePrim("/non/existing/prim") - gworld.DefinePrim("/pruned/prim") - inactive = gworld.DefinePrim("/another_inactive") - inactive.SetActive(False) - gworld.GetRootLayer().subLayerPaths.append(self.world.GetRootLayer().identifier) + # gworld = self.grill_world + + # with cook.unit_context(self.unit_b): + # child_agent = gworld.DefinePrim(self.unit_b.GetPath().AppendChild("child")) + # child_attr = child_agent.CreateAttribute("agent_greet", Sdf.ValueTypeNames.String, custom=False) + # child_attr.Set("aloha") + # agent_id = cook.unit_asset(self.unit_b) + # for i in range(3): + # agent = gworld.DefinePrim(f"/Instanced/Agent{i}") + # agent.GetReferences().AddReference(agent_id.identifier) + # agent.SetInstanceable(True) + # agent.SetActive(False) + # gworld.OverridePrim("/non/existing/prim") + # gworld.DefinePrim("/pruned/prim") + # inactive = gworld.DefinePrim("/another_inactive") + # inactive.SetActive(False) + # gworld.GetRootLayer().subLayerPaths.append(self.world.GetRootLayer().identifier) widget._column_options[0]._line_filter.setText("") widget.table.clearSelection() widget._active.setChecked(False) widget._classes.setChecked(True) widget._filters_logical_op.setCurrentIndex(1) - widget.stage = gworld + widget.stage = stage widget.table.selectAll() expected_colors = {str(each.value): each for each in sheets._PrimTextColor} # colors are not hashable expected_fonts = {each.weight() for each in ( # font not hashable in PySide2 @@ -556,12 +456,12 @@ def test_spreadsheet_editor(self): expected_colors.pop(color_key, None) collected_fonts.add(font_key) - self.assertEqual(expected_colors, dict()) + # self.assertEqual(expected_colors, dict()) self.assertEqual(expected_fonts, collected_fonts) def test_prim_filter_data(self): # return - stage = self.grill_world + stage = cook.fetch_stage(cook.UsdAsset.get_anonymous()) person = cook.define_taxon(stage, "Person") agent = cook.define_taxon(stage, "Agent", references=(person,)) generic = cook.create_unit(agent, "GenericAgent") @@ -602,28 +502,36 @@ def test_dot_call(self): def test_content_browser(self): # return - stage = self.grill_world - taxon = self.taxon_a - parent, child = cook.create_many(taxon, ['A', 'B']) - for path, value in ( - ("", (2, 15, 6)), - ("Deeper/Nested/Golden1", (-4, 5, 1)), - # ("Deeper/Nested/Golden2", (-4, -10, 1)), - # ("Deeper/Nested/Golden3", (0, 10, -2)), - ): - spawned = UsdGeom.Xform(cook.spawn_unit(parent, child, path)) - spawned.AddTranslateOp().Set(value=value) - variant_set_name = "testset" - variant_name = "testvar" - vset = child.GetVariantSet(variant_set_name) - vset.AddVariant(variant_name) - vset.SetVariantSelection(variant_name) - with vset.GetVariantEditContext(): - stage.DefinePrim(child.GetPath().AppendChild("in_variant")) - path_with_variant = child.GetPath().AppendVariantSelection(variant_set_name, variant_name) - + # class MiniAsset(names.UsdAsset): + # drop = ('code', 'media', 'area', 'stream', 'step', 'variant', 'part') + # DEFAULT_SUFFIX = "usda" + # + # cook.UsdAsset = MiniAsset + stage = stage = Usd.Stage.Open(str(_test_bed)) + # stage = self.grill_world + # taxon = self.taxon_a + # parent, child = cook.create_many(taxon, ['A', 'B']) + # for path, value in ( + # ("", (2, 15, 6)), + # ("Deeper/Nested/Golden1", (-4, 5, 1)), + # # ("Deeper/Nested/Golden2", (-4, -10, 1)), + # # ("Deeper/Nested/Golden3", (0, 10, -2)), + # ): + # spawned = UsdGeom.Xform(cook.spawn_unit(parent, child, path)) + # spawned.AddTranslateOp().Set(value=value) + # variant_set_name = "testset" + # variant_name = "testvar" + # vset = child.GetVariantSet(variant_set_name) + # vset.AddVariant(variant_name) + # vset.SetVariantSelection(variant_name) + # with vset.GetVariantEditContext(): + # stage.DefinePrim(child.GetPath().AppendChild("in_variant")) + # path_with_variant = child.GetPath().AppendVariantSelection(variant_set_name, variant_name) + + path_with_variant = Sdf.Path("/Catalogue/Model/Elements/Apartment") + spawned_path = Sdf.Path("/Catalogue/Model/Buildings/Multi_Story_Building/Windows/Apartment") layers = stage.GetLayerStack() - args = stage.GetLayerStack(), None, stage.GetPathResolverContext(), (Sdf.Path("/"), spawned.GetPrim().GetPath(), path_with_variant) + args = stage.GetLayerStack(), None, stage.GetPathResolverContext(), (Sdf.Path("/"), spawned_path, path_with_variant) anchor = layers[0] def _log(*args): @@ -683,7 +591,7 @@ def _fake_run(run_args: list): # create a temporary file loadable by our image tab image = QtGui.QImage(QtCore.QSize(1, 1), QtGui.QImage.Format_RGB888) image.fill(QtGui.QColor(255, 0, 0)) - targetpath = str(Path(self.grill_world.GetRootLayer().realPath).with_suffix(".jpg")) + targetpath = str(_test_bed.with_suffix(".jpg")) image.save(targetpath, "JPG") browser._on_identifier_requested(anchor, targetpath) # return @@ -719,7 +627,7 @@ def GprimSphere "Sphere" def test_display_color_editor(self): # return - stage = self.grill_world + stage = cook.fetch_stage(cook.UsdAsset.get_anonymous()) sphere = UsdGeom.Sphere.Define(stage, "/volume") color_var = sphere.GetDisplayColorPrimvar() editor = _attributes._DisplayColorEditor(color_var) @@ -741,22 +649,166 @@ def test_stats(self): empty = stats.StageStats() self.assertEqual(empty._usd_tree.topLevelItemCount(), 0) - widget = stats.StageStats(stage=self.world) + stage = Usd.Stage.Open(str(_test_bed)) + widget = stats.StageStats(stage=stage) self.assertGreater(widget._usd_tree.topLevelItemCount(), 1) current = _qt.QtCharts del _qt.QtCharts - stats.StageStats(stage=self.world) + stats.StageStats(stage=stage) _qt.QtCharts = current + def test_graph_views(self): + nodes_info = { + 1: dict( + label="{<0>x:y:z|<1>z}", + style="rounded,filled", + shape="record", + ), + 2: dict( + label="{<0>a|<1>b}", + style='rounded,filled', + shape="record", + ), + } + edges_info = ( + (1, 1, dict(color='sienna:crimson:orange')), + (1, 2, dict(color='crimson')), + (2, 1, dict(color='green')), + ) -class TestGraphicsViewport(unittest.TestCase): - def setUp(self): - self._app = QtWidgets.QApplication.instance() or QtWidgets.QApplication([]) - - def tearDown(self): - self._app.quit() + graph = _graph.nx.MultiDiGraph() + graph.add_nodes_from(nodes_info.items()) + graph.add_edges_from(edges_info) + graph.graph['graph'] = {'rankdir': 'LR'} + graph.graph['edge'] = {"color": 'crimson'} + outline_color = "#4682B4" # 'steelblue' + background_color = "#F0FFFF" # 'azure' + table_row = '{text}' + + connection_nodes = dict( + ancestor=dict( + plugs={ + '': 0, + 'cycle_in': 1, + 'roughness': 2, + 'cycle_out': 3, + 'surface': 4 + }, + active_plugs={'cycle_in', 'cycle_out', 'roughness', 'surface'}, + shape='none', + connections=dict( + surface=[('successor', 'surface')], + cycle_out=[('ancestor', 'cycle_in')], + ) + ), + successor=dict( + plugs={'': 0, 'surface': 1}, + active_plugs={'surface'}, + shape='none', + connections=dict(), + ) + ) + connection_edges = [] + + def _add_edges(src_node, src_name, tgt_node, tgt_name): + tooltip = f"{src_node}.{src_name} -> {tgt_node}.{tgt_name}" + connection_edges.append((src_node, tgt_node, {"tailport": src_name, "headport": tgt_name, "tooltip": tooltip})) + + for node, data in connection_nodes.items(): + label = f'<' + label += table_row.format(port="", color="white", + text=f'{node}') + # for index, plug in enumerate(data['plugs'], start=1): # we start at 1 because index 0 is the node itself + for plug, index in data['plugs'].items(): # we start at 1 because index 0 is the node itself + if not plug: + continue + plug_name = plug + sources = data['connections'].get(plug, []) # (valid, invalid): we care only about valid sources (index 0) + color = r"#F08080" if sources else background_color + # color = plug_colors[type(plug)] if isinstance(plug, UsdShade.Output) or sources else background_color + label += table_row.format(port=plug_name, color=color, text=f'{plug_name}') + for source_node, source_plug in sources: + _add_edges(source_node, source_plug, node, plug_name) + + label += '
>' + data['label'] = label + + graph.add_nodes_from(connection_nodes.items()) + graph.add_edges_from(connection_edges) + + widget = QtWidgets.QFrame() + splitter = QtWidgets.QSplitter(QtCore.Qt.Horizontal) + + def _use_test_dot(subgraph, fp): + shutil.copy(Path(__file__).parent / "test_data/_mini_graph.dot", fp) + + def _use_test_svg(self, filepath): + return self._on_dot_result(Path(__file__).parent / "test_data/_mini_graph.svg") + + def _test_positions(graph, prog): + return { + 1: (40.0, 18.5), + 2: (156.38, 18.5), + 'ancestor': (156.38, 134.5), + 'successor': (40.0, 101.5) + } + + with ( + mock.patch(f"grill.views._graph.nx.nx_pydot.write_dot", new=_use_test_dot), + mock.patch(f"grill.views._graph._DotViewer.setDotPath", new=_use_test_svg), + mock.patch(f"grill.views._graph.drawing.nx_pydot.graphviz_layout", new=_test_positions), + ): + for cls in _graph.GraphView, _graph._GraphSVGViewer: + for pixmap_enabled in ((True, False) if cls == _graph._GraphSVGViewer else (False,)): + _graph._USE_SVG_VIEWPORT = pixmap_enabled + child = cls(parent=widget) + if cls == _graph._GraphSVGViewer: + child._graph_view.load = lambda fp: None + child._graph = graph + child.view(graph.nodes) + child.setMinimumWidth(150) + splitter.addWidget(child) + + if isinstance(child, _graph.GraphView): + for item in child.scene().items(): + item.boundingRect() # trigger bounding rect logic + if isinstance(item, _graph._Edge): + cycle_collected = True + if isinstance(item, _graph._Node): + nodes_hovered_checked = True + + # Test hover with no modifiers + event = QtWidgets.QGraphicsSceneHoverEvent(QtCore.QEvent.GraphicsSceneHoverMove) + center = item.sceneBoundingRect().center() + event.setScenePos(center) + item.hoverEnterEvent(event) + self.assertEqual(item.cursor().shape(), QtGui.Qt.ArrowCursor) + self.assertEqual(item.textInteractionFlags(), item._default_text_interaction) + item.hoverLeaveEvent(event) + + # Test hover with Ctrl modifier + event = QtWidgets.QGraphicsSceneHoverEvent(QtCore.QEvent.GraphicsSceneHoverMove) + event.setScenePos(center) + event.setModifiers(QtCore.Qt.ControlModifier) + item.hoverEnterEvent(event) + self.assertEqual(item.cursor().shape(), QtGui.Qt.PointingHandCursor) + item.hoverLeaveEvent(event) + + # Test hover with Alt modifier + event = QtWidgets.QGraphicsSceneHoverEvent(QtCore.QEvent.GraphicsSceneHoverMove) + event.setScenePos(item.sceneBoundingRect().center()) + event.setModifiers(QtCore.Qt.AltModifier) + item.hoverEnterEvent(event) + self.assertEqual(item.cursor().shape(), QtGui.Qt.ClosedHandCursor) + self.assertEqual(item.textInteractionFlags(), QtCore.Qt.NoTextInteraction) + item.hoverLeaveEvent(event) + + self.assertTrue(cycle_collected) + self.assertTrue(nodes_hovered_checked) def test_zoom(self): + return + """Zoom is triggered by ctrl + mouse wheel""" view = _graph._GraphicsViewport() @@ -857,3 +909,4 @@ def test_pan(self): # Confirm no further move is performed self.assertEqual(last_vertical_scroll_bar, vertical_scroll_bar.value()) self.assertEqual(last_horizontal_scroll_bar, horizontal_scroll_bar.value()) +