From b4f62011b1e982ff6cdfafe1bb8daf16d16662f7 Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Wed, 28 Sep 2022 10:53:20 +0100 Subject: [PATCH 01/87] Activated orientation viewer - currently calls external sasmodels window --- src/sas/qtgui/MainWindow/GuiManager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sas/qtgui/MainWindow/GuiManager.py b/src/sas/qtgui/MainWindow/GuiManager.py index 9e1fa83178..d0d90730db 100644 --- a/src/sas/qtgui/MainWindow/GuiManager.py +++ b/src/sas/qtgui/MainWindow/GuiManager.py @@ -646,7 +646,7 @@ def addTriggers(self): #self._workspace.actionImage_Viewer.setVisible(False) self._workspace.actionCombine_Batch_Fit.setVisible(False) # orientation viewer set to invisible SASVIEW-1132 - self._workspace.actionOrientation_Viewer.setVisible(False) + self._workspace.actionOrientation_Viewer.setVisible(True) # File self._workspace.actionLoadData.triggered.connect(self.actionLoadData) From c985d92b1e73e39c7c2c23ec92d0a2f2aa76d261 Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Mon, 10 Oct 2022 09:49:44 +0100 Subject: [PATCH 02/87] Orientation viewer stuff --- src/sas/qtgui/Utilities/OrientationViewer.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/sas/qtgui/Utilities/OrientationViewer.py diff --git a/src/sas/qtgui/Utilities/OrientationViewer.py b/src/sas/qtgui/Utilities/OrientationViewer.py new file mode 100644 index 0000000000..71b4e73c08 --- /dev/null +++ b/src/sas/qtgui/Utilities/OrientationViewer.py @@ -0,0 +1,19 @@ + +from sas.qtgui.Utilities.UI.OrientationViewerUI import Ui_OrientationViewer + +from PyQt5 import QtWidgets + +class OrientationViewer(QtWidgets.QMainWindow, Ui_OrientationViewer): + + def __init__(self, parent=None): + super().__init__(parent._parent) + + self.parent = parent + self.setupUi(self) + + +def main(): + pass + +if __name__ == "__main__": + main() \ No newline at end of file From 5188623804d8755ce780da5562412191828fe9d5 Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Mon, 10 Oct 2022 09:50:57 +0100 Subject: [PATCH 03/87] Viewer .UI file --- .../qtgui/Utilities/UI/OrientationViewerUI.ui | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 src/sas/qtgui/Utilities/UI/OrientationViewerUI.ui diff --git a/src/sas/qtgui/Utilities/UI/OrientationViewerUI.ui b/src/sas/qtgui/Utilities/UI/OrientationViewerUI.ui new file mode 100644 index 0000000000..a971779f0a --- /dev/null +++ b/src/sas/qtgui/Utilities/UI/OrientationViewerUI.ui @@ -0,0 +1,85 @@ + + + OrientationViewer + + + + 0 + 0 + 697 + 615 + + + + Form + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + TextLabel + + + + + + + Qt::Horizontal + + + + + + + TextLabel + + + + + + + Qt::Horizontal + + + + + + + TextLabel + + + + + + + Qt::Horizontal + + + + + + + + + + + From 5d30f62ed409618796cb5b4e704c18af9877e071 Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Tue, 11 Oct 2022 14:48:48 +0100 Subject: [PATCH 04/87] Qt stuff --- src/sas/qtgui/Utilities/OrientationViewer.py | 10 ++++- .../qtgui/Utilities/UI/OrientationViewerUI.ui | 43 ++++++++++++++----- 2 files changed, 41 insertions(+), 12 deletions(-) diff --git a/src/sas/qtgui/Utilities/OrientationViewer.py b/src/sas/qtgui/Utilities/OrientationViewer.py index 71b4e73c08..66a9795215 100644 --- a/src/sas/qtgui/Utilities/OrientationViewer.py +++ b/src/sas/qtgui/Utilities/OrientationViewer.py @@ -6,14 +6,20 @@ class OrientationViewer(QtWidgets.QMainWindow, Ui_OrientationViewer): def __init__(self, parent=None): - super().__init__(parent._parent) + super().__init__(parent) self.parent = parent self.setupUi(self) def main(): - pass + """ Show a demo of the slider """ + app = QtWidgets.QApplication([]) + viewer = OrientationViewer() + + viewer.show() + app.exec_() + if __name__ == "__main__": main() \ No newline at end of file diff --git a/src/sas/qtgui/Utilities/UI/OrientationViewerUI.ui b/src/sas/qtgui/Utilities/UI/OrientationViewerUI.ui index a971779f0a..cc08d6a56a 100644 --- a/src/sas/qtgui/Utilities/UI/OrientationViewerUI.ui +++ b/src/sas/qtgui/Utilities/UI/OrientationViewerUI.ui @@ -6,33 +6,56 @@ 0 0 - 697 - 615 + 682 + 519 + + + 100 + 100 + + + + + 16777211 + 16777215 + + Form - - - QFrame::StyledPanel - - - QFrame::Raised - - + + + + 0 + 0 + + + + + 16777215 + 60 + + QFrame::StyledPanel QFrame::Raised + + 0 + + + 10 + From d777fe2237ed43a02c539733453c57b5d7f0eaf9 Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Thu, 13 Oct 2022 11:45:04 +0100 Subject: [PATCH 05/87] Some progress, some issues Qt designer remain --- src/sas/qtgui/MainWindow/MainWindow.py | 1 + src/sas/qtgui/Utilities/OrientationViewer.py | 17 +- .../qtgui/Utilities/UI/OrientationViewerUI.ui | 177 ++++++++++-------- 3 files changed, 113 insertions(+), 82 deletions(-) diff --git a/src/sas/qtgui/MainWindow/MainWindow.py b/src/sas/qtgui/MainWindow/MainWindow.py index 1545e1086b..720707ce73 100644 --- a/src/sas/qtgui/MainWindow/MainWindow.py +++ b/src/sas/qtgui/MainWindow/MainWindow.py @@ -91,6 +91,7 @@ def run_sasview(): splash = SplashScreen() splash.show() app.setAttribute(Qt.AA_EnableHighDpiScaling) + app.setStyleSheet("* {font-size: 11pt;}") # Main application style. #app.setStyle('Fusion') diff --git a/src/sas/qtgui/Utilities/OrientationViewer.py b/src/sas/qtgui/Utilities/OrientationViewer.py index 66a9795215..e0ee6ae741 100644 --- a/src/sas/qtgui/Utilities/OrientationViewer.py +++ b/src/sas/qtgui/Utilities/OrientationViewer.py @@ -1,23 +1,28 @@ -from sas.qtgui.Utilities.UI.OrientationViewerUI import Ui_OrientationViewer +from sas.qtgui.Utilities.UI.OrientationViewerUIBackup import Ui_OrientationViewer from PyQt5 import QtWidgets -class OrientationViewer(QtWidgets.QMainWindow, Ui_OrientationViewer): +class OrientationViewer(QtWidgets.QWidget, Ui_OrientationViewer): def __init__(self, parent=None): - super().__init__(parent) + super().__init__() self.parent = parent - self.setupUi(self) + self.setupUi(parent) def main(): """ Show a demo of the slider """ app = QtWidgets.QApplication([]) - viewer = OrientationViewer() + app.setStyleSheet("* {font-size: 11pt;}") - viewer.show() + mainWindow = QtWidgets.QMainWindow() + viewer = OrientationViewer(mainWindow) + + mainWindow.setCentralWidget(viewer) + + mainWindow.show() app.exec_() diff --git a/src/sas/qtgui/Utilities/UI/OrientationViewerUI.ui b/src/sas/qtgui/Utilities/UI/OrientationViewerUI.ui index cc08d6a56a..e26c3798cd 100644 --- a/src/sas/qtgui/Utilities/UI/OrientationViewerUI.ui +++ b/src/sas/qtgui/Utilities/UI/OrientationViewerUI.ui @@ -6,8 +6,8 @@ 0 0 - 682 - 519 + 642 + 664 @@ -25,81 +25,106 @@ Form - + - - - - - - - 0 - 0 - - - - - 16777215 - 60 - - - - QFrame::StyledPanel - - - QFrame::Raised - - - 0 - - - - 10 - - - - - TextLabel - - - - - - - Qt::Horizontal - - - - - - - TextLabel - - - - - - - Qt::Horizontal - - - - - - - TextLabel - - - - - - - Qt::Horizontal - - - - - + + + + + + + + 0 + 0 + + + + + 200 + 60 + + + + + 16777215 + 60 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + 0 + + + + 10 + + + + + TextLabel + + + + + + + Qt::Horizontal + + + + + + + TextLabel + + + + + + + Qt::Horizontal + + + + + + + TextLabel + + + + + + + Qt::Horizontal + + + + + + + + + + PushButton + + + + + + + PushButton + + + + + + From c4a5618542afe9e7bdad0827904fa675bf1eae39 Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Tue, 25 Oct 2022 13:26:06 +0100 Subject: [PATCH 06/87] Basics of the orientation viewer --- src/sas/qtgui/Utilities/OrientationViewer.py | 39 ++++- .../Utilities/OrientationViewerController.py | 9 ++ .../UI/OrientationViewerControllerUI.ui | 63 +++++++++ .../qtgui/Utilities/UI/OrientationViewerUI.ui | 133 ------------------ 4 files changed, 105 insertions(+), 139 deletions(-) create mode 100644 src/sas/qtgui/Utilities/OrientationViewerController.py create mode 100644 src/sas/qtgui/Utilities/UI/OrientationViewerControllerUI.ui delete mode 100644 src/sas/qtgui/Utilities/UI/OrientationViewerUI.ui diff --git a/src/sas/qtgui/Utilities/OrientationViewer.py b/src/sas/qtgui/Utilities/OrientationViewer.py index e0ee6ae741..c549e6fc49 100644 --- a/src/sas/qtgui/Utilities/OrientationViewer.py +++ b/src/sas/qtgui/Utilities/OrientationViewer.py @@ -1,21 +1,48 @@ -from sas.qtgui.Utilities.UI.OrientationViewerUIBackup import Ui_OrientationViewer - from PyQt5 import QtWidgets +import pyqtgraph.opengl as gl + -class OrientationViewer(QtWidgets.QWidget, Ui_OrientationViewer): +class OrientationViewer(QtWidgets.QWidget): def __init__(self, parent=None): super().__init__() - self.parent = parent - self.setupUi(parent) + self.resize(600, 700) + + self.graph = gl.GLViewWidget() + layout = QtWidgets.QVBoxLayout() + + # Add widgets to the layout + layout.addWidget(self.graph) + layout.addWidget(QtWidgets.QPushButton("Top")) + layout.addWidget(QtWidgets.QPushButton("Center")) + layout.addWidget(QtWidgets.QPushButton("Bottom")) + # Set the layout on the application's window + self.setLayout(layout) + + ## create three grids, add each to the view + xgrid = gl.GLGridItem() + ygrid = gl.GLGridItem() + zgrid = gl.GLGridItem() + self.graph.addItem(xgrid) + self.graph.addItem(ygrid) + self.graph.addItem(zgrid) + + ## rotate x and y grids to face the correct direction + xgrid.rotate(90, 0, 1, 0) + ygrid.rotate(90, 1, 0, 0) + + ## scale each grid differently + xgrid.scale(0.2, 0.1, 0.1) + ygrid.scale(0.2, 0.1, 0.1) + zgrid.scale(0.1, 0.2, 0.1) + def main(): """ Show a demo of the slider """ app = QtWidgets.QApplication([]) - app.setStyleSheet("* {font-size: 11pt;}") mainWindow = QtWidgets.QMainWindow() viewer = OrientationViewer(mainWindow) diff --git a/src/sas/qtgui/Utilities/OrientationViewerController.py b/src/sas/qtgui/Utilities/OrientationViewerController.py new file mode 100644 index 0000000000..710ea1bf4e --- /dev/null +++ b/src/sas/qtgui/Utilities/OrientationViewerController.py @@ -0,0 +1,9 @@ +from PyQt5 import QtWidgets + +from sas.qtgui.Utilities.UI.OrientationViewerControllerUI import Ui_OrientationViewierControllerUI + + +class AddMultEditor(QtWidgets.QDialog, Ui_OrientationViewierControllerUI): + def __init__(self, parent=None): + super().__init__() + diff --git a/src/sas/qtgui/Utilities/UI/OrientationViewerControllerUI.ui b/src/sas/qtgui/Utilities/UI/OrientationViewerControllerUI.ui new file mode 100644 index 0000000000..4545b0cf8b --- /dev/null +++ b/src/sas/qtgui/Utilities/UI/OrientationViewerControllerUI.ui @@ -0,0 +1,63 @@ + + + OrientationViewierControllerUI + + + + 0 + 0 + 483 + 57 + + + + Form + + + + + + θ + + + + + + + Qt::Horizontal + + + + + + + φ + + + + + + + ψ + + + + + + + Qt::Horizontal + + + + + + + Qt::Horizontal + + + + + + + + diff --git a/src/sas/qtgui/Utilities/UI/OrientationViewerUI.ui b/src/sas/qtgui/Utilities/UI/OrientationViewerUI.ui deleted file mode 100644 index e26c3798cd..0000000000 --- a/src/sas/qtgui/Utilities/UI/OrientationViewerUI.ui +++ /dev/null @@ -1,133 +0,0 @@ - - - OrientationViewer - - - - 0 - 0 - 642 - 664 - - - - - 100 - 100 - - - - - 16777211 - 16777215 - - - - Form - - - - - - - - - - - 0 - 0 - - - - - 200 - 60 - - - - - 16777215 - 60 - - - - QFrame::StyledPanel - - - QFrame::Raised - - - 0 - - - - 10 - - - - - TextLabel - - - - - - - Qt::Horizontal - - - - - - - TextLabel - - - - - - - Qt::Horizontal - - - - - - - TextLabel - - - - - - - Qt::Horizontal - - - - - - - - - - PushButton - - - - - - - PushButton - - - - - - - - - - - - From 8ad4d1746c6578e9d58a4c88a38c0bc5337b7f01 Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Tue, 25 Oct 2022 14:21:28 +0100 Subject: [PATCH 07/87] Test window --- .../Utilities/OrientationViewerController.py | 24 ++++++++++++++++++- .../UI/OrientationViewerControllerUI.ui | 18 ++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/sas/qtgui/Utilities/OrientationViewerController.py b/src/sas/qtgui/Utilities/OrientationViewerController.py index 710ea1bf4e..7bf87f85b9 100644 --- a/src/sas/qtgui/Utilities/OrientationViewerController.py +++ b/src/sas/qtgui/Utilities/OrientationViewerController.py @@ -3,7 +3,29 @@ from sas.qtgui.Utilities.UI.OrientationViewerControllerUI import Ui_OrientationViewierControllerUI -class AddMultEditor(QtWidgets.QDialog, Ui_OrientationViewierControllerUI): +class OrientationViewierController(QtWidgets.QDialog, Ui_OrientationViewierControllerUI): def __init__(self, parent=None): super().__init__() + self.setupUi(self) + + + + +def main(): + """ Show a demo of the slider """ + app = QtWidgets.QApplication([]) + + mainWindow = QtWidgets.QMainWindow() + viewer = OrientationViewierController(mainWindow) + + mainWindow.setCentralWidget(viewer) + + mainWindow.show() + + mainWindow.resize(700, 100) + app.exec_() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/sas/qtgui/Utilities/UI/OrientationViewerControllerUI.ui b/src/sas/qtgui/Utilities/UI/OrientationViewerControllerUI.ui index 4545b0cf8b..70d4d120c8 100644 --- a/src/sas/qtgui/Utilities/UI/OrientationViewerControllerUI.ui +++ b/src/sas/qtgui/Utilities/UI/OrientationViewerControllerUI.ui @@ -23,6 +23,12 @@ + + 360 + + + 15 + Qt::Horizontal @@ -44,6 +50,12 @@ + + 360 + + + 15 + Qt::Horizontal @@ -51,6 +63,12 @@ + + 360 + + + 15 + Qt::Horizontal From 1f7f5614326735fcdeff36720aef210c6f519597 Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Tue, 25 Oct 2022 14:22:39 +0100 Subject: [PATCH 08/87] Added dependencies to requirements.txt --- build_tools/requirements.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/build_tools/requirements.txt b/build_tools/requirements.txt index b33dd69d73..e2682073a5 100644 --- a/build_tools/requirements.txt +++ b/build_tools/requirements.txt @@ -32,4 +32,6 @@ html5lib importlib-resources bumps html2text -jsonschema \ No newline at end of file +jsonschema +pyqtgraph +pyopengl \ No newline at end of file From 6fe330bef992243079d8377884ab13e2674b9978 Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Tue, 25 Oct 2022 14:51:38 +0100 Subject: [PATCH 09/87] Basic layout --- src/sas/qtgui/Utilities/OrientationViewer.py | 23 ++++++++++++------- .../UI/OrientationViewerControllerUI.ui | 6 +++++ 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/sas/qtgui/Utilities/OrientationViewer.py b/src/sas/qtgui/Utilities/OrientationViewer.py index c549e6fc49..23236e66c3 100644 --- a/src/sas/qtgui/Utilities/OrientationViewer.py +++ b/src/sas/qtgui/Utilities/OrientationViewer.py @@ -1,27 +1,32 @@ from PyQt5 import QtWidgets +from PyQt5.QtWidgets import QSizePolicy import pyqtgraph.opengl as gl +from sas.qtgui.Utilities.OrientationViewerController import OrientationViewierController + class OrientationViewer(QtWidgets.QWidget): def __init__(self, parent=None): super().__init__() - self.resize(600, 700) - self.graph = gl.GLViewWidget() + self.graph.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding) + + self.controller = OrientationViewierController() layout = QtWidgets.QVBoxLayout() - # Add widgets to the layout layout.addWidget(self.graph) - layout.addWidget(QtWidgets.QPushButton("Top")) - layout.addWidget(QtWidgets.QPushButton("Center")) - layout.addWidget(QtWidgets.QPushButton("Bottom")) - # Set the layout on the application's window + layout.addWidget(self.controller) self.setLayout(layout) - ## create three grids, add each to the view + + + + + + xgrid = gl.GLGridItem() ygrid = gl.GLGridItem() zgrid = gl.GLGridItem() @@ -50,6 +55,8 @@ def main(): mainWindow.setCentralWidget(viewer) mainWindow.show() + + mainWindow.resize(700, 700) app.exec_() diff --git a/src/sas/qtgui/Utilities/UI/OrientationViewerControllerUI.ui b/src/sas/qtgui/Utilities/UI/OrientationViewerControllerUI.ui index 70d4d120c8..8d9033264d 100644 --- a/src/sas/qtgui/Utilities/UI/OrientationViewerControllerUI.ui +++ b/src/sas/qtgui/Utilities/UI/OrientationViewerControllerUI.ui @@ -10,6 +10,12 @@ 57 + + + 0 + 0 + + Form From 7b369547c187d4cbf665af58d95744ec556019f6 Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Tue, 25 Oct 2022 20:27:22 +0100 Subject: [PATCH 10/87] Nicer cuboid --- .../Invariant/InvariantPerspective.py | 2 +- src/sas/qtgui/Plotting/Plotter.py | 2 +- src/sas/qtgui/Plotting/Plotter2D.py | 2 +- .../qtgui/Plotting/Slicers/AnnulusSlicer.py | 2 +- src/sas/qtgui/Plotting/Slicers/BoxSlicer.py | 2 +- .../qtgui/Plotting/Slicers/SectorSlicer.py | 2 +- src/sas/qtgui/Utilities/OrientationViewer.py | 81 ++++++++++++++++++- .../Utilities/OrientationViewerController.py | 26 ++++++ .../UI/OrientationViewerControllerUI.ui | 33 +++++++- test/sasinvariant/utest_data_handling.py | 6 +- 10 files changed, 142 insertions(+), 16 deletions(-) diff --git a/src/sas/qtgui/Perspectives/Invariant/InvariantPerspective.py b/src/sas/qtgui/Perspectives/Invariant/InvariantPerspective.py index d9bb712a1a..c07420e329 100644 --- a/src/sas/qtgui/Perspectives/Invariant/InvariantPerspective.py +++ b/src/sas/qtgui/Perspectives/Invariant/InvariantPerspective.py @@ -330,7 +330,7 @@ def calculateThread(self, extrapolation): # Pythonic name, typing # Update calculator with background, scale, and data values self._calculator.background = self._background - self._calculator.scale = self._scale + self._calculator.cuboid_scaling = self._scale self._calculator.set_data(temp_data) # Low Q extrapolation calculations diff --git a/src/sas/qtgui/Plotting/Plotter.py b/src/sas/qtgui/Plotting/Plotter.py index 357707501c..5db10aee74 100644 --- a/src/sas/qtgui/Plotting/Plotter.py +++ b/src/sas/qtgui/Plotting/Plotter.py @@ -108,7 +108,7 @@ def data(self, value): else: self.yLabel = "%s"%(value._yaxis) - if value.scale == 'linear' or value.isSesans: + if value.cuboid_scaling == 'linear' or value.isSesans: self.xscale = 'linear' self.yscale = 'linear' self.title(title=value.name) diff --git a/src/sas/qtgui/Plotting/Plotter2D.py b/src/sas/qtgui/Plotting/Plotter2D.py index 2b5bf79eb8..e585e53eeb 100644 --- a/src/sas/qtgui/Plotting/Plotter2D.py +++ b/src/sas/qtgui/Plotting/Plotter2D.py @@ -289,7 +289,7 @@ def circularAverage(self): # Define axes if not done yet. new_plot.xaxis("\\rm{Q}", "A^{-1}") if hasattr(self.data0, "scale") and \ - self.data0.scale == 'linear': + self.data0.cuboid_scaling == 'linear': new_plot.ytransform = 'y' new_plot.yaxis("\\rm{Residuals} ", "normalized") else: diff --git a/src/sas/qtgui/Plotting/Slicers/AnnulusSlicer.py b/src/sas/qtgui/Plotting/Slicers/AnnulusSlicer.py index 8f78d41bd5..288e69bdea 100644 --- a/src/sas/qtgui/Plotting/Slicers/AnnulusSlicer.py +++ b/src/sas/qtgui/Plotting/Slicers/AnnulusSlicer.py @@ -133,7 +133,7 @@ def _post_data(self, nbins=None): # If the data file does not tell us what the axes are, just assume... new_plot.xaxis("\\rm{\phi}", 'degrees') new_plot.yaxis("\\rm{Intensity} ", "cm^{-1}") - if hasattr(data, "scale") and data.scale == 'linear' and \ + if hasattr(data, "scale") and data.cuboid_scaling == 'linear' and \ self.data.name.count("Residuals") > 0: new_plot.ytransform = 'y' new_plot.yaxis("\\rm{Residuals} ", "/") diff --git a/src/sas/qtgui/Plotting/Slicers/BoxSlicer.py b/src/sas/qtgui/Plotting/Slicers/BoxSlicer.py index 1494b397d1..569705fc27 100644 --- a/src/sas/qtgui/Plotting/Slicers/BoxSlicer.py +++ b/src/sas/qtgui/Plotting/Slicers/BoxSlicer.py @@ -179,7 +179,7 @@ def post_data(self, new_slab=None, nbins=None, direction=None): new_plot.yaxis("\\rm{Intensity} ", "cm^{-1}") data = self.data - if hasattr(data, "scale") and data.scale == 'linear' and \ + if hasattr(data, "scale") and data.cuboid_scaling == 'linear' and \ self.data.name.count("Residuals") > 0: new_plot.ytransform = 'y' new_plot.yaxis("\\rm{Residuals} ", "/") diff --git a/src/sas/qtgui/Plotting/Slicers/SectorSlicer.py b/src/sas/qtgui/Plotting/Slicers/SectorSlicer.py index 00294a87f0..d48996f9d1 100644 --- a/src/sas/qtgui/Plotting/Slicers/SectorSlicer.py +++ b/src/sas/qtgui/Plotting/Slicers/SectorSlicer.py @@ -160,7 +160,7 @@ def _post_data(self, nbins=None): # If the data file does not tell us what the axes are, just assume them. new_plot.xaxis("\\rm{Q}", "A^{-1}") new_plot.yaxis("\\rm{Intensity}", "cm^{-1}") - if hasattr(data, "scale") and data.scale == 'linear' and \ + if hasattr(data, "scale") and data.cuboid_scaling == 'linear' and \ self.data.name.count("Residuals") > 0: new_plot.ytransform = 'y' new_plot.yaxis("\\rm{Residuals} ", "/") diff --git a/src/sas/qtgui/Utilities/OrientationViewer.py b/src/sas/qtgui/Utilities/OrientationViewer.py index 23236e66c3..0d9939a976 100644 --- a/src/sas/qtgui/Utilities/OrientationViewer.py +++ b/src/sas/qtgui/Utilities/OrientationViewer.py @@ -1,13 +1,17 @@ - +import numpy as np from PyQt5 import QtWidgets from PyQt5.QtWidgets import QSizePolicy import pyqtgraph.opengl as gl +from pyqtgraph.Transform3D import Transform3D -from sas.qtgui.Utilities.OrientationViewerController import OrientationViewierController +from sas.qtgui.Utilities.OrientationViewerController import OrientationViewierController, Orientation +from sasmodels.sasmodels.jitter import Rx, Ry, Rz class OrientationViewer(QtWidgets.QWidget): + cuboid_scaling = [0.1, 0.4, 1.0] + def __init__(self, parent=None): super().__init__() @@ -22,14 +26,16 @@ def __init__(self, parent=None): self.setLayout(layout) - - + self.cube = self.create_cube() xgrid = gl.GLGridItem() ygrid = gl.GLGridItem() zgrid = gl.GLGridItem() + + self.graph.addItem(self.cube) + self.graph.addItem(xgrid) self.graph.addItem(ygrid) self.graph.addItem(zgrid) @@ -43,6 +49,73 @@ def __init__(self, parent=None): ygrid.scale(0.2, 0.1, 0.1) zgrid.scale(0.1, 0.2, 0.1) + self.cube.setTransform(self.createTransform(0, 0, 0)) + + self.controller.valueEdited.connect(self.on_angle_change) + + @staticmethod + def hypercube(n): + if n <= 1: + return [[0], [1]] + else: + hypersquare = OrientationViewer.hypercube(n-1) + return [[0] + vert for vert in hypersquare] + [[1] + vert for vert in hypersquare] + + + @staticmethod + def create_cube(): + # Sorry + vertices = OrientationViewer.hypercube(3) + + faces_and_colors = [] + + for fixed_dim in range(3): + for zero_one in range(2): + this_face = [(ind, v) for ind, v in enumerate(vertices) if v[fixed_dim] == zero_one] + + def sort_key(x): + _, v = x + other_dims = v[:fixed_dim] + v[fixed_dim+1:] + return (v[fixed_dim] - 0.5)*np.arctan2(other_dims[0]-0.5, other_dims[1]-0.5) + + this_face = sorted(this_face, key=sort_key) + + color = [0.6,0.6,0.6,1] + color[fixed_dim]=0.4 + + # faces_and_colors.append([[this_face[x][0] for x in range(4)], color]) + + faces_and_colors.append(([this_face[x][0] for x in (0,1,2)], color)) + faces_and_colors.append(([this_face[x][0] for x in (2,3,0)], color)) + + vertices = np.array(vertices, dtype=float) - 0.5 + faces = np.array([face for face, _ in faces_and_colors]) + colors = np.array([color for _, color in faces_and_colors]) + + return gl.GLMeshItem(vertexes=vertices, faces=faces, faceColors=colors, + drawEdges=False, edgeColor=(0, 0, 0, 1), smooth=False) + + + @staticmethod + def createTransform(theta_deg: float, phi_deg: float, psi_deg: float) -> Transform3D: + + # Get rotation matrix + r_mat = Rz(phi_deg)@Ry(theta_deg)@Rz(psi_deg)@np.diag(OrientationViewer.cuboid_scaling) + + # Get the 4x4 transformation matrix, by (1) padding by zeros (2) setting the corner element to 1 + trans_mat = np.pad(r_mat, ((0, 1), (0, 1))) + trans_mat[-1, -1] = 1 + + return Transform3D(trans_mat) + + + def on_angle_change(self, orientation: Orientation): + self.cube.setTransform( + self.createTransform( + orientation.theta, + orientation.phi, + orientation.psi)) + def main(): diff --git a/src/sas/qtgui/Utilities/OrientationViewerController.py b/src/sas/qtgui/Utilities/OrientationViewerController.py index 7bf87f85b9..b58452aabd 100644 --- a/src/sas/qtgui/Utilities/OrientationViewerController.py +++ b/src/sas/qtgui/Utilities/OrientationViewerController.py @@ -1,14 +1,40 @@ +from typing import NamedTuple + from PyQt5 import QtWidgets +from PyQt5.QtCore import pyqtSignal + + from sas.qtgui.Utilities.UI.OrientationViewerControllerUI import Ui_OrientationViewierControllerUI +class Orientation(NamedTuple): + """ Data sent when updating the plot""" + theta: int + phi: int + psi: int class OrientationViewierController(QtWidgets.QDialog, Ui_OrientationViewierControllerUI): + + """ Widget that controls the orientation viewer""" + + valueEdited = pyqtSignal(Orientation, name='valueEdited') + def __init__(self, parent=None): super().__init__() self.setupUi(self) + # All sliders emit the same signal - the angular coordinates in degrees + self.thetaSlider.valueChanged.connect(self.onAngleChange) + self.phiSlider.valueChanged.connect(self.onAngleChange) + self.psiSlider.valueChanged.connect(self.onAngleChange) + + def onAngleChange(self): + theta = self.thetaSlider.value() + phi = self.phiSlider.value() + psi = self.psiSlider.value() + + self.valueEdited.emit(Orientation(theta, phi, psi)) diff --git a/src/sas/qtgui/Utilities/UI/OrientationViewerControllerUI.ui b/src/sas/qtgui/Utilities/UI/OrientationViewerControllerUI.ui index 8d9033264d..7917661171 100644 --- a/src/sas/qtgui/Utilities/UI/OrientationViewerControllerUI.ui +++ b/src/sas/qtgui/Utilities/UI/OrientationViewerControllerUI.ui @@ -29,8 +29,11 @@ + + -90 + - 360 + 90 15 @@ -38,6 +41,12 @@ Qt::Horizontal + + QSlider::TicksBelow + + + 15 + @@ -56,8 +65,11 @@ + + -90 + - 360 + 90 15 @@ -65,12 +77,21 @@ Qt::Horizontal + + QSlider::TicksBelow + + + 15 + + + -90 + - 360 + 90 15 @@ -78,6 +99,12 @@ Qt::Horizontal + + QSlider::TicksBelow + + + 15 + diff --git a/test/sasinvariant/utest_data_handling.py b/test/sasinvariant/utest_data_handling.py index ecade8a754..972afb0dee 100644 --- a/test/sasinvariant/utest_data_handling.py +++ b/test/sasinvariant/utest_data_handling.py @@ -425,7 +425,7 @@ def test_low_q(self): qmax=qmax, power=inv._low_extrapolation_power) self.assertAlmostEqual(self.scale, - inv._low_extrapolation_function.scale, 6) + inv._low_extrapolation_function.cuboid_scaling, 6) self.assertAlmostEqual(self.rg, inv._low_extrapolation_function.radius, 6) @@ -477,7 +477,7 @@ def test_low_q(self): power=inv._low_extrapolation_power) self.assertAlmostEqual(self.scale, - inv._low_extrapolation_function.scale, 6) + inv._low_extrapolation_function.cuboid_scaling, 6) self.assertAlmostEqual(self.m, inv._low_extrapolation_function.power, 6) @@ -568,7 +568,7 @@ def test_low_q(self): qmax=qmax, power=inv._low_extrapolation_power) self.assertAlmostEqual(self.scale, - inv._low_extrapolation_function.scale, 6) + inv._low_extrapolation_function.cuboid_scaling, 6) self.assertAlmostEqual(self.rg, inv._low_extrapolation_function.radius, 6) From 00f09419e48ca88c57c158b54d37eb4dbb282c96 Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Wed, 26 Oct 2022 10:22:51 +0100 Subject: [PATCH 11/87] 3D elements, more gui --- src/sas/qtgui/MainWindow/MainWindow.py | 7 +- src/sas/qtgui/Utilities/OrientationViewer.py | 80 +++- .../UI/OrientationViewerControllerUI.ui | 429 +++++++++++++++++- 3 files changed, 485 insertions(+), 31 deletions(-) diff --git a/src/sas/qtgui/MainWindow/MainWindow.py b/src/sas/qtgui/MainWindow/MainWindow.py index d928fac0c1..80ac2b187e 100644 --- a/src/sas/qtgui/MainWindow/MainWindow.py +++ b/src/sas/qtgui/MainWindow/MainWindow.py @@ -94,10 +94,13 @@ def run_sasview(): signal.signal(signal.SIGINT, signal.SIG_DFL) # Main must have reference to the splash screen, so making it explicit - splash = SplashScreen() - splash.show() + app.setAttribute(Qt.AA_EnableHighDpiScaling) app.setStyleSheet("* {font-size: 11pt;}") + + splash = SplashScreen() + splash.show() + # Main application style. #app.setStyle('Fusion') diff --git a/src/sas/qtgui/Utilities/OrientationViewer.py b/src/sas/qtgui/Utilities/OrientationViewer.py index 0d9939a976..5926491a03 100644 --- a/src/sas/qtgui/Utilities/OrientationViewer.py +++ b/src/sas/qtgui/Utilities/OrientationViewer.py @@ -1,6 +1,9 @@ import numpy as np + from PyQt5 import QtWidgets from PyQt5.QtWidgets import QSizePolicy +from PyQt5.QtCore import Qt + import pyqtgraph.opengl as gl from pyqtgraph.Transform3D import Transform3D @@ -16,17 +19,26 @@ def __init__(self, parent=None): super().__init__() self.graph = gl.GLViewWidget() - self.graph.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding) + self.graph.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.controller = OrientationViewierController() - layout = QtWidgets.QVBoxLayout() + # + # controller_container = QtWidgets.QHBoxLayout() + # controller_container.addSpacerItem(QtWidgets.QSpacerItem(10000,10000)) + # controller_container.addWidget(self.controller) + # controller_container.addSpacerItem(QtWidgets.QSpacerItem(10000,10000)) + + + + layout = QtWidgets.QVBoxLayout() layout.addWidget(self.graph) layout.addWidget(self.controller) self.setLayout(layout) self.cube = self.create_cube() + self.arrow = self.create_arrow() @@ -35,6 +47,7 @@ def __init__(self, parent=None): zgrid = gl.GLGridItem() self.graph.addItem(self.cube) + self.graph.addItem(self.arrow) self.graph.addItem(xgrid) self.graph.addItem(ygrid) @@ -49,14 +62,19 @@ def __init__(self, parent=None): ygrid.scale(0.2, 0.1, 0.1) zgrid.scale(0.1, 0.2, 0.1) + + self.arrow.rotate(180, 1, 0, 0) + self.arrow.scale(0.05, 0.05, 0.2) + self.cube.setTransform(self.createTransform(0, 0, 0)) self.controller.valueEdited.connect(self.on_angle_change) @staticmethod def hypercube(n): - if n <= 1: - return [[0], [1]] + """ Coordinates of a hypercube in with 'binary' ordering""" + if n <= 0: + return [[]] else: hypersquare = OrientationViewer.hypercube(n-1) return [[0] + vert for vert in hypersquare] + [[1] + vert for vert in hypersquare] @@ -64,6 +82,7 @@ def hypercube(n): @staticmethod def create_cube(): + """ Mesh for the main cuboid""" # Sorry vertices = OrientationViewer.hypercube(3) @@ -95,6 +114,55 @@ def sort_key(x): return gl.GLMeshItem(vertexes=vertices, faces=faces, faceColors=colors, drawEdges=False, edgeColor=(0, 0, 0, 1), smooth=False) + @staticmethod + def create_arrow(n: int = 30, tail_length=10, tail_width=0.6): + """ Mesh for an arrow """ + # Thanks, I hate it. + + # top and tail + points = [[0, 0, 1], [0, 0, -tail_length]] + faces = [] + colors = [] + + # widest ring + for angle in 2 * np.pi * np.arange(0, n) / n: + points.append([np.sin(angle), np.cos(angle), 0]) + + # middle ring + for angle in 2 * np.pi * np.arange(0, n) / n: + points.append([tail_width*np.sin(angle), tail_width*np.cos(angle), 0]) + + # bottom ring + for angle in 2 * np.pi * np.arange(0, n) / n: + points.append([tail_width*np.sin(angle), tail_width*np.cos(angle), -tail_length]) + + for i in range(n): + # laterial indices + j = (i+1) % n + + # Pointy bit + faces.append([i + 2, j+2 + 2, 0]) + + # # Top ring + faces.append([i + n + 2, j + n + 2, j + 2]) + faces.append([i + n + 2, i + 2, j + 2]) + + # Cylinder sides + faces.append([i + 2 * n + 2, j + 2 * n + 2, j + n + 2]) + faces.append([i + 2 * n + 2, i + n + 2, j + n + 2]) + + # Cylinder base + faces.append([i+2*n + 2, j + 2 * n + 2, 1]) + + for i in range(6*n): + colors.append([0.6] * 3 + [1]) + + points = np.array(points) - np.array([0,0,-0.5*(tail_length-1)]) + faces = np.array(faces) + colors = np.array(colors) + + return gl.GLMeshItem(vertexes=points, faces=faces, faceColors=colors, + drawEdges=False, edgeColor=(0, 0, 0, 1), smooth=True) @staticmethod def createTransform(theta_deg: float, phi_deg: float, psi_deg: float) -> Transform3D: @@ -122,6 +190,8 @@ def main(): """ Show a demo of the slider """ app = QtWidgets.QApplication([]) + app.setAttribute(Qt.AA_EnableHighDpiScaling) + mainWindow = QtWidgets.QMainWindow() viewer = OrientationViewer(mainWindow) @@ -129,7 +199,7 @@ def main(): mainWindow.show() - mainWindow.resize(700, 700) + mainWindow.resize(1000, 1000) app.exec_() diff --git a/src/sas/qtgui/Utilities/UI/OrientationViewerControllerUI.ui b/src/sas/qtgui/Utilities/UI/OrientationViewerControllerUI.ui index 7917661171..557c7aa200 100644 --- a/src/sas/qtgui/Utilities/UI/OrientationViewerControllerUI.ui +++ b/src/sas/qtgui/Utilities/UI/OrientationViewerControllerUI.ui @@ -6,12 +6,12 @@ 0 0 - 483 - 57 + 613 + 116 - + 0 0 @@ -19,21 +19,266 @@ Form - - - + + + + + + 0 + 0 + + + + + 50 + 0 + + + + + 100 + 16777215 + + + + 60 + + + Qt::Horizontal + + + + + + + + 0 + 0 + + + + + 50 + 0 + + + + + 100 + 16777215 + + + + 60 + + + Qt::Horizontal + + + + + + + + 10 + + - θ + TextLabel + + + Qt::AlignCenter - - + + + + + 10 + + + + TextLabel + + + Qt::AlignCenter + + + + + + + + 10 + + + + TextLabel + + + Qt::AlignCenter + + + + + + + + 10 + + + + TextLabel + + + Qt::AlignCenter + + + + + + + + 10 + + + + TextLabel + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + + 50 + 0 + + + + + 100 + 16777215 + + + + 60 + + + Qt::Horizontal + + + + + + + + 10 + + + + TextLabel + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + + 30 + 0 + + + + + 30 + 16777215 + + + + + 11 + + + + Δθ + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + + 30 + 0 + + + + + 30 + 16777215 + + + + + 11 + + + + Δφ + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 400 + 0 + + + + + 400 + 16777215 + + - -90 + -180 - 90 + 180 15 @@ -49,22 +294,50 @@ - - - - φ - - - - + + + + 0 + 0 + + + + + 20 + 0 + + + + + 20 + 16777215 + + + + + 11 + + ψ - - + + + + + 400 + 0 + + + + + 400 + 16777215 + + -90 @@ -85,13 +358,58 @@ - + + + + + 0 + 0 + + + + + 20 + 0 + + + + + 20 + 16777215 + + + + + 11 + + + + φ + + + + + + + 400 + 0 + + + + + 400 + 16777215 + + - -90 + -180 - 90 + 180 + + + 1 15 @@ -107,6 +425,69 @@ + + + + + 0 + 0 + + + + + 30 + 0 + + + + + 30 + 16777215 + + + + + 11 + + + + Δψ + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + + 20 + 0 + + + + + 20 + 16777215 + + + + + 11 + + + + θ + + + From 17f65db0decebf873ce9854bbc1b428f527b8b5e Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Wed, 26 Oct 2022 19:44:44 +0100 Subject: [PATCH 12/87] Work on ghosts --- src/sas/qtgui/Utilities/OrientationViewer.py | 137 +++++++++------- .../Utilities/OrientationViewerController.py | 12 +- .../UI/OrientationViewerControllerUI.ui | 152 ++++++++++-------- 3 files changed, 183 insertions(+), 118 deletions(-) diff --git a/src/sas/qtgui/Utilities/OrientationViewer.py b/src/sas/qtgui/Utilities/OrientationViewer.py index 5926491a03..82cecf80e6 100644 --- a/src/sas/qtgui/Utilities/OrientationViewer.py +++ b/src/sas/qtgui/Utilities/OrientationViewer.py @@ -4,8 +4,11 @@ from PyQt5.QtWidgets import QSizePolicy from PyQt5.QtCore import Qt +import matplotlib as mpl + import pyqtgraph.opengl as gl from pyqtgraph.Transform3D import Transform3D +from OpenGL.GL import * from sas.qtgui.Utilities.OrientationViewerController import OrientationViewierController, Orientation @@ -14,61 +17,81 @@ class OrientationViewer(QtWidgets.QWidget): cuboid_scaling = [0.1, 0.4, 1.0] + n_ghosts_per_perameter = 10 + @staticmethod + def colormap_scale(data): + x = data.copy() + x -= np.min(x) + x /= np.max(x) + return x def __init__(self, parent=None): super().__init__() self.graph = gl.GLViewWidget() - self.graph.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + glBlendFunc(GL_ONE_MINUS_SRC_ALPHA, GL_SRC_ALPHA) + glEnable(GL_BLEND) + glEnable(GL_COLOR_MATERIAL) - self.controller = OrientationViewierController() # - # controller_container = QtWidgets.QHBoxLayout() - # controller_container.addSpacerItem(QtWidgets.QSpacerItem(10000,10000)) - # controller_container.addWidget(self.controller) - # controller_container.addSpacerItem(QtWidgets.QSpacerItem(10000,10000)) - - - - layout = QtWidgets.QVBoxLayout() - layout.addWidget(self.graph) - layout.addWidget(self.controller) - self.setLayout(layout) - - - self.cube = self.create_cube() - self.arrow = self.create_arrow() - - - - xgrid = gl.GLGridItem() - ygrid = gl.GLGridItem() - zgrid = gl.GLGridItem() - - self.graph.addItem(self.cube) - self.graph.addItem(self.arrow) - - self.graph.addItem(xgrid) - self.graph.addItem(ygrid) - self.graph.addItem(zgrid) - - ## rotate x and y grids to face the correct direction - xgrid.rotate(90, 0, 1, 0) - ygrid.rotate(90, 1, 0, 0) - - ## scale each grid differently - xgrid.scale(0.2, 0.1, 0.1) - ygrid.scale(0.2, 0.1, 0.1) - zgrid.scale(0.1, 0.2, 0.1) - - - self.arrow.rotate(180, 1, 0, 0) - self.arrow.scale(0.05, 0.05, 0.2) - - self.cube.setTransform(self.createTransform(0, 0, 0)) + # self.graph.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + # + # self.controller = OrientationViewierController() + # + # layout = QtWidgets.QVBoxLayout() + # layout.addWidget(self.graph) + # layout.addWidget(self.controller) + # self.setLayout(layout) + # + # self.arrow = self.create_arrow() + # self.image_plane_coordinate_points = np.linspace(-2, 2, 256) + # + # # temporary plot data + # X, Y = np.meshgrid(self.image_plane_coordinate_points, self.image_plane_coordinate_points) + # + # R2 = (X**2 + Y**2) + # + # self.image_plane_data = 0.5*(1+np.cos(5*np.sqrt(R2))) / (5*R2+1) + # + # self.colormap = mpl.colormaps["viridis"] + # # for i in range(101): + # # print(self.colormap(i)) + # + # self.image_plane_colors = self.colormap(OrientationViewer.colormap_scale(self.image_plane_data)) + # + # self.image_plane = gl.GLSurfacePlotItem( + # self.image_plane_coordinate_points, + # self.image_plane_coordinate_points, + # self.image_plane_data, + # self.image_plane_colors + # ) + # + # + # ghost_alpha = 1/(OrientationViewer.n_ghosts_per_perameter**3) #0.9**(1/(OrientationViewer.n_ghosts_per_perameter**3)) + # self.ghosts = [] + # for a in np.linspace(-1, 1, OrientationViewer.n_ghosts_per_perameter): + # for b in np.linspace(-1, 1, OrientationViewer.n_ghosts_per_perameter): + # for c in np.linspace(-1, 1, OrientationViewer.n_ghosts_per_perameter): + # ghost = OrientationViewer.create_cube(ghost_alpha) + # self.graph.addItem(ghost) + # self.ghosts.append((a, b, c, ghost)) + # + # + # + # # self.graph.addItem(self.arrow) + # # self.graph.addItem(self.image_plane) + # + # self.arrow.rotate(180, 1, 0, 0) + # self.arrow.scale(0.05, 0.05, 0.2) + # + # for _, _, _, ghost in self.ghosts: + # ghost.setTransform(self.createTransform(0, 0, 0)) + # + # self.image_plane.translate(0,0,-2) + # + # self.controller.valueEdited.connect(self.on_angle_change) - self.controller.valueEdited.connect(self.on_angle_change) @staticmethod def hypercube(n): @@ -81,7 +104,7 @@ def hypercube(n): @staticmethod - def create_cube(): + def create_cube(alpha=1.0): """ Mesh for the main cuboid""" # Sorry vertices = OrientationViewer.hypercube(3) @@ -99,7 +122,7 @@ def sort_key(x): this_face = sorted(this_face, key=sort_key) - color = [0.6,0.6,0.6,1] + color = [255,255,255,255] #[0.6,0.6,0.6,alpha] color[fixed_dim]=0.4 # faces_and_colors.append([[this_face[x][0] for x in range(4)], color]) @@ -178,16 +201,22 @@ def createTransform(theta_deg: float, phi_deg: float, psi_deg: float) -> Transfo def on_angle_change(self, orientation: Orientation): - self.cube.setTransform( - self.createTransform( - orientation.theta, - orientation.phi, - orientation.psi)) + + for a, b, c, ghost in self.ghosts: + + ghost.setTransform( + self.createTransform( + orientation.theta + 0.5*a*orientation.dtheta, + orientation.phi + 0.5*b*orientation.dphi, + orientation.psi + 0.5*c*orientation.dpsi)) def main(): """ Show a demo of the slider """ + import os + + os.environ["QT_ENABLE_HIGHDPI_SCALING"] = "1" app = QtWidgets.QApplication([]) app.setAttribute(Qt.AA_EnableHighDpiScaling) @@ -199,7 +228,7 @@ def main(): mainWindow.show() - mainWindow.resize(1000, 1000) + mainWindow.resize(600, 600) app.exec_() diff --git a/src/sas/qtgui/Utilities/OrientationViewerController.py b/src/sas/qtgui/Utilities/OrientationViewerController.py index b58452aabd..286433687c 100644 --- a/src/sas/qtgui/Utilities/OrientationViewerController.py +++ b/src/sas/qtgui/Utilities/OrientationViewerController.py @@ -12,6 +12,9 @@ class Orientation(NamedTuple): theta: int phi: int psi: int + dtheta: int + dphi: int + dpsi: int class OrientationViewierController(QtWidgets.QDialog, Ui_OrientationViewierControllerUI): @@ -28,13 +31,20 @@ def __init__(self, parent=None): self.thetaSlider.valueChanged.connect(self.onAngleChange) self.phiSlider.valueChanged.connect(self.onAngleChange) self.psiSlider.valueChanged.connect(self.onAngleChange) + self.deltaTheta.valueChanged.connect(self.onAngleChange) + self.deltaPhi.valueChanged.connect(self.onAngleChange) + self.deltaPsi.valueChanged.connect(self.onAngleChange) def onAngleChange(self): theta = self.thetaSlider.value() phi = self.phiSlider.value() psi = self.psiSlider.value() - self.valueEdited.emit(Orientation(theta, phi, psi)) + dtheta = self.deltaTheta.value() + dphi = self.deltaPhi.value() + dpsi = self.deltaPsi.value() + + self.valueEdited.emit(Orientation(theta, phi, psi, dtheta, dphi, dpsi)) diff --git a/src/sas/qtgui/Utilities/UI/OrientationViewerControllerUI.ui b/src/sas/qtgui/Utilities/UI/OrientationViewerControllerUI.ui index 557c7aa200..e9a61b24e3 100644 --- a/src/sas/qtgui/Utilities/UI/OrientationViewerControllerUI.ui +++ b/src/sas/qtgui/Utilities/UI/OrientationViewerControllerUI.ui @@ -6,8 +6,8 @@ 0 0 - 613 - 116 + 664 + 250 @@ -20,8 +20,8 @@ Form - - + + 0 @@ -48,8 +48,8 @@ - - + + 0 @@ -76,8 +76,8 @@ - - + + 10 @@ -91,8 +91,8 @@ - - + + 10 @@ -106,8 +106,8 @@ - - + + 10 @@ -121,8 +121,8 @@ - - + + 10 @@ -136,7 +136,7 @@ - + @@ -151,7 +151,7 @@ - + @@ -179,7 +179,7 @@ - + @@ -194,7 +194,7 @@ - + @@ -227,7 +227,7 @@ - + @@ -260,8 +260,8 @@ - - + + 400 @@ -280,6 +280,9 @@ 180 + + 1 + 15 @@ -294,7 +297,7 @@ - + @@ -324,41 +327,7 @@ - - - - - 400 - 0 - - - - - 400 - 16777215 - - - - -90 - - - 90 - - - 15 - - - Qt::Horizontal - - - QSlider::TicksBelow - - - 15 - - - - + @@ -388,8 +357,8 @@ - - + + 400 @@ -408,8 +377,39 @@ 180 - - 1 + + 15 + + + Qt::Horizontal + + + QSlider::TicksBelow + + + 15 + + + + + + + + 400 + 0 + + + + + 400 + 16777215 + + + + -90 + + + 90 15 @@ -425,7 +425,7 @@ - + @@ -458,7 +458,7 @@ - + @@ -488,6 +488,32 @@ + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + From a4941c7c3a9f57f0c2db502ac5760ac783a5800c Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Fri, 28 Oct 2022 14:45:42 +0100 Subject: [PATCH 13/87] ughh --- src/sas/qtgui/Utilities/OrientationViewer.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/sas/qtgui/Utilities/OrientationViewer.py b/src/sas/qtgui/Utilities/OrientationViewer.py index 82cecf80e6..876409bd51 100644 --- a/src/sas/qtgui/Utilities/OrientationViewer.py +++ b/src/sas/qtgui/Utilities/OrientationViewer.py @@ -28,11 +28,16 @@ def colormap_scale(data): def __init__(self, parent=None): super().__init__() + self.graph = gl.GLViewWidget() + + glBegin(GL_VERTEX_ARRAY) + + glEnd() + glBlendFunc(GL_ONE_MINUS_SRC_ALPHA, GL_SRC_ALPHA) - glEnable(GL_BLEND) glEnable(GL_COLOR_MATERIAL) - + glEnable(GL_BLEND) # # self.graph.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) @@ -220,6 +225,8 @@ def main(): app = QtWidgets.QApplication([]) app.setAttribute(Qt.AA_EnableHighDpiScaling) + app.setAttribute(Qt.AA_ShareOpenGLContexts) + mainWindow = QtWidgets.QMainWindow() viewer = OrientationViewer(mainWindow) From c3deb4b93249fc174c793260043def20b522fc41 Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Sat, 29 Oct 2022 11:31:26 +0100 Subject: [PATCH 14/87] Moved some of the old viewer over --- src/sas/qtgui/Utilities/OrientationViewer.py | 223 ++++++------------ .../Utilities/OrientationViewerGraphics.py | 118 +++++++++ 2 files changed, 190 insertions(+), 151 deletions(-) create mode 100644 src/sas/qtgui/Utilities/OrientationViewerGraphics.py diff --git a/src/sas/qtgui/Utilities/OrientationViewer.py b/src/sas/qtgui/Utilities/OrientationViewer.py index 876409bd51..fca89b84d9 100644 --- a/src/sas/qtgui/Utilities/OrientationViewer.py +++ b/src/sas/qtgui/Utilities/OrientationViewer.py @@ -10,14 +10,26 @@ from pyqtgraph.Transform3D import Transform3D from OpenGL.GL import * +from sasmodels.core import load_model_info, build_model +from sasmodels.data import empty_data2D +from sasmodels.direct_model import DirectModel +from sasmodels.jitter import Rx, Ry, Rz + from sas.qtgui.Utilities.OrientationViewerController import OrientationViewierController, Orientation +from sas.qtgui.Utilities.OrientationViewerGraphics import OrientationViewerGraphics + + + + -from sasmodels.sasmodels.jitter import Rx, Ry, Rz class OrientationViewer(QtWidgets.QWidget): cuboid_scaling = [0.1, 0.4, 1.0] n_ghosts_per_perameter = 10 + log_I_max = 3 + log_I_min = -3 + @staticmethod def colormap_scale(data): x = data.copy() @@ -30,179 +42,74 @@ def __init__(self, parent=None): self.graph = gl.GLViewWidget() - - glBegin(GL_VERTEX_ARRAY) - - glEnd() - - glBlendFunc(GL_ONE_MINUS_SRC_ALPHA, GL_SRC_ALPHA) - glEnable(GL_COLOR_MATERIAL) - glEnable(GL_BLEND) - - # - # self.graph.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) - # - # self.controller = OrientationViewierController() - # - # layout = QtWidgets.QVBoxLayout() - # layout.addWidget(self.graph) - # layout.addWidget(self.controller) - # self.setLayout(layout) - # - # self.arrow = self.create_arrow() - # self.image_plane_coordinate_points = np.linspace(-2, 2, 256) - # - # # temporary plot data - # X, Y = np.meshgrid(self.image_plane_coordinate_points, self.image_plane_coordinate_points) - # - # R2 = (X**2 + Y**2) - # - # self.image_plane_data = 0.5*(1+np.cos(5*np.sqrt(R2))) / (5*R2+1) - # - # self.colormap = mpl.colormaps["viridis"] - # # for i in range(101): - # # print(self.colormap(i)) - # - # self.image_plane_colors = self.colormap(OrientationViewer.colormap_scale(self.image_plane_data)) - # - # self.image_plane = gl.GLSurfacePlotItem( - # self.image_plane_coordinate_points, - # self.image_plane_coordinate_points, - # self.image_plane_data, - # self.image_plane_colors - # ) # + # glBegin(GL_VERTEX_ARRAY) # - # ghost_alpha = 1/(OrientationViewer.n_ghosts_per_perameter**3) #0.9**(1/(OrientationViewer.n_ghosts_per_perameter**3)) - # self.ghosts = [] - # for a in np.linspace(-1, 1, OrientationViewer.n_ghosts_per_perameter): - # for b in np.linspace(-1, 1, OrientationViewer.n_ghosts_per_perameter): - # for c in np.linspace(-1, 1, OrientationViewer.n_ghosts_per_perameter): - # ghost = OrientationViewer.create_cube(ghost_alpha) - # self.graph.addItem(ghost) - # self.ghosts.append((a, b, c, ghost)) + # glEnd() # - # - # - # # self.graph.addItem(self.arrow) - # # self.graph.addItem(self.image_plane) - # - # self.arrow.rotate(180, 1, 0, 0) - # self.arrow.scale(0.05, 0.05, 0.2) - # - # for _, _, _, ghost in self.ghosts: - # ghost.setTransform(self.createTransform(0, 0, 0)) - # - # self.image_plane.translate(0,0,-2) - # - # self.controller.valueEdited.connect(self.on_angle_change) + # glBlendFunc(GL_ONE_MINUS_SRC_ALPHA, GL_SRC_ALPHA) + # glEnable(GL_COLOR_MATERIAL) + # glEnable(GL_BLEND) - @staticmethod - def hypercube(n): - """ Coordinates of a hypercube in with 'binary' ordering""" - if n <= 0: - return [[]] - else: - hypersquare = OrientationViewer.hypercube(n-1) - return [[0] + vert for vert in hypersquare] + [[1] + vert for vert in hypersquare] + self.graph.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + self.controller = OrientationViewierController() - @staticmethod - def create_cube(alpha=1.0): - """ Mesh for the main cuboid""" - # Sorry - vertices = OrientationViewer.hypercube(3) + layout = QtWidgets.QVBoxLayout() + layout.addWidget(self.graph) + layout.addWidget(self.controller) + self.setLayout(layout) - faces_and_colors = [] + self.arrow = self.create_arrow() + self.image_plane_coordinate_points = np.linspace(-2, 2, 256) - for fixed_dim in range(3): - for zero_one in range(2): - this_face = [(ind, v) for ind, v in enumerate(vertices) if v[fixed_dim] == zero_one] + # temporary plot data + X, Y = np.meshgrid(self.image_plane_coordinate_points, self.image_plane_coordinate_points) - def sort_key(x): - _, v = x - other_dims = v[:fixed_dim] + v[fixed_dim+1:] - return (v[fixed_dim] - 0.5)*np.arctan2(other_dims[0]-0.5, other_dims[1]-0.5) + R2 = (X**2 + Y**2) - this_face = sorted(this_face, key=sort_key) + self.image_plane_data = 0.5*(1+np.cos(5*np.sqrt(R2))) / (5*R2+1) - color = [255,255,255,255] #[0.6,0.6,0.6,alpha] - color[fixed_dim]=0.4 + self.colormap = mpl.colormaps["viridis"] + # for i in range(101): + # print(self.colormap(i)) - # faces_and_colors.append([[this_face[x][0] for x in range(4)], color]) + self.image_plane_colors = self.colormap(OrientationViewer.colormap_scale(self.image_plane_data)) - faces_and_colors.append(([this_face[x][0] for x in (0,1,2)], color)) - faces_and_colors.append(([this_face[x][0] for x in (2,3,0)], color)) + self.image_plane = gl.GLSurfacePlotItem( + self.image_plane_coordinate_points, + self.image_plane_coordinate_points, + self.image_plane_data, + self.image_plane_colors + ) - vertices = np.array(vertices, dtype=float) - 0.5 - faces = np.array([face for face, _ in faces_and_colors]) - colors = np.array([color for _, color in faces_and_colors]) - return gl.GLMeshItem(vertexes=vertices, faces=faces, faceColors=colors, - drawEdges=False, edgeColor=(0, 0, 0, 1), smooth=False) - - @staticmethod - def create_arrow(n: int = 30, tail_length=10, tail_width=0.6): - """ Mesh for an arrow """ - # Thanks, I hate it. + ghost_alpha = 1/(OrientationViewer.n_ghosts_per_perameter**3) #0.9**(1/(OrientationViewer.n_ghosts_per_perameter**3)) + self.ghosts = [] + for a in np.linspace(-1, 1, OrientationViewer.n_ghosts_per_perameter): + for b in np.linspace(-1, 1, OrientationViewer.n_ghosts_per_perameter): + for c in np.linspace(-1, 1, OrientationViewer.n_ghosts_per_perameter): + ghost = OrientationViewerGraphics.create_cube(ghost_alpha) + self.graph.addItem(ghost) + self.ghosts.append((a, b, c, ghost)) - # top and tail - points = [[0, 0, 1], [0, 0, -tail_length]] - faces = [] - colors = [] - # widest ring - for angle in 2 * np.pi * np.arange(0, n) / n: - points.append([np.sin(angle), np.cos(angle), 0]) - # middle ring - for angle in 2 * np.pi * np.arange(0, n) / n: - points.append([tail_width*np.sin(angle), tail_width*np.cos(angle), 0]) + # self.graph.addItem(self.arrow) + # self.graph.addItem(self.image_plane) - # bottom ring - for angle in 2 * np.pi * np.arange(0, n) / n: - points.append([tail_width*np.sin(angle), tail_width*np.cos(angle), -tail_length]) + self.arrow.rotate(180, 1, 0, 0) + self.arrow.scale(0.05, 0.05, 0.2) - for i in range(n): - # laterial indices - j = (i+1) % n + for _, _, _, ghost in self.ghosts: + ghost.setTransform(OrientationViewerGraphics.createTransform(0, 0, 0, OrientationViewer.cuboid_scaling)) - # Pointy bit - faces.append([i + 2, j+2 + 2, 0]) + self.image_plane.translate(0,0,-2) - # # Top ring - faces.append([i + n + 2, j + n + 2, j + 2]) - faces.append([i + n + 2, i + 2, j + 2]) + self.controller.valueEdited.connect(self.on_angle_change) - # Cylinder sides - faces.append([i + 2 * n + 2, j + 2 * n + 2, j + n + 2]) - faces.append([i + 2 * n + 2, i + n + 2, j + n + 2]) - # Cylinder base - faces.append([i+2*n + 2, j + 2 * n + 2, 1]) - - for i in range(6*n): - colors.append([0.6] * 3 + [1]) - - points = np.array(points) - np.array([0,0,-0.5*(tail_length-1)]) - faces = np.array(faces) - colors = np.array(colors) - - return gl.GLMeshItem(vertexes=points, faces=faces, faceColors=colors, - drawEdges=False, edgeColor=(0, 0, 0, 1), smooth=True) - - @staticmethod - def createTransform(theta_deg: float, phi_deg: float, psi_deg: float) -> Transform3D: - - # Get rotation matrix - r_mat = Rz(phi_deg)@Ry(theta_deg)@Rz(psi_deg)@np.diag(OrientationViewer.cuboid_scaling) - - # Get the 4x4 transformation matrix, by (1) padding by zeros (2) setting the corner element to 1 - trans_mat = np.pad(r_mat, ((0, 1), (0, 1))) - trans_mat[-1, -1] = 1 - - return Transform3D(trans_mat) def on_angle_change(self, orientation: Orientation): @@ -210,11 +117,25 @@ def on_angle_change(self, orientation: Orientation): for a, b, c, ghost in self.ghosts: ghost.setTransform( - self.createTransform( + OrientationViewerGraphics.createTransform( orientation.theta + 0.5*a*orientation.dtheta, orientation.phi + 0.5*b*orientation.dphi, - orientation.psi + 0.5*c*orientation.dpsi)) + orientation.psi + 0.5*c*orientation.dpsi, + OrientationViewer.cuboid_scaling)) + @staticmethod + def create_calculator(n=256, qmax=0.5): + """ + Make a parallelepiped model calculator for q range -qmax to qmax with n samples + """ + + model_info = load_model_info("parallelepiped") + model = build_model(model_info) + q = np.linspace(-qmax, qmax, n) + data = empty_data2D(q, q) + calculator = DirectModel(data, model) + + return calculator def main(): diff --git a/src/sas/qtgui/Utilities/OrientationViewerGraphics.py b/src/sas/qtgui/Utilities/OrientationViewerGraphics.py new file mode 100644 index 0000000000..f80b0c66df --- /dev/null +++ b/src/sas/qtgui/Utilities/OrientationViewerGraphics.py @@ -0,0 +1,118 @@ +from typing import List + +import numpy as np + +import pyqtgraph.opengl as gl +from pyqtgraph.Transform3D import Transform3D +from OpenGL.GL import * + +from sasmodels.jitter import Rx, Ry, Rz + + +class OrientationViewerGraphics: + + @staticmethod + def hypercube(n): + """ Coordinates of a hypercube in with 'binary' ordering""" + if n <= 0: + return [[]] + else: + hypersquare = OrientationViewerGraphics.hypercube(n-1) + return [[0] + vert for vert in hypersquare] + [[1] + vert for vert in hypersquare] + + + @staticmethod + def create_cube(alpha=1.0): + """ Mesh for the main cuboid""" + # Sorry + vertices = OrientationViewerGraphics.hypercube(3) + + faces_and_colors = [] + + for fixed_dim in range(3): + for zero_one in range(2): + this_face = [(ind, v) for ind, v in enumerate(vertices) if v[fixed_dim] == zero_one] + + def sort_key(x): + _, v = x + other_dims = v[:fixed_dim] + v[fixed_dim+1:] + return (v[fixed_dim] - 0.5)*np.arctan2(other_dims[0]-0.5, other_dims[1]-0.5) + + this_face = sorted(this_face, key=sort_key) + + color = [255,255,255,255] #[0.6,0.6,0.6,alpha] + color[fixed_dim]=0.4 + + # faces_and_colors.append([[this_face[x][0] for x in range(4)], color]) + + faces_and_colors.append(([this_face[x][0] for x in (0,1,2)], color)) + faces_and_colors.append(([this_face[x][0] for x in (2,3,0)], color)) + + vertices = np.array(vertices, dtype=float) - 0.5 + faces = np.array([face for face, _ in faces_and_colors]) + colors = np.array([color for _, color in faces_and_colors]) + + return gl.GLMeshItem(vertexes=vertices, faces=faces, faceColors=colors, + drawEdges=False, edgeColor=(0, 0, 0, 1), smooth=False) + + @staticmethod + def create_arrow(n: int = 30, tail_length=10, tail_width=0.6): + """ Mesh for an arrow """ + # Thanks, I hate it. + + # top and tail + points = [[0, 0, 1], [0, 0, -tail_length]] + faces = [] + colors = [] + + # widest ring + for angle in 2 * np.pi * np.arange(0, n) / n: + points.append([np.sin(angle), np.cos(angle), 0]) + + # middle ring + for angle in 2 * np.pi * np.arange(0, n) / n: + points.append([tail_width*np.sin(angle), tail_width*np.cos(angle), 0]) + + # bottom ring + for angle in 2 * np.pi * np.arange(0, n) / n: + points.append([tail_width*np.sin(angle), tail_width*np.cos(angle), -tail_length]) + + for i in range(n): + # laterial indices + j = (i+1) % n + + # Pointy bit + faces.append([i + 2, j+2 + 2, 0]) + + # # Top ring + faces.append([i + n + 2, j + n + 2, j + 2]) + faces.append([i + n + 2, i + 2, j + 2]) + + # Cylinder sides + faces.append([i + 2 * n + 2, j + 2 * n + 2, j + n + 2]) + faces.append([i + 2 * n + 2, i + n + 2, j + n + 2]) + + # Cylinder base + faces.append([i+2*n + 2, j + 2 * n + 2, 1]) + + for i in range(6*n): + colors.append([0.6] * 3 + [1]) + + points = np.array(points) - np.array([0,0,-0.5*(tail_length-1)]) + faces = np.array(faces) + colors = np.array(colors) + + return gl.GLMeshItem(vertexes=points, faces=faces, faceColors=colors, + drawEdges=False, edgeColor=(0, 0, 0, 1), smooth=True) + + @staticmethod + def createTransform(theta_deg: float, phi_deg: float, psi_deg: float, scaling: List[float]) -> Transform3D: + + # Get rotation matrix + r_mat = Rz(phi_deg)@Ry(theta_deg)@Rz(psi_deg)@np.diag(scaling) + + # Get the 4x4 transformation matrix, by (1) padding by zeros (2) setting the corner element to 1 + trans_mat = np.pad(r_mat, ((0, 1), (0, 1))) + trans_mat[-1, -1] = 1 + + return Transform3D(trans_mat) \ No newline at end of file From c35694c614953b85ab88eec2e403c70061b21b5d Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Sat, 29 Oct 2022 16:30:56 +0100 Subject: [PATCH 15/87] Basic OV done, just needs the correct cuboid coordinates and wiring up --- src/sas/qtgui/Utilities/OrientationViewer.py | 164 -------------- .../OrientationViewer/FloodBarrier.py | 36 +++ .../OrientationViewer/OrientationViewer.py | 208 ++++++++++++++++++ .../OrientationViewerController.py | 14 +- .../OrientationViewerGraphics.py | 0 .../Utilities/OrientationViewer/__init__.py | 0 6 files changed, 252 insertions(+), 170 deletions(-) delete mode 100644 src/sas/qtgui/Utilities/OrientationViewer.py create mode 100644 src/sas/qtgui/Utilities/OrientationViewer/FloodBarrier.py create mode 100644 src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py rename src/sas/qtgui/Utilities/{ => OrientationViewer}/OrientationViewerController.py (93%) rename src/sas/qtgui/Utilities/{ => OrientationViewer}/OrientationViewerGraphics.py (100%) create mode 100644 src/sas/qtgui/Utilities/OrientationViewer/__init__.py diff --git a/src/sas/qtgui/Utilities/OrientationViewer.py b/src/sas/qtgui/Utilities/OrientationViewer.py deleted file mode 100644 index fca89b84d9..0000000000 --- a/src/sas/qtgui/Utilities/OrientationViewer.py +++ /dev/null @@ -1,164 +0,0 @@ -import numpy as np - -from PyQt5 import QtWidgets -from PyQt5.QtWidgets import QSizePolicy -from PyQt5.QtCore import Qt - -import matplotlib as mpl - -import pyqtgraph.opengl as gl -from pyqtgraph.Transform3D import Transform3D -from OpenGL.GL import * - -from sasmodels.core import load_model_info, build_model -from sasmodels.data import empty_data2D -from sasmodels.direct_model import DirectModel -from sasmodels.jitter import Rx, Ry, Rz - -from sas.qtgui.Utilities.OrientationViewerController import OrientationViewierController, Orientation -from sas.qtgui.Utilities.OrientationViewerGraphics import OrientationViewerGraphics - - - - - - -class OrientationViewer(QtWidgets.QWidget): - - cuboid_scaling = [0.1, 0.4, 1.0] - n_ghosts_per_perameter = 10 - log_I_max = 3 - log_I_min = -3 - - @staticmethod - def colormap_scale(data): - x = data.copy() - x -= np.min(x) - x /= np.max(x) - return x - - def __init__(self, parent=None): - super().__init__() - - - self.graph = gl.GLViewWidget() - # - # glBegin(GL_VERTEX_ARRAY) - # - # glEnd() - # - # glBlendFunc(GL_ONE_MINUS_SRC_ALPHA, GL_SRC_ALPHA) - # glEnable(GL_COLOR_MATERIAL) - # glEnable(GL_BLEND) - - - self.graph.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) - - self.controller = OrientationViewierController() - - layout = QtWidgets.QVBoxLayout() - layout.addWidget(self.graph) - layout.addWidget(self.controller) - self.setLayout(layout) - - self.arrow = self.create_arrow() - self.image_plane_coordinate_points = np.linspace(-2, 2, 256) - - # temporary plot data - X, Y = np.meshgrid(self.image_plane_coordinate_points, self.image_plane_coordinate_points) - - R2 = (X**2 + Y**2) - - self.image_plane_data = 0.5*(1+np.cos(5*np.sqrt(R2))) / (5*R2+1) - - self.colormap = mpl.colormaps["viridis"] - # for i in range(101): - # print(self.colormap(i)) - - self.image_plane_colors = self.colormap(OrientationViewer.colormap_scale(self.image_plane_data)) - - self.image_plane = gl.GLSurfacePlotItem( - self.image_plane_coordinate_points, - self.image_plane_coordinate_points, - self.image_plane_data, - self.image_plane_colors - ) - - - ghost_alpha = 1/(OrientationViewer.n_ghosts_per_perameter**3) #0.9**(1/(OrientationViewer.n_ghosts_per_perameter**3)) - self.ghosts = [] - for a in np.linspace(-1, 1, OrientationViewer.n_ghosts_per_perameter): - for b in np.linspace(-1, 1, OrientationViewer.n_ghosts_per_perameter): - for c in np.linspace(-1, 1, OrientationViewer.n_ghosts_per_perameter): - ghost = OrientationViewerGraphics.create_cube(ghost_alpha) - self.graph.addItem(ghost) - self.ghosts.append((a, b, c, ghost)) - - - - # self.graph.addItem(self.arrow) - # self.graph.addItem(self.image_plane) - - self.arrow.rotate(180, 1, 0, 0) - self.arrow.scale(0.05, 0.05, 0.2) - - for _, _, _, ghost in self.ghosts: - ghost.setTransform(OrientationViewerGraphics.createTransform(0, 0, 0, OrientationViewer.cuboid_scaling)) - - self.image_plane.translate(0,0,-2) - - self.controller.valueEdited.connect(self.on_angle_change) - - - - - def on_angle_change(self, orientation: Orientation): - - for a, b, c, ghost in self.ghosts: - - ghost.setTransform( - OrientationViewerGraphics.createTransform( - orientation.theta + 0.5*a*orientation.dtheta, - orientation.phi + 0.5*b*orientation.dphi, - orientation.psi + 0.5*c*orientation.dpsi, - OrientationViewer.cuboid_scaling)) - - @staticmethod - def create_calculator(n=256, qmax=0.5): - """ - Make a parallelepiped model calculator for q range -qmax to qmax with n samples - """ - - model_info = load_model_info("parallelepiped") - model = build_model(model_info) - q = np.linspace(-qmax, qmax, n) - data = empty_data2D(q, q) - calculator = DirectModel(data, model) - - return calculator - - -def main(): - """ Show a demo of the slider """ - import os - - os.environ["QT_ENABLE_HIGHDPI_SCALING"] = "1" - app = QtWidgets.QApplication([]) - - app.setAttribute(Qt.AA_EnableHighDpiScaling) - app.setAttribute(Qt.AA_ShareOpenGLContexts) - - - mainWindow = QtWidgets.QMainWindow() - viewer = OrientationViewer(mainWindow) - - mainWindow.setCentralWidget(viewer) - - mainWindow.show() - - mainWindow.resize(600, 600) - app.exec_() - - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/src/sas/qtgui/Utilities/OrientationViewer/FloodBarrier.py b/src/sas/qtgui/Utilities/OrientationViewer/FloodBarrier.py new file mode 100644 index 0000000000..2365254678 --- /dev/null +++ b/src/sas/qtgui/Utilities/OrientationViewer/FloodBarrier.py @@ -0,0 +1,36 @@ +from typing import TypeVar, Generic, Callable, Any, List, Optional + +import time +import threading + +T = TypeVar('T') + +class FloodBarrier(Generic[T], threading.Event): + """ A single entry LIFO queue holds up a function call for a period + of time, waiting to see if another value comes along. If another value + does come along, then it relplaces it and waits. + + This should prevent a flood of messages being sent to a calculation.""" + def __init__(self, done_call: Callable[[T], Any], wait_time_s: float = 0.05): + super().__init__() + + self.done_call = done_call + self.wait_time_s = wait_time_s + + self.value: Optional[T] = None + + self.current_timer: Optional[threading.Timer] = None + + def on_timout(self): + self.done_call(self.value) + self.value = None + + def __call__(self, value: T): + + if self.current_timer is not None: + self.current_timer.cancel() + + self.value = value + + self.current_timer = threading.Timer(self.wait_time_s, self.on_timout) + self.current_timer.start() diff --git a/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py b/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py new file mode 100644 index 0000000000..e2e2411697 --- /dev/null +++ b/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py @@ -0,0 +1,208 @@ + +import numpy as np + +from PyQt5 import QtWidgets +from PyQt5.QtWidgets import QSizePolicy +from PyQt5.QtCore import Qt + +import matplotlib as mpl + +import pyqtgraph.opengl as gl + +from sasmodels.core import load_model_info, build_model +from sasmodels.data import empty_data2D +from sasmodels.direct_model import DirectModel + +from sas.qtgui.Utilities.OrientationViewer.OrientationViewerController import OrientationViewierController, Orientation +from sas.qtgui.Utilities.OrientationViewer.OrientationViewerGraphics import OrientationViewerGraphics +from sas.qtgui.Utilities.OrientationViewer.FloodBarrier import FloodBarrier + + + + + +class OrientationViewer(QtWidgets.QWidget): + + # Dimensions of scattering cuboid + a = 0.1 + b = 0.4 + c = 1.0 + + cuboid_scaling = [a, b, c] + + n_ghosts_per_perameter = 8 + n_q_samples = 256 + log_I_max = 10 + log_I_min = -3 + q_max = 0.5 + polydispersity_distribution = "gaussian" + + log_I_range = log_I_max - log_I_min + + def __init__(self, parent=None): + super().__init__() + + # Put a barrier that will stop a flood of events going to the calculator + self.set_image_data = FloodBarrier[Orientation](self._set_image_data, 0.5) + + self.graph = gl.GLViewWidget() + + self.graph.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + + self.controller = OrientationViewierController() + + layout = QtWidgets.QVBoxLayout() + layout.addWidget(self.graph) + layout.addWidget(self.controller) + self.setLayout(layout) + + self.arrow = OrientationViewerGraphics.create_arrow() + self.image_plane_coordinate_points = np.linspace(-3, 3, 256) + + # temporary plot data + x, y = np.meshgrid(self.image_plane_coordinate_points, self.image_plane_coordinate_points) + self.image_plane_data = np.zeros_like(x) + + self.colormap = mpl.colormaps["viridis"] + + self.image_plane_colors = self.colormap(self.image_plane_data) + + self.image_plane = gl.GLSurfacePlotItem( + self.image_plane_coordinate_points, + self.image_plane_coordinate_points, + self.image_plane_data, + self.image_plane_colors + ) + + ghost_alpha = 1/(OrientationViewer.n_ghosts_per_perameter**3) + self.ghosts = [] + for a in np.linspace(-1, 1, OrientationViewer.n_ghosts_per_perameter): + for b in np.linspace(-1, 1, OrientationViewer.n_ghosts_per_perameter): + for c in np.linspace(-1, 1, OrientationViewer.n_ghosts_per_perameter): + ghost = OrientationViewerGraphics.create_cube(ghost_alpha) + self.graph.addItem(ghost) + self.ghosts.append((a, b, c, ghost)) + + + + self.graph.addItem(self.arrow) + self.graph.addItem(self.image_plane) + + self.arrow.rotate(180, 1, 0, 0) + self.arrow.scale(0.05, 0.05, 0.05) + self.arrow.translate(0,0,1) + + self.image_plane.translate(0,0,-2) + + for _, _, _, ghost in self.ghosts: + ghost.setTransform(OrientationViewerGraphics.createTransform(0, 0, 0, OrientationViewer.cuboid_scaling)) + + + self.controller.valueEdited.connect(self.on_angle_change) + + self.calculator = OrientationViewer.create_calculator() + self.on_angle_change(Orientation()) + + + + def _set_image_data(self, orientation: Orientation): + + data = self.scatering_data(orientation) + + scaled_data = (np.log(data) - OrientationViewer.log_I_min) / OrientationViewer.log_I_range + self.image_plane_data = np.clip(scaled_data, 0, 1) + + self.image_plane_colors = self.colormap(self.image_plane_data) + + self.image_plane.setData(z=self.image_plane_data, colors=self.image_plane_colors) + + + + def on_angle_change(self, orientation: Orientation): + + for a, b, c, ghost in self.ghosts: + + ghost.setTransform( + OrientationViewerGraphics.createTransform( + orientation.theta + 0.5*a*orientation.dtheta, + orientation.phi + 0.5*b*orientation.dphi, + orientation.psi + 0.5*c*orientation.dpsi, + OrientationViewer.cuboid_scaling)) + + self.set_image_data(orientation) + + @staticmethod + def create_calculator(): + """ + Make a parallelepiped model calculator for q range -qmax to qmax with n samples + """ + + model_info = load_model_info("parallelepiped") + model = build_model(model_info) + q = np.linspace(-OrientationViewer.q_max, OrientationViewer.q_max, OrientationViewer.n_q_samples) + data = empty_data2D(q, q) + calculator = DirectModel(data, model) + + return calculator + + + def polydispersity_sample_count(self, orientation): + """ Work out how many samples to do for the polydispersity""" + polydispersity = [orientation.dtheta, orientation.dphi, orientation.dpsi] + is_polydisperse = [1 if x > 0 else 0 for x in polydispersity] + n_polydisperse = np.sum(is_polydisperse) + + samples = int(200 / (n_polydisperse**2.2 + 1)) # + + return (samples * x for x in is_polydisperse) + + def scatering_data(self, orientation: Orientation) -> np.ndarray: + + # add the orientation parameters to the model parameters + + theta_pd_n, phi_pd_n, psi_pd_n = self.polydispersity_sample_count(orientation) + + data = self.calculator( + theta=orientation.theta, + theta_pd=orientation.dtheta, + theta_pd_type=OrientationViewer.polydispersity_distribution, + theta_pd_n=theta_pd_n, + phi=orientation.phi, + phi_pd=orientation.dphi, + phi_pd_type=OrientationViewer.polydispersity_distribution, + phi_pd_n=phi_pd_n, + psi=orientation.psi, + psi_pd=orientation.dpsi, + psi_pd_type=OrientationViewer.polydispersity_distribution, + psi_pd_n=psi_pd_n, + a=OrientationViewer.a, + b=OrientationViewer.b, + c=OrientationViewer.c, + background=np.exp(OrientationViewer.log_I_min)) + + return np.reshape(data, (OrientationViewer.n_q_samples, OrientationViewer.n_q_samples)) + +def main(): + """ Show a demo of the slider """ + import os + + os.environ["QT_ENABLE_HIGHDPI_SCALING"] = "1" + app = QtWidgets.QApplication([]) + + app.setAttribute(Qt.AA_EnableHighDpiScaling) + app.setAttribute(Qt.AA_ShareOpenGLContexts) + + + mainWindow = QtWidgets.QMainWindow() + viewer = OrientationViewer(mainWindow) + + mainWindow.setCentralWidget(viewer) + + mainWindow.show() + + mainWindow.resize(600, 600) + app.exec_() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/sas/qtgui/Utilities/OrientationViewerController.py b/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewerController.py similarity index 93% rename from src/sas/qtgui/Utilities/OrientationViewerController.py rename to src/sas/qtgui/Utilities/OrientationViewer/OrientationViewerController.py index 286433687c..86140fa714 100644 --- a/src/sas/qtgui/Utilities/OrientationViewerController.py +++ b/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewerController.py @@ -9,12 +9,14 @@ class Orientation(NamedTuple): """ Data sent when updating the plot""" - theta: int - phi: int - psi: int - dtheta: int - dphi: int - dpsi: int + theta: int = 0 + phi: int = 0 + psi: int = 0 + dtheta: int = 0 + dphi: int = 0 + dpsi: int = 0 + + class OrientationViewierController(QtWidgets.QDialog, Ui_OrientationViewierControllerUI): diff --git a/src/sas/qtgui/Utilities/OrientationViewerGraphics.py b/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewerGraphics.py similarity index 100% rename from src/sas/qtgui/Utilities/OrientationViewerGraphics.py rename to src/sas/qtgui/Utilities/OrientationViewer/OrientationViewerGraphics.py diff --git a/src/sas/qtgui/Utilities/OrientationViewer/__init__.py b/src/sas/qtgui/Utilities/OrientationViewer/__init__.py new file mode 100644 index 0000000000..e69de29bb2 From 9f01b71208e7d1205a8bef7ff996399d5e9d6b5a Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Sat, 29 Oct 2022 16:52:10 +0100 Subject: [PATCH 16/87] queue bugfix --- .../qtgui/Utilities/OrientationViewer/FloodBarrier.py | 6 ++---- .../Utilities/OrientationViewer/OrientationViewer.py | 9 ++++++--- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/sas/qtgui/Utilities/OrientationViewer/FloodBarrier.py b/src/sas/qtgui/Utilities/OrientationViewer/FloodBarrier.py index 2365254678..a40b43b6b2 100644 --- a/src/sas/qtgui/Utilities/OrientationViewer/FloodBarrier.py +++ b/src/sas/qtgui/Utilities/OrientationViewer/FloodBarrier.py @@ -11,19 +11,17 @@ class FloodBarrier(Generic[T], threading.Event): does come along, then it relplaces it and waits. This should prevent a flood of messages being sent to a calculation.""" - def __init__(self, done_call: Callable[[T], Any], wait_time_s: float = 0.05): + def __init__(self, done_call: Callable[[T], Any], initial_value: T, wait_time_s: float = 0.05): super().__init__() self.done_call = done_call self.wait_time_s = wait_time_s - - self.value: Optional[T] = None + self.value = initial_value self.current_timer: Optional[threading.Timer] = None def on_timout(self): self.done_call(self.value) - self.value = None def __call__(self, value: T): diff --git a/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py b/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py index e2e2411697..f50293d593 100644 --- a/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py +++ b/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py @@ -1,4 +1,4 @@ - +from typing import Optional import numpy as np from PyQt5 import QtWidgets @@ -43,7 +43,7 @@ def __init__(self, parent=None): super().__init__() # Put a barrier that will stop a flood of events going to the calculator - self.set_image_data = FloodBarrier[Orientation](self._set_image_data, 0.5) + self.set_image_data = FloodBarrier[Orientation](self._set_image_data, Orientation(), 0.5) self.graph = gl.GLViewWidget() @@ -118,7 +118,10 @@ def _set_image_data(self, orientation: Orientation): - def on_angle_change(self, orientation: Orientation): + def on_angle_change(self, orientation: Optional[Orientation]): + + if orientation is None: + return for a, b, c, ghost in self.ghosts: From 7c7e2b98d308672399222127d0c4622e9bc402ab Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Sat, 29 Oct 2022 16:59:03 +0100 Subject: [PATCH 17/87] Label values --- .../OrientationViewerController.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewerController.py b/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewerController.py index 86140fa714..ce9ca13dcb 100644 --- a/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewerController.py +++ b/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewerController.py @@ -29,6 +29,8 @@ def __init__(self, parent=None): self.setupUi(self) + self.setLabels(Orientation()) + # All sliders emit the same signal - the angular coordinates in degrees self.thetaSlider.valueChanged.connect(self.onAngleChange) self.phiSlider.valueChanged.connect(self.onAngleChange) @@ -37,6 +39,17 @@ def __init__(self, parent=None): self.deltaPhi.valueChanged.connect(self.onAngleChange) self.deltaPsi.valueChanged.connect(self.onAngleChange) + def setLabels(self, orientation: Orientation): + + self.thetaNumber.setText(f"{orientation.theta}°") + self.phiNumber.setText(f"{orientation.phi}°") + self.psiNumber.setText(f"{orientation.psi}°") + + self.deltaThetaNumber.setText(f"{orientation.dtheta}°") + self.deltaPhiNumber.setText(f"{orientation.dphi}°") + self.deltaPsiNumber.setText(f"{orientation.dpsi}°") + + def onAngleChange(self): theta = self.thetaSlider.value() phi = self.phiSlider.value() From 1b23d59c8e3f316590ef3421dcaf577e23f05bb2 Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Sat, 29 Oct 2022 17:05:20 +0100 Subject: [PATCH 18/87] Translate everything up a bit --- .../qtgui/Utilities/OrientationViewer/OrientationViewer.py | 6 +++--- .../OrientationViewer/OrientationViewerGraphics.py | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py b/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py index f50293d593..504021643a 100644 --- a/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py +++ b/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py @@ -92,10 +92,10 @@ def __init__(self, parent=None): self.arrow.scale(0.05, 0.05, 0.05) self.arrow.translate(0,0,1) - self.image_plane.translate(0,0,-2) + self.image_plane.translate(0,0,-0.5) # It's 1 unit thick, so half way for _, _, _, ghost in self.ghosts: - ghost.setTransform(OrientationViewerGraphics.createTransform(0, 0, 0, OrientationViewer.cuboid_scaling)) + ghost.setTransform(OrientationViewerGraphics.createCubeTransform(0, 0, 0, OrientationViewer.cuboid_scaling)) self.controller.valueEdited.connect(self.on_angle_change) @@ -126,7 +126,7 @@ def on_angle_change(self, orientation: Optional[Orientation]): for a, b, c, ghost in self.ghosts: ghost.setTransform( - OrientationViewerGraphics.createTransform( + OrientationViewerGraphics.createCubeTransform( orientation.theta + 0.5*a*orientation.dtheta, orientation.phi + 0.5*b*orientation.dphi, orientation.psi + 0.5*c*orientation.dpsi, diff --git a/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewerGraphics.py b/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewerGraphics.py index f80b0c66df..f8e446e83c 100644 --- a/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewerGraphics.py +++ b/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewerGraphics.py @@ -106,7 +106,7 @@ def create_arrow(n: int = 30, tail_length=10, tail_width=0.6): drawEdges=False, edgeColor=(0, 0, 0, 1), smooth=True) @staticmethod - def createTransform(theta_deg: float, phi_deg: float, psi_deg: float, scaling: List[float]) -> Transform3D: + def createCubeTransform(theta_deg: float, phi_deg: float, psi_deg: float, scaling: List[float]) -> Transform3D: # Get rotation matrix r_mat = Rz(phi_deg)@Ry(theta_deg)@Rz(psi_deg)@np.diag(scaling) @@ -114,5 +114,6 @@ def createTransform(theta_deg: float, phi_deg: float, psi_deg: float, scaling: L # Get the 4x4 transformation matrix, by (1) padding by zeros (2) setting the corner element to 1 trans_mat = np.pad(r_mat, ((0, 1), (0, 1))) trans_mat[-1, -1] = 1 + trans_mat[2, -1] = 2 return Transform3D(trans_mat) \ No newline at end of file From 539de6470d75d772c7e48bd3451a8beb380dafb0 Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Sat, 29 Oct 2022 17:22:22 +0100 Subject: [PATCH 19/87] Updated slider appearance --- .../OrientationViewerController.py | 5 ++-- .../UI/OrientationViewerControllerUI.ui | 24 ++++++++++++++++--- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewerController.py b/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewerController.py index ce9ca13dcb..ac66a6b415 100644 --- a/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewerController.py +++ b/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewerController.py @@ -59,8 +59,9 @@ def onAngleChange(self): dphi = self.deltaPhi.value() dpsi = self.deltaPsi.value() - self.valueEdited.emit(Orientation(theta, phi, psi, dtheta, dphi, dpsi)) - + orientation = Orientation(theta, phi, psi, dtheta, dphi, dpsi) + self.valueEdited.emit(orientation) + self.setLabels(orientation) def main(): diff --git a/src/sas/qtgui/Utilities/UI/OrientationViewerControllerUI.ui b/src/sas/qtgui/Utilities/UI/OrientationViewerControllerUI.ui index e9a61b24e3..627b08750b 100644 --- a/src/sas/qtgui/Utilities/UI/OrientationViewerControllerUI.ui +++ b/src/sas/qtgui/Utilities/UI/OrientationViewerControllerUI.ui @@ -46,6 +46,12 @@ Qt::Horizontal + + QSlider::TicksBothSides + + + 15 + @@ -74,6 +80,12 @@ Qt::Horizontal + + QSlider::TicksBothSides + + + 15 + @@ -177,6 +189,12 @@ Qt::Horizontal + + QSlider::TicksBothSides + + + 15 + @@ -290,7 +308,7 @@ Qt::Horizontal - QSlider::TicksBelow + QSlider::TicksBothSides 15 @@ -384,7 +402,7 @@ Qt::Horizontal - QSlider::TicksBelow + QSlider::TicksBothSides 15 @@ -418,7 +436,7 @@ Qt::Horizontal - QSlider::TicksBelow + QSlider::TicksBothSides 15 From 8cd2971445ed42aba964cad55322c8911f9c7d5e Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Sat, 29 Oct 2022 17:36:02 +0100 Subject: [PATCH 20/87] Wired in OV window --- src/sas/qtgui/MainWindow/GuiManager.py | 9 ++++----- .../Utilities/OrientationViewer/OrientationViewer.py | 2 ++ 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/sas/qtgui/MainWindow/GuiManager.py b/src/sas/qtgui/MainWindow/GuiManager.py index f9808a8e56..fdba483dd5 100644 --- a/src/sas/qtgui/MainWindow/GuiManager.py +++ b/src/sas/qtgui/MainWindow/GuiManager.py @@ -31,6 +31,7 @@ from sas.qtgui.Utilities.PluginManager import PluginManager from sas.qtgui.Utilities.GridPanel import BatchOutputPanel from sas.qtgui.Utilities.ResultPanel import ResultPanel +from sas.qtgui.Utilities.OrientationViewer.OrientationViewer import OrientationViewer from sas.qtgui.Utilities.Reports.ReportDialog import ReportDialog from sas.qtgui.MainWindow.Acknowledgements import Acknowledgements @@ -192,6 +193,8 @@ def addWidgets(self): self.DataOperation = DataOperationUtilityPanel(self) self.FileConverter = FileConverterWidget(self) + self.orientation_viewer = OrientationViewer() + def loadAllPerspectives(self): # Close any existing perspectives to prevent multiple open instances self.closeAllPerspectives() @@ -1010,11 +1013,7 @@ def actionOrientation_Viewer(self): """ Make sasmodels orientation & jitter viewer available """ - from sasmodels.jitter import run as orientation_run - try: - orientation_run() - except Exception as ex: - logging.error(str(ex)) + self.orientation_viewer.show() def actionImage_Viewer(self): """ diff --git a/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py b/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py index 504021643a..a8cbc00d4c 100644 --- a/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py +++ b/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py @@ -42,6 +42,8 @@ class OrientationViewer(QtWidgets.QWidget): def __init__(self, parent=None): super().__init__() + self.parent = parent + # Put a barrier that will stop a flood of events going to the calculator self.set_image_data = FloodBarrier[Orientation](self._set_image_data, Orientation(), 0.5) From ed84cd542e4fb0209221691998833be231254195 Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Sun, 30 Oct 2022 14:48:15 +0000 Subject: [PATCH 21/87] Lets just make a gooder gl widget --- src/sas/qtgui/GL/Arrow.py | 0 src/sas/qtgui/GL/Cube.py | 0 src/sas/qtgui/GL/GraphWidget.py | 0 src/sas/qtgui/GL/Mesh.py | 0 src/sas/qtgui/GL/Renderable.py | 12 ++ src/sas/qtgui/GL/SolidModel.py | 15 ++ src/sas/qtgui/GL/Surface.py | 0 src/sas/qtgui/GL/WireModel.py | 15 ++ src/sas/qtgui/GL/__init__.py | 0 src/sas/qtgui/GL/transformations/Rotation.py | 36 +++++ src/sas/qtgui/GL/transformations/Scale.py | 0 src/sas/qtgui/GL/transformations/Transform.py | 37 +++++ .../qtgui/GL/transformations/Translation.py | 36 +++++ src/sas/qtgui/GL/transformations/__init__.py | 0 .../OrientationViewer/OrientationViewer.py | 129 +++++++++++------- 15 files changed, 232 insertions(+), 48 deletions(-) create mode 100644 src/sas/qtgui/GL/Arrow.py create mode 100644 src/sas/qtgui/GL/Cube.py create mode 100644 src/sas/qtgui/GL/GraphWidget.py create mode 100644 src/sas/qtgui/GL/Mesh.py create mode 100644 src/sas/qtgui/GL/Renderable.py create mode 100644 src/sas/qtgui/GL/SolidModel.py create mode 100644 src/sas/qtgui/GL/Surface.py create mode 100644 src/sas/qtgui/GL/WireModel.py create mode 100644 src/sas/qtgui/GL/__init__.py create mode 100644 src/sas/qtgui/GL/transformations/Rotation.py create mode 100644 src/sas/qtgui/GL/transformations/Scale.py create mode 100644 src/sas/qtgui/GL/transformations/Transform.py create mode 100644 src/sas/qtgui/GL/transformations/Translation.py create mode 100644 src/sas/qtgui/GL/transformations/__init__.py diff --git a/src/sas/qtgui/GL/Arrow.py b/src/sas/qtgui/GL/Arrow.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/sas/qtgui/GL/Cube.py b/src/sas/qtgui/GL/Cube.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/sas/qtgui/GL/GraphWidget.py b/src/sas/qtgui/GL/GraphWidget.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/sas/qtgui/GL/Mesh.py b/src/sas/qtgui/GL/Mesh.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/sas/qtgui/GL/Renderable.py b/src/sas/qtgui/GL/Renderable.py new file mode 100644 index 0000000000..82613b365e --- /dev/null +++ b/src/sas/qtgui/GL/Renderable.py @@ -0,0 +1,12 @@ +from abc import ABC, abstractmethod + +class Renderable(ABC): + """ Interface for everything that can be rendered with the OpenGL widget""" + + @abstractmethod + def render_wireframe(self): + pass + + @abstractmethod + def render_solid(self): + pass \ No newline at end of file diff --git a/src/sas/qtgui/GL/SolidModel.py b/src/sas/qtgui/GL/SolidModel.py new file mode 100644 index 0000000000..eb42fe7e37 --- /dev/null +++ b/src/sas/qtgui/GL/SolidModel.py @@ -0,0 +1,15 @@ +from abc import ABC, abstractmethod + +import numpy as np + +class SolidModel(ABC): + + @abstractmethod + @property + def vertices(self) -> np.ndarray: + pass + + @abstractmethod + @property + def faces(self) -> np.ndarray: + pass \ No newline at end of file diff --git a/src/sas/qtgui/GL/Surface.py b/src/sas/qtgui/GL/Surface.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/sas/qtgui/GL/WireModel.py b/src/sas/qtgui/GL/WireModel.py new file mode 100644 index 0000000000..17432b7c7e --- /dev/null +++ b/src/sas/qtgui/GL/WireModel.py @@ -0,0 +1,15 @@ +from abc import ABC, abstractmethod + +import numpy as np + +class WireModel(ABC): + + @abstractmethod + @property + def vertices(self) -> np.ndarray: + pass + + @abstractmethod + @property + def edges(self) -> np.ndarray: + pass \ No newline at end of file diff --git a/src/sas/qtgui/GL/__init__.py b/src/sas/qtgui/GL/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/sas/qtgui/GL/transformations/Rotation.py b/src/sas/qtgui/GL/transformations/Rotation.py new file mode 100644 index 0000000000..81a2001736 --- /dev/null +++ b/src/sas/qtgui/GL/transformations/Rotation.py @@ -0,0 +1,36 @@ +import numpy as np +from sas.qtgui.GL.Renderable import Renderable + +from OpenGL.GL import * +from OpenGL.GLU import * + +class Rotation(Renderable): + """ + General transform - also doubles as a scene graph node + + For the sake of speed, the transformation matrix shape is not checked. + It should be a 4x4 transformation matrix + """ + + def __init__(self, rotation: np.ndarray, *children: Renderable): + super().__init__() + self.rotation = rotation + self.children = children + + def render_solid(self): + # Apply transform + + for child in self.children: + child.render_solid() + + # unapply transform + + def render_wireframe(self): + # Apply transform + + for child in self.children: + child.render_wireframe() + + # unapply transform + + diff --git a/src/sas/qtgui/GL/transformations/Scale.py b/src/sas/qtgui/GL/transformations/Scale.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/sas/qtgui/GL/transformations/Transform.py b/src/sas/qtgui/GL/transformations/Transform.py new file mode 100644 index 0000000000..38aaf24d72 --- /dev/null +++ b/src/sas/qtgui/GL/transformations/Transform.py @@ -0,0 +1,37 @@ +import numpy as np + +from OpenGL.GL import * +from OpenGL.GLU import * + +from sas.qtgui.GL.Renderable import Renderable + +class Transform(Renderable): + """ + General transform - also doubles as a scene graph node + + For the sake of speed, the transformation matrix shape is not checked. + It should be a 4x4 transformation matrix + """ + + def __init__(self, matrix: np.ndarray, *children: Renderable): + super().__init__() + self.matrix = matrix + self.children = children + + def render_solid(self): + # Apply transform + + for child in self.children: + child.render_solid() + + # unapply transform + + def render_wireframe(self): + # Apply transform + + for child in self.children: + child.render_wireframe() + + # unapply transform + + diff --git a/src/sas/qtgui/GL/transformations/Translation.py b/src/sas/qtgui/GL/transformations/Translation.py new file mode 100644 index 0000000000..05a5a9804a --- /dev/null +++ b/src/sas/qtgui/GL/transformations/Translation.py @@ -0,0 +1,36 @@ +import numpy as np +from sas.qtgui.GL.Renderable import Renderable + +from OpenGL.GL import * +from OpenGL.GLU import * + +class Translation(Renderable): + """ + General transform - also doubles as a scene graph node + + For the sake of speed, the transformation matrix shape is not checked. + It should be a 3 element vector + """ + + def __init__(self, translation: np.ndarray, *children: Renderable): + super().__init__() + self.rotation = rotation + self.children = children + + def render_solid(self): + # Apply transform + + for child in self.children: + child.render_solid() + + # unapply transform + + def render_wireframe(self): + # Apply transform + + for child in self.children: + child.render_wireframe() + + # unapply transform + + diff --git a/src/sas/qtgui/GL/transformations/__init__.py b/src/sas/qtgui/GL/transformations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py b/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py index a8cbc00d4c..a04927c231 100644 --- a/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py +++ b/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py @@ -8,6 +8,9 @@ import matplotlib as mpl import pyqtgraph.opengl as gl +from OpenGL.GL import * +from OpenGL.GLU import * +from PyQt5.QtOpenGL import QGLWidget from sasmodels.core import load_model_info, build_model from sasmodels.data import empty_data2D @@ -47,7 +50,7 @@ def __init__(self, parent=None): # Put a barrier that will stop a flood of events going to the calculator self.set_image_data = FloodBarrier[Orientation](self._set_image_data, Orientation(), 0.5) - self.graph = gl.GLViewWidget() + self.graph = TestWidget(self) self.graph.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) @@ -57,53 +60,53 @@ def __init__(self, parent=None): layout.addWidget(self.graph) layout.addWidget(self.controller) self.setLayout(layout) - - self.arrow = OrientationViewerGraphics.create_arrow() - self.image_plane_coordinate_points = np.linspace(-3, 3, 256) - - # temporary plot data - x, y = np.meshgrid(self.image_plane_coordinate_points, self.image_plane_coordinate_points) - self.image_plane_data = np.zeros_like(x) - - self.colormap = mpl.colormaps["viridis"] - - self.image_plane_colors = self.colormap(self.image_plane_data) - - self.image_plane = gl.GLSurfacePlotItem( - self.image_plane_coordinate_points, - self.image_plane_coordinate_points, - self.image_plane_data, - self.image_plane_colors - ) - - ghost_alpha = 1/(OrientationViewer.n_ghosts_per_perameter**3) - self.ghosts = [] - for a in np.linspace(-1, 1, OrientationViewer.n_ghosts_per_perameter): - for b in np.linspace(-1, 1, OrientationViewer.n_ghosts_per_perameter): - for c in np.linspace(-1, 1, OrientationViewer.n_ghosts_per_perameter): - ghost = OrientationViewerGraphics.create_cube(ghost_alpha) - self.graph.addItem(ghost) - self.ghosts.append((a, b, c, ghost)) - - - - self.graph.addItem(self.arrow) - self.graph.addItem(self.image_plane) - - self.arrow.rotate(180, 1, 0, 0) - self.arrow.scale(0.05, 0.05, 0.05) - self.arrow.translate(0,0,1) - - self.image_plane.translate(0,0,-0.5) # It's 1 unit thick, so half way - - for _, _, _, ghost in self.ghosts: - ghost.setTransform(OrientationViewerGraphics.createCubeTransform(0, 0, 0, OrientationViewer.cuboid_scaling)) - - - self.controller.valueEdited.connect(self.on_angle_change) - - self.calculator = OrientationViewer.create_calculator() - self.on_angle_change(Orientation()) + # + # self.arrow = OrientationViewerGraphics.create_arrow() + # self.image_plane_coordinate_points = np.linspace(-3, 3, 256) + # + # # temporary plot data + # x, y = np.meshgrid(self.image_plane_coordinate_points, self.image_plane_coordinate_points) + # self.image_plane_data = np.zeros_like(x) + # + # self.colormap = mpl.colormaps["viridis"] + # + # self.image_plane_colors = self.colormap(self.image_plane_data) + # + # self.image_plane = gl.GLSurfacePlotItem( + # self.image_plane_coordinate_points, + # self.image_plane_coordinate_points, + # self.image_plane_data, + # self.image_plane_colors + # ) + # + # ghost_alpha = 1/(OrientationViewer.n_ghosts_per_perameter**3) + # self.ghosts = [] + # for a in np.linspace(-1, 1, OrientationViewer.n_ghosts_per_perameter): + # for b in np.linspace(-1, 1, OrientationViewer.n_ghosts_per_perameter): + # for c in np.linspace(-1, 1, OrientationViewer.n_ghosts_per_perameter): + # ghost = OrientationViewerGraphics.create_cube(ghost_alpha) + # self.graph.addItem(ghost) + # self.ghosts.append((a, b, c, ghost)) + # + # + # + # self.graph.addItem(self.arrow) + # self.graph.addItem(self.image_plane) + # + # self.arrow.rotate(180, 1, 0, 0) + # self.arrow.scale(0.05, 0.05, 0.05) + # self.arrow.translate(0,0,1) + # + # self.image_plane.translate(0,0,-0.5) # It's 1 unit thick, so half way + # + # for _, _, _, ghost in self.ghosts: + # ghost.setTransform(OrientationViewerGraphics.createCubeTransform(0, 0, 0, OrientationViewer.cuboid_scaling)) + # + # + # self.controller.valueEdited.connect(self.on_angle_change) + # + # self.calculator = OrientationViewer.create_calculator() + # self.on_angle_change(Orientation()) @@ -187,6 +190,36 @@ def scatering_data(self, orientation: Orientation) -> np.ndarray: return np.reshape(data, (OrientationViewer.n_q_samples, OrientationViewer.n_q_samples)) + +class TestWidget(QGLWidget): + + def __init__(self, parent): + super().__init__(parent) + self.setMinimumSize(640, 480) + + def paintGL(self): + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) + glLoadIdentity() + glTranslatef(-2.5, 0.5, -6.0) + glColor3f( 1.0, 1.5, 0.0 ) + glPolygonMode(GL_FRONT, GL_FILL) + glBegin(GL_TRIANGLES) + glVertex3f(2.0,-1.2,0.0) + glVertex3f(2.6,0.0,0.0) + glVertex3f(2.9,-1.2,0.0) + glEnd() + glFlush() + + def initializeGL(self): + glClearDepth(1.0) + glDepthFunc(GL_LESS) + glEnable(GL_DEPTH_TEST) + glShadeModel(GL_SMOOTH) + glMatrixMode(GL_PROJECTION) + glLoadIdentity() + gluPerspective(45.0,1.33,0.1, 100.0) + glMatrixMode(GL_MODELVIEW) + def main(): """ Show a demo of the slider """ import os From 497396a827ff5a12f0dcc1005eb1317015d38b76 Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Sun, 30 Oct 2022 19:02:45 +0000 Subject: [PATCH 22/87] Added some meat --- src/sas/qtgui/GL/WireModel.py | 13 +++++-- src/sas/qtgui/GL/transformations/Rotation.py | 7 ++-- src/sas/qtgui/GL/transformations/Scale.py | 36 +++++++++++++++++++ .../GL/transformations/SceneGraphNode.py | 26 ++++++++++++++ src/sas/qtgui/GL/transformations/Transform.py | 7 ++-- .../qtgui/GL/transformations/Translation.py | 10 +++--- 6 files changed, 86 insertions(+), 13 deletions(-) create mode 100644 src/sas/qtgui/GL/transformations/SceneGraphNode.py diff --git a/src/sas/qtgui/GL/WireModel.py b/src/sas/qtgui/GL/WireModel.py index 17432b7c7e..91e7b34a23 100644 --- a/src/sas/qtgui/GL/WireModel.py +++ b/src/sas/qtgui/GL/WireModel.py @@ -1,8 +1,12 @@ -from abc import ABC, abstractmethod + + +from abc import abstractmethod import numpy as np -class WireModel(ABC): +from sas.qtgui.GL.Renderable import Renderable + +class WireModel(Renderable): @abstractmethod @property @@ -12,4 +16,9 @@ def vertices(self) -> np.ndarray: @abstractmethod @property def edges(self) -> np.ndarray: + pass + + @abstractmethod + @property + def edge_colors(self) -> np.ndarray: pass \ No newline at end of file diff --git a/src/sas/qtgui/GL/transformations/Rotation.py b/src/sas/qtgui/GL/transformations/Rotation.py index 81a2001736..6a0b19100f 100644 --- a/src/sas/qtgui/GL/transformations/Rotation.py +++ b/src/sas/qtgui/GL/transformations/Rotation.py @@ -1,10 +1,12 @@ import numpy as np + +from sas.qtgui.GL.transformations.SceneGraphNode import SceneGraphNode from sas.qtgui.GL.Renderable import Renderable from OpenGL.GL import * from OpenGL.GLU import * -class Rotation(Renderable): +class Rotation(SceneGraphNode): """ General transform - also doubles as a scene graph node @@ -13,9 +15,8 @@ class Rotation(Renderable): """ def __init__(self, rotation: np.ndarray, *children: Renderable): - super().__init__() + super().__init__(*children) self.rotation = rotation - self.children = children def render_solid(self): # Apply transform diff --git a/src/sas/qtgui/GL/transformations/Scale.py b/src/sas/qtgui/GL/transformations/Scale.py index e69de29bb2..ba68bf7de1 100644 --- a/src/sas/qtgui/GL/transformations/Scale.py +++ b/src/sas/qtgui/GL/transformations/Scale.py @@ -0,0 +1,36 @@ +import numpy as np +from sas.qtgui.GL.transformations.SceneGraphNode import SceneGraphNode +from sas.qtgui.GL.Renderable import Renderable + +from OpenGL.GL import * +from OpenGL.GLU import * + +class Scale(SceneGraphNode): + """ + General transform - also doubles as a scene graph node + + For the sake of speed, the transformation matrix shape is not checked. + It should be a 3 element vector + """ + + def __init__(self, scale: np.ndarray, *children: Renderable): + super().__init__(*children) + self.scale = scale + + def render_solid(self): + # Apply transform + + for child in self.children: + child.render_solid() + + # unapply transform + + def render_wireframe(self): + # Apply transform + + for child in self.children: + child.render_wireframe() + + # unapply transform + + diff --git a/src/sas/qtgui/GL/transformations/SceneGraphNode.py b/src/sas/qtgui/GL/transformations/SceneGraphNode.py new file mode 100644 index 0000000000..8a8bb47e77 --- /dev/null +++ b/src/sas/qtgui/GL/transformations/SceneGraphNode.py @@ -0,0 +1,26 @@ +from typing import List + +import numpy as np + +from OpenGL.GL import * +from OpenGL.GLU import * + +from sas.qtgui.GL.Renderable import Renderable + +class SceneGraphNode(Renderable): + """ + General transform - also doubles as a scene graph node + + For the sake of speed, the transformation matrix shape is not checked. + It should be a 4x4 transformation matrix + """ + + def __init__(self, matrix: np.ndarray, *children: Renderable): + super().__init__() + self.matrix = matrix + self.children: List[Renderable] = list(children) + + def add_child(self, child: Renderable): + self.children.append(child) + + diff --git a/src/sas/qtgui/GL/transformations/Transform.py b/src/sas/qtgui/GL/transformations/Transform.py index 38aaf24d72..5304fa2211 100644 --- a/src/sas/qtgui/GL/transformations/Transform.py +++ b/src/sas/qtgui/GL/transformations/Transform.py @@ -4,8 +4,10 @@ from OpenGL.GLU import * from sas.qtgui.GL.Renderable import Renderable +from sas.qtgui.GL.transformations.SceneGraphNode import SceneGraphNode -class Transform(Renderable): + +class Transform(SceneGraphNode): """ General transform - also doubles as a scene graph node @@ -14,9 +16,8 @@ class Transform(Renderable): """ def __init__(self, matrix: np.ndarray, *children: Renderable): - super().__init__() + super().__init__(*children) self.matrix = matrix - self.children = children def render_solid(self): # Apply transform diff --git a/src/sas/qtgui/GL/transformations/Translation.py b/src/sas/qtgui/GL/transformations/Translation.py index 05a5a9804a..518af3f88f 100644 --- a/src/sas/qtgui/GL/transformations/Translation.py +++ b/src/sas/qtgui/GL/transformations/Translation.py @@ -1,21 +1,21 @@ import numpy as np from sas.qtgui.GL.Renderable import Renderable +from sas.qtgui.GL.transformations.SceneGraphNode import SceneGraphNode from OpenGL.GL import * from OpenGL.GLU import * -class Translation(Renderable): +class Translation(SceneGraphNode): """ General transform - also doubles as a scene graph node - For the sake of speed, the transformation matrix shape is not checked. + For the sake of speed, the rotation matrix shape is not checked. It should be a 3 element vector """ - def __init__(self, translation: np.ndarray, *children: Renderable): - super().__init__() + def __init__(self, rotation: np.ndarray, *children: Renderable): + super().__init__(*children) self.rotation = rotation - self.children = children def render_solid(self): # Apply transform From 1144a7d86784d3508bde9140ef58207744bbc126 Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Mon, 31 Oct 2022 13:57:59 +0000 Subject: [PATCH 23/87] More progress towards GL --- src/sas/qtgui/GL/GraphWidget.py | 112 ++++++++++++++++ src/sas/qtgui/MainWindow/MainWindow.py | 3 + .../OrientationViewer/OrientationViewer.py | 124 +++++++----------- 3 files changed, 163 insertions(+), 76 deletions(-) diff --git a/src/sas/qtgui/GL/GraphWidget.py b/src/sas/qtgui/GL/GraphWidget.py index e69de29bb2..1ebcdc41d6 100644 --- a/src/sas/qtgui/GL/GraphWidget.py +++ b/src/sas/qtgui/GL/GraphWidget.py @@ -0,0 +1,112 @@ +from typing import Optional, Tuple +import numpy as np + +from PyQt5 import QtWidgets, Qt, QtGui, QtOpenGL + +from OpenGL.GL import * +from OpenGL.GLU import * +class GraphWidget(QtOpenGL.QGLWidget): + + + def __init__(self, parent): + super().__init__(parent) + self.setMinimumSize(640, 480) + + self.view_azimuth = 0.0 + self.view_elevation = 0.0 + self.view_distance = 5.0 + self.view_centre = (0.0, 0.0, 0.0) + self.view_fov = 60 + + self.background_color = (0, 0, 0, 0) + + def test_paint(self): + glTranslatef(-2.5, 0.5, -6.0) + glColor3f( 1.0, 1.5, 0.0 ) + glPolygonMode(GL_FRONT, GL_FILL) + glBegin(GL_TRIANGLES) + glVertex3f(2.0,-1.2,0.0) + glVertex3f(2.6,0.0,0.0) + glVertex3f(2.9,-1.2,0.0) + glEnd() + glFlush() + + def initializeGL(self): + glClearDepth(1.0) + glDepthFunc(GL_LESS) + glEnable(GL_DEPTH_TEST) + glShadeModel(GL_SMOOTH) + glMatrixMode(GL_PROJECTION) + glLoadIdentity() + gluPerspective(45.0,1.33,0.1, 100.0) + glMatrixMode(GL_MODELVIEW) + + + def default_viewport(self): + return 0, 0, int(self.width() * self.devicePixelRatioF()), int(self.height() * self.devicePixelRatioF()) + + def paintGL(self): + """ + Paint the GL viewport + """ + # glViewport(*self.default_viewport()) + + self.set_projection() + # self.set_model_view() + + glClearColor(*self.background_color) + glClear( GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT ) + + self.test_paint() + + # self.drawItemTree(useItemNames=useItemNames) + + def projection_matrix(self): + x0, y0, w, h = self.default_viewport() + dist = self.view_distance + nearClip = dist * 0.001 + farClip = dist * 1000. + + r = nearClip * np.tan(0.5 * np.radians(self.view_fov)) + t = r * h / w + + tr = QtGui.QMatrix4x4() + tr.frustum(-r, r, -t, t, nearClip, farClip) + return tr + + def set_projection(self): + glMatrixMode(GL_PROJECTION) + glLoadMatrixf(np.array(self.projection_matrix().data(), dtype=np.float32)) + + def view_matrix(self): + tr = QtGui.QMatrix4x4() + tr.translate( 0.0, 0.0, -self.view_distance) + tr.rotate(self.view_elevation-90, 1, 0, 0) + tr.rotate(self.view_azimuth+90, 0, 0, -1) + tr.translate(*self.view_centre) + return tr + + def set_model_view(self): + glMatrixMode(GL_MODELVIEW) + glLoadMatrixf(np.array(self.view_matrix().data(), dtype=np.float32)) + +def main(): + """ Show a demo of the opengl window """ + import os + + os.environ["QT_ENABLE_HIGHDPI_SCALING"] = "1" + app = QtWidgets.QApplication([]) + + mainWindow = QtWidgets.QMainWindow() + viewer = GraphWidget(mainWindow) + + mainWindow.setCentralWidget(viewer) + + mainWindow.show() + + mainWindow.resize(600, 600) + app.exec_() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/sas/qtgui/MainWindow/MainWindow.py b/src/sas/qtgui/MainWindow/MainWindow.py index 1c7374ca9c..f3838830b2 100644 --- a/src/sas/qtgui/MainWindow/MainWindow.py +++ b/src/sas/qtgui/MainWindow/MainWindow.py @@ -80,6 +80,9 @@ def run_sasview(): app = QApplication([]) + app.setAttribute(Qt.AA_ShareOpenGLContexts) + + #Initialize logger from sas.system.log import SetupLogger SetupLogger(__name__).config_development() diff --git a/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py b/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py index a04927c231..d41a435b0d 100644 --- a/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py +++ b/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py @@ -50,7 +50,7 @@ def __init__(self, parent=None): # Put a barrier that will stop a flood of events going to the calculator self.set_image_data = FloodBarrier[Orientation](self._set_image_data, Orientation(), 0.5) - self.graph = TestWidget(self) + self.graph = gl.GLViewWidget() self.graph.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) @@ -60,53 +60,53 @@ def __init__(self, parent=None): layout.addWidget(self.graph) layout.addWidget(self.controller) self.setLayout(layout) - # - # self.arrow = OrientationViewerGraphics.create_arrow() - # self.image_plane_coordinate_points = np.linspace(-3, 3, 256) - # - # # temporary plot data - # x, y = np.meshgrid(self.image_plane_coordinate_points, self.image_plane_coordinate_points) - # self.image_plane_data = np.zeros_like(x) - # - # self.colormap = mpl.colormaps["viridis"] - # - # self.image_plane_colors = self.colormap(self.image_plane_data) - # - # self.image_plane = gl.GLSurfacePlotItem( - # self.image_plane_coordinate_points, - # self.image_plane_coordinate_points, - # self.image_plane_data, - # self.image_plane_colors - # ) - # - # ghost_alpha = 1/(OrientationViewer.n_ghosts_per_perameter**3) - # self.ghosts = [] - # for a in np.linspace(-1, 1, OrientationViewer.n_ghosts_per_perameter): - # for b in np.linspace(-1, 1, OrientationViewer.n_ghosts_per_perameter): - # for c in np.linspace(-1, 1, OrientationViewer.n_ghosts_per_perameter): - # ghost = OrientationViewerGraphics.create_cube(ghost_alpha) - # self.graph.addItem(ghost) - # self.ghosts.append((a, b, c, ghost)) - # - # - # - # self.graph.addItem(self.arrow) - # self.graph.addItem(self.image_plane) - # - # self.arrow.rotate(180, 1, 0, 0) - # self.arrow.scale(0.05, 0.05, 0.05) - # self.arrow.translate(0,0,1) - # - # self.image_plane.translate(0,0,-0.5) # It's 1 unit thick, so half way - # - # for _, _, _, ghost in self.ghosts: - # ghost.setTransform(OrientationViewerGraphics.createCubeTransform(0, 0, 0, OrientationViewer.cuboid_scaling)) - # - # - # self.controller.valueEdited.connect(self.on_angle_change) - # - # self.calculator = OrientationViewer.create_calculator() - # self.on_angle_change(Orientation()) + + self.arrow = OrientationViewerGraphics.create_arrow() + self.image_plane_coordinate_points = np.linspace(-3, 3, 256) + + # temporary plot data + x, y = np.meshgrid(self.image_plane_coordinate_points, self.image_plane_coordinate_points) + self.image_plane_data = np.zeros_like(x) + + self.colormap = mpl.colormaps["viridis"] + + self.image_plane_colors = self.colormap(self.image_plane_data) + + self.image_plane = gl.GLSurfacePlotItem( + self.image_plane_coordinate_points, + self.image_plane_coordinate_points, + self.image_plane_data, + self.image_plane_colors + ) + + ghost_alpha = 1/(OrientationViewer.n_ghosts_per_perameter**3) + self.ghosts = [] + for a in np.linspace(-1, 1, OrientationViewer.n_ghosts_per_perameter): + for b in np.linspace(-1, 1, OrientationViewer.n_ghosts_per_perameter): + for c in np.linspace(-1, 1, OrientationViewer.n_ghosts_per_perameter): + ghost = OrientationViewerGraphics.create_cube(ghost_alpha) + self.graph.addItem(ghost) + self.ghosts.append((a, b, c, ghost)) + + + + self.graph.addItem(self.arrow) + self.graph.addItem(self.image_plane) + + self.arrow.rotate(180, 1, 0, 0) + self.arrow.scale(0.05, 0.05, 0.05) + self.arrow.translate(0,0,1) + + self.image_plane.translate(0,0,-0.5) # It's 1 unit thick, so half way + + for _, _, _, ghost in self.ghosts: + ghost.setTransform(OrientationViewerGraphics.createCubeTransform(0, 0, 0, OrientationViewer.cuboid_scaling)) + + + self.controller.valueEdited.connect(self.on_angle_change) + + self.calculator = OrientationViewer.create_calculator() + self.on_angle_change(Orientation()) @@ -191,34 +191,6 @@ def scatering_data(self, orientation: Orientation) -> np.ndarray: return np.reshape(data, (OrientationViewer.n_q_samples, OrientationViewer.n_q_samples)) -class TestWidget(QGLWidget): - - def __init__(self, parent): - super().__init__(parent) - self.setMinimumSize(640, 480) - - def paintGL(self): - glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) - glLoadIdentity() - glTranslatef(-2.5, 0.5, -6.0) - glColor3f( 1.0, 1.5, 0.0 ) - glPolygonMode(GL_FRONT, GL_FILL) - glBegin(GL_TRIANGLES) - glVertex3f(2.0,-1.2,0.0) - glVertex3f(2.6,0.0,0.0) - glVertex3f(2.9,-1.2,0.0) - glEnd() - glFlush() - - def initializeGL(self): - glClearDepth(1.0) - glDepthFunc(GL_LESS) - glEnable(GL_DEPTH_TEST) - glShadeModel(GL_SMOOTH) - glMatrixMode(GL_PROJECTION) - glLoadIdentity() - gluPerspective(45.0,1.33,0.1, 100.0) - glMatrixMode(GL_MODELVIEW) def main(): """ Show a demo of the slider """ From 57d2620907d3cc11c1837b7451b718ca7ed3757d Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Mon, 31 Oct 2022 15:53:22 +0000 Subject: [PATCH 24/87] This is why working with opengl sucks --- src/sas/qtgui/GL/GraphWidget.py | 73 +++++++++++++++++++++++++++------ 1 file changed, 60 insertions(+), 13 deletions(-) diff --git a/src/sas/qtgui/GL/GraphWidget.py b/src/sas/qtgui/GL/GraphWidget.py index 1ebcdc41d6..ea09fbcd6c 100644 --- a/src/sas/qtgui/GL/GraphWidget.py +++ b/src/sas/qtgui/GL/GraphWidget.py @@ -1,7 +1,7 @@ from typing import Optional, Tuple import numpy as np -from PyQt5 import QtWidgets, Qt, QtGui, QtOpenGL +from PyQt5 import QtWidgets, Qt, QtGui, QtOpenGL, QtCore from OpenGL.GL import * from OpenGL.GLU import * @@ -20,6 +20,18 @@ def __init__(self, parent): self.background_color = (0, 0, 0, 0) + # Mouse control settings + self.mouse_sensitivity_azimuth = 0.01 + self.mouse_sensitivity_elevation = 0.01 + self.mouse_sensitivity_distance = 1.0 + self.mouse_sensitivity_position = 1.0 + + # Mouse control variables + self.mouse_position = None + self.view_centre_difference = None + self.view_azimuth_difference = None + self.view_elevation_difference = None + def test_paint(self): glTranslatef(-2.5, 0.5, -6.0) glColor3f( 1.0, 1.5, 0.0 ) @@ -36,11 +48,6 @@ def initializeGL(self): glDepthFunc(GL_LESS) glEnable(GL_DEPTH_TEST) glShadeModel(GL_SMOOTH) - glMatrixMode(GL_PROJECTION) - glLoadIdentity() - gluPerspective(45.0,1.33,0.1, 100.0) - glMatrixMode(GL_MODELVIEW) - def default_viewport(self): return 0, 0, int(self.width() * self.devicePixelRatioF()), int(self.height() * self.devicePixelRatioF()) @@ -49,17 +56,15 @@ def paintGL(self): """ Paint the GL viewport """ - # glViewport(*self.default_viewport()) self.set_projection() - # self.set_model_view() + self.set_model_view() glClearColor(*self.background_color) glClear( GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT ) self.test_paint() - # self.drawItemTree(useItemNames=useItemNames) def projection_matrix(self): x0, y0, w, h = self.default_viewport() @@ -75,14 +80,27 @@ def projection_matrix(self): return tr def set_projection(self): + glMatrixMode(GL_PROJECTION) - glLoadMatrixf(np.array(self.projection_matrix().data(), dtype=np.float32)) + glLoadIdentity() + gluPerspective(45.0,1.33,0.1, 100.0) + # glLoadMatrixf(np.array(self.projection_matrix().data(), dtype=np.float32)) def view_matrix(self): tr = QtGui.QMatrix4x4() - tr.translate( 0.0, 0.0, -self.view_distance) - tr.rotate(self.view_elevation-90, 1, 0, 0) - tr.rotate(self.view_azimuth+90, 0, 0, -1) + tr.translate(0.0, 0.0, -self.view_distance) + + azimuth = self.view_azimuth + 90 + elevation = self.view_elevation - 90 + + if self.view_azimuth_difference is not None: + azimuth += self.view_azimuth_difference + + if self.view_elevation_difference is not None: + elevation += self.view_elevation_difference + + tr.rotate(elevation, 1, 0, 0) + tr.rotate(azimuth, 0, 0, -1) tr.translate(*self.view_centre) return tr @@ -90,6 +108,35 @@ def set_model_view(self): glMatrixMode(GL_MODELVIEW) glLoadMatrixf(np.array(self.view_matrix().data(), dtype=np.float32)) + def mousePressEvent(self, ev): + self.mouse_position = ev.localPos() + + def mouseMoveEvent(self, ev): + new_mouse_position = ev.localPos() + diff = new_mouse_position - self.mouse_position + + if ev.buttons() == QtCore.Qt.MouseButton.LeftButton: + self.view_azimuth_difference = self.mouse_sensitivity_azimuth * diff.x() + self.view_elevation_difference = self.mouse_sensitivity_elevation * diff.y() + + elif ev.buttons() == QtCore.Qt.MouseButton.RightButton: + self.view_centre_difference = [self.mouse_sensitivity_position * diff.x(), 0, + self.mouse_sensitivity_position * diff.y()] + + self.update() + def mouseReleaseEvent(self, ev): + # Mouse released, add dragging offset the view variables + + # self.view_centre += np.array(self.view_centre_difference) + # self.view_elevation += self.view_elevation_difference + # self.view_azimuth += self.view_azimuth_difference + + self.view_centre_difference = None + self.view_azimuth_difference = None + self.view_elevation_difference = None + + self.update() + def main(): """ Show a demo of the opengl window """ import os From 681d7e3e9aa093c516e2fdd0bc38086631c74f59 Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Mon, 31 Oct 2022 19:28:35 +0000 Subject: [PATCH 25/87] Mouse control kind of works --- src/sas/qtgui/GL/GraphWidget.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/sas/qtgui/GL/GraphWidget.py b/src/sas/qtgui/GL/GraphWidget.py index ea09fbcd6c..c20025af12 100644 --- a/src/sas/qtgui/GL/GraphWidget.py +++ b/src/sas/qtgui/GL/GraphWidget.py @@ -21,8 +21,8 @@ def __init__(self, parent): self.background_color = (0, 0, 0, 0) # Mouse control settings - self.mouse_sensitivity_azimuth = 0.01 - self.mouse_sensitivity_elevation = 0.01 + self.mouse_sensitivity_azimuth = 0.1 + self.mouse_sensitivity_elevation = 0.1 self.mouse_sensitivity_distance = 1.0 self.mouse_sensitivity_position = 1.0 @@ -37,9 +37,9 @@ def test_paint(self): glColor3f( 1.0, 1.5, 0.0 ) glPolygonMode(GL_FRONT, GL_FILL) glBegin(GL_TRIANGLES) - glVertex3f(2.0,-1.2,0.0) - glVertex3f(2.6,0.0,0.0) - glVertex3f(2.9,-1.2,0.0) + glVertex3f(1.0,0.0,0.0) + glVertex3f(0.0,1.0,0.0) + glVertex3f(0.0,0.0,0.0) glEnd() glFlush() @@ -58,11 +58,13 @@ def paintGL(self): """ self.set_projection() - self.set_model_view() glClearColor(*self.background_color) glClear( GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT ) + self.set_model_view() + + self.test_paint() @@ -91,7 +93,7 @@ def view_matrix(self): tr.translate(0.0, 0.0, -self.view_distance) azimuth = self.view_azimuth + 90 - elevation = self.view_elevation - 90 + elevation = self.view_elevation if self.view_azimuth_difference is not None: azimuth += self.view_azimuth_difference @@ -99,7 +101,7 @@ def view_matrix(self): if self.view_elevation_difference is not None: elevation += self.view_elevation_difference - tr.rotate(elevation, 1, 0, 0) + # tr.rotate(elevation, 1, 0, 0) tr.rotate(azimuth, 0, 0, -1) tr.translate(*self.view_centre) return tr @@ -128,8 +130,8 @@ def mouseReleaseEvent(self, ev): # Mouse released, add dragging offset the view variables # self.view_centre += np.array(self.view_centre_difference) - # self.view_elevation += self.view_elevation_difference - # self.view_azimuth += self.view_azimuth_difference + self.view_elevation += self.view_elevation_difference + self.view_azimuth += self.view_azimuth_difference self.view_centre_difference = None self.view_azimuth_difference = None From 464623ba42131b4f1b426917926770239b021be9 Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Mon, 7 Nov 2022 15:22:49 +0000 Subject: [PATCH 26/87] Minor changes --- src/sas/qtgui/GL/GraphWidget.py | 8 ++++---- .../Utilities/OrientationViewer/OrientationViewer.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/sas/qtgui/GL/GraphWidget.py b/src/sas/qtgui/GL/GraphWidget.py index c20025af12..5ee3f47377 100644 --- a/src/sas/qtgui/GL/GraphWidget.py +++ b/src/sas/qtgui/GL/GraphWidget.py @@ -85,14 +85,14 @@ def set_projection(self): glMatrixMode(GL_PROJECTION) glLoadIdentity() - gluPerspective(45.0,1.33,0.1, 100.0) - # glLoadMatrixf(np.array(self.projection_matrix().data(), dtype=np.float32)) + # gluPerspective(45.0,1.33,0.1, 100.0) + glLoadMatrixf(np.array(self.projection_matrix().data(), dtype=np.float32)) def view_matrix(self): tr = QtGui.QMatrix4x4() tr.translate(0.0, 0.0, -self.view_distance) - azimuth = self.view_azimuth + 90 + azimuth = self.view_azimuth elevation = self.view_elevation if self.view_azimuth_difference is not None: @@ -101,7 +101,7 @@ def view_matrix(self): if self.view_elevation_difference is not None: elevation += self.view_elevation_difference - # tr.rotate(elevation, 1, 0, 0) + tr.rotate(elevation, 1, 0, 0) tr.rotate(azimuth, 0, 0, -1) tr.translate(*self.view_centre) return tr diff --git a/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py b/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py index d41a435b0d..fbf2d85531 100644 --- a/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py +++ b/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py @@ -193,7 +193,7 @@ def scatering_data(self, orientation: Orientation) -> np.ndarray: def main(): - """ Show a demo of the slider """ + import os os.environ["QT_ENABLE_HIGHDPI_SCALING"] = "1" From e4f5f46c7d266926aca654471a07e5a1fbdf4125 Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Tue, 8 Nov 2022 16:56:42 +0000 Subject: [PATCH 27/87] GL rendering structure --- src/sas/qtgui/GL/Color.py | 11 +++++ src/sas/qtgui/GL/Cube.py | 18 +++++++ src/sas/qtgui/GL/Renderable.py | 9 +++- src/sas/qtgui/GL/SolidModel.py | 38 ++++++++++++-- src/sas/qtgui/GL/WireModel.py | 49 +++++++++++++++++-- .../qtgui/GL/transformations/RenderFilter.py | 10 ++++ 6 files changed, 124 insertions(+), 11 deletions(-) create mode 100644 src/sas/qtgui/GL/Color.py create mode 100644 src/sas/qtgui/GL/transformations/RenderFilter.py diff --git a/src/sas/qtgui/GL/Color.py b/src/sas/qtgui/GL/Color.py new file mode 100644 index 0000000000..1340285a0e --- /dev/null +++ b/src/sas/qtgui/GL/Color.py @@ -0,0 +1,11 @@ +from OpenGL.GL import glColor4f + +class Color(): + def __init__(self, r: float, g: float, b: float, alpha: float=1.0): + self.r = r + self.g = g + self.b = b + self.alpha = alpha + + def set(self): + glColor4f(self.r, self.g, self.b, self.alpha) \ No newline at end of file diff --git a/src/sas/qtgui/GL/Cube.py b/src/sas/qtgui/GL/Cube.py index e69de29bb2..28bd3b7188 100644 --- a/src/sas/qtgui/GL/Cube.py +++ b/src/sas/qtgui/GL/Cube.py @@ -0,0 +1,18 @@ +from sas.qtgui.GL.WireModel import WireModel +from sas.qtgui.GL.SolidModel import SolidModel + + +class Cube(WireModel, SolidModel): + """ Unit cube centred at 0,0,0""" + + vertices = [ + + ] + + def __init__(self, face_colors=None, edge_colors=None): + self.wireframe_render_enabled = edge_colors is not None + self.solid_render_enabled = face_colors is not None + + + + diff --git a/src/sas/qtgui/GL/Renderable.py b/src/sas/qtgui/GL/Renderable.py index 82613b365e..f29c72cf75 100644 --- a/src/sas/qtgui/GL/Renderable.py +++ b/src/sas/qtgui/GL/Renderable.py @@ -1,12 +1,17 @@ from abc import ABC, abstractmethod +import logging + +logger = logging.getLogger("GL Subsystem") + class Renderable(ABC): """ Interface for everything that can be rendered with the OpenGL widget""" @abstractmethod def render_wireframe(self): - pass + logger.debug(f"{self.__class__} does not support wireframe rendering") + @abstractmethod def render_solid(self): - pass \ No newline at end of file + logger.debug(f"{self.__class__} does not support solid rendering") diff --git a/src/sas/qtgui/GL/SolidModel.py b/src/sas/qtgui/GL/SolidModel.py index eb42fe7e37..680fe525ad 100644 --- a/src/sas/qtgui/GL/SolidModel.py +++ b/src/sas/qtgui/GL/SolidModel.py @@ -1,15 +1,45 @@ -from abc import ABC, abstractmethod +from typing import Sequence, Tuple, Union +from abc import abstractmethod import numpy as np -class SolidModel(ABC): +from sas.qtgui.GL.Renderable import Renderable +from sas.qtgui.GL.Color import Color + + +class SolidModel(Renderable): + """ Base class for the two solid models""" + @property + def solid_render_enabled(self) -> bool: + return True + + @abstractmethod + @property + def vertices(self) -> Sequence[Tuple[float, float, float]]: + pass + + @abstractmethod + @property + def meshes(self) -> Sequence[Sequence[Sequence[int]]]: + pass + + +class SolidVertexModel(SolidModel): @abstractmethod @property - def vertices(self) -> np.ndarray: + def vertex_colors(self) -> Union[Sequence[Color], Color]: pass + def render_solid(self): + pass + +class SolidFaceModel(SolidModel): + @abstractmethod @property - def faces(self) -> np.ndarray: + def face_colours(self) -> np.ndarray: + pass + + def render_solid(self): pass \ No newline at end of file diff --git a/src/sas/qtgui/GL/WireModel.py b/src/sas/qtgui/GL/WireModel.py index 91e7b34a23..dc14727f52 100644 --- a/src/sas/qtgui/GL/WireModel.py +++ b/src/sas/qtgui/GL/WireModel.py @@ -1,24 +1,63 @@ - +from typing import Union, Tuple, Sequence from abc import abstractmethod import numpy as np +from OpenGL.GL import * +from OpenGL.GLU import * + from sas.qtgui.GL.Renderable import Renderable +from sas.qtgui.GL.Color import Color + class WireModel(Renderable): + @property + def wireframe_render_enabled(self) -> bool: + return True + + @abstractmethod + @property + def vertices(self) -> Sequence[Tuple[float, float, float]]: + pass + @abstractmethod @property - def vertices(self) -> np.ndarray: + def edges(self) -> Sequence[Tuple[int, int]]: pass @abstractmethod @property - def edges(self) -> np.ndarray: + def edge_colors(self) -> Union[Sequence[Color], Color]: pass @abstractmethod @property - def edge_colors(self) -> np.ndarray: - pass \ No newline at end of file + def render_wireframe(self): + if self.wireframe_render_enabled: + vertices = self.vertices + colors = self.edge_colors + + if isinstance(colors, Color): + + glBegin(GL_LINES) + colors.set() + + for edge in self.edges: + glVertex3f(vertices[edge[0]]) + glVertex3f(vertices[edge[1]]) + + glEnd(GL_LINES) + + else: # Assume here that the type is correctly specified, thus colors are a sequence + + glBegin(GL_LINES) + for edge, color in zip(self.edges, colors): + + color.set() + + glVertex3f(vertices[edge[0]]) + glVertex3f(vertices[edge[1]]) + + glEnd(GL_LINES) \ No newline at end of file diff --git a/src/sas/qtgui/GL/transformations/RenderFilter.py b/src/sas/qtgui/GL/transformations/RenderFilter.py new file mode 100644 index 0000000000..c1cb290dd7 --- /dev/null +++ b/src/sas/qtgui/GL/transformations/RenderFilter.py @@ -0,0 +1,10 @@ +import numpy as np + +from sas.qtgui.GL.transformations.SceneGraphNode import SceneGraphNode +from sas.qtgui.GL.Renderable import Renderable + +from OpenGL.GL import * +from OpenGL.GLU import * + +class RenderFlag(SceneGraphNode): + pass \ No newline at end of file From 3a2594db57ca36748ad672652579388386039728 Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Wed, 9 Nov 2022 10:35:52 +0000 Subject: [PATCH 28/87] Cube wireframe now vaguely functional --- src/sas/qtgui/GL/Color.py | 14 ++-- src/sas/qtgui/GL/Cube.py | 60 +++++++++++++-- src/sas/qtgui/GL/GraphWidget.py | 19 ++++- src/sas/qtgui/GL/Models.py | 127 ++++++++++++++++++++++++++++++++ src/sas/qtgui/GL/Renderable.py | 2 - src/sas/qtgui/GL/SolidModel.py | 45 ----------- src/sas/qtgui/GL/WireModel.py | 63 ---------------- 7 files changed, 207 insertions(+), 123 deletions(-) create mode 100644 src/sas/qtgui/GL/Models.py delete mode 100644 src/sas/qtgui/GL/SolidModel.py delete mode 100644 src/sas/qtgui/GL/WireModel.py diff --git a/src/sas/qtgui/GL/Color.py b/src/sas/qtgui/GL/Color.py index 1340285a0e..f55b423ead 100644 --- a/src/sas/qtgui/GL/Color.py +++ b/src/sas/qtgui/GL/Color.py @@ -2,10 +2,14 @@ class Color(): def __init__(self, r: float, g: float, b: float, alpha: float=1.0): - self.r = r - self.g = g - self.b = b - self.alpha = alpha + self.r = float(r) + self.g = float(g) + self.b = float(b) + self.alpha = float(alpha) def set(self): - glColor4f(self.r, self.g, self.b, self.alpha) \ No newline at end of file + glColor4f(self.r, self.g, self.b, self.alpha) + + + def __repr__(self): + return f"{self.__class__.__name__}({self.r}, {self.g}, {self.b}, {self.alpha})" \ No newline at end of file diff --git a/src/sas/qtgui/GL/Cube.py b/src/sas/qtgui/GL/Cube.py index 28bd3b7188..e0865454c6 100644 --- a/src/sas/qtgui/GL/Cube.py +++ b/src/sas/qtgui/GL/Cube.py @@ -1,17 +1,63 @@ -from sas.qtgui.GL.WireModel import WireModel -from sas.qtgui.GL.SolidModel import SolidModel +from typing import Optional, Union, Sequence +from sas.qtgui.GL.Models import FullFaceModel, WireModel +from sas.qtgui.GL.Color import Color -class Cube(WireModel, SolidModel): + +class Cube(WireModel): """ Unit cube centred at 0,0,0""" - vertices = [ + cube_vertices = [ + (-0.5, -0.5, -0.5), + (-0.5, -0.5, 0.5), + (-0.5, 0.5, -0.5), + (-0.5, 0.5, 0.5), + ( 0.5, -0.5, -0.5), + ( 0.5, -0.5, 0.5), + ( 0.5, 0.5, -0.5), + ( 0.5, 0.5, 0.5) + ] + cube_edges = [ + (0, 1), # Front face + (1, 5), + (5, 4), + (4, 0), + (2, 3), # Back face + (3, 7), + (7, 6), + (6, 2), + (1, 3), # between faces + (5, 7), + (4, 6), + (0, 2) ] - def __init__(self, face_colors=None, edge_colors=None): - self.wireframe_render_enabled = edge_colors is not None - self.solid_render_enabled = face_colors is not None + def __init__(self, + face_colors: Optional[Union[Sequence[Color],Color]]=None, + edge_colors: Optional[Union[Sequence[Color],Color]]=None): + + super().__init__( + vertices=Cube.cube_vertices, + edges=Cube.cube_edges, + edge_colors=edge_colors) + + self.vertices = Cube.cube_vertices + self.edges = Cube.cube_edges + + if edge_colors is None: + self.wireframe_render_enabled = False + self.edge_colors = [] + else: + self.wireframe_render_enabled = True + self.edge_colors = edge_colors + + if face_colors is not None: + self.solid_render_enabled = False + self.face_colors = [] + else: + self.solid_render_enabled = True + self.face_colors = face_colors diff --git a/src/sas/qtgui/GL/GraphWidget.py b/src/sas/qtgui/GL/GraphWidget.py index 5ee3f47377..6c73266bcf 100644 --- a/src/sas/qtgui/GL/GraphWidget.py +++ b/src/sas/qtgui/GL/GraphWidget.py @@ -1,10 +1,14 @@ -from typing import Optional, Tuple +from typing import Optional, Tuple, List import numpy as np from PyQt5 import QtWidgets, Qt, QtGui, QtOpenGL, QtCore from OpenGL.GL import * from OpenGL.GLU import * + +from sas.qtgui.GL.Renderable import Renderable +from sas.qtgui.GL.Color import Color + class GraphWidget(QtOpenGL.QGLWidget): @@ -32,6 +36,8 @@ def __init__(self, parent): self.view_azimuth_difference = None self.view_elevation_difference = None + self._items: List[Renderable] = [] + def test_paint(self): glTranslatef(-2.5, 0.5, -6.0) glColor3f( 1.0, 1.5, 0.0 ) @@ -67,6 +73,11 @@ def paintGL(self): self.test_paint() + for item in self._items: + item.render_solid() + item.render_wireframe() + + def projection_matrix(self): x0, y0, w, h = self.default_viewport() @@ -139,9 +150,13 @@ def mouseReleaseEvent(self, ev): self.update() + def add(self, item: Renderable): + self._items.append(item) + def main(): """ Show a demo of the opengl window """ import os + from sas.qtgui.GL.Cube import Cube os.environ["QT_ENABLE_HIGHDPI_SCALING"] = "1" app = QtWidgets.QApplication([]) @@ -149,6 +164,8 @@ def main(): mainWindow = QtWidgets.QMainWindow() viewer = GraphWidget(mainWindow) + viewer.add(Cube(edge_colors=Color(1,1,1))) + mainWindow.setCentralWidget(viewer) mainWindow.show() diff --git a/src/sas/qtgui/GL/Models.py b/src/sas/qtgui/GL/Models.py new file mode 100644 index 0000000000..35484c5beb --- /dev/null +++ b/src/sas/qtgui/GL/Models.py @@ -0,0 +1,127 @@ +from typing import Sequence, Tuple, Union, Optional +from abc import abstractmethod + +import numpy as np + +from OpenGL.GL import * + +from sas.qtgui.GL.Renderable import Renderable +from sas.qtgui.GL.Color import Color + + +class SolidModel(Renderable): + """ Base class for the two solid models""" + def __init__(self, + vertices: Sequence[Tuple[float, float, float]], + meshes: Sequence[Sequence[int]]): + + self.solid_render_enabled = False + self.vertices = vertices + self.meshes = meshes + + +class SolidVertexModel(SolidModel): + def __init__(self, + vertices: Sequence[Tuple[float, float, float]], + meshes: Sequence[Sequence[int]], + vertex_colours: Optional[Union[Sequence[Color], Color]]): + + super().__init__(vertices, meshes) + + self.vertex_colours = vertex_colours + + def render_solid(self): + pass + + +class SolidFaceModel(SolidModel): + def __init__(self, + vertices: Sequence[Tuple[float, float, float]], + meshes: Sequence[Sequence[int]], + face_colours: Optional[Union[Sequence[Color], Color]]): + + super().__init__(vertices, meshes) + + self.face_colours = face_colours + + def render_solid(self): + pass + + +class WireModel(Renderable): + def __init__(self, + vertices: Sequence[Tuple[float, float, float]], + edges: Sequence[Tuple[int, int]], + edge_colors: Optional[Union[Sequence[Color], Color]]): + + self.wireframe_render_enabled = False + self.edges = edges + self.vertices = vertices + self.edge_colors = edge_colors + + def render_wireframe(self): + if self.wireframe_render_enabled: + vertices = self.vertices + colors = self.edge_colors + + if isinstance(colors, Color): + + glBegin(GL_LINES) + colors.set() + + for edge in self.edges: + glVertex3f(*vertices[edge[0]]) + glVertex3f(*vertices[edge[1]]) + + glEnd() + + else: # Assume here that the type is correctly specified, thus colors are a sequence + + glBegin(GL_LINES) + for edge, color in zip(self.edges, colors): + + color.set() + + glVertex3f(*vertices[edge[0]]) + glVertex3f(*vertices[edge[1]]) + + glEnd() + + +class FullVertexModel(SolidVertexModel, WireModel): + """ Model that has both wireframe and solid, vertex coloured rendering enabled""" + def __init__(self, + vertices: Sequence[Tuple[float, float, float]], + edges: Sequence[Tuple[int, int]], + meshes: Sequence[Sequence[int]], + edge_colors: Optional[Union[Sequence[Color], Color]], + vertex_colors: Optional[Union[Sequence[Color], Color]]): + + SolidVertexModel.__init__(self, + vertices=vertices, + meshes=meshes, + vertex_colours=vertex_colors) + WireModel.__init__(self, + vertices=vertices, + edges=edges, + edge_colors=edge_colors) + + +class FullFaceModel(SolidFaceModel, WireModel): + """ Model that has both wireframe and solid, face coloured rendering enabled""" + + def __init__(self, + vertices: Sequence[Tuple[float, float, float]], + edges: Sequence[Tuple[int, int]], + meshes: Sequence[Sequence[int]], + edge_colors: Optional[Union[Sequence[Color], Color]], + face_colors: Optional[Union[Sequence[Color], Color]]): + + SolidFaceModel.__init__(self, + vertices=vertices, + meshes=meshes, + face_colours=face_colors) + WireModel.__init__(self, + vertices=vertices, + edges=edges, + edge_colors=edge_colors) \ No newline at end of file diff --git a/src/sas/qtgui/GL/Renderable.py b/src/sas/qtgui/GL/Renderable.py index f29c72cf75..b12a275578 100644 --- a/src/sas/qtgui/GL/Renderable.py +++ b/src/sas/qtgui/GL/Renderable.py @@ -7,11 +7,9 @@ class Renderable(ABC): """ Interface for everything that can be rendered with the OpenGL widget""" - @abstractmethod def render_wireframe(self): logger.debug(f"{self.__class__} does not support wireframe rendering") - @abstractmethod def render_solid(self): logger.debug(f"{self.__class__} does not support solid rendering") diff --git a/src/sas/qtgui/GL/SolidModel.py b/src/sas/qtgui/GL/SolidModel.py deleted file mode 100644 index 680fe525ad..0000000000 --- a/src/sas/qtgui/GL/SolidModel.py +++ /dev/null @@ -1,45 +0,0 @@ -from typing import Sequence, Tuple, Union -from abc import abstractmethod - -import numpy as np - -from sas.qtgui.GL.Renderable import Renderable -from sas.qtgui.GL.Color import Color - - -class SolidModel(Renderable): - """ Base class for the two solid models""" - @property - def solid_render_enabled(self) -> bool: - return True - - @abstractmethod - @property - def vertices(self) -> Sequence[Tuple[float, float, float]]: - pass - - @abstractmethod - @property - def meshes(self) -> Sequence[Sequence[Sequence[int]]]: - pass - - -class SolidVertexModel(SolidModel): - - @abstractmethod - @property - def vertex_colors(self) -> Union[Sequence[Color], Color]: - pass - - def render_solid(self): - pass - -class SolidFaceModel(SolidModel): - - @abstractmethod - @property - def face_colours(self) -> np.ndarray: - pass - - def render_solid(self): - pass \ No newline at end of file diff --git a/src/sas/qtgui/GL/WireModel.py b/src/sas/qtgui/GL/WireModel.py deleted file mode 100644 index dc14727f52..0000000000 --- a/src/sas/qtgui/GL/WireModel.py +++ /dev/null @@ -1,63 +0,0 @@ -from typing import Union, Tuple, Sequence - -from abc import abstractmethod - -import numpy as np - -from OpenGL.GL import * -from OpenGL.GLU import * - -from sas.qtgui.GL.Renderable import Renderable -from sas.qtgui.GL.Color import Color - - -class WireModel(Renderable): - - @property - def wireframe_render_enabled(self) -> bool: - return True - - @abstractmethod - @property - def vertices(self) -> Sequence[Tuple[float, float, float]]: - pass - - @abstractmethod - @property - def edges(self) -> Sequence[Tuple[int, int]]: - pass - - @abstractmethod - @property - def edge_colors(self) -> Union[Sequence[Color], Color]: - pass - - @abstractmethod - @property - def render_wireframe(self): - if self.wireframe_render_enabled: - vertices = self.vertices - colors = self.edge_colors - - if isinstance(colors, Color): - - glBegin(GL_LINES) - colors.set() - - for edge in self.edges: - glVertex3f(vertices[edge[0]]) - glVertex3f(vertices[edge[1]]) - - glEnd(GL_LINES) - - else: # Assume here that the type is correctly specified, thus colors are a sequence - - glBegin(GL_LINES) - for edge, color in zip(self.edges, colors): - - color.set() - - glVertex3f(vertices[edge[0]]) - glVertex3f(vertices[edge[1]]) - - glEnd(GL_LINES) \ No newline at end of file From 52143c9a7ac7d01beccf66134524936edd7aca25 Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Fri, 11 Nov 2022 11:48:43 +0000 Subject: [PATCH 29/87] Filled in cube --- src/sas/qtgui/GL/Color.py | 3 + src/sas/qtgui/GL/Cube.py | 25 ++++++- src/sas/qtgui/GL/GraphWidget.py | 2 +- src/sas/qtgui/GL/Models.py | 128 ++++++++++++++++++++++++++------ 4 files changed, 132 insertions(+), 26 deletions(-) diff --git a/src/sas/qtgui/GL/Color.py b/src/sas/qtgui/GL/Color.py index f55b423ead..8959124062 100644 --- a/src/sas/qtgui/GL/Color.py +++ b/src/sas/qtgui/GL/Color.py @@ -1,4 +1,5 @@ from OpenGL.GL import glColor4f +import numpy as np class Color(): def __init__(self, r: float, g: float, b: float, alpha: float=1.0): @@ -10,6 +11,8 @@ def __init__(self, r: float, g: float, b: float, alpha: float=1.0): def set(self): glColor4f(self.r, self.g, self.b, self.alpha) + def to_array(self): + return np.array([self.r, self.g, self.b, self.alpha]) def __repr__(self): return f"{self.__class__.__name__}({self.r}, {self.g}, {self.b}, {self.alpha})" \ No newline at end of file diff --git a/src/sas/qtgui/GL/Cube.py b/src/sas/qtgui/GL/Cube.py index e0865454c6..e6a434bf2c 100644 --- a/src/sas/qtgui/GL/Cube.py +++ b/src/sas/qtgui/GL/Cube.py @@ -1,10 +1,10 @@ from typing import Optional, Union, Sequence -from sas.qtgui.GL.Models import FullFaceModel, WireModel +from sas.qtgui.GL.Models import FullVertexModel, WireModel from sas.qtgui.GL.Color import Color -class Cube(WireModel): +class Cube(FullVertexModel): """ Unit cube centred at 0,0,0""" cube_vertices = [ @@ -33,6 +33,21 @@ class Cube(WireModel): (0, 2) ] + cube_triangles = [ + [(1,2,3), + (1,0,2)], + [(0,6,2), + (0,4,6)], + [(4,7,6), + (4,5,7)], + [(5,3,7), + (5,1,3)], + [(2,7,3), + (2,6,7)], + [(1,4,0), + (1,5,4)] + ] + def __init__(self, face_colors: Optional[Union[Sequence[Color],Color]]=None, edge_colors: Optional[Union[Sequence[Color],Color]]=None): @@ -40,7 +55,9 @@ def __init__(self, super().__init__( vertices=Cube.cube_vertices, edges=Cube.cube_edges, - edge_colors=edge_colors) + triangle_meshes=Cube.cube_triangles, + edge_colors=edge_colors, + vertex_colors=face_colors) self.vertices = Cube.cube_vertices self.edges = Cube.cube_edges @@ -52,7 +69,7 @@ def __init__(self, self.wireframe_render_enabled = True self.edge_colors = edge_colors - if face_colors is not None: + if face_colors is None: self.solid_render_enabled = False self.face_colors = [] else: diff --git a/src/sas/qtgui/GL/GraphWidget.py b/src/sas/qtgui/GL/GraphWidget.py index 6c73266bcf..90555ddcfd 100644 --- a/src/sas/qtgui/GL/GraphWidget.py +++ b/src/sas/qtgui/GL/GraphWidget.py @@ -164,7 +164,7 @@ def main(): mainWindow = QtWidgets.QMainWindow() viewer = GraphWidget(mainWindow) - viewer.add(Cube(edge_colors=Color(1,1,1))) + viewer.add(Cube(edge_colors=Color(1,1,1), face_colors=Color(0,1,0))) mainWindow.setCentralWidget(viewer) diff --git a/src/sas/qtgui/GL/Models.py b/src/sas/qtgui/GL/Models.py index 35484c5beb..8a4f4ed9bb 100644 --- a/src/sas/qtgui/GL/Models.py +++ b/src/sas/qtgui/GL/Models.py @@ -1,5 +1,4 @@ from typing import Sequence, Tuple, Union, Optional -from abc import abstractmethod import numpy as np @@ -9,38 +8,120 @@ from sas.qtgui.GL.Color import Color -class SolidModel(Renderable): +def color_sequence_to_array(colors: Union[Sequence[Color], Color]): + if isinstance(colors, Color): + return None + else: + return np.array([color.to_array for color in colors], dtype=float) + + +class ModelBase(Renderable): + """ Base class for all models""" + + def __init__(self, vertices: Sequence[Tuple[float, float, float]]): + self._vertices = vertices + self._vertex_array = np.array(vertices, dtype=float) + + @property + def vertices(self): + return self._vertices + + @vertices.setter + def vertices(self, new_vertices): + self._vertices = new_vertices + self._vertex_array = np.array(new_vertices, dtype=float) + self._recalculate_normals() + + def _recalculate_normals(self): + pass + + +class SolidModel(ModelBase): """ Base class for the two solid models""" def __init__(self, vertices: Sequence[Tuple[float, float, float]], - meshes: Sequence[Sequence[int]]): + triangle_meshes: Sequence[Sequence[Tuple[int, int, int]]]): + + ModelBase.__init__(self, vertices) self.solid_render_enabled = False - self.vertices = vertices - self.meshes = meshes + + self._triangle_meshes = triangle_meshes + self._triangle_mesh_arrays = [np.array(x) for x in triangle_meshes] + + @property + def triangle_meshes(self) -> Sequence[Sequence[Tuple[int, int, int]]]: + return self._triangle_meshes + + @triangle_meshes.setter + def triangle_meshes(self, new_triangle_meshes: Sequence[Sequence[int]]): + self._triangle_meshes = new_triangle_meshes + self._triangle_mesh_arrays = [np.array(x) for x in new_triangle_meshes] + self._recalculate_normals() + + def _recalculate_normals(self): + # TODO: Will be required if a reflectance model is used + pass class SolidVertexModel(SolidModel): def __init__(self, vertices: Sequence[Tuple[float, float, float]], - meshes: Sequence[Sequence[int]], - vertex_colours: Optional[Union[Sequence[Color], Color]]): + triangle_meshes: Sequence[Sequence[Tuple[int, int, int]]], + vertex_colors: Optional[Union[Sequence[Color], Color]]): + + super().__init__(vertices, triangle_meshes) + + self._vertex_colors = vertex_colors + self._vertex_color_array = None if isinstance(vertex_colors, Color) else np.array(vertex_colors, dtype=float) - super().__init__(vertices, meshes) + self.solid_render_enabled = self.vertex_colors is not None - self.vertex_colours = vertex_colours + @property + def vertex_colors(self): + return self._vertex_colors + + @vertex_colors.setter + def vertex_colors(self, new_vertex_colors): + self.vertex_colors = new_vertex_colors + self._vertex_color_array = color_sequence_to_array(new_vertex_colors) + self.solid_render_enabled = self.vertex_colors is not None def render_solid(self): - pass + if self.solid_render_enabled: + + if isinstance(self._vertex_colors, Color): + + glEnableClientState(GL_VERTEX_ARRAY) + self._vertex_colors.set() + + glVertexPointerf(self._vertex_array) + + for triangle_mesh in self._triangle_mesh_arrays: + glDrawElementsui(GL_TRIANGLES, triangle_mesh) + + + else: + + # TODO: This branch of the if clause needs testing + + glEnableClientState(GL_VERTEX_ARRAY) + glEnableClientState(GL_COLOR_ARRAY) + + glVertexPointerf(self._vertex_array) + glColorPointerf(self._vertex_color_array) + + for triangle_mesh in self._triangle_mesh_arrays: + glDrawElementsui(GL_TRIANGLES, triangle_mesh) class SolidFaceModel(SolidModel): def __init__(self, vertices: Sequence[Tuple[float, float, float]], - meshes: Sequence[Sequence[int]], + triangle_meshes: Sequence[Sequence[Tuple[int, int, int]]], face_colours: Optional[Union[Sequence[Color], Color]]): - super().__init__(vertices, meshes) + super().__init__(vertices, triangle_meshes) self.face_colours = face_colours @@ -48,15 +129,18 @@ def render_solid(self): pass -class WireModel(Renderable): +class WireModel(ModelBase): + """ Wireframe Model """ + def __init__(self, vertices: Sequence[Tuple[float, float, float]], edges: Sequence[Tuple[int, int]], edge_colors: Optional[Union[Sequence[Color], Color]]): + super().__init__(vertices) + self.wireframe_render_enabled = False self.edges = edges - self.vertices = vertices self.edge_colors = edge_colors def render_wireframe(self): @@ -75,7 +159,7 @@ def render_wireframe(self): glEnd() - else: # Assume here that the type is correctly specified, thus colors are a sequence + else: # Assume here that the correct type is supplied, thus colors are a sequence glBegin(GL_LINES) for edge, color in zip(self.edges, colors): @@ -93,35 +177,37 @@ class FullVertexModel(SolidVertexModel, WireModel): def __init__(self, vertices: Sequence[Tuple[float, float, float]], edges: Sequence[Tuple[int, int]], - meshes: Sequence[Sequence[int]], + triangle_meshes: Sequence[Sequence[Tuple[int, int, int]]], edge_colors: Optional[Union[Sequence[Color], Color]], vertex_colors: Optional[Union[Sequence[Color], Color]]): SolidVertexModel.__init__(self, vertices=vertices, - meshes=meshes, - vertex_colours=vertex_colors) + triangle_meshes=triangle_meshes, + vertex_colors=vertex_colors) WireModel.__init__(self, vertices=vertices, edges=edges, edge_colors=edge_colors) + + class FullFaceModel(SolidFaceModel, WireModel): """ Model that has both wireframe and solid, face coloured rendering enabled""" def __init__(self, vertices: Sequence[Tuple[float, float, float]], edges: Sequence[Tuple[int, int]], - meshes: Sequence[Sequence[int]], + triangle_meshes: Sequence[Sequence[Tuple[int, int, int]]], edge_colors: Optional[Union[Sequence[Color], Color]], face_colors: Optional[Union[Sequence[Color], Color]]): SolidFaceModel.__init__(self, vertices=vertices, - meshes=meshes, + triangle_meshes=triangle_meshes, face_colours=face_colors) WireModel.__init__(self, vertices=vertices, edges=edges, - edge_colors=edge_colors) \ No newline at end of file + edge_colors=edge_colors) From f4ef7f39275c5a03c242b6c3db4d87d660187fed Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Fri, 11 Nov 2022 16:32:09 +0000 Subject: [PATCH 30/87] New widget functioning in main GUI --- src/sas/qtgui/GL/GraphWidget.py | 2 +- .../OrientationViewer/OrientationViewer.py | 104 ++++----- .../OrientationViewer/OrientationViewerOld.py | 218 ++++++++++++++++++ src/sas/system/log.ini | 12 +- 4 files changed, 278 insertions(+), 58 deletions(-) create mode 100644 src/sas/qtgui/Utilities/OrientationViewer/OrientationViewerOld.py diff --git a/src/sas/qtgui/GL/GraphWidget.py b/src/sas/qtgui/GL/GraphWidget.py index 90555ddcfd..91ef1cc76e 100644 --- a/src/sas/qtgui/GL/GraphWidget.py +++ b/src/sas/qtgui/GL/GraphWidget.py @@ -12,7 +12,7 @@ class GraphWidget(QtOpenGL.QGLWidget): - def __init__(self, parent): + def __init__(self, parent=None): super().__init__(parent) self.setMinimumSize(640, 480) diff --git a/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py b/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py index fbf2d85531..715bb4eb67 100644 --- a/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py +++ b/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py @@ -7,15 +7,12 @@ import matplotlib as mpl -import pyqtgraph.opengl as gl -from OpenGL.GL import * -from OpenGL.GLU import * -from PyQt5.QtOpenGL import QGLWidget - from sasmodels.core import load_model_info, build_model from sasmodels.data import empty_data2D from sasmodels.direct_model import DirectModel +from sas.qtgui.GL.GraphWidget import GraphWidget + from sas.qtgui.Utilities.OrientationViewer.OrientationViewerController import OrientationViewierController, Orientation from sas.qtgui.Utilities.OrientationViewer.OrientationViewerGraphics import OrientationViewerGraphics from sas.qtgui.Utilities.OrientationViewer.FloodBarrier import FloodBarrier @@ -23,7 +20,6 @@ - class OrientationViewer(QtWidgets.QWidget): # Dimensions of scattering cuboid @@ -50,7 +46,7 @@ def __init__(self, parent=None): # Put a barrier that will stop a flood of events going to the calculator self.set_image_data = FloodBarrier[Orientation](self._set_image_data, Orientation(), 0.5) - self.graph = gl.GLViewWidget() + self.graph = GraphWidget() self.graph.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) @@ -60,53 +56,53 @@ def __init__(self, parent=None): layout.addWidget(self.graph) layout.addWidget(self.controller) self.setLayout(layout) - - self.arrow = OrientationViewerGraphics.create_arrow() - self.image_plane_coordinate_points = np.linspace(-3, 3, 256) - - # temporary plot data - x, y = np.meshgrid(self.image_plane_coordinate_points, self.image_plane_coordinate_points) - self.image_plane_data = np.zeros_like(x) - - self.colormap = mpl.colormaps["viridis"] - - self.image_plane_colors = self.colormap(self.image_plane_data) - - self.image_plane = gl.GLSurfacePlotItem( - self.image_plane_coordinate_points, - self.image_plane_coordinate_points, - self.image_plane_data, - self.image_plane_colors - ) - - ghost_alpha = 1/(OrientationViewer.n_ghosts_per_perameter**3) - self.ghosts = [] - for a in np.linspace(-1, 1, OrientationViewer.n_ghosts_per_perameter): - for b in np.linspace(-1, 1, OrientationViewer.n_ghosts_per_perameter): - for c in np.linspace(-1, 1, OrientationViewer.n_ghosts_per_perameter): - ghost = OrientationViewerGraphics.create_cube(ghost_alpha) - self.graph.addItem(ghost) - self.ghosts.append((a, b, c, ghost)) - - - - self.graph.addItem(self.arrow) - self.graph.addItem(self.image_plane) - - self.arrow.rotate(180, 1, 0, 0) - self.arrow.scale(0.05, 0.05, 0.05) - self.arrow.translate(0,0,1) - - self.image_plane.translate(0,0,-0.5) # It's 1 unit thick, so half way - - for _, _, _, ghost in self.ghosts: - ghost.setTransform(OrientationViewerGraphics.createCubeTransform(0, 0, 0, OrientationViewer.cuboid_scaling)) - - - self.controller.valueEdited.connect(self.on_angle_change) - - self.calculator = OrientationViewer.create_calculator() - self.on_angle_change(Orientation()) + # + # self.arrow = OrientationViewerGraphics.create_arrow() + # self.image_plane_coordinate_points = np.linspace(-3, 3, 256) + # + # # temporary plot data + # x, y = np.meshgrid(self.image_plane_coordinate_points, self.image_plane_coordinate_points) + # self.image_plane_data = np.zeros_like(x) + # + # self.colormap = mpl.colormaps["viridis"] + # + # self.image_plane_colors = self.colormap(self.image_plane_data) + # + # self.image_plane = gl.GLSurfacePlotItem( + # self.image_plane_coordinate_points, + # self.image_plane_coordinate_points, + # self.image_plane_data, + # self.image_plane_colors + # ) + # + # ghost_alpha = 1/(OrientationViewer.n_ghosts_per_perameter**3) + # self.ghosts = [] + # for a in np.linspace(-1, 1, OrientationViewer.n_ghosts_per_perameter): + # for b in np.linspace(-1, 1, OrientationViewer.n_ghosts_per_perameter): + # for c in np.linspace(-1, 1, OrientationViewer.n_ghosts_per_perameter): + # ghost = OrientationViewerGraphics.create_cube(ghost_alpha) + # self.graph.addItem(ghost) + # self.ghosts.append((a, b, c, ghost)) + # + # + # + # self.graph.addItem(self.arrow) + # self.graph.addItem(self.image_plane) + # + # self.arrow.rotate(180, 1, 0, 0) + # self.arrow.scale(0.05, 0.05, 0.05) + # self.arrow.translate(0,0,1) + # + # self.image_plane.translate(0,0,-0.5) # It's 1 unit thick, so half way + # + # for _, _, _, ghost in self.ghosts: + # ghost.setTransform(OrientationViewerGraphics.createCubeTransform(0, 0, 0, OrientationViewer.cuboid_scaling)) + # + # + # self.controller.valueEdited.connect(self.on_angle_change) + # + # self.calculator = OrientationViewer.create_calculator() + # self.on_angle_change(Orientation()) diff --git a/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewerOld.py b/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewerOld.py new file mode 100644 index 0000000000..fbf2d85531 --- /dev/null +++ b/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewerOld.py @@ -0,0 +1,218 @@ +from typing import Optional +import numpy as np + +from PyQt5 import QtWidgets +from PyQt5.QtWidgets import QSizePolicy +from PyQt5.QtCore import Qt + +import matplotlib as mpl + +import pyqtgraph.opengl as gl +from OpenGL.GL import * +from OpenGL.GLU import * +from PyQt5.QtOpenGL import QGLWidget + +from sasmodels.core import load_model_info, build_model +from sasmodels.data import empty_data2D +from sasmodels.direct_model import DirectModel + +from sas.qtgui.Utilities.OrientationViewer.OrientationViewerController import OrientationViewierController, Orientation +from sas.qtgui.Utilities.OrientationViewer.OrientationViewerGraphics import OrientationViewerGraphics +from sas.qtgui.Utilities.OrientationViewer.FloodBarrier import FloodBarrier + + + + + +class OrientationViewer(QtWidgets.QWidget): + + # Dimensions of scattering cuboid + a = 0.1 + b = 0.4 + c = 1.0 + + cuboid_scaling = [a, b, c] + + n_ghosts_per_perameter = 8 + n_q_samples = 256 + log_I_max = 10 + log_I_min = -3 + q_max = 0.5 + polydispersity_distribution = "gaussian" + + log_I_range = log_I_max - log_I_min + + def __init__(self, parent=None): + super().__init__() + + self.parent = parent + + # Put a barrier that will stop a flood of events going to the calculator + self.set_image_data = FloodBarrier[Orientation](self._set_image_data, Orientation(), 0.5) + + self.graph = gl.GLViewWidget() + + self.graph.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + + self.controller = OrientationViewierController() + + layout = QtWidgets.QVBoxLayout() + layout.addWidget(self.graph) + layout.addWidget(self.controller) + self.setLayout(layout) + + self.arrow = OrientationViewerGraphics.create_arrow() + self.image_plane_coordinate_points = np.linspace(-3, 3, 256) + + # temporary plot data + x, y = np.meshgrid(self.image_plane_coordinate_points, self.image_plane_coordinate_points) + self.image_plane_data = np.zeros_like(x) + + self.colormap = mpl.colormaps["viridis"] + + self.image_plane_colors = self.colormap(self.image_plane_data) + + self.image_plane = gl.GLSurfacePlotItem( + self.image_plane_coordinate_points, + self.image_plane_coordinate_points, + self.image_plane_data, + self.image_plane_colors + ) + + ghost_alpha = 1/(OrientationViewer.n_ghosts_per_perameter**3) + self.ghosts = [] + for a in np.linspace(-1, 1, OrientationViewer.n_ghosts_per_perameter): + for b in np.linspace(-1, 1, OrientationViewer.n_ghosts_per_perameter): + for c in np.linspace(-1, 1, OrientationViewer.n_ghosts_per_perameter): + ghost = OrientationViewerGraphics.create_cube(ghost_alpha) + self.graph.addItem(ghost) + self.ghosts.append((a, b, c, ghost)) + + + + self.graph.addItem(self.arrow) + self.graph.addItem(self.image_plane) + + self.arrow.rotate(180, 1, 0, 0) + self.arrow.scale(0.05, 0.05, 0.05) + self.arrow.translate(0,0,1) + + self.image_plane.translate(0,0,-0.5) # It's 1 unit thick, so half way + + for _, _, _, ghost in self.ghosts: + ghost.setTransform(OrientationViewerGraphics.createCubeTransform(0, 0, 0, OrientationViewer.cuboid_scaling)) + + + self.controller.valueEdited.connect(self.on_angle_change) + + self.calculator = OrientationViewer.create_calculator() + self.on_angle_change(Orientation()) + + + + def _set_image_data(self, orientation: Orientation): + + data = self.scatering_data(orientation) + + scaled_data = (np.log(data) - OrientationViewer.log_I_min) / OrientationViewer.log_I_range + self.image_plane_data = np.clip(scaled_data, 0, 1) + + self.image_plane_colors = self.colormap(self.image_plane_data) + + self.image_plane.setData(z=self.image_plane_data, colors=self.image_plane_colors) + + + + def on_angle_change(self, orientation: Optional[Orientation]): + + if orientation is None: + return + + for a, b, c, ghost in self.ghosts: + + ghost.setTransform( + OrientationViewerGraphics.createCubeTransform( + orientation.theta + 0.5*a*orientation.dtheta, + orientation.phi + 0.5*b*orientation.dphi, + orientation.psi + 0.5*c*orientation.dpsi, + OrientationViewer.cuboid_scaling)) + + self.set_image_data(orientation) + + @staticmethod + def create_calculator(): + """ + Make a parallelepiped model calculator for q range -qmax to qmax with n samples + """ + + model_info = load_model_info("parallelepiped") + model = build_model(model_info) + q = np.linspace(-OrientationViewer.q_max, OrientationViewer.q_max, OrientationViewer.n_q_samples) + data = empty_data2D(q, q) + calculator = DirectModel(data, model) + + return calculator + + + def polydispersity_sample_count(self, orientation): + """ Work out how many samples to do for the polydispersity""" + polydispersity = [orientation.dtheta, orientation.dphi, orientation.dpsi] + is_polydisperse = [1 if x > 0 else 0 for x in polydispersity] + n_polydisperse = np.sum(is_polydisperse) + + samples = int(200 / (n_polydisperse**2.2 + 1)) # + + return (samples * x for x in is_polydisperse) + + def scatering_data(self, orientation: Orientation) -> np.ndarray: + + # add the orientation parameters to the model parameters + + theta_pd_n, phi_pd_n, psi_pd_n = self.polydispersity_sample_count(orientation) + + data = self.calculator( + theta=orientation.theta, + theta_pd=orientation.dtheta, + theta_pd_type=OrientationViewer.polydispersity_distribution, + theta_pd_n=theta_pd_n, + phi=orientation.phi, + phi_pd=orientation.dphi, + phi_pd_type=OrientationViewer.polydispersity_distribution, + phi_pd_n=phi_pd_n, + psi=orientation.psi, + psi_pd=orientation.dpsi, + psi_pd_type=OrientationViewer.polydispersity_distribution, + psi_pd_n=psi_pd_n, + a=OrientationViewer.a, + b=OrientationViewer.b, + c=OrientationViewer.c, + background=np.exp(OrientationViewer.log_I_min)) + + return np.reshape(data, (OrientationViewer.n_q_samples, OrientationViewer.n_q_samples)) + + + +def main(): + + import os + + os.environ["QT_ENABLE_HIGHDPI_SCALING"] = "1" + app = QtWidgets.QApplication([]) + + app.setAttribute(Qt.AA_EnableHighDpiScaling) + app.setAttribute(Qt.AA_ShareOpenGLContexts) + + + mainWindow = QtWidgets.QMainWindow() + viewer = OrientationViewer(mainWindow) + + mainWindow.setCentralWidget(viewer) + + mainWindow.show() + + mainWindow.resize(600, 600) + app.exec_() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/sas/system/log.ini b/src/sas/system/log.ini index 60b82fb5bc..295eacd525 100644 --- a/src/sas/system/log.ini +++ b/src/sas/system/log.ini @@ -9,10 +9,10 @@ keys=simple,detailed [formatter_simple] -#format=%(asctime)s - %(name)s - %(levelname)s - %(message)s +format=%(asctime)s - %(name)s - %(levelname)s - %(message)s #format=%(asctime)s - %(levelname)s : %(name)s:%(pathname)s:%(lineno)4d: %(message)s #format=%(asctime)s - %(levelname)s : %(name)s:%(lineno)4d: %(message)s -format=%(asctime)s - %(levelname)s: %(message)s +#format=%(asctime)s - %(levelname)s: %(message)s datefmt=%H:%M:%S [formatter_detailed] @@ -41,7 +41,7 @@ args=(os.path.join(os.path.expanduser("~"),'sasview.log'),"a") # Loggers [loggers] -keys=root,saspr,sasgui,sascalc,sasmodels,h5py +keys=root,saspr,sasgui,sascalc,sasmodels,h5py,glshaders [logger_root] level=DEBUG @@ -76,4 +76,10 @@ propagate=0 level=DEBUG qualname=h5py handlers= +propagate=0 + +[logger_glshaders] +level=DEBUG +qualname=OpenGL.GL.shaders +handlers= propagate=0 \ No newline at end of file From fa5824d710b8ecacd959633648acea637c186cfe Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Fri, 11 Nov 2022 16:56:48 +0000 Subject: [PATCH 31/87] Better mouse response --- src/sas/qtgui/GL/GraphWidget.py | 39 +++++++++++++++------------------ 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/src/sas/qtgui/GL/GraphWidget.py b/src/sas/qtgui/GL/GraphWidget.py index 91ef1cc76e..2dcebf44e5 100644 --- a/src/sas/qtgui/GL/GraphWidget.py +++ b/src/sas/qtgui/GL/GraphWidget.py @@ -19,7 +19,7 @@ def __init__(self, parent=None): self.view_azimuth = 0.0 self.view_elevation = 0.0 self.view_distance = 5.0 - self.view_centre = (0.0, 0.0, 0.0) + self.view_centre = np.array([0.0, 0.0, 0.0]) self.view_fov = 60 self.background_color = (0, 0, 0, 0) @@ -28,13 +28,13 @@ def __init__(self, parent=None): self.mouse_sensitivity_azimuth = 0.1 self.mouse_sensitivity_elevation = 0.1 self.mouse_sensitivity_distance = 1.0 - self.mouse_sensitivity_position = 1.0 + self.mouse_sensitivity_position = 0.01 # Mouse control variables self.mouse_position = None - self.view_centre_difference = None - self.view_azimuth_difference = None - self.view_elevation_difference = None + self.view_centre_difference = np.array([0.0, 0.0, 0.0]) + self.view_azimuth_difference = 0.0 + self.view_elevation_difference = 0.0 self._items: List[Renderable] = [] @@ -70,7 +70,6 @@ def paintGL(self): self.set_model_view() - self.test_paint() for item in self._items: @@ -103,18 +102,13 @@ def view_matrix(self): tr = QtGui.QMatrix4x4() tr.translate(0.0, 0.0, -self.view_distance) - azimuth = self.view_azimuth - elevation = self.view_elevation - - if self.view_azimuth_difference is not None: - azimuth += self.view_azimuth_difference - - if self.view_elevation_difference is not None: - elevation += self.view_elevation_difference + azimuth = self.view_azimuth + self.view_azimuth_difference + elevation = self.view_elevation + self.view_elevation_difference + centre = self.view_centre + self.view_centre_difference tr.rotate(elevation, 1, 0, 0) tr.rotate(azimuth, 0, 0, -1) - tr.translate(*self.view_centre) + tr.translate(*centre) return tr def set_model_view(self): @@ -133,20 +127,23 @@ def mouseMoveEvent(self, ev): self.view_elevation_difference = self.mouse_sensitivity_elevation * diff.y() elif ev.buttons() == QtCore.Qt.MouseButton.RightButton: - self.view_centre_difference = [self.mouse_sensitivity_position * diff.x(), 0, - self.mouse_sensitivity_position * diff.y()] + print("right button") + self.view_centre_difference = np.array([self.mouse_sensitivity_position * diff.x(), 0, + self.mouse_sensitivity_position * diff.y()]) self.update() def mouseReleaseEvent(self, ev): # Mouse released, add dragging offset the view variables - # self.view_centre += np.array(self.view_centre_difference) + print(self.view_centre, self.view_centre_difference) + + self.view_centre += np.array(self.view_centre_difference) self.view_elevation += self.view_elevation_difference self.view_azimuth += self.view_azimuth_difference - self.view_centre_difference = None - self.view_azimuth_difference = None - self.view_elevation_difference = None + self.view_centre_difference = np.array([0.0,0.0,0.0]) + self.view_azimuth_difference = 0.0 + self.view_elevation_difference = 0.0 self.update() From 1a3c98098c116617fb40ddb04338f5fb4fc4c199 Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Mon, 21 Nov 2022 14:12:50 +0000 Subject: [PATCH 32/87] Rotations for the graph widget work, still need to set viewport --- src/sas/qtgui/GL/GraphWidget.py | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/src/sas/qtgui/GL/GraphWidget.py b/src/sas/qtgui/GL/GraphWidget.py index 2dcebf44e5..1b02504366 100644 --- a/src/sas/qtgui/GL/GraphWidget.py +++ b/src/sas/qtgui/GL/GraphWidget.py @@ -26,7 +26,7 @@ def __init__(self, parent=None): # Mouse control settings self.mouse_sensitivity_azimuth = 0.1 - self.mouse_sensitivity_elevation = 0.1 + self.mouse_sensitivity_elevation = 0.5 self.mouse_sensitivity_distance = 1.0 self.mouse_sensitivity_position = 0.01 @@ -70,7 +70,7 @@ def paintGL(self): self.set_model_view() - self.test_paint() + # self.test_paint() for item in self._items: item.render_solid() @@ -98,22 +98,26 @@ def set_projection(self): # gluPerspective(45.0,1.33,0.1, 100.0) glLoadMatrixf(np.array(self.projection_matrix().data(), dtype=np.float32)) - def view_matrix(self): + def set_model_view(self): + + tr = QtGui.QMatrix4x4() - tr.translate(0.0, 0.0, -self.view_distance) + # tr.translate(0.0, 0.0, -self.view_distance) + tr.translate(0.0,0.0,-self.view_distance) + - azimuth = self.view_azimuth + self.view_azimuth_difference - elevation = self.view_elevation + self.view_elevation_difference + azimuth = np.radians(self.view_azimuth + self.view_azimuth_difference) + elevation = np.radians(np.clip(self.view_elevation + self.view_elevation_difference, -90, 90)) centre = self.view_centre + self.view_centre_difference - tr.rotate(elevation, 1, 0, 0) - tr.rotate(azimuth, 0, 0, -1) - tr.translate(*centre) - return tr + x = centre[0] + self.view_distance*np.cos(azimuth)*np.cos(elevation) + y = centre[1] - self.view_distance*np.sin(azimuth)*np.cos(elevation) + z = centre[2] + self.view_distance*np.sin(elevation) - def set_model_view(self): - glMatrixMode(GL_MODELVIEW) - glLoadMatrixf(np.array(self.view_matrix().data(), dtype=np.float32)) + gluLookAt( + x, y, z, + centre[0], centre[1], centre[2], + 0.0, 0.0, 1.0) def mousePressEvent(self, ev): self.mouse_position = ev.localPos() @@ -127,7 +131,6 @@ def mouseMoveEvent(self, ev): self.view_elevation_difference = self.mouse_sensitivity_elevation * diff.y() elif ev.buttons() == QtCore.Qt.MouseButton.RightButton: - print("right button") self.view_centre_difference = np.array([self.mouse_sensitivity_position * diff.x(), 0, self.mouse_sensitivity_position * diff.y()]) @@ -135,8 +138,6 @@ def mouseMoveEvent(self, ev): def mouseReleaseEvent(self, ev): # Mouse released, add dragging offset the view variables - print(self.view_centre, self.view_centre_difference) - self.view_centre += np.array(self.view_centre_difference) self.view_elevation += self.view_elevation_difference self.view_azimuth += self.view_azimuth_difference From 869c15638301ffd21ca3f7ea837950894b482a6e Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Mon, 21 Nov 2022 14:34:04 +0000 Subject: [PATCH 33/87] Viewport is correct --- src/sas/qtgui/GL/GraphWidget.py | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/src/sas/qtgui/GL/GraphWidget.py b/src/sas/qtgui/GL/GraphWidget.py index 1b02504366..4072af02e6 100644 --- a/src/sas/qtgui/GL/GraphWidget.py +++ b/src/sas/qtgui/GL/GraphWidget.py @@ -38,17 +38,6 @@ def __init__(self, parent=None): self._items: List[Renderable] = [] - def test_paint(self): - glTranslatef(-2.5, 0.5, -6.0) - glColor3f( 1.0, 1.5, 0.0 ) - glPolygonMode(GL_FRONT, GL_FILL) - glBegin(GL_TRIANGLES) - glVertex3f(1.0,0.0,0.0) - glVertex3f(0.0,1.0,0.0) - glVertex3f(0.0,0.0,0.0) - glEnd() - glFlush() - def initializeGL(self): glClearDepth(1.0) glDepthFunc(GL_LESS) @@ -56,13 +45,19 @@ def initializeGL(self): glShadeModel(GL_SMOOTH) def default_viewport(self): - return 0, 0, int(self.width() * self.devicePixelRatioF()), int(self.height() * self.devicePixelRatioF()) + x = int(self.width() * self.devicePixelRatioF()) + y = int(self.height() * self.devicePixelRatioF()) + # return -x//2, -y//2, x//2, y//2 + # return -y//2, -x//2, x, y + return 0, 0, x, y + def paintGL(self): """ Paint the GL viewport """ + glViewport(*self.default_viewport()) self.set_projection() glClearColor(*self.background_color) From ab9a918dce0582753994e7d6139cfe2574202115 Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Mon, 21 Nov 2022 17:21:02 +0000 Subject: [PATCH 34/87] Work on surface plot --- src/sas/qtgui/GL/Mesh.py | 0 src/sas/qtgui/GL/Surface.py | 55 +++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) delete mode 100644 src/sas/qtgui/GL/Mesh.py diff --git a/src/sas/qtgui/GL/Mesh.py b/src/sas/qtgui/GL/Mesh.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/sas/qtgui/GL/Surface.py b/src/sas/qtgui/GL/Surface.py index e69de29bb2..5990ef6679 100644 --- a/src/sas/qtgui/GL/Surface.py +++ b/src/sas/qtgui/GL/Surface.py @@ -0,0 +1,55 @@ +from typing import Optional, Tuple +import numpy as np + +from matplotlib import cm as ColorMap + +from Models import FullVertexModel + + +class Surface(FullVertexModel): + + """ Surface plot + + + x_values: 1D array of x values + y_values: 1D array of y values + z_data: 2D array of z values + colormap: Optional colour map + + """ + + @staticmethod + def calculate_edge_indices(nx, ny): + all_edges = [] + for i in range(nx-1): + for j in range(ny): + all_edges.append((j*nx + i, j*nx + i + 1)) + + for i in range(nx): + for j in range(ny-1): + all_edges.append((j*nx + i, (j+1)*nx + i)) + + return all_edges + + def __init__(self, + x_values: np.ndarray, + y_values: np.ndarray, + z_data: np.ndarray, + colormap: Optional[ColorMap]=None): + + + self.x_data, self.y_data = np.meshgrid(x_values, y_values) + self.z_data = z_data + + super().__init__( + vertices=Cube.cube_vertices, + edges=self.calculate_edge_indices(len(x_values), len(y_values)), + triangle_meshes=Cube.cube_triangles, + edge_colors=edge_colors, + vertex_colors=self._get_colors(z_data, colormap)) + + def _get_colors(self, z_values, colormap) -> Sequence[Color]: + return [] + + def set_z_data(self, z_data): + pass \ No newline at end of file From 002bf63e73bb702443a480aec38b2f51fd8f0448 Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Tue, 22 Nov 2022 16:18:48 +0000 Subject: [PATCH 35/87] Minor change --- src/sas/qtgui/GL/Surface.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/sas/qtgui/GL/Surface.py b/src/sas/qtgui/GL/Surface.py index 5990ef6679..16134a948e 100644 --- a/src/sas/qtgui/GL/Surface.py +++ b/src/sas/qtgui/GL/Surface.py @@ -37,6 +37,8 @@ def __init__(self, z_data: np.ndarray, colormap: Optional[ColorMap]=None): + if colormap is None: + pass self.x_data, self.y_data = np.meshgrid(x_values, y_values) self.z_data = z_data From 0e3031fb96150a3cf2ec25eabe593f07d468121a Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Tue, 22 Nov 2022 17:12:36 +0000 Subject: [PATCH 36/87] Wireframe surface works --- src/sas/qtgui/GL/GraphWidget.py | 11 ++++++- src/sas/qtgui/GL/Surface.py | 51 +++++++++++++++++++-------------- 2 files changed, 39 insertions(+), 23 deletions(-) diff --git a/src/sas/qtgui/GL/GraphWidget.py b/src/sas/qtgui/GL/GraphWidget.py index 4072af02e6..8b73b8b27b 100644 --- a/src/sas/qtgui/GL/GraphWidget.py +++ b/src/sas/qtgui/GL/GraphWidget.py @@ -8,6 +8,7 @@ from sas.qtgui.GL.Renderable import Renderable from sas.qtgui.GL.Color import Color +from sas.qtgui.GL.Surface import Surface class GraphWidget(QtOpenGL.QGLWidget): @@ -157,7 +158,15 @@ def main(): mainWindow = QtWidgets.QMainWindow() viewer = GraphWidget(mainWindow) - viewer.add(Cube(edge_colors=Color(1,1,1), face_colors=Color(0,1,0))) + # viewer.add(Cube(edge_colors=Color(1,1,1), face_colors=Color(0,1,0))) + x = np.linspace(-1, 1, 101) + y = np.linspace(-1, 1, 101) + x_grid, y_grid = np.meshgrid(x, y) + + r_sq = x_grid**2 + y_grid**2 + z = np.cos(np.sqrt(r_sq))/(r_sq+1) + + viewer.add(Surface(x, y, z)) mainWindow.setCentralWidget(viewer) diff --git a/src/sas/qtgui/GL/Surface.py b/src/sas/qtgui/GL/Surface.py index 16134a948e..cb8f5b0ad7 100644 --- a/src/sas/qtgui/GL/Surface.py +++ b/src/sas/qtgui/GL/Surface.py @@ -1,22 +1,14 @@ from typing import Optional, Tuple import numpy as np -from matplotlib import cm as ColorMap +from matplotlib.colors import Colormap -from Models import FullVertexModel +from sas.qtgui.GL.Models import FullVertexModel, WireModel +from sas.qtgui.GL.Color import Color -class Surface(FullVertexModel): +class Surface(WireModel): - """ Surface plot - - - x_values: 1D array of x values - y_values: 1D array of y values - z_data: 2D array of z values - colormap: Optional colour map - - """ @staticmethod def calculate_edge_indices(nx, ny): @@ -35,7 +27,17 @@ def __init__(self, x_values: np.ndarray, y_values: np.ndarray, z_data: np.ndarray, - colormap: Optional[ColorMap]=None): + colormap: Optional[Colormap]=None): + + """ Surface plot + + + :param x_values: 1D array of x values + :param y_values: 1D array of y values + :param z_data: 2D array of z values + :param colormap: Optional[Colormap] colour map + + """ if colormap is None: pass @@ -43,15 +45,20 @@ def __init__(self, self.x_data, self.y_data = np.meshgrid(x_values, y_values) self.z_data = z_data + self.n_x = len(x_values) + self.n_y = len(y_values) + super().__init__( - vertices=Cube.cube_vertices, - edges=self.calculate_edge_indices(len(x_values), len(y_values)), - triangle_meshes=Cube.cube_triangles, - edge_colors=edge_colors, - vertex_colors=self._get_colors(z_data, colormap)) + vertices=[(float(x), float(y), float(z)) for x, y, z in zip(np.nditer(self.x_data), np.nditer(self.y_data), np.nditer(self.z_data))], + edges=self.calculate_edge_indices(self.n_x, self.n_y), + edge_colors=Color(1.0,1.0,1.0), + ) + + self.wireframe_render_enabled = True - def _get_colors(self, z_values, colormap) -> Sequence[Color]: - return [] - def set_z_data(self, z_data): - pass \ No newline at end of file + # def _get_colors(self, z_values, colormap) -> Sequence[Color]: + # return [] + # + # def set_z_data(self, z_data): + # pass \ No newline at end of file From 1c05fc8274e21b0861c2adcc9cd74f5a9677da22 Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Wed, 23 Nov 2022 10:19:15 +0000 Subject: [PATCH 37/87] Scrolling working, solid meshes --- src/sas/qtgui/GL/GraphWidget.py | 24 ++++++++++++++++++++++++ src/sas/qtgui/GL/Surface.py | 16 ++++++++++++++-- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/src/sas/qtgui/GL/GraphWidget.py b/src/sas/qtgui/GL/GraphWidget.py index 8b73b8b27b..039200b83e 100644 --- a/src/sas/qtgui/GL/GraphWidget.py +++ b/src/sas/qtgui/GL/GraphWidget.py @@ -25,11 +25,15 @@ def __init__(self, parent=None): self.background_color = (0, 0, 0, 0) + self.min_distance = 0.1 + self.max_distance = 250 + # Mouse control settings self.mouse_sensitivity_azimuth = 0.1 self.mouse_sensitivity_elevation = 0.5 self.mouse_sensitivity_distance = 1.0 self.mouse_sensitivity_position = 0.01 + self.scroll_sensitivity = 0.0005 # Mouse control variables self.mouse_position = None @@ -144,6 +148,26 @@ def mouseReleaseEvent(self, ev): self.update() + def wheelEvent(self, event: QtGui.QWheelEvent): + scroll_amount = event.angleDelta().y() + + print(scroll_amount) + + self.view_distance *= np.exp(scroll_amount * self.scroll_sensitivity) + + print(self.view_distance, type(self.view_distance)) + + # + # if self.view_distance < self.min_distance: + # self.view_distance = self.min_distance + # + # if self.view_distance > self.max_distance: + # self.view_distance = self.max_distance + + event.accept() + + self.update() + def add(self, item: Renderable): self._items.append(item) diff --git a/src/sas/qtgui/GL/Surface.py b/src/sas/qtgui/GL/Surface.py index cb8f5b0ad7..b68f8bd89b 100644 --- a/src/sas/qtgui/GL/Surface.py +++ b/src/sas/qtgui/GL/Surface.py @@ -7,7 +7,7 @@ from sas.qtgui.GL.Color import Color -class Surface(WireModel): +class Surface(FullVertexModel): @staticmethod @@ -23,6 +23,15 @@ def calculate_edge_indices(nx, ny): return all_edges + @staticmethod + def calculate_triangles(nx, ny): + triangles = [] + for i in range(nx-1): + for j in range(ny-1): + triangles.append((j*nx + i, (j+1)*nx+(i+1), j*nx + (i + 1))) + triangles.append((j*nx + i, (j+1)*nx + i, (j+1)*nx + (i+1))) + return triangles + def __init__(self, x_values: np.ndarray, y_values: np.ndarray, @@ -50,11 +59,14 @@ def __init__(self, super().__init__( vertices=[(float(x), float(y), float(z)) for x, y, z in zip(np.nditer(self.x_data), np.nditer(self.y_data), np.nditer(self.z_data))], - edges=self.calculate_edge_indices(self.n_x, self.n_y), + edges=Surface.calculate_edge_indices(self.n_x, self.n_y), + triangle_meshes=[Surface.calculate_triangles(self.n_x, self.n_y)], edge_colors=Color(1.0,1.0,1.0), + vertex_colors=Color(0.0,0.4,0.0) ) self.wireframe_render_enabled = True + self.solid_render_enabled = True # def _get_colors(self, z_values, colormap) -> Sequence[Color]: From 592a0a1300b5975e28f6044ce8dcba34dc83229f Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Thu, 24 Nov 2022 13:50:42 +0000 Subject: [PATCH 38/87] Surface looks good --- src/sas/qtgui/GL/Color.py | 36 +++++++++++++++++++++++++++++++-- src/sas/qtgui/GL/Cylinder.py | 0 src/sas/qtgui/GL/GraphWidget.py | 19 +++++++++-------- src/sas/qtgui/GL/Models.py | 4 ++-- src/sas/qtgui/GL/Sphere.py | 0 src/sas/qtgui/GL/Surface.py | 31 ++++++++++++++++------------ 6 files changed, 63 insertions(+), 27 deletions(-) create mode 100644 src/sas/qtgui/GL/Cylinder.py create mode 100644 src/sas/qtgui/GL/Sphere.py diff --git a/src/sas/qtgui/GL/Color.py b/src/sas/qtgui/GL/Color.py index 8959124062..b436f8ae06 100644 --- a/src/sas/qtgui/GL/Color.py +++ b/src/sas/qtgui/GL/Color.py @@ -1,6 +1,15 @@ -from OpenGL.GL import glColor4f +from typing import Sequence + +import logging + import numpy as np +import matplotlib as mpl + +from OpenGL.GL import glColor4f + +logger = logging.getLogger("GL.Color") + class Color(): def __init__(self, r: float, g: float, b: float, alpha: float=1.0): self.r = float(r) @@ -15,4 +24,27 @@ def to_array(self): return np.array([self.r, self.g, self.b, self.alpha]) def __repr__(self): - return f"{self.__class__.__name__}({self.r}, {self.g}, {self.b}, {self.alpha})" \ No newline at end of file + return f"{self.__class__.__name__}({self.r}, {self.g}, {self.b}, {self.alpha})" + + +class ColorMap(): + + _default_colormap = 'rainbow' + def __init__(self, colormap_name=_default_colormap, min_value=0.0, max_value=1.0): + + try: + self.colormap = mpl.colormaps[colormap_name] + except KeyError: + logger.warning(f"Bad colormap name '{colormap_name}'") + self.colormap = mpl.colormaps[ColorMap._default_colormap] + + self.min_value = min_value + self.max_value = max_value + + def color(self, value): + scaled = (value - self.min_value) / (self.max_value - self.min_value) + scaled = np.clip(scaled, 0, 1) + return Color(*self.colormap(scaled)) + + def color_array(self, values: Sequence[float]) -> Sequence[Color]: + return [self.color(value) for value in values] diff --git a/src/sas/qtgui/GL/Cylinder.py b/src/sas/qtgui/GL/Cylinder.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/sas/qtgui/GL/GraphWidget.py b/src/sas/qtgui/GL/GraphWidget.py index 039200b83e..315f362e81 100644 --- a/src/sas/qtgui/GL/GraphWidget.py +++ b/src/sas/qtgui/GL/GraphWidget.py @@ -71,11 +71,15 @@ def paintGL(self): self.set_model_view() # self.test_paint() + glEnable(GL_POLYGON_OFFSET_FILL) for item in self._items: + glPolygonOffset(0.0, 20.0) item.render_solid() + glPolygonOffset(0.0, 0.0) item.render_wireframe() + glPolygonOffset(0.0, 0.0) def projection_matrix(self): @@ -151,18 +155,13 @@ def mouseReleaseEvent(self, ev): def wheelEvent(self, event: QtGui.QWheelEvent): scroll_amount = event.angleDelta().y() - print(scroll_amount) - self.view_distance *= np.exp(scroll_amount * self.scroll_sensitivity) - print(self.view_distance, type(self.view_distance)) + if self.view_distance < self.min_distance: + self.view_distance = self.min_distance - # - # if self.view_distance < self.min_distance: - # self.view_distance = self.min_distance - # - # if self.view_distance > self.max_distance: - # self.view_distance = self.max_distance + if self.view_distance > self.max_distance: + self.view_distance = self.max_distance event.accept() @@ -190,7 +189,7 @@ def main(): r_sq = x_grid**2 + y_grid**2 z = np.cos(np.sqrt(r_sq))/(r_sq+1) - viewer.add(Surface(x, y, z)) + viewer.add(Surface(x, y, z, edge_skip=4)) mainWindow.setCentralWidget(viewer) diff --git a/src/sas/qtgui/GL/Models.py b/src/sas/qtgui/GL/Models.py index 8a4f4ed9bb..e88e705169 100644 --- a/src/sas/qtgui/GL/Models.py +++ b/src/sas/qtgui/GL/Models.py @@ -73,7 +73,7 @@ def __init__(self, super().__init__(vertices, triangle_meshes) self._vertex_colors = vertex_colors - self._vertex_color_array = None if isinstance(vertex_colors, Color) else np.array(vertex_colors, dtype=float) + self._vertex_color_array = None if isinstance(vertex_colors, Color) else np.array([color.to_array() for color in vertex_colors], dtype=float) self.solid_render_enabled = self.vertex_colors is not None @@ -84,7 +84,7 @@ def vertex_colors(self): @vertex_colors.setter def vertex_colors(self, new_vertex_colors): self.vertex_colors = new_vertex_colors - self._vertex_color_array = color_sequence_to_array(new_vertex_colors) + self._vertex_color_array = None if isinstance(new_vertex_colors, Color) else np.array([color.to_array() for color in new_vertex_colors], dtype=float) self.solid_render_enabled = self.vertex_colors is not None def render_solid(self): diff --git a/src/sas/qtgui/GL/Sphere.py b/src/sas/qtgui/GL/Sphere.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/sas/qtgui/GL/Surface.py b/src/sas/qtgui/GL/Surface.py index b68f8bd89b..58aecbc6c4 100644 --- a/src/sas/qtgui/GL/Surface.py +++ b/src/sas/qtgui/GL/Surface.py @@ -1,23 +1,25 @@ +import logging from typing import Optional, Tuple import numpy as np -from matplotlib.colors import Colormap +import matplotlib as mpl from sas.qtgui.GL.Models import FullVertexModel, WireModel -from sas.qtgui.GL.Color import Color +from sas.qtgui.GL.Color import Color, ColorMap +logger = logging.getLogger("GL.Surface") class Surface(FullVertexModel): @staticmethod - def calculate_edge_indices(nx, ny): + def calculate_edge_indices(nx, ny, gap=1): all_edges = [] for i in range(nx-1): - for j in range(ny): + for j in range(0, ny, gap): all_edges.append((j*nx + i, j*nx + i + 1)) - for i in range(nx): + for i in range(0, nx, gap): for j in range(ny-1): all_edges.append((j*nx + i, (j+1)*nx + i)) @@ -36,7 +38,8 @@ def __init__(self, x_values: np.ndarray, y_values: np.ndarray, z_data: np.ndarray, - colormap: Optional[Colormap]=None): + colormap: str= ColorMap._default_colormap, + edge_skip: int=1): """ Surface plot @@ -44,12 +47,10 @@ def __init__(self, :param x_values: 1D array of x values :param y_values: 1D array of y values :param z_data: 2D array of z values - :param colormap: Optional[Colormap] colour map - + :param colormap: name of a matplotlib colour map + :param edge_skip: skip every `edge_skip` index when drawing wireframe """ - if colormap is None: - pass self.x_data, self.y_data = np.meshgrid(x_values, y_values) self.z_data = z_data @@ -57,12 +58,16 @@ def __init__(self, self.n_x = len(x_values) self.n_y = len(y_values) + self.colormap = ColorMap(colormap) + + verts = [(float(x), float(y), float(z)) for x, y, z in zip(np.nditer(self.x_data), np.nditer(self.y_data), np.nditer(self.z_data))] + super().__init__( - vertices=[(float(x), float(y), float(z)) for x, y, z in zip(np.nditer(self.x_data), np.nditer(self.y_data), np.nditer(self.z_data))], - edges=Surface.calculate_edge_indices(self.n_x, self.n_y), + vertices=verts, + edges=Surface.calculate_edge_indices(self.n_x, self.n_y, edge_skip), triangle_meshes=[Surface.calculate_triangles(self.n_x, self.n_y)], edge_colors=Color(1.0,1.0,1.0), - vertex_colors=Color(0.0,0.4,0.0) + vertex_colors=self.colormap.color_array([z for _, _, z in verts]) ) self.wireframe_render_enabled = True From 78a09b46acdce29f56ea20c77c05f3eb6b065672 Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Thu, 24 Nov 2022 15:38:37 +0000 Subject: [PATCH 39/87] Cone primative, refactored some things into a library for checking over --- src/sas/qtgui/GL/Cone.py | 59 +++++++++++++++++ src/sas/qtgui/GL/GraphWidget.py | 19 ++++-- src/sas/qtgui/GL/Models.py | 3 + src/sas/qtgui/GL/PrimativeLibrary.py | 98 ++++++++++++++++++++++++++++ 4 files changed, 174 insertions(+), 5 deletions(-) create mode 100644 src/sas/qtgui/GL/Cone.py create mode 100644 src/sas/qtgui/GL/PrimativeLibrary.py diff --git a/src/sas/qtgui/GL/Cone.py b/src/sas/qtgui/GL/Cone.py new file mode 100644 index 0000000000..bcd8288ed5 --- /dev/null +++ b/src/sas/qtgui/GL/Cone.py @@ -0,0 +1,59 @@ +from typing import Optional, Union, Sequence, List, Tuple + +import numpy as np + +from sas.qtgui.GL.Models import FullVertexModel, WireModel +from sas.qtgui.GL.Color import Color + + +class Cone(FullVertexModel): + """ Graphics primitive: Radius 1, Height 2 cone "centred" at (0,0,0)""" + + @staticmethod + def cone_vertices(n) -> List[Tuple[float, float, float]]: + return [(0.0, 0.0, 1.0)] + [ + (np.sin(angle), np.cos(angle), -1.0) + for angle in 2*np.pi*np.arange(0, n)/n] + [(0.0, 0.0, -1.0)] + + @staticmethod + def cone_edges(n): + return [(0, i+1) for i in range(n)] + [(i+1, (i+1)%n+1) for i in range(n)] + + @staticmethod + def cone_tip_triangles(n) -> List[Tuple[int, int, int]]: + return [(0, i + 1, (i + 1) % n + 1) for i in range(n)] + + @staticmethod + def cone_base_triangles(n) -> List[Tuple[int, int, int]]: + return [((i + 1) % n + 1, i + 1, n+1) for i in range(n)] + + @staticmethod + def cone_triangles(n) -> List[List[Tuple[int, int, int]]]: + return [Cone.cone_base_triangles(n), + Cone.cone_tip_triangles(n)] + + def __init__(self, + n: int = 20, + vertex_colors: Optional[Union[Sequence[Color], Color]]=None, + edge_colors: Optional[Union[Sequence[Color],Color]]=None): + + super().__init__( + vertices=Cone.cone_vertices(n), + edges=Cone.cone_edges(n), + triangle_meshes=Cone.cone_triangles(n), + edge_colors=edge_colors, + vertex_colors=vertex_colors) + + if edge_colors is None: + self.wireframe_render_enabled = False + self.edge_colors = [] + else: + self.wireframe_render_enabled = True + self.edge_colors = edge_colors + + if vertex_colors is None: + self.solid_render_enabled = False + self.face_colors = [] + else: + self.solid_render_enabled = True + self.face_colors = vertex_colors diff --git a/src/sas/qtgui/GL/GraphWidget.py b/src/sas/qtgui/GL/GraphWidget.py index 315f362e81..304ef07a4a 100644 --- a/src/sas/qtgui/GL/GraphWidget.py +++ b/src/sas/qtgui/GL/GraphWidget.py @@ -1,4 +1,4 @@ -from typing import Optional, Tuple, List +from typing import Optional, Tuple, List, Callable import numpy as np from PyQt5 import QtWidgets, Qt, QtGui, QtOpenGL, QtCore @@ -9,11 +9,15 @@ from sas.qtgui.GL.Renderable import Renderable from sas.qtgui.GL.Color import Color from sas.qtgui.GL.Surface import Surface +from sas.qtgui.GL.Cone import Cone class GraphWidget(QtOpenGL.QGLWidget): - def __init__(self, parent=None): + def __init__(self, + parent=None, + on_key: Callable[[int], None]=lambda x: None): + super().__init__(parent) self.setMinimumSize(640, 480) @@ -29,7 +33,7 @@ def __init__(self, parent=None): self.max_distance = 250 # Mouse control settings - self.mouse_sensitivity_azimuth = 0.1 + self.mouse_sensitivity_azimuth = 0.2 self.mouse_sensitivity_elevation = 0.5 self.mouse_sensitivity_distance = 1.0 self.mouse_sensitivity_position = 0.01 @@ -43,6 +47,9 @@ def __init__(self, parent=None): self._items: List[Renderable] = [] + # Save the key callback + self.on_key = on_key + def initializeGL(self): glClearDepth(1.0) glDepthFunc(GL_LESS) @@ -170,10 +177,13 @@ def wheelEvent(self, event: QtGui.QWheelEvent): def add(self, item: Renderable): self._items.append(item) + def keyPressEvent(self, event: QtGui.QKeyEvent): + print("Key press") + self.on_key(event.key()) + def main(): """ Show a demo of the opengl window """ import os - from sas.qtgui.GL.Cube import Cube os.environ["QT_ENABLE_HIGHDPI_SCALING"] = "1" app = QtWidgets.QApplication([]) @@ -181,7 +191,6 @@ def main(): mainWindow = QtWidgets.QMainWindow() viewer = GraphWidget(mainWindow) - # viewer.add(Cube(edge_colors=Color(1,1,1), face_colors=Color(0,1,0))) x = np.linspace(-1, 1, 101) y = np.linspace(-1, 1, 101) x_grid, y_grid = np.meshgrid(x, y) diff --git a/src/sas/qtgui/GL/Models.py b/src/sas/qtgui/GL/Models.py index e88e705169..eab8aa2f08 100644 --- a/src/sas/qtgui/GL/Models.py +++ b/src/sas/qtgui/GL/Models.py @@ -114,6 +114,9 @@ def render_solid(self): for triangle_mesh in self._triangle_mesh_arrays: glDrawElementsui(GL_TRIANGLES, triangle_mesh) + glDisableClientState(GL_COLOR_ARRAY) + glDisableClientState(GL_VERTEX_ARRAY) + class SolidFaceModel(SolidModel): def __init__(self, diff --git a/src/sas/qtgui/GL/PrimativeLibrary.py b/src/sas/qtgui/GL/PrimativeLibrary.py new file mode 100644 index 0000000000..91bdccf5da --- /dev/null +++ b/src/sas/qtgui/GL/PrimativeLibrary.py @@ -0,0 +1,98 @@ +""" As close a thing as there are to tests for GL""" + +from typing import Optional, Tuple, List, Callable +import numpy as np + +from PyQt5 import QtWidgets, Qt, QtGui, QtOpenGL, QtCore + +from sas.qtgui.GL.GraphWidget import GraphWidget +from sas.qtgui.GL.Models import WireModel, SolidModel, ModelBase +from sas.qtgui.GL.Color import Color +from sas.qtgui.GL.Surface import Surface +from sas.qtgui.GL.Cone import Cone +from sas.qtgui.GL.Cube import Cube + + +def mesh_example(): + x = np.linspace(-1, 1, 101) + y = np.linspace(-1, 1, 101) + x_grid, y_grid = np.meshgrid(x, y) + + r_sq = x_grid**2 + y_grid**2 + z = np.cos(np.sqrt(r_sq))/(r_sq+1) + + return Surface(x, y, z, edge_skip=4) + + +def primative_library(): + """ Shows all the existing primitives that can be rendered, space to go through them""" + + import os + + os.environ["QT_ENABLE_HIGHDPI_SCALING"] = "1" + app = QtWidgets.QApplication([]) + + item_list = [ + mesh_example(), + Cube(edge_colors=Color(1, 1, 1), face_colors=Color(0, 1, 0)), + Cone(edge_colors=Color(1, 1, 1), vertex_colors=Color(0, 1, 0))] + + # Turn off all of them + for item in item_list: + item.solid_render_enabled = False + item.wireframe_render_enabled = False + + + # Thing for going through each of the draw types of the primatives + + def item_states(item: ModelBase): + + item.solid_render_enabled = True + item.solid_render_enabled = True + + yield None + + item.solid_render_enabled = False + item.solid_render_enabled = True + + yield None + + item.solid_render_enabled = True + item.solid_render_enabled = False + + yield None + + item.solid_render_enabled = False + item.solid_render_enabled = False + + def scan_states(): + while True: + for item in item_list: + for _ in item_states(item): + yield None + + state = scan_states() + next(state) + + # Keyboard callback + def enable_disable(key: int): + print(key) + next(state) + + mainWindow = QtWidgets.QMainWindow() + viewer = GraphWidget(mainWindow, on_key=enable_disable) + + + for item in item_list: + viewer.add(item) + + mainWindow.setCentralWidget(viewer) + + mainWindow.show() + + mainWindow.resize(600, 600) + app.exec_() + + +if __name__ == "__main__": + primative_library() \ No newline at end of file From 111fb080762a6d20cd575da2bd09a83a492f2806 Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Thu, 24 Nov 2022 16:43:18 +0000 Subject: [PATCH 40/87] Cylinder almost ready --- src/sas/qtgui/GL/Cylinder.py | 65 ++++++++++++++++++++++++++++ src/sas/qtgui/GL/GraphWidget.py | 10 ++--- src/sas/qtgui/GL/PrimativeLibrary.py | 28 +++++++----- 3 files changed, 87 insertions(+), 16 deletions(-) diff --git a/src/sas/qtgui/GL/Cylinder.py b/src/sas/qtgui/GL/Cylinder.py index e69de29bb2..a2b5a8ac4a 100644 --- a/src/sas/qtgui/GL/Cylinder.py +++ b/src/sas/qtgui/GL/Cylinder.py @@ -0,0 +1,65 @@ +from typing import Optional, Union, Sequence, List, Tuple + +import numpy as np + +from sas.qtgui.GL.Models import FullVertexModel, WireModel +from sas.qtgui.GL.Color import Color + + +class Cylinder(FullVertexModel): + """ Graphics primitive: Radius 1, Height 2 cone "centred" at (0,0,0)""" + + @staticmethod + def cylinder_vertices(n) -> List[Tuple[float, float, float]]: + return [(0.0, 0.0, 1.0)] + \ + [ (np.sin(angle), np.cos(angle), 1.0) for angle in 2*np.pi*np.arange(0, n)/n] + \ + [(0.0, 0.0, -1.0)] + \ + [ (np.sin(angle), np.cos(angle), -1.0) for angle in 2*np.pi*np.arange(0, n)/n] + + + @staticmethod + def cylinder_edges(n): + return [(i+1, (i+1)%n+1) for i in range(n)] + \ + [(i + n + 2, (i+1)%n + n + 2) for i in range(n)] + \ + [(i+1, i + n + 2) for i in range(n)] + + @staticmethod + def cylinder_face_triangles(n, offset) -> List[Tuple[int, int, int]]: + return [(i+offset + 1, (i + 1) % n + offset + 1, offset) for i in range(n)] + + @staticmethod + def cylinder_side_triangles(n): + pass + + @staticmethod + def cylinder_triangles(n) -> List[List[Tuple[int, int, int]]]: + return [ + Cylinder.cylinder_face_triangles(n, 0), + [tuple(reversed(x)) for x in Cylinder.cylinder_face_triangles(n, n+1)] + ] + + def __init__(self, + n: int = 20, + vertex_colors: Optional[Union[Sequence[Color], Color]]=None, + edge_colors: Optional[Union[Sequence[Color],Color]]=None): + + super().__init__( + vertices=Cylinder.cylinder_vertices(n), + edges=Cylinder.cylinder_edges(n), + triangle_meshes=Cylinder.cylinder_triangles(n), + edge_colors=edge_colors, + vertex_colors=vertex_colors) + + if edge_colors is None: + self.wireframe_render_enabled = False + self.edge_colors = [] + else: + self.wireframe_render_enabled = True + self.edge_colors = edge_colors + + if vertex_colors is None: + self.solid_render_enabled = False + self.face_colors = [] + else: + self.solid_render_enabled = True + self.face_colors = vertex_colors diff --git a/src/sas/qtgui/GL/GraphWidget.py b/src/sas/qtgui/GL/GraphWidget.py index 304ef07a4a..cd4403f421 100644 --- a/src/sas/qtgui/GL/GraphWidget.py +++ b/src/sas/qtgui/GL/GraphWidget.py @@ -14,9 +14,7 @@ class GraphWidget(QtOpenGL.QGLWidget): - def __init__(self, - parent=None, - on_key: Callable[[int], None]=lambda x: None): + def __init__(self, on_key: Callable[[int], None] = lambda x: None, parent=None): super().__init__(parent) self.setMinimumSize(640, 480) @@ -47,9 +45,12 @@ def __init__(self, self._items: List[Renderable] = [] - # Save the key callback + self.setFocusPolicy(QtCore.Qt.FocusPolicy.StrongFocus) + + # Save the on_key callback self.on_key = on_key + def initializeGL(self): glClearDepth(1.0) glDepthFunc(GL_LESS) @@ -178,7 +179,6 @@ def add(self, item: Renderable): self._items.append(item) def keyPressEvent(self, event: QtGui.QKeyEvent): - print("Key press") self.on_key(event.key()) def main(): diff --git a/src/sas/qtgui/GL/PrimativeLibrary.py b/src/sas/qtgui/GL/PrimativeLibrary.py index 91bdccf5da..7cc21b21e8 100644 --- a/src/sas/qtgui/GL/PrimativeLibrary.py +++ b/src/sas/qtgui/GL/PrimativeLibrary.py @@ -11,6 +11,7 @@ from sas.qtgui.GL.Surface import Surface from sas.qtgui.GL.Cone import Cone from sas.qtgui.GL.Cube import Cube +from sas.qtgui.GL.Cylinder import Cylinder def mesh_example(): @@ -33,9 +34,11 @@ def primative_library(): app = QtWidgets.QApplication([]) item_list = [ - mesh_example(), - Cube(edge_colors=Color(1, 1, 1), face_colors=Color(0, 1, 0)), - Cone(edge_colors=Color(1, 1, 1), vertex_colors=Color(0, 1, 0))] + # mesh_example(), + # Cube(edge_colors=Color(1, 1, 1), face_colors=Color(0.7, 0, 0)), + # Cone(edge_colors=Color(1, 1, 1), vertex_colors=Color(0, 0.7, 0)), + Cylinder(edge_colors=Color(1, 1, 1), vertex_colors=Color(0, 0, 0.7)) + ] # Turn off all of them for item in item_list: @@ -48,22 +51,22 @@ def primative_library(): def item_states(item: ModelBase): item.solid_render_enabled = True - item.solid_render_enabled = True + item.wireframe_render_enabled = True yield None item.solid_render_enabled = False - item.solid_render_enabled = True + item.wireframe_render_enabled = True yield None item.solid_render_enabled = True - item.solid_render_enabled = False + item.wireframe_render_enabled = False yield None item.solid_render_enabled = False - item.solid_render_enabled = False + item.wireframe_render_enabled = False def scan_states(): while True: @@ -74,14 +77,17 @@ def scan_states(): state = scan_states() next(state) + + mainWindow = QtWidgets.QMainWindow() + viewer = GraphWidget(parent=mainWindow) + # Keyboard callback - def enable_disable(key: int): + def enable_disable(key): print(key) next(state) + viewer.update() - mainWindow = QtWidgets.QMainWindow() - viewer = GraphWidget(mainWindow, on_key=enable_disable) - + viewer.on_key = enable_disable for item in item_list: viewer.add(item) From 96f581174372043c41b605f35ed3c2937208ad1d Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Thu, 24 Nov 2022 17:03:27 +0000 Subject: [PATCH 41/87] Cylinder works --- src/sas/qtgui/GL/Cylinder.py | 13 +++++++++++-- src/sas/qtgui/GL/GraphWidget.py | 2 +- src/sas/qtgui/GL/PrimativeLibrary.py | 7 +++---- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/sas/qtgui/GL/Cylinder.py b/src/sas/qtgui/GL/Cylinder.py index a2b5a8ac4a..65fda03dcb 100644 --- a/src/sas/qtgui/GL/Cylinder.py +++ b/src/sas/qtgui/GL/Cylinder.py @@ -28,13 +28,22 @@ def cylinder_face_triangles(n, offset) -> List[Tuple[int, int, int]]: return [(i+offset + 1, (i + 1) % n + offset + 1, offset) for i in range(n)] @staticmethod - def cylinder_side_triangles(n): - pass + def cylinder_side_triangles(n) -> List[Tuple[int, int, int]]: + sides = [] + for i in range(n): + # Squares into triangles + + # Bottom left, top right, bottom right + sides.append((i + 1, (i + 1) % n + n + 2, (i + 1) % n + 1)) + # Top right, bottom left, top left + sides.append(((i + 1) % n + n + 2, i + 1, i + n + 2)) + return sides @staticmethod def cylinder_triangles(n) -> List[List[Tuple[int, int, int]]]: return [ Cylinder.cylinder_face_triangles(n, 0), + Cylinder.cylinder_side_triangles(n), [tuple(reversed(x)) for x in Cylinder.cylinder_face_triangles(n, n+1)] ] diff --git a/src/sas/qtgui/GL/GraphWidget.py b/src/sas/qtgui/GL/GraphWidget.py index cd4403f421..74bb1c08cb 100644 --- a/src/sas/qtgui/GL/GraphWidget.py +++ b/src/sas/qtgui/GL/GraphWidget.py @@ -82,7 +82,7 @@ def paintGL(self): glEnable(GL_POLYGON_OFFSET_FILL) for item in self._items: - glPolygonOffset(0.0, 20.0) + glPolygonOffset(1.0, 10.0) item.render_solid() glPolygonOffset(0.0, 0.0) item.render_wireframe() diff --git a/src/sas/qtgui/GL/PrimativeLibrary.py b/src/sas/qtgui/GL/PrimativeLibrary.py index 7cc21b21e8..9e44f07ace 100644 --- a/src/sas/qtgui/GL/PrimativeLibrary.py +++ b/src/sas/qtgui/GL/PrimativeLibrary.py @@ -34,9 +34,9 @@ def primative_library(): app = QtWidgets.QApplication([]) item_list = [ - # mesh_example(), - # Cube(edge_colors=Color(1, 1, 1), face_colors=Color(0.7, 0, 0)), - # Cone(edge_colors=Color(1, 1, 1), vertex_colors=Color(0, 0.7, 0)), + mesh_example(), + Cube(edge_colors=Color(1, 1, 1), face_colors=Color(0.7, 0, 0)), + Cone(edge_colors=Color(1, 1, 1), vertex_colors=Color(0, 0.7, 0)), Cylinder(edge_colors=Color(1, 1, 1), vertex_colors=Color(0, 0, 0.7)) ] @@ -83,7 +83,6 @@ def scan_states(): # Keyboard callback def enable_disable(key): - print(key) next(state) viewer.update() From 2494d972ed631f6343f6a90ed95501105c72a2b8 Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Thu, 24 Nov 2022 18:17:21 +0000 Subject: [PATCH 42/87] added icosahedron, and renamed primitives to lower case --- src/sas/qtgui/GL/{GraphWidget.py => Scene.py} | 8 +- src/sas/qtgui/GL/{Arrow.py => arrow.py} | 0 src/sas/qtgui/GL/{Color.py => color.py} | 0 src/sas/qtgui/GL/{Cone.py => cone.py} | 4 +- src/sas/qtgui/GL/{Cube.py => cube.py} | 4 +- src/sas/qtgui/GL/{Cylinder.py => cylinder.py} | 4 +- .../qtgui/GL/{Sphere.py => icosahedron.py} | 0 src/sas/qtgui/GL/{Models.py => models.py} | 4 +- ...imativeLibrary.py => primitive_library.py} | 24 ++-- .../qtgui/GL/{Renderable.py => renderable.py} | 0 src/sas/qtgui/GL/sphere.py | 136 ++++++++++++++++++ src/sas/qtgui/GL/{Surface.py => surface.py} | 4 +- .../qtgui/GL/transformations/RenderFilter.py | 2 +- src/sas/qtgui/GL/transformations/Rotation.py | 2 +- src/sas/qtgui/GL/transformations/Scale.py | 2 +- .../GL/transformations/SceneGraphNode.py | 2 +- src/sas/qtgui/GL/transformations/Transform.py | 2 +- .../qtgui/GL/transformations/Translation.py | 2 +- .../OrientationViewer/OrientationViewer.py | 2 +- 19 files changed, 170 insertions(+), 32 deletions(-) rename src/sas/qtgui/GL/{GraphWidget.py => Scene.py} (97%) rename src/sas/qtgui/GL/{Arrow.py => arrow.py} (100%) rename src/sas/qtgui/GL/{Color.py => color.py} (100%) rename src/sas/qtgui/GL/{Cone.py => cone.py} (95%) rename src/sas/qtgui/GL/{Cube.py => cube.py} (94%) rename src/sas/qtgui/GL/{Cylinder.py => cylinder.py} (96%) rename src/sas/qtgui/GL/{Sphere.py => icosahedron.py} (100%) rename src/sas/qtgui/GL/{Models.py => models.py} (98%) rename src/sas/qtgui/GL/{PrimativeLibrary.py => primitive_library.py} (74%) rename src/sas/qtgui/GL/{Renderable.py => renderable.py} (100%) create mode 100644 src/sas/qtgui/GL/sphere.py rename src/sas/qtgui/GL/{Surface.py => surface.py} (95%) diff --git a/src/sas/qtgui/GL/GraphWidget.py b/src/sas/qtgui/GL/Scene.py similarity index 97% rename from src/sas/qtgui/GL/GraphWidget.py rename to src/sas/qtgui/GL/Scene.py index 74bb1c08cb..13a2e67b51 100644 --- a/src/sas/qtgui/GL/GraphWidget.py +++ b/src/sas/qtgui/GL/Scene.py @@ -6,10 +6,10 @@ from OpenGL.GL import * from OpenGL.GLU import * -from sas.qtgui.GL.Renderable import Renderable -from sas.qtgui.GL.Color import Color -from sas.qtgui.GL.Surface import Surface -from sas.qtgui.GL.Cone import Cone +from sas.qtgui.GL.renderable import Renderable +from sas.qtgui.GL.color import Color +from sas.qtgui.GL.surface import Surface +from sas.qtgui.GL.cone import Cone class GraphWidget(QtOpenGL.QGLWidget): diff --git a/src/sas/qtgui/GL/Arrow.py b/src/sas/qtgui/GL/arrow.py similarity index 100% rename from src/sas/qtgui/GL/Arrow.py rename to src/sas/qtgui/GL/arrow.py diff --git a/src/sas/qtgui/GL/Color.py b/src/sas/qtgui/GL/color.py similarity index 100% rename from src/sas/qtgui/GL/Color.py rename to src/sas/qtgui/GL/color.py diff --git a/src/sas/qtgui/GL/Cone.py b/src/sas/qtgui/GL/cone.py similarity index 95% rename from src/sas/qtgui/GL/Cone.py rename to src/sas/qtgui/GL/cone.py index bcd8288ed5..d4ef6888da 100644 --- a/src/sas/qtgui/GL/Cone.py +++ b/src/sas/qtgui/GL/cone.py @@ -2,8 +2,8 @@ import numpy as np -from sas.qtgui.GL.Models import FullVertexModel, WireModel -from sas.qtgui.GL.Color import Color +from sas.qtgui.GL.models import FullVertexModel, WireModel +from sas.qtgui.GL.color import Color class Cone(FullVertexModel): diff --git a/src/sas/qtgui/GL/Cube.py b/src/sas/qtgui/GL/cube.py similarity index 94% rename from src/sas/qtgui/GL/Cube.py rename to src/sas/qtgui/GL/cube.py index e6a434bf2c..5deca733c7 100644 --- a/src/sas/qtgui/GL/Cube.py +++ b/src/sas/qtgui/GL/cube.py @@ -1,7 +1,7 @@ from typing import Optional, Union, Sequence -from sas.qtgui.GL.Models import FullVertexModel, WireModel -from sas.qtgui.GL.Color import Color +from sas.qtgui.GL.models import FullVertexModel, WireModel +from sas.qtgui.GL.color import Color class Cube(FullVertexModel): diff --git a/src/sas/qtgui/GL/Cylinder.py b/src/sas/qtgui/GL/cylinder.py similarity index 96% rename from src/sas/qtgui/GL/Cylinder.py rename to src/sas/qtgui/GL/cylinder.py index 65fda03dcb..5711a767b0 100644 --- a/src/sas/qtgui/GL/Cylinder.py +++ b/src/sas/qtgui/GL/cylinder.py @@ -2,8 +2,8 @@ import numpy as np -from sas.qtgui.GL.Models import FullVertexModel, WireModel -from sas.qtgui.GL.Color import Color +from sas.qtgui.GL.models import FullVertexModel, WireModel +from sas.qtgui.GL.color import Color class Cylinder(FullVertexModel): diff --git a/src/sas/qtgui/GL/Sphere.py b/src/sas/qtgui/GL/icosahedron.py similarity index 100% rename from src/sas/qtgui/GL/Sphere.py rename to src/sas/qtgui/GL/icosahedron.py diff --git a/src/sas/qtgui/GL/Models.py b/src/sas/qtgui/GL/models.py similarity index 98% rename from src/sas/qtgui/GL/Models.py rename to src/sas/qtgui/GL/models.py index eab8aa2f08..ab3c77fb72 100644 --- a/src/sas/qtgui/GL/Models.py +++ b/src/sas/qtgui/GL/models.py @@ -4,8 +4,8 @@ from OpenGL.GL import * -from sas.qtgui.GL.Renderable import Renderable -from sas.qtgui.GL.Color import Color +from sas.qtgui.GL.renderable import Renderable +from sas.qtgui.GL.color import Color def color_sequence_to_array(colors: Union[Sequence[Color], Color]): diff --git a/src/sas/qtgui/GL/PrimativeLibrary.py b/src/sas/qtgui/GL/primitive_library.py similarity index 74% rename from src/sas/qtgui/GL/PrimativeLibrary.py rename to src/sas/qtgui/GL/primitive_library.py index 9e44f07ace..8f0d4a7c07 100644 --- a/src/sas/qtgui/GL/PrimativeLibrary.py +++ b/src/sas/qtgui/GL/primitive_library.py @@ -5,13 +5,14 @@ from PyQt5 import QtWidgets, Qt, QtGui, QtOpenGL, QtCore -from sas.qtgui.GL.GraphWidget import GraphWidget -from sas.qtgui.GL.Models import WireModel, SolidModel, ModelBase -from sas.qtgui.GL.Color import Color -from sas.qtgui.GL.Surface import Surface -from sas.qtgui.GL.Cone import Cone -from sas.qtgui.GL.Cube import Cube -from sas.qtgui.GL.Cylinder import Cylinder +from sas.qtgui.GL.Scene import GraphWidget +from sas.qtgui.GL.models import WireModel, SolidModel, ModelBase +from sas.qtgui.GL.color import Color +from sas.qtgui.GL.surface import Surface +from sas.qtgui.GL.cone import Cone +from sas.qtgui.GL.cube import Cube +from sas.qtgui.GL.cylinder import Cylinder +from sas.qtgui.GL.sphere import Icosahedron def mesh_example(): @@ -34,10 +35,11 @@ def primative_library(): app = QtWidgets.QApplication([]) item_list = [ - mesh_example(), - Cube(edge_colors=Color(1, 1, 1), face_colors=Color(0.7, 0, 0)), - Cone(edge_colors=Color(1, 1, 1), vertex_colors=Color(0, 0.7, 0)), - Cylinder(edge_colors=Color(1, 1, 1), vertex_colors=Color(0, 0, 0.7)) + # mesh_example(), + # Cube(edge_colors=Color(1, 1, 1), face_colors=Color(0.7, 0.2, 0)), + # Cone(edge_colors=Color(1, 1, 1), vertex_colors=Color(0, 0.7, 0.2)), + # Cylinder(edge_colors=Color(1, 1, 1), vertex_colors=Color(0, 0.2, 0.7)), + Icosahedron(edge_colors=Color(1, 1, 1), vertex_colors=Color(0.7, 0, 0.7)) ] # Turn off all of them diff --git a/src/sas/qtgui/GL/Renderable.py b/src/sas/qtgui/GL/renderable.py similarity index 100% rename from src/sas/qtgui/GL/Renderable.py rename to src/sas/qtgui/GL/renderable.py diff --git a/src/sas/qtgui/GL/sphere.py b/src/sas/qtgui/GL/sphere.py new file mode 100644 index 0000000000..fe43edbc7b --- /dev/null +++ b/src/sas/qtgui/GL/sphere.py @@ -0,0 +1,136 @@ +from typing import Optional, Union, Sequence + +import numpy as np + +from sas.qtgui.GL.models import FullVertexModel +from sas.qtgui.GL.color import Color + +ico_ring_h = np.sqrt(1/5) +ico_ring_r = np.sqrt(4/5) + +class Icosahedron(FullVertexModel): + """ Icosahedron centred at 0,0,0""" + + + + ico_vertices = \ + [(0.0, 0.0, 1.0)] + \ + [(ico_ring_r * np.cos(angle), ico_ring_r * np.sin(angle), ico_ring_h) for angle in 2*np.pi*np.arange(5)/5] + \ + [(ico_ring_r * np.cos(angle), ico_ring_r * np.sin(angle), -ico_ring_h) for angle in 2*np.pi*(np.arange(5)+0.5)/5] + \ + [(0.0, 0.0, -1.0)] + + ico_edges = [ + (0, 1), # Top converging + (0, 2), + (0, 3), + (0, 4), + (0, 5), + (1, 2), # Top radial + (2, 3), + (3, 4), + (4, 5), + (5, 1), # Middle diagonals, advanced + (1, 6), + (2, 7), + (3, 8), + (4, 9), + (5, 10), + (1, 10), # Middle diagonals, delayed + (2, 6), + (3, 7), + (4, 8), + (5, 9), + (6, 7), # Bottom radial + (7, 8), + (8, 9), + (9, 10), + (10, 6), + (6, 11), # Bottom converging + (7, 11), + (8, 11), + (9, 11), + (10, 11), + ] + + ico_triangles = [[ + (0, 1, 2), # Top cap + (0, 2, 3), + (0, 3, 4), + (0, 4, 5), + (0, 5, 1), + (2, 1, 6), # Top middle ring + (3, 2, 7), + (4, 3, 8), + (5, 4, 9), + (1, 5, 10), + (6, 10, 1), # Bottom middle ring + (7, 6, 2), + (8, 7, 3), + (9, 8, 4), + (10, 9, 5), + (6, 7, 11), # Bottom cap + (7, 8, 11), + (8, 9, 11), + (9, 10, 11), + (10, 6, 11) + ]] + + def __init__(self, + vertex_colors: Optional[Union[Sequence[Color], Color]]=None, + edge_colors: Optional[Union[Sequence[Color],Color]]=None): + + super().__init__( + vertices=Icosahedron.ico_vertices, + edges=Icosahedron.ico_edges, + triangle_meshes=Icosahedron.ico_triangles, + edge_colors=edge_colors, + vertex_colors=vertex_colors) + + + if edge_colors is None: + self.wireframe_render_enabled = False + self.edge_colors = [] + else: + self.wireframe_render_enabled = True + self.edge_colors = edge_colors + + if vertex_colors is None: + self.solid_render_enabled = False + self.face_colors = [] + else: + self.solid_render_enabled = True + self.face_colors = vertex_colors + + + +class UVSphere(): + + def __init__(self, + n_horizontal: int = 5, + n_segments: int = 5, + face_colors: Optional[Union[Sequence[Color],Color]]=None, + edge_colors: Optional[Union[Sequence[Color],Color]]=None): + + super().__init__( + vertices=Cube.cube_vertices, + edges=Cube.cube_edges, + triangle_meshes=Cube.cube_triangles, + edge_colors=edge_colors, + vertex_colors=face_colors) + + self.vertices = Cube.cube_vertices + self.edges = Cube.cube_edges + + if edge_colors is None: + self.wireframe_render_enabled = False + self.edge_colors = [] + else: + self.wireframe_render_enabled = True + self.edge_colors = edge_colors + + if face_colors is None: + self.solid_render_enabled = False + self.face_colors = [] + else: + self.solid_render_enabled = True + self.face_colors = face_colors diff --git a/src/sas/qtgui/GL/Surface.py b/src/sas/qtgui/GL/surface.py similarity index 95% rename from src/sas/qtgui/GL/Surface.py rename to src/sas/qtgui/GL/surface.py index 58aecbc6c4..5567a8040a 100644 --- a/src/sas/qtgui/GL/Surface.py +++ b/src/sas/qtgui/GL/surface.py @@ -4,8 +4,8 @@ import matplotlib as mpl -from sas.qtgui.GL.Models import FullVertexModel, WireModel -from sas.qtgui.GL.Color import Color, ColorMap +from sas.qtgui.GL.models import FullVertexModel, WireModel +from sas.qtgui.GL.color import Color, ColorMap logger = logging.getLogger("GL.Surface") diff --git a/src/sas/qtgui/GL/transformations/RenderFilter.py b/src/sas/qtgui/GL/transformations/RenderFilter.py index c1cb290dd7..f5dbf0078b 100644 --- a/src/sas/qtgui/GL/transformations/RenderFilter.py +++ b/src/sas/qtgui/GL/transformations/RenderFilter.py @@ -1,7 +1,7 @@ import numpy as np from sas.qtgui.GL.transformations.SceneGraphNode import SceneGraphNode -from sas.qtgui.GL.Renderable import Renderable +from sas.qtgui.GL.renderable import Renderable from OpenGL.GL import * from OpenGL.GLU import * diff --git a/src/sas/qtgui/GL/transformations/Rotation.py b/src/sas/qtgui/GL/transformations/Rotation.py index 6a0b19100f..bfb5f52e64 100644 --- a/src/sas/qtgui/GL/transformations/Rotation.py +++ b/src/sas/qtgui/GL/transformations/Rotation.py @@ -1,7 +1,7 @@ import numpy as np from sas.qtgui.GL.transformations.SceneGraphNode import SceneGraphNode -from sas.qtgui.GL.Renderable import Renderable +from sas.qtgui.GL.renderable import Renderable from OpenGL.GL import * from OpenGL.GLU import * diff --git a/src/sas/qtgui/GL/transformations/Scale.py b/src/sas/qtgui/GL/transformations/Scale.py index ba68bf7de1..605e4b3755 100644 --- a/src/sas/qtgui/GL/transformations/Scale.py +++ b/src/sas/qtgui/GL/transformations/Scale.py @@ -1,6 +1,6 @@ import numpy as np from sas.qtgui.GL.transformations.SceneGraphNode import SceneGraphNode -from sas.qtgui.GL.Renderable import Renderable +from sas.qtgui.GL.renderable import Renderable from OpenGL.GL import * from OpenGL.GLU import * diff --git a/src/sas/qtgui/GL/transformations/SceneGraphNode.py b/src/sas/qtgui/GL/transformations/SceneGraphNode.py index 8a8bb47e77..c6e0e320f0 100644 --- a/src/sas/qtgui/GL/transformations/SceneGraphNode.py +++ b/src/sas/qtgui/GL/transformations/SceneGraphNode.py @@ -5,7 +5,7 @@ from OpenGL.GL import * from OpenGL.GLU import * -from sas.qtgui.GL.Renderable import Renderable +from sas.qtgui.GL.renderable import Renderable class SceneGraphNode(Renderable): """ diff --git a/src/sas/qtgui/GL/transformations/Transform.py b/src/sas/qtgui/GL/transformations/Transform.py index 5304fa2211..75306d7ea4 100644 --- a/src/sas/qtgui/GL/transformations/Transform.py +++ b/src/sas/qtgui/GL/transformations/Transform.py @@ -3,7 +3,7 @@ from OpenGL.GL import * from OpenGL.GLU import * -from sas.qtgui.GL.Renderable import Renderable +from sas.qtgui.GL.renderable import Renderable from sas.qtgui.GL.transformations.SceneGraphNode import SceneGraphNode diff --git a/src/sas/qtgui/GL/transformations/Translation.py b/src/sas/qtgui/GL/transformations/Translation.py index 518af3f88f..c04a151c8b 100644 --- a/src/sas/qtgui/GL/transformations/Translation.py +++ b/src/sas/qtgui/GL/transformations/Translation.py @@ -1,5 +1,5 @@ import numpy as np -from sas.qtgui.GL.Renderable import Renderable +from sas.qtgui.GL.renderable import Renderable from sas.qtgui.GL.transformations.SceneGraphNode import SceneGraphNode from OpenGL.GL import * diff --git a/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py b/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py index 715bb4eb67..6446d366d9 100644 --- a/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py +++ b/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py @@ -11,7 +11,7 @@ from sasmodels.data import empty_data2D from sasmodels.direct_model import DirectModel -from sas.qtgui.GL.GraphWidget import GraphWidget +from sas.qtgui.GL.Scene import GraphWidget from sas.qtgui.Utilities.OrientationViewer.OrientationViewerController import OrientationViewierController, Orientation from sas.qtgui.Utilities.OrientationViewer.OrientationViewerGraphics import OrientationViewerGraphics From 7fe44498787d79fd7cbedc2edec909d66fa69192 Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Thu, 24 Nov 2022 18:19:51 +0000 Subject: [PATCH 43/87] moved ico --- src/sas/qtgui/GL/icosahedron.py | 103 ++++++++++++++++++++++++++ src/sas/qtgui/GL/primitive_library.py | 12 +-- src/sas/qtgui/GL/sphere.py | 100 ------------------------- 3 files changed, 109 insertions(+), 106 deletions(-) diff --git a/src/sas/qtgui/GL/icosahedron.py b/src/sas/qtgui/GL/icosahedron.py index e69de29bb2..6bc77ee614 100644 --- a/src/sas/qtgui/GL/icosahedron.py +++ b/src/sas/qtgui/GL/icosahedron.py @@ -0,0 +1,103 @@ +from typing import Optional, Union, Sequence + +import numpy as np + +from sas.qtgui.GL.color import Color +from sas.qtgui.GL.models import FullVertexModel + +ico_ring_h = np.sqrt(1/5) +ico_ring_r = np.sqrt(4/5) + + +class Icosahedron(FullVertexModel): + """ Icosahedron centred at 0,0,0""" + + + + ico_vertices = \ + [(0.0, 0.0, 1.0)] + \ + [(ico_ring_r * np.cos(angle), ico_ring_r * np.sin(angle), ico_ring_h) for angle in 2*np.pi*np.arange(5)/5] + \ + [(ico_ring_r * np.cos(angle), ico_ring_r * np.sin(angle), -ico_ring_h) for angle in 2*np.pi*(np.arange(5)+0.5)/5] + \ + [(0.0, 0.0, -1.0)] + + ico_edges = [ + (0, 1), # Top converging + (0, 2), + (0, 3), + (0, 4), + (0, 5), + (1, 2), # Top radial + (2, 3), + (3, 4), + (4, 5), + (5, 1), # Middle diagonals, advanced + (1, 6), + (2, 7), + (3, 8), + (4, 9), + (5, 10), + (1, 10), # Middle diagonals, delayed + (2, 6), + (3, 7), + (4, 8), + (5, 9), + (6, 7), # Bottom radial + (7, 8), + (8, 9), + (9, 10), + (10, 6), + (6, 11), # Bottom converging + (7, 11), + (8, 11), + (9, 11), + (10, 11), + ] + + ico_triangles = [[ + (0, 1, 2), # Top cap + (0, 2, 3), + (0, 3, 4), + (0, 4, 5), + (0, 5, 1), + (2, 1, 6), # Top middle ring + (3, 2, 7), + (4, 3, 8), + (5, 4, 9), + (1, 5, 10), + (6, 10, 1), # Bottom middle ring + (7, 6, 2), + (8, 7, 3), + (9, 8, 4), + (10, 9, 5), + (6, 7, 11), # Bottom cap + (7, 8, 11), + (8, 9, 11), + (9, 10, 11), + (10, 6, 11) + ]] + + def __init__(self, + vertex_colors: Optional[Union[Sequence[Color], Color]]=None, + edge_colors: Optional[Union[Sequence[Color],Color]]=None): + + super().__init__( + vertices=Icosahedron.ico_vertices, + edges=Icosahedron.ico_edges, + triangle_meshes=Icosahedron.ico_triangles, + edge_colors=edge_colors, + vertex_colors=vertex_colors) + + + if edge_colors is None: + self.wireframe_render_enabled = False + self.edge_colors = [] + else: + self.wireframe_render_enabled = True + self.edge_colors = edge_colors + + if vertex_colors is None: + self.solid_render_enabled = False + self.face_colors = [] + else: + self.solid_render_enabled = True + self.face_colors = vertex_colors diff --git a/src/sas/qtgui/GL/primitive_library.py b/src/sas/qtgui/GL/primitive_library.py index 8f0d4a7c07..9484cde3ac 100644 --- a/src/sas/qtgui/GL/primitive_library.py +++ b/src/sas/qtgui/GL/primitive_library.py @@ -12,7 +12,7 @@ from sas.qtgui.GL.cone import Cone from sas.qtgui.GL.cube import Cube from sas.qtgui.GL.cylinder import Cylinder -from sas.qtgui.GL.sphere import Icosahedron +from sas.qtgui.GL.icosahedron import Icosahedron def mesh_example(): @@ -27,7 +27,7 @@ def mesh_example(): def primative_library(): - """ Shows all the existing primitives that can be rendered, space to go through them""" + """ Shows all the existing primitives that can be rendered, press a key to go through them""" import os @@ -35,10 +35,10 @@ def primative_library(): app = QtWidgets.QApplication([]) item_list = [ - # mesh_example(), - # Cube(edge_colors=Color(1, 1, 1), face_colors=Color(0.7, 0.2, 0)), - # Cone(edge_colors=Color(1, 1, 1), vertex_colors=Color(0, 0.7, 0.2)), - # Cylinder(edge_colors=Color(1, 1, 1), vertex_colors=Color(0, 0.2, 0.7)), + mesh_example(), + Cube(edge_colors=Color(1, 1, 1), face_colors=Color(0.7, 0.2, 0)), + Cone(edge_colors=Color(1, 1, 1), vertex_colors=Color(0, 0.7, 0.2)), + Cylinder(edge_colors=Color(1, 1, 1), vertex_colors=Color(0, 0.2, 0.7)), Icosahedron(edge_colors=Color(1, 1, 1), vertex_colors=Color(0.7, 0, 0.7)) ] diff --git a/src/sas/qtgui/GL/sphere.py b/src/sas/qtgui/GL/sphere.py index fe43edbc7b..48eb6396e8 100644 --- a/src/sas/qtgui/GL/sphere.py +++ b/src/sas/qtgui/GL/sphere.py @@ -1,107 +1,7 @@ from typing import Optional, Union, Sequence -import numpy as np - -from sas.qtgui.GL.models import FullVertexModel from sas.qtgui.GL.color import Color -ico_ring_h = np.sqrt(1/5) -ico_ring_r = np.sqrt(4/5) - -class Icosahedron(FullVertexModel): - """ Icosahedron centred at 0,0,0""" - - - - ico_vertices = \ - [(0.0, 0.0, 1.0)] + \ - [(ico_ring_r * np.cos(angle), ico_ring_r * np.sin(angle), ico_ring_h) for angle in 2*np.pi*np.arange(5)/5] + \ - [(ico_ring_r * np.cos(angle), ico_ring_r * np.sin(angle), -ico_ring_h) for angle in 2*np.pi*(np.arange(5)+0.5)/5] + \ - [(0.0, 0.0, -1.0)] - - ico_edges = [ - (0, 1), # Top converging - (0, 2), - (0, 3), - (0, 4), - (0, 5), - (1, 2), # Top radial - (2, 3), - (3, 4), - (4, 5), - (5, 1), # Middle diagonals, advanced - (1, 6), - (2, 7), - (3, 8), - (4, 9), - (5, 10), - (1, 10), # Middle diagonals, delayed - (2, 6), - (3, 7), - (4, 8), - (5, 9), - (6, 7), # Bottom radial - (7, 8), - (8, 9), - (9, 10), - (10, 6), - (6, 11), # Bottom converging - (7, 11), - (8, 11), - (9, 11), - (10, 11), - ] - - ico_triangles = [[ - (0, 1, 2), # Top cap - (0, 2, 3), - (0, 3, 4), - (0, 4, 5), - (0, 5, 1), - (2, 1, 6), # Top middle ring - (3, 2, 7), - (4, 3, 8), - (5, 4, 9), - (1, 5, 10), - (6, 10, 1), # Bottom middle ring - (7, 6, 2), - (8, 7, 3), - (9, 8, 4), - (10, 9, 5), - (6, 7, 11), # Bottom cap - (7, 8, 11), - (8, 9, 11), - (9, 10, 11), - (10, 6, 11) - ]] - - def __init__(self, - vertex_colors: Optional[Union[Sequence[Color], Color]]=None, - edge_colors: Optional[Union[Sequence[Color],Color]]=None): - - super().__init__( - vertices=Icosahedron.ico_vertices, - edges=Icosahedron.ico_edges, - triangle_meshes=Icosahedron.ico_triangles, - edge_colors=edge_colors, - vertex_colors=vertex_colors) - - - if edge_colors is None: - self.wireframe_render_enabled = False - self.edge_colors = [] - else: - self.wireframe_render_enabled = True - self.edge_colors = edge_colors - - if vertex_colors is None: - self.solid_render_enabled = False - self.face_colors = [] - else: - self.solid_render_enabled = True - self.face_colors = vertex_colors - - class UVSphere(): From 155547734ff502c409d1f2f295da7b482a6a4aa4 Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Thu, 24 Nov 2022 19:02:41 +0000 Subject: [PATCH 44/87] UV Sphere --- src/sas/qtgui/GL/primitive_library.py | 7 +- src/sas/qtgui/GL/sphere.py | 100 ++++++++++++++++++++++---- 2 files changed, 92 insertions(+), 15 deletions(-) diff --git a/src/sas/qtgui/GL/primitive_library.py b/src/sas/qtgui/GL/primitive_library.py index 9484cde3ac..e2f3109713 100644 --- a/src/sas/qtgui/GL/primitive_library.py +++ b/src/sas/qtgui/GL/primitive_library.py @@ -6,13 +6,14 @@ from PyQt5 import QtWidgets, Qt, QtGui, QtOpenGL, QtCore from sas.qtgui.GL.Scene import GraphWidget -from sas.qtgui.GL.models import WireModel, SolidModel, ModelBase +from sas.qtgui.GL.models import ModelBase from sas.qtgui.GL.color import Color from sas.qtgui.GL.surface import Surface from sas.qtgui.GL.cone import Cone from sas.qtgui.GL.cube import Cube from sas.qtgui.GL.cylinder import Cylinder from sas.qtgui.GL.icosahedron import Icosahedron +from sas.qtgui.GL.sphere import Sphere def mesh_example(): @@ -39,7 +40,9 @@ def primative_library(): Cube(edge_colors=Color(1, 1, 1), face_colors=Color(0.7, 0.2, 0)), Cone(edge_colors=Color(1, 1, 1), vertex_colors=Color(0, 0.7, 0.2)), Cylinder(edge_colors=Color(1, 1, 1), vertex_colors=Color(0, 0.2, 0.7)), - Icosahedron(edge_colors=Color(1, 1, 1), vertex_colors=Color(0.7, 0, 0.7)) + Icosahedron(edge_colors=Color(1, 1, 1), vertex_colors=Color(0.7, 0, 0.7)), + Sphere(edge_colors=Color(1, 1, 1), vertex_colors=Color(0.7, 0.7, 0.0)), + Sphere(edge_colors=Color(1, 1, 1), vertex_colors=Color(0.7, 0.4, 0.0), grid_gap=4) ] # Turn off all of them diff --git a/src/sas/qtgui/GL/sphere.py b/src/sas/qtgui/GL/sphere.py index 48eb6396e8..703d2d26fa 100644 --- a/src/sas/qtgui/GL/sphere.py +++ b/src/sas/qtgui/GL/sphere.py @@ -1,25 +1,99 @@ from typing import Optional, Union, Sequence +import numpy as np + +from sas.qtgui.GL.models import FullVertexModel from sas.qtgui.GL.color import Color -class UVSphere(): +class Sphere(FullVertexModel): + @staticmethod + def sphere_vertices(n_horizontal, n_segments): + vertices = [(0.0, 0.0, 1.0)] + + for theta in (np.pi/n_horizontal)*np.arange(0.5, n_horizontal): + for phi in (2*np.pi/n_segments)*np.arange(n_segments): + sin_theta = np.sin(theta) + x = sin_theta * np.cos(phi) + y = sin_theta * np.sin(phi) + z = np.cos(theta) + vertices.append((x,y,z)) + + vertices.append((0.0, 0.0, -1.0)) + + return vertices + + @staticmethod + def sphere_edges(n_horizontal, n_segments, grid_gap): + edges = [] + + # Bands + for i in range(0, n_horizontal, grid_gap): + for j in range(n_segments): + edges.append((i*n_segments + j + 1, i*n_segments + (j+1)%n_segments + 1)) + + # Vertical lines + for i in range(n_horizontal-1): + for j in range(0, n_segments, grid_gap): + edges.append((i*n_segments + j + 1, (i+1)*n_segments + j + 1)) + + return edges + + @staticmethod + def sphere_triangles(n_horizontal, n_segments): + triangles = [] + last_index = n_horizontal*n_segments + 1 + + # Top cap + for j in range(n_segments): + triangles.append((j+1, (j+1)%n_segments + 1, 0)) + + # Mid bands + for i in range(n_horizontal-1): + for j in range(n_segments): + triangles.append((i*n_segments + j + 1, (i+1)*n_segments + (j+1)%n_segments + 1, (i+1)*n_segments + j+1)) + triangles.append(((i+1)*n_segments + (j+1)%n_segments + 1, i*n_segments + j + 1, i*n_segments + (j+1)%n_segments + 1)) + + # Bottom cap + for j in range(n_segments): + triangles.append(((n_horizontal-1)*n_segments + j + 1, (n_horizontal-1)*n_segments + (j + 1) % n_segments + 1, last_index)) + + return [triangles] def __init__(self, - n_horizontal: int = 5, - n_segments: int = 5, - face_colors: Optional[Union[Sequence[Color],Color]]=None, + n_horizontal: int = 21, + n_segments: int = 28, + grid_gap: int = 1, + vertex_colors: Optional[Union[Sequence[Color], Color]]=None, edge_colors: Optional[Union[Sequence[Color],Color]]=None): + """ + + UV Sphere Primitive + + :param n_horizontal: Number of horizontal bands + :param n_segments: Number of segments (angular) + :param grid_gap: Coarse grain the wireframe by skipping every 'grid_gap' coordinates + :param vertex_colors: List of colours for each vertex, or a single color for all + :param edge_colors: List of colours for each edge, or a single color for all + + Note: For aesthetically pleasing results with `grid_gap`, `n_segments` should be a multiple + of `grid_gap`, and `n_horizontal - 1` should be too. + + Default parameters should work with a grid gap of 2 or 4. + """ + if n_segments < 3: + raise ValueError(f"Sphere must have at least 3 segments, got {n_segments}") + + if n_horizontal < 2: + raise ValueError(f"Sphere must have at least 2 horizontal strips, got {n_horizontal}") + super().__init__( - vertices=Cube.cube_vertices, - edges=Cube.cube_edges, - triangle_meshes=Cube.cube_triangles, + vertices=Sphere.sphere_vertices(n_horizontal, n_segments), + edges=Sphere.sphere_edges(n_horizontal, n_segments, grid_gap), + triangle_meshes=Sphere.sphere_triangles(n_horizontal, n_segments), edge_colors=edge_colors, - vertex_colors=face_colors) - - self.vertices = Cube.cube_vertices - self.edges = Cube.cube_edges + vertex_colors=vertex_colors) if edge_colors is None: self.wireframe_render_enabled = False @@ -28,9 +102,9 @@ def __init__(self, self.wireframe_render_enabled = True self.edge_colors = edge_colors - if face_colors is None: + if vertex_colors is None: self.solid_render_enabled = False self.face_colors = [] else: self.solid_render_enabled = True - self.face_colors = face_colors + self.face_colors = vertex_colors From 89ea45600eacdad909c45c8ec330b26a2f117ad5 Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Thu, 24 Nov 2022 20:07:28 +0000 Subject: [PATCH 45/87] Filling in and refactoring transforms --- src/sas/qtgui/GL/{Scene.py => scene.py} | 5 + .../qtgui/GL/transformations/RenderFilter.py | 10 -- src/sas/qtgui/GL/transformations/Rotation.py | 37 ------ src/sas/qtgui/GL/transformations/Scale.py | 36 ------ .../GL/transformations/SceneGraphNode.py | 26 ---- src/sas/qtgui/GL/transformations/Transform.py | 38 ------ .../qtgui/GL/transformations/Translation.py | 36 ------ src/sas/qtgui/GL/transforms.py | 122 ++++++++++++++++++ .../__init__.py | 0 .../{ => visual_checks}/primitive_library.py | 3 +- .../GL/visual_checks/transform_library.py | 115 +++++++++++++++++ .../OrientationViewer/OrientationViewer.py | 2 +- 12 files changed, 245 insertions(+), 185 deletions(-) rename src/sas/qtgui/GL/{Scene.py => scene.py} (99%) delete mode 100644 src/sas/qtgui/GL/transformations/RenderFilter.py delete mode 100644 src/sas/qtgui/GL/transformations/Rotation.py delete mode 100644 src/sas/qtgui/GL/transformations/Scale.py delete mode 100644 src/sas/qtgui/GL/transformations/SceneGraphNode.py delete mode 100644 src/sas/qtgui/GL/transformations/Transform.py delete mode 100644 src/sas/qtgui/GL/transformations/Translation.py create mode 100644 src/sas/qtgui/GL/transforms.py rename src/sas/qtgui/GL/{transformations => visual_checks}/__init__.py (100%) rename src/sas/qtgui/GL/{ => visual_checks}/primitive_library.py (98%) create mode 100644 src/sas/qtgui/GL/visual_checks/transform_library.py diff --git a/src/sas/qtgui/GL/Scene.py b/src/sas/qtgui/GL/scene.py similarity index 99% rename from src/sas/qtgui/GL/Scene.py rename to src/sas/qtgui/GL/scene.py index 13a2e67b51..00ea66d77a 100644 --- a/src/sas/qtgui/GL/Scene.py +++ b/src/sas/qtgui/GL/scene.py @@ -181,6 +181,11 @@ def add(self, item: Renderable): def keyPressEvent(self, event: QtGui.QKeyEvent): self.on_key(event.key()) + + + + + def main(): """ Show a demo of the opengl window """ import os diff --git a/src/sas/qtgui/GL/transformations/RenderFilter.py b/src/sas/qtgui/GL/transformations/RenderFilter.py deleted file mode 100644 index f5dbf0078b..0000000000 --- a/src/sas/qtgui/GL/transformations/RenderFilter.py +++ /dev/null @@ -1,10 +0,0 @@ -import numpy as np - -from sas.qtgui.GL.transformations.SceneGraphNode import SceneGraphNode -from sas.qtgui.GL.renderable import Renderable - -from OpenGL.GL import * -from OpenGL.GLU import * - -class RenderFlag(SceneGraphNode): - pass \ No newline at end of file diff --git a/src/sas/qtgui/GL/transformations/Rotation.py b/src/sas/qtgui/GL/transformations/Rotation.py deleted file mode 100644 index bfb5f52e64..0000000000 --- a/src/sas/qtgui/GL/transformations/Rotation.py +++ /dev/null @@ -1,37 +0,0 @@ -import numpy as np - -from sas.qtgui.GL.transformations.SceneGraphNode import SceneGraphNode -from sas.qtgui.GL.renderable import Renderable - -from OpenGL.GL import * -from OpenGL.GLU import * - -class Rotation(SceneGraphNode): - """ - General transform - also doubles as a scene graph node - - For the sake of speed, the transformation matrix shape is not checked. - It should be a 4x4 transformation matrix - """ - - def __init__(self, rotation: np.ndarray, *children: Renderable): - super().__init__(*children) - self.rotation = rotation - - def render_solid(self): - # Apply transform - - for child in self.children: - child.render_solid() - - # unapply transform - - def render_wireframe(self): - # Apply transform - - for child in self.children: - child.render_wireframe() - - # unapply transform - - diff --git a/src/sas/qtgui/GL/transformations/Scale.py b/src/sas/qtgui/GL/transformations/Scale.py deleted file mode 100644 index 605e4b3755..0000000000 --- a/src/sas/qtgui/GL/transformations/Scale.py +++ /dev/null @@ -1,36 +0,0 @@ -import numpy as np -from sas.qtgui.GL.transformations.SceneGraphNode import SceneGraphNode -from sas.qtgui.GL.renderable import Renderable - -from OpenGL.GL import * -from OpenGL.GLU import * - -class Scale(SceneGraphNode): - """ - General transform - also doubles as a scene graph node - - For the sake of speed, the transformation matrix shape is not checked. - It should be a 3 element vector - """ - - def __init__(self, scale: np.ndarray, *children: Renderable): - super().__init__(*children) - self.scale = scale - - def render_solid(self): - # Apply transform - - for child in self.children: - child.render_solid() - - # unapply transform - - def render_wireframe(self): - # Apply transform - - for child in self.children: - child.render_wireframe() - - # unapply transform - - diff --git a/src/sas/qtgui/GL/transformations/SceneGraphNode.py b/src/sas/qtgui/GL/transformations/SceneGraphNode.py deleted file mode 100644 index c6e0e320f0..0000000000 --- a/src/sas/qtgui/GL/transformations/SceneGraphNode.py +++ /dev/null @@ -1,26 +0,0 @@ -from typing import List - -import numpy as np - -from OpenGL.GL import * -from OpenGL.GLU import * - -from sas.qtgui.GL.renderable import Renderable - -class SceneGraphNode(Renderable): - """ - General transform - also doubles as a scene graph node - - For the sake of speed, the transformation matrix shape is not checked. - It should be a 4x4 transformation matrix - """ - - def __init__(self, matrix: np.ndarray, *children: Renderable): - super().__init__() - self.matrix = matrix - self.children: List[Renderable] = list(children) - - def add_child(self, child: Renderable): - self.children.append(child) - - diff --git a/src/sas/qtgui/GL/transformations/Transform.py b/src/sas/qtgui/GL/transformations/Transform.py deleted file mode 100644 index 75306d7ea4..0000000000 --- a/src/sas/qtgui/GL/transformations/Transform.py +++ /dev/null @@ -1,38 +0,0 @@ -import numpy as np - -from OpenGL.GL import * -from OpenGL.GLU import * - -from sas.qtgui.GL.renderable import Renderable -from sas.qtgui.GL.transformations.SceneGraphNode import SceneGraphNode - - -class Transform(SceneGraphNode): - """ - General transform - also doubles as a scene graph node - - For the sake of speed, the transformation matrix shape is not checked. - It should be a 4x4 transformation matrix - """ - - def __init__(self, matrix: np.ndarray, *children: Renderable): - super().__init__(*children) - self.matrix = matrix - - def render_solid(self): - # Apply transform - - for child in self.children: - child.render_solid() - - # unapply transform - - def render_wireframe(self): - # Apply transform - - for child in self.children: - child.render_wireframe() - - # unapply transform - - diff --git a/src/sas/qtgui/GL/transformations/Translation.py b/src/sas/qtgui/GL/transformations/Translation.py deleted file mode 100644 index c04a151c8b..0000000000 --- a/src/sas/qtgui/GL/transformations/Translation.py +++ /dev/null @@ -1,36 +0,0 @@ -import numpy as np -from sas.qtgui.GL.renderable import Renderable -from sas.qtgui.GL.transformations.SceneGraphNode import SceneGraphNode - -from OpenGL.GL import * -from OpenGL.GLU import * - -class Translation(SceneGraphNode): - """ - General transform - also doubles as a scene graph node - - For the sake of speed, the rotation matrix shape is not checked. - It should be a 3 element vector - """ - - def __init__(self, rotation: np.ndarray, *children: Renderable): - super().__init__(*children) - self.rotation = rotation - - def render_solid(self): - # Apply transform - - for child in self.children: - child.render_solid() - - # unapply transform - - def render_wireframe(self): - # Apply transform - - for child in self.children: - child.render_wireframe() - - # unapply transform - - diff --git a/src/sas/qtgui/GL/transforms.py b/src/sas/qtgui/GL/transforms.py new file mode 100644 index 0000000000..7aa97d4e35 --- /dev/null +++ b/src/sas/qtgui/GL/transforms.py @@ -0,0 +1,122 @@ +from typing import List + +import numpy as np + +from OpenGL.GL import * +from OpenGL.GLU import * + +from sas.qtgui.GL.renderable import Renderable + + + + +class SceneGraphNode(Renderable): + """ + General transform - also doubles as a scene graph node + + For the sake of speed, the transformation matrix shape is not checked. + It should be a 4x4 transformation matrix + """ + + def __init__(self, *children: Renderable): + super().__init__() + self.children: List[Renderable] = list(children) + self.solid_render_enabled = True + self.wireframe_render_enabled = True + + + def add_child(self, child: Renderable): + """ Add a renderable object to this scene graph node""" + self.children.append(child) + + def apply(self): + pass + + def render_solid(self): + if self.solid_render_enabled: + + # Apply transform + glPushMatrix() + self.apply() + + # Render children + for child in self.children: + child.render_solid() + + # Unapply + glPopMatrix() + + + def render_wireframe(self): + + if self.wireframe_render_enabled: + # Apply transform + glPushMatrix() + self.apply() + + # Render children + for child in self.children: + child.render_wireframe() + + # unapply transform + glPopMatrix() + + +class Rotation(SceneGraphNode): + + + def __init__(self, angle, axis, *children: Renderable): + """ + Rotate the children of this node + + :param angle: angle of rotation in TODO + :param axis: axis for rotation + """ + super().__init__(*children) + self.angle = angle + self.axis = axis + + def apply(self): + pass + + + +class Translation(SceneGraphNode): + + + def __init__(self, x: float, y: float, z: float, *children: Renderable): + """ + Translate the children of this node + + :param x: x translation + :param y: y translation + :param z: z translation + """ + super().__init__(*children) + self.x = x + self.y = y + self.z = z + + def apply(self): + glTranslate(self.x, self.y, self.z) + + +class Scale(SceneGraphNode): + + def __init__(self, x: float, y: float, z: float, *children: Renderable): + """ + Scale the children of this node + + :param x: x scale + :param y: y scale + :param z: z scale + """ + + super().__init__(*children) + self.x = x + self.y = y + self.z = z + + def apply(self): + glScale(self.x, self.y, self.z) + diff --git a/src/sas/qtgui/GL/transformations/__init__.py b/src/sas/qtgui/GL/visual_checks/__init__.py similarity index 100% rename from src/sas/qtgui/GL/transformations/__init__.py rename to src/sas/qtgui/GL/visual_checks/__init__.py diff --git a/src/sas/qtgui/GL/primitive_library.py b/src/sas/qtgui/GL/visual_checks/primitive_library.py similarity index 98% rename from src/sas/qtgui/GL/primitive_library.py rename to src/sas/qtgui/GL/visual_checks/primitive_library.py index e2f3109713..595bd808ad 100644 --- a/src/sas/qtgui/GL/primitive_library.py +++ b/src/sas/qtgui/GL/visual_checks/primitive_library.py @@ -5,7 +5,7 @@ from PyQt5 import QtWidgets, Qt, QtGui, QtOpenGL, QtCore -from sas.qtgui.GL.Scene import GraphWidget +from sas.qtgui.GL.scene import GraphWidget from sas.qtgui.GL.models import ModelBase from sas.qtgui.GL.color import Color from sas.qtgui.GL.surface import Surface @@ -35,6 +35,7 @@ def primative_library(): os.environ["QT_ENABLE_HIGHDPI_SCALING"] = "1" app = QtWidgets.QApplication([]) + item_list = [ mesh_example(), Cube(edge_colors=Color(1, 1, 1), face_colors=Color(0.7, 0.2, 0)), diff --git a/src/sas/qtgui/GL/visual_checks/transform_library.py b/src/sas/qtgui/GL/visual_checks/transform_library.py new file mode 100644 index 0000000000..474bad48a9 --- /dev/null +++ b/src/sas/qtgui/GL/visual_checks/transform_library.py @@ -0,0 +1,115 @@ +""" As close a thing as there are to tests for GL""" + +from typing import Optional, Tuple, List, Callable +import numpy as np + +from PyQt5 import QtWidgets, Qt, QtGui, QtOpenGL, QtCore + +from sas.qtgui.GL.scene import GraphWidget +from sas.qtgui.GL.models import ModelBase +from sas.qtgui.GL.color import Color +from sas.qtgui.GL.surface import Surface +from sas.qtgui.GL.cone import Cone +from sas.qtgui.GL.cube import Cube +from sas.qtgui.GL.cylinder import Cylinder +from sas.qtgui.GL.icosahedron import Icosahedron +from sas.qtgui.GL.sphere import Sphere +from sas.qtgui.GL.transforms import SceneGraphNode, Translation, Rotation, Scale + +def transform_tests(): + """ Shows all the existing primitives that can be rendered, press a key to go through them + + 1) 4 shapes in a vertical 2 unit x 2 unit grid - as cylinder and cone have r=1, they should touch + 2) 27 cubes in 3x3x3 grid, with scaling changing (1/2, 1, 2) in the same dimension as the translation + 3) + + """ + + import os + + os.environ["QT_ENABLE_HIGHDPI_SCALING"] = "1" + app = QtWidgets.QApplication([]) + + cube = Cube(edge_colors=Color(1, 1, 1), face_colors=Color(0.7, 0.2, 0)) + cone = Cone(edge_colors=Color(1, 1, 1), vertex_colors=Color(0, 0.7, 0.2)) + cylinder = Cylinder(edge_colors=Color(1, 1, 1), vertex_colors=Color(0, 0.2, 0.7)) + icos = Icosahedron(edge_colors=Color(1, 1, 1), vertex_colors=Color(0.7, 0, 0.7)) + + + # Translations + translate_test = \ + SceneGraphNode( + Translation(0,0,1, + Translation(0,-1,0,cube), + Translation(0,1,0,cone)), + Translation(0,0,-1, + Translation(0,-1,0,cylinder), + Translation(0,1,0,icos))) + + # Scaling + scaling_components = [] + for i in range(3): + for j in range(3): + for k in range(3): + components = Translation(-2*(i-1), -2*(j-1), -2*(k-1), Scale(2**(i-1), 2**(j-1), 2**(k-1), cube)) + scaling_components.append(components) + + scaling_test = Scale(0.5, 0.5, 0.5, *scaling_components) + + + item_list = [ + translate_test, + scaling_test + ] + + # Turn off all of them + for item in item_list: + item.solid_render_enabled = False + item.wireframe_render_enabled = False + + + # Thing for going through each of the draw types of the primatives + + def item_states(item: ModelBase): + + item.solid_render_enabled = True + item.wireframe_render_enabled = True + + yield None + + item.solid_render_enabled = False + item.wireframe_render_enabled = False + + def scan_states(): + while True: + for item in item_list: + for _ in item_states(item): + yield None + + state = scan_states() + next(state) + + + mainWindow = QtWidgets.QMainWindow() + viewer = GraphWidget(parent=mainWindow) + + # Keyboard callback + def enable_disable(key): + next(state) + viewer.update() + + viewer.on_key = enable_disable + + for item in item_list: + viewer.add(item) + + mainWindow.setCentralWidget(viewer) + + mainWindow.show() + + mainWindow.resize(600, 600) + app.exec_() + + +if __name__ == "__main__": + transform_tests() \ No newline at end of file diff --git a/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py b/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py index 6446d366d9..5897a264a3 100644 --- a/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py +++ b/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py @@ -11,7 +11,7 @@ from sasmodels.data import empty_data2D from sasmodels.direct_model import DirectModel -from sas.qtgui.GL.Scene import GraphWidget +from sas.qtgui.GL.scene import GraphWidget from sas.qtgui.Utilities.OrientationViewer.OrientationViewerController import OrientationViewierController, Orientation from sas.qtgui.Utilities.OrientationViewer.OrientationViewerGraphics import OrientationViewerGraphics From 132e916e0961a07f65d269ac28947b7aca24eeae Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Thu, 24 Nov 2022 20:12:02 +0000 Subject: [PATCH 46/87] minor change --- src/sas/qtgui/GL/visual_checks/transform_library.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sas/qtgui/GL/visual_checks/transform_library.py b/src/sas/qtgui/GL/visual_checks/transform_library.py index 474bad48a9..a7506ac35e 100644 --- a/src/sas/qtgui/GL/visual_checks/transform_library.py +++ b/src/sas/qtgui/GL/visual_checks/transform_library.py @@ -22,7 +22,7 @@ def transform_tests(): 1) 4 shapes in a vertical 2 unit x 2 unit grid - as cylinder and cone have r=1, they should touch 2) 27 cubes in 3x3x3 grid, with scaling changing (1/2, 1, 2) in the same dimension as the translation 3) - + """ import os From ef62f4b1b31d79d07b32a27a77d50c8e365462bc Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Thu, 24 Nov 2022 20:19:20 +0000 Subject: [PATCH 47/87] translate test --- .../GL/visual_checks/transform_library.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/sas/qtgui/GL/visual_checks/transform_library.py b/src/sas/qtgui/GL/visual_checks/transform_library.py index a7506ac35e..b1f9e7c455 100644 --- a/src/sas/qtgui/GL/visual_checks/transform_library.py +++ b/src/sas/qtgui/GL/visual_checks/transform_library.py @@ -21,7 +21,7 @@ def transform_tests(): 1) 4 shapes in a vertical 2 unit x 2 unit grid - as cylinder and cone have r=1, they should touch 2) 27 cubes in 3x3x3 grid, with scaling changing (1/2, 1, 2) in the same dimension as the translation - 3) + 3) As above but rotations by 45 degrees in around axis in the same dimension as the translation """ @@ -57,6 +57,23 @@ def transform_tests(): scaling_test = Scale(0.5, 0.5, 0.5, *scaling_components) + cone_sphere = Scale(0.5, 0.5, 0.5, + cone, + Translation(0,1,0, + Scale(0.5, 0.5, 0.5, + icos))) + + # Rotations + scaling_components = [] + for i in range(3): + for j in range(3): + for k in range(3): + components = Translation(-2*(i-1), -2*(j-1), -2*(k-1), Scale(2**(i-1), 2**(j-1), 2**(k-1), cone_sphere)) + scaling_components.append(components) + + scaling_test = Scale(0.5, 0.5, 0.5, *scaling_components) + + item_list = [ translate_test, scaling_test From 412329ec9bf2ca9fefe69371609f7cd10102033a Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Thu, 24 Nov 2022 20:42:50 +0000 Subject: [PATCH 48/87] Fixed nasty GL bug around matrix stack size limits --- src/sas/qtgui/GL/scene.py | 7 +++-- src/sas/qtgui/GL/transforms.py | 14 +++++++-- .../GL/visual_checks/transform_library.py | 31 ++++++++++--------- 3 files changed, 33 insertions(+), 19 deletions(-) diff --git a/src/sas/qtgui/GL/scene.py b/src/sas/qtgui/GL/scene.py index 00ea66d77a..8f3fd40fa8 100644 --- a/src/sas/qtgui/GL/scene.py +++ b/src/sas/qtgui/GL/scene.py @@ -14,7 +14,7 @@ class GraphWidget(QtOpenGL.QGLWidget): - def __init__(self, on_key: Callable[[int], None] = lambda x: None, parent=None): + def __init__(self, parent=None, on_key: Callable[[int], None] = lambda x: None): super().__init__(parent) self.setMinimumSize(640, 480) @@ -112,7 +112,6 @@ def set_projection(self): def set_model_view(self): - tr = QtGui.QMatrix4x4() # tr.translate(0.0, 0.0, -self.view_distance) tr.translate(0.0,0.0,-self.view_distance) @@ -131,6 +130,10 @@ def set_model_view(self): centre[0], centre[1], centre[2], 0.0, 0.0, 1.0) + + glMatrixMode(GL_MODELVIEW) + + def mousePressEvent(self, ev): self.mouse_position = ev.localPos() diff --git a/src/sas/qtgui/GL/transforms.py b/src/sas/qtgui/GL/transforms.py index 7aa97d4e35..f6365003db 100644 --- a/src/sas/qtgui/GL/transforms.py +++ b/src/sas/qtgui/GL/transforms.py @@ -1,3 +1,4 @@ +import logging from typing import List import numpy as np @@ -8,7 +9,7 @@ from sas.qtgui.GL.renderable import Renderable - +logger = logging.getLogger("GL.transforms") class SceneGraphNode(Renderable): """ @@ -39,6 +40,10 @@ def render_solid(self): glPushMatrix() self.apply() + # Check stack + if glGetIntegerv(GL_MODELVIEW_STACK_DEPTH) >= 16: + logger.info("GL Stack size utilisation {glGetIntegerv(GL_MODELVIEW_STACK_DEPTH))}, the limit could be as low as is 16") + # Render children for child in self.children: child.render_solid() @@ -54,6 +59,11 @@ def render_wireframe(self): glPushMatrix() self.apply() + # Check stack + if glGetIntegerv(GL_MODELVIEW_STACK_DEPTH) >= 16: + logger.info("GL Stack size utilisation {glGetIntegerv(GL_MODELVIEW_STACK_DEPTH))}, the limit could be as low as is 16") + + # Render children for child in self.children: child.render_wireframe() @@ -101,7 +111,7 @@ def apply(self): glTranslate(self.x, self.y, self.z) -class Scale(SceneGraphNode): +class Scaling(SceneGraphNode): def __init__(self, x: float, y: float, z: float, *children: Renderable): """ diff --git a/src/sas/qtgui/GL/visual_checks/transform_library.py b/src/sas/qtgui/GL/visual_checks/transform_library.py index b1f9e7c455..e6481ed5c5 100644 --- a/src/sas/qtgui/GL/visual_checks/transform_library.py +++ b/src/sas/qtgui/GL/visual_checks/transform_library.py @@ -14,7 +14,7 @@ from sas.qtgui.GL.cylinder import Cylinder from sas.qtgui.GL.icosahedron import Icosahedron from sas.qtgui.GL.sphere import Sphere -from sas.qtgui.GL.transforms import SceneGraphNode, Translation, Rotation, Scale +from sas.qtgui.GL.transforms import SceneGraphNode, Translation, Rotation, Scaling def transform_tests(): """ Shows all the existing primitives that can be rendered, press a key to go through them @@ -51,32 +51,33 @@ def transform_tests(): for i in range(3): for j in range(3): for k in range(3): - components = Translation(-2*(i-1), -2*(j-1), -2*(k-1), Scale(2**(i-1), 2**(j-1), 2**(k-1), cube)) - scaling_components.append(components) + component = Translation(-2 * (i-1), -2 * (j-1), -2 * (k-1), Scaling(2 ** (i - 1), 2 ** (j - 1), 2 ** (k - 1), cube)) + scaling_components.append(component) - scaling_test = Scale(0.5, 0.5, 0.5, *scaling_components) + scaling_test = Scaling(0.5, 0.5, 0.5, *scaling_components) - cone_sphere = Scale(0.5, 0.5, 0.5, - cone, - Translation(0,1,0, - Scale(0.5, 0.5, 0.5, - icos))) + cone_sphere = Scaling(0.5, 0.5, 0.5, + cone, + Translation(0, 1, 1, + Scaling(0.5, 0.5, 0.5, + icos))) # Rotations scaling_components = [] for i in range(3): for j in range(3): for k in range(3): - components = Translation(-2*(i-1), -2*(j-1), -2*(k-1), Scale(2**(i-1), 2**(j-1), 2**(k-1), cone_sphere)) - scaling_components.append(components) + component = Translation(-2 * (i-1), -2 * (j-1), -2 * (k-1), Scaling(2 ** (i - 1), 2 ** (j - 1), 2 ** (k - 1), cone_sphere)) + scaling_components.append(component) - scaling_test = Scale(0.5, 0.5, 0.5, *scaling_components) + rotation_test = Scaling(0.5, 0.5, 0.5, *scaling_components) item_list = [ - translate_test, - scaling_test + # translate_test, + # scaling_test, + rotation_test ] # Turn off all of them @@ -87,7 +88,7 @@ def transform_tests(): # Thing for going through each of the draw types of the primatives - def item_states(item: ModelBase): + def item_states(item: SceneGraphNode): item.solid_render_enabled = True item.wireframe_render_enabled = True From 26348802c9facfae105a228cb2d9cef4f6fdd8fe Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Thu, 24 Nov 2022 20:49:13 +0000 Subject: [PATCH 49/87] Some comments and clean up --- src/sas/qtgui/GL/arrow.py | 0 src/sas/qtgui/GL/color.py | 11 +++++++++-- src/sas/qtgui/GL/cone.py | 5 +++++ src/sas/qtgui/GL/visual_checks/__init__.py | 0 4 files changed, 14 insertions(+), 2 deletions(-) delete mode 100644 src/sas/qtgui/GL/arrow.py delete mode 100644 src/sas/qtgui/GL/visual_checks/__init__.py diff --git a/src/sas/qtgui/GL/arrow.py b/src/sas/qtgui/GL/arrow.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/sas/qtgui/GL/color.py b/src/sas/qtgui/GL/color.py index b436f8ae06..61c68c1441 100644 --- a/src/sas/qtgui/GL/color.py +++ b/src/sas/qtgui/GL/color.py @@ -8,9 +8,12 @@ from OpenGL.GL import glColor4f +"Helper classes for dealing with colours" + logger = logging.getLogger("GL.Color") class Color(): + """ A basic color class that makes it easy to set a colour""" def __init__(self, r: float, g: float, b: float, alpha: float=1.0): self.r = float(r) self.g = float(g) @@ -18,9 +21,11 @@ def __init__(self, r: float, g: float, b: float, alpha: float=1.0): self.alpha = float(alpha) def set(self): + """ Set the GL draw color to this color""" glColor4f(self.r, self.g, self.b, self.alpha) def to_array(self): + """ length 4 array of values, r,g,b,alpha""" return np.array([self.r, self.g, self.b, self.alpha]) def __repr__(self): @@ -31,7 +36,7 @@ class ColorMap(): _default_colormap = 'rainbow' def __init__(self, colormap_name=_default_colormap, min_value=0.0, max_value=1.0): - + """ Utility class for colormaps, principally used for mapping data in Surface""" try: self.colormap = mpl.colormaps[colormap_name] except KeyError: @@ -41,10 +46,12 @@ def __init__(self, colormap_name=_default_colormap, min_value=0.0, max_value=1.0 self.min_value = min_value self.max_value = max_value - def color(self, value): + def color(self, value: float): + """ Current colormap at this value""" scaled = (value - self.min_value) / (self.max_value - self.min_value) scaled = np.clip(scaled, 0, 1) return Color(*self.colormap(scaled)) def color_array(self, values: Sequence[float]) -> Sequence[Color]: + """ Array of the right form for working with Renderables""" return [self.color(value) for value in values] diff --git a/src/sas/qtgui/GL/cone.py b/src/sas/qtgui/GL/cone.py index d4ef6888da..d4fa847d76 100644 --- a/src/sas/qtgui/GL/cone.py +++ b/src/sas/qtgui/GL/cone.py @@ -11,24 +11,29 @@ class Cone(FullVertexModel): @staticmethod def cone_vertices(n) -> List[Tuple[float, float, float]]: + """ Helper function: Vertices of the cone primitive""" return [(0.0, 0.0, 1.0)] + [ (np.sin(angle), np.cos(angle), -1.0) for angle in 2*np.pi*np.arange(0, n)/n] + [(0.0, 0.0, -1.0)] @staticmethod def cone_edges(n): + """ Helper function: Edges of the cone primitive""" return [(0, i+1) for i in range(n)] + [(i+1, (i+1)%n+1) for i in range(n)] @staticmethod def cone_tip_triangles(n) -> List[Tuple[int, int, int]]: + """ Helper function: Triangles in tip of the cone primitive""" return [(0, i + 1, (i + 1) % n + 1) for i in range(n)] @staticmethod def cone_base_triangles(n) -> List[Tuple[int, int, int]]: + """ Helper function: Triangles in base the cone primitive""" return [((i + 1) % n + 1, i + 1, n+1) for i in range(n)] @staticmethod def cone_triangles(n) -> List[List[Tuple[int, int, int]]]: + """ Helper function: The two separate meshes for triangles of the cone primitive""" return [Cone.cone_base_triangles(n), Cone.cone_tip_triangles(n)] diff --git a/src/sas/qtgui/GL/visual_checks/__init__.py b/src/sas/qtgui/GL/visual_checks/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 From a8a2c4d7dbe2388213630575e6e3e7b935ec5c12 Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Fri, 25 Nov 2022 12:44:28 +0000 Subject: [PATCH 50/87] Rotations + rotation check implemented, some comments added, cleanup --- src/sas/qtgui/GL/cone.py | 4 +- src/sas/qtgui/GL/cube.py | 4 +- src/sas/qtgui/GL/cylinder.py | 9 +- src/sas/qtgui/GL/icosahedron.py | 4 +- src/sas/qtgui/GL/models.py | 127 +++++++++--------- src/sas/qtgui/GL/sphere.py | 4 +- src/sas/qtgui/GL/surface.py | 4 +- src/sas/qtgui/GL/transforms.py | 13 +- .../GL/visual_checks/transform_library.py | 10 +- 9 files changed, 95 insertions(+), 84 deletions(-) diff --git a/src/sas/qtgui/GL/cone.py b/src/sas/qtgui/GL/cone.py index d4fa847d76..1ca5c5e52c 100644 --- a/src/sas/qtgui/GL/cone.py +++ b/src/sas/qtgui/GL/cone.py @@ -2,11 +2,11 @@ import numpy as np -from sas.qtgui.GL.models import FullVertexModel, WireModel +from sas.qtgui.GL.models import FullModel, WireModel from sas.qtgui.GL.color import Color -class Cone(FullVertexModel): +class Cone(FullModel): """ Graphics primitive: Radius 1, Height 2 cone "centred" at (0,0,0)""" @staticmethod diff --git a/src/sas/qtgui/GL/cube.py b/src/sas/qtgui/GL/cube.py index 5deca733c7..644124cb13 100644 --- a/src/sas/qtgui/GL/cube.py +++ b/src/sas/qtgui/GL/cube.py @@ -1,10 +1,10 @@ from typing import Optional, Union, Sequence -from sas.qtgui.GL.models import FullVertexModel, WireModel +from sas.qtgui.GL.models import FullModel, WireModel from sas.qtgui.GL.color import Color -class Cube(FullVertexModel): +class Cube(FullModel): """ Unit cube centred at 0,0,0""" cube_vertices = [ diff --git a/src/sas/qtgui/GL/cylinder.py b/src/sas/qtgui/GL/cylinder.py index 5711a767b0..0dc99435db 100644 --- a/src/sas/qtgui/GL/cylinder.py +++ b/src/sas/qtgui/GL/cylinder.py @@ -2,15 +2,16 @@ import numpy as np -from sas.qtgui.GL.models import FullVertexModel, WireModel +from sas.qtgui.GL.models import FullModel, WireModel from sas.qtgui.GL.color import Color -class Cylinder(FullVertexModel): +class Cylinder(FullModel): """ Graphics primitive: Radius 1, Height 2 cone "centred" at (0,0,0)""" @staticmethod def cylinder_vertices(n) -> List[Tuple[float, float, float]]: + """ Helper function: Vertices of the cylinder primitive""" return [(0.0, 0.0, 1.0)] + \ [ (np.sin(angle), np.cos(angle), 1.0) for angle in 2*np.pi*np.arange(0, n)/n] + \ [(0.0, 0.0, -1.0)] + \ @@ -19,16 +20,19 @@ def cylinder_vertices(n) -> List[Tuple[float, float, float]]: @staticmethod def cylinder_edges(n): + """ Helper function: Edges of the cylinder primitive""" return [(i+1, (i+1)%n+1) for i in range(n)] + \ [(i + n + 2, (i+1)%n + n + 2) for i in range(n)] + \ [(i+1, i + n + 2) for i in range(n)] @staticmethod def cylinder_face_triangles(n, offset) -> List[Tuple[int, int, int]]: + """ Helper function: Faces of the ends of cylinder primitive""" return [(i+offset + 1, (i + 1) % n + offset + 1, offset) for i in range(n)] @staticmethod def cylinder_side_triangles(n) -> List[Tuple[int, int, int]]: + """ Helper function: Faces of the sides of the cylinder primitive""" sides = [] for i in range(n): # Squares into triangles @@ -41,6 +45,7 @@ def cylinder_side_triangles(n) -> List[Tuple[int, int, int]]: @staticmethod def cylinder_triangles(n) -> List[List[Tuple[int, int, int]]]: + """ Helper function: All faces of the cylinder primitive""" return [ Cylinder.cylinder_face_triangles(n, 0), Cylinder.cylinder_side_triangles(n), diff --git a/src/sas/qtgui/GL/icosahedron.py b/src/sas/qtgui/GL/icosahedron.py index 6bc77ee614..dd997dc2b3 100644 --- a/src/sas/qtgui/GL/icosahedron.py +++ b/src/sas/qtgui/GL/icosahedron.py @@ -3,13 +3,13 @@ import numpy as np from sas.qtgui.GL.color import Color -from sas.qtgui.GL.models import FullVertexModel +from sas.qtgui.GL.models import FullModel ico_ring_h = np.sqrt(1/5) ico_ring_r = np.sqrt(4/5) -class Icosahedron(FullVertexModel): +class Icosahedron(FullModel): """ Icosahedron centred at 0,0,0""" diff --git a/src/sas/qtgui/GL/models.py b/src/sas/qtgui/GL/models.py index ab3c77fb72..e0a33e644d 100644 --- a/src/sas/qtgui/GL/models.py +++ b/src/sas/qtgui/GL/models.py @@ -22,6 +22,7 @@ def __init__(self, vertices: Sequence[Tuple[float, float, float]]): self._vertices = vertices self._vertex_array = np.array(vertices, dtype=float) + # Vertices set and got as sequences of tuples, but a list of @property def vertices(self): return self._vertices @@ -30,14 +31,11 @@ def vertices(self): def vertices(self, new_vertices): self._vertices = new_vertices self._vertex_array = np.array(new_vertices, dtype=float) - self._recalculate_normals() - def _recalculate_normals(self): - pass class SolidModel(ModelBase): - """ Base class for the two solid models""" + """ Base class for the solid models""" def __init__(self, vertices: Sequence[Tuple[float, float, float]], triangle_meshes: Sequence[Sequence[Tuple[int, int, int]]]): @@ -57,35 +55,45 @@ def triangle_meshes(self) -> Sequence[Sequence[Tuple[int, int, int]]]: def triangle_meshes(self, new_triangle_meshes: Sequence[Sequence[int]]): self._triangle_meshes = new_triangle_meshes self._triangle_mesh_arrays = [np.array(x) for x in new_triangle_meshes] - self._recalculate_normals() - - def _recalculate_normals(self): - # TODO: Will be required if a reflectance model is used - pass class SolidVertexModel(SolidModel): def __init__(self, vertices: Sequence[Tuple[float, float, float]], triangle_meshes: Sequence[Sequence[Tuple[int, int, int]]], - vertex_colors: Optional[Union[Sequence[Color], Color]]): + colors: Optional[Union[Sequence[Color], Color]], + color_by_mesh: bool = False): + + """ + + + :vertices: Sequence[Tuple[float, float, float]], vertices of the model + :triangle_meshes: Sequence[Sequence[Tuple[int, int, int]]], sequence of triangle + meshes indices making up the shape + :colors: Optional[Union[Sequence[Color], Color]], single color for shape, or array with a colour for + each mesh or vertex (color_by_mesh selects which of these it is) + :color_by_mesh: bool = False, Colour in each mesh with a colour specified by colours + + """ super().__init__(vertices, triangle_meshes) - self._vertex_colors = vertex_colors - self._vertex_color_array = None if isinstance(vertex_colors, Color) else np.array([color.to_array() for color in vertex_colors], dtype=float) + self.color_by_mesh = color_by_mesh - self.solid_render_enabled = self.vertex_colors is not None + self._vertex_colors = colors + self._vertex_color_array = None if isinstance(colors, Color) or color_by_mesh else np.array([color.to_array() for color in colors], dtype=float) + + self.solid_render_enabled = self.colors is not None @property - def vertex_colors(self): + def colors(self): return self._vertex_colors - @vertex_colors.setter - def vertex_colors(self, new_vertex_colors): - self.vertex_colors = new_vertex_colors + @colors.setter + def colors(self, new_vertex_colors): + self.colors = new_vertex_colors self._vertex_color_array = None if isinstance(new_vertex_colors, Color) else np.array([color.to_array() for color in new_vertex_colors], dtype=float) - self.solid_render_enabled = self.vertex_colors is not None + self.solid_render_enabled = self.colors is not None def render_solid(self): if self.solid_render_enabled: @@ -101,45 +109,52 @@ def render_solid(self): glDrawElementsui(GL_TRIANGLES, triangle_mesh) - else: + glDisableClientState(GL_VERTEX_ARRAY) - # TODO: This branch of the if clause needs testing - glEnableClientState(GL_VERTEX_ARRAY) - glEnableClientState(GL_COLOR_ARRAY) + else: - glVertexPointerf(self._vertex_array) - glColorPointerf(self._vertex_color_array) + if self.color_by_mesh: - for triangle_mesh in self._triangle_mesh_arrays: - glDrawElementsui(GL_TRIANGLES, triangle_mesh) + glEnableClientState(GL_VERTEX_ARRAY) - glDisableClientState(GL_COLOR_ARRAY) - glDisableClientState(GL_VERTEX_ARRAY) + glVertexPointerf(self._vertex_array) + for triangle_mesh, color in zip(self._triangle_mesh_arrays, self.colors): + color.set() + glDrawElementsui(GL_TRIANGLES, triangle_mesh) -class SolidFaceModel(SolidModel): - def __init__(self, - vertices: Sequence[Tuple[float, float, float]], - triangle_meshes: Sequence[Sequence[Tuple[int, int, int]]], - face_colours: Optional[Union[Sequence[Color], Color]]): + glDisableClientState(GL_VERTEX_ARRAY) - super().__init__(vertices, triangle_meshes) + else: + glEnableClientState(GL_VERTEX_ARRAY) + glEnableClientState(GL_COLOR_ARRAY) - self.face_colours = face_colours + glVertexPointerf(self._vertex_array) + glColorPointerf(self._vertex_color_array) - def render_solid(self): - pass + for triangle_mesh in self._triangle_mesh_arrays: + glDrawElementsui(GL_TRIANGLES, triangle_mesh) + + glDisableClientState(GL_COLOR_ARRAY) + glDisableClientState(GL_VERTEX_ARRAY) class WireModel(ModelBase): - """ Wireframe Model """ def __init__(self, vertices: Sequence[Tuple[float, float, float]], edges: Sequence[Tuple[int, int]], edge_colors: Optional[Union[Sequence[Color], Color]]): + """ Wireframe Model + + :vertices: Sequence[Tuple[float, float, float]], vertices of the model + :edges: Sequence[Tuple[int, int]], indices of the points making up the edges + :edge_colors: Optional[Union[Sequence[Color], Color]], color of the individual edges or a single color for them all + """ + + super().__init__(vertices) self.wireframe_render_enabled = False @@ -175,42 +190,28 @@ def render_wireframe(self): glEnd() -class FullVertexModel(SolidVertexModel, WireModel): - """ Model that has both wireframe and solid, vertex coloured rendering enabled""" +class FullModel(SolidVertexModel, WireModel): + """ Model that has both wireframe and solid, vertex coloured rendering enabled, + + See SolidVertexModel and WireModel + """ def __init__(self, vertices: Sequence[Tuple[float, float, float]], edges: Sequence[Tuple[int, int]], triangle_meshes: Sequence[Sequence[Tuple[int, int, int]]], edge_colors: Optional[Union[Sequence[Color], Color]], - vertex_colors: Optional[Union[Sequence[Color], Color]]): + vertex_colors: Optional[Union[Sequence[Color], Color]], + color_by_mesh: bool = False): + + SolidVertexModel.__init__(self, vertices=vertices, triangle_meshes=triangle_meshes, - vertex_colors=vertex_colors) + colors=vertex_colors, + color_by_mesh=color_by_mesh) WireModel.__init__(self, vertices=vertices, edges=edges, edge_colors=edge_colors) - - - -class FullFaceModel(SolidFaceModel, WireModel): - """ Model that has both wireframe and solid, face coloured rendering enabled""" - - def __init__(self, - vertices: Sequence[Tuple[float, float, float]], - edges: Sequence[Tuple[int, int]], - triangle_meshes: Sequence[Sequence[Tuple[int, int, int]]], - edge_colors: Optional[Union[Sequence[Color], Color]], - face_colors: Optional[Union[Sequence[Color], Color]]): - - SolidFaceModel.__init__(self, - vertices=vertices, - triangle_meshes=triangle_meshes, - face_colours=face_colors) - WireModel.__init__(self, - vertices=vertices, - edges=edges, - edge_colors=edge_colors) diff --git a/src/sas/qtgui/GL/sphere.py b/src/sas/qtgui/GL/sphere.py index 703d2d26fa..ce169b1644 100644 --- a/src/sas/qtgui/GL/sphere.py +++ b/src/sas/qtgui/GL/sphere.py @@ -2,11 +2,11 @@ import numpy as np -from sas.qtgui.GL.models import FullVertexModel +from sas.qtgui.GL.models import FullModel from sas.qtgui.GL.color import Color -class Sphere(FullVertexModel): +class Sphere(FullModel): @staticmethod def sphere_vertices(n_horizontal, n_segments): vertices = [(0.0, 0.0, 1.0)] diff --git a/src/sas/qtgui/GL/surface.py b/src/sas/qtgui/GL/surface.py index 5567a8040a..4928a51737 100644 --- a/src/sas/qtgui/GL/surface.py +++ b/src/sas/qtgui/GL/surface.py @@ -4,12 +4,12 @@ import matplotlib as mpl -from sas.qtgui.GL.models import FullVertexModel, WireModel +from sas.qtgui.GL.models import FullModel, WireModel from sas.qtgui.GL.color import Color, ColorMap logger = logging.getLogger("GL.Surface") -class Surface(FullVertexModel): +class Surface(FullModel): @staticmethod diff --git a/src/sas/qtgui/GL/transforms.py b/src/sas/qtgui/GL/transforms.py index f6365003db..6bf3321bad 100644 --- a/src/sas/qtgui/GL/transforms.py +++ b/src/sas/qtgui/GL/transforms.py @@ -31,7 +31,7 @@ def add_child(self, child: Renderable): self.children.append(child) def apply(self): - pass + """ GL operations needed to apply any transformations associated with this node """ def render_solid(self): if self.solid_render_enabled: @@ -75,20 +75,21 @@ def render_wireframe(self): class Rotation(SceneGraphNode): - def __init__(self, angle, axis, *children: Renderable): + def __init__(self, angle, x, y, z, *children: Renderable): """ Rotate the children of this node - :param angle: angle of rotation in TODO + :param angle: angle of rotation in degrees :param axis: axis for rotation """ super().__init__(*children) self.angle = angle - self.axis = axis + self.x = x + self.y = y + self.z = z def apply(self): - pass - + glRotate(self.angle, self.x, self.y, self.z) class Translation(SceneGraphNode): diff --git a/src/sas/qtgui/GL/visual_checks/transform_library.py b/src/sas/qtgui/GL/visual_checks/transform_library.py index e6481ed5c5..1eea2026b2 100644 --- a/src/sas/qtgui/GL/visual_checks/transform_library.py +++ b/src/sas/qtgui/GL/visual_checks/transform_library.py @@ -68,15 +68,19 @@ def transform_tests(): for i in range(3): for j in range(3): for k in range(3): - component = Translation(-2 * (i-1), -2 * (j-1), -2 * (k-1), Scaling(2 ** (i - 1), 2 ** (j - 1), 2 ** (k - 1), cone_sphere)) + component = Translation(-2 * (i-1), -2 * (j-1), -2 * (k-1), + Rotation(45*(i - 1), 1, 0, 0, + Rotation(45*(j-1), 0, 1, 0, + Rotation(45*(k-1), 0, 0, 1, + cone_sphere)))) scaling_components.append(component) rotation_test = Scaling(0.5, 0.5, 0.5, *scaling_components) item_list = [ - # translate_test, - # scaling_test, + translate_test, + scaling_test, rotation_test ] From 3ffa1ca25744d42c22d165197428506aafb0dec5 Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Fri, 25 Nov 2022 13:19:33 +0000 Subject: [PATCH 51/87] More documentation --- src/sas/qtgui/GL/cube.py | 6 +++-- src/sas/qtgui/GL/models.py | 7 +++++- src/sas/qtgui/GL/sphere.py | 4 ++++ src/sas/qtgui/GL/surface.py | 24 +++++++++++-------- .../GL/visual_checks/primitive_library.py | 3 +-- .../GL/visual_checks/transform_library.py | 19 ++++++++------- 6 files changed, 40 insertions(+), 23 deletions(-) diff --git a/src/sas/qtgui/GL/cube.py b/src/sas/qtgui/GL/cube.py index 644124cb13..791cf1118d 100644 --- a/src/sas/qtgui/GL/cube.py +++ b/src/sas/qtgui/GL/cube.py @@ -50,14 +50,16 @@ class Cube(FullModel): def __init__(self, face_colors: Optional[Union[Sequence[Color],Color]]=None, - edge_colors: Optional[Union[Sequence[Color],Color]]=None): + edge_colors: Optional[Union[Sequence[Color],Color]]=None, + color_by_mesh: bool=False): super().__init__( vertices=Cube.cube_vertices, edges=Cube.cube_edges, triangle_meshes=Cube.cube_triangles, edge_colors=edge_colors, - vertex_colors=face_colors) + vertex_colors=face_colors, + color_by_mesh=color_by_mesh) self.vertices = Cube.cube_vertices self.edges = Cube.cube_edges diff --git a/src/sas/qtgui/GL/models.py b/src/sas/qtgui/GL/models.py index e0a33e644d..cccd83f7cd 100644 --- a/src/sas/qtgui/GL/models.py +++ b/src/sas/qtgui/GL/models.py @@ -1,3 +1,9 @@ + +""" +3D Model classes +""" + + from typing import Sequence, Tuple, Union, Optional import numpy as np @@ -7,7 +13,6 @@ from sas.qtgui.GL.renderable import Renderable from sas.qtgui.GL.color import Color - def color_sequence_to_array(colors: Union[Sequence[Color], Color]): if isinstance(colors, Color): return None diff --git a/src/sas/qtgui/GL/sphere.py b/src/sas/qtgui/GL/sphere.py index ce169b1644..0cfc50c271 100644 --- a/src/sas/qtgui/GL/sphere.py +++ b/src/sas/qtgui/GL/sphere.py @@ -9,6 +9,7 @@ class Sphere(FullModel): @staticmethod def sphere_vertices(n_horizontal, n_segments): + """ Helper function: Vertices of the UV sphere primitive""" vertices = [(0.0, 0.0, 1.0)] for theta in (np.pi/n_horizontal)*np.arange(0.5, n_horizontal): @@ -25,6 +26,8 @@ def sphere_vertices(n_horizontal, n_segments): @staticmethod def sphere_edges(n_horizontal, n_segments, grid_gap): + + """ Helper function: Edges of the UV sphere primitive""" edges = [] # Bands @@ -41,6 +44,7 @@ def sphere_edges(n_horizontal, n_segments, grid_gap): @staticmethod def sphere_triangles(n_horizontal, n_segments): + """ Helper function: Triangles of the UV sphere primitive""" triangles = [] last_index = n_horizontal*n_segments + 1 diff --git a/src/sas/qtgui/GL/surface.py b/src/sas/qtgui/GL/surface.py index 4928a51737..76dd468727 100644 --- a/src/sas/qtgui/GL/surface.py +++ b/src/sas/qtgui/GL/surface.py @@ -1,10 +1,7 @@ import logging -from typing import Optional, Tuple import numpy as np -import matplotlib as mpl - -from sas.qtgui.GL.models import FullModel, WireModel +from sas.qtgui.GL.models import FullModel from sas.qtgui.GL.color import Color, ColorMap logger = logging.getLogger("GL.Surface") @@ -14,6 +11,7 @@ class Surface(FullModel): @staticmethod def calculate_edge_indices(nx, ny, gap=1): + """ Helper function to calculate the indices of the edges""" all_edges = [] for i in range(nx-1): for j in range(0, ny, gap): @@ -27,6 +25,7 @@ def calculate_edge_indices(nx, ny, gap=1): @staticmethod def calculate_triangles(nx, ny): + """ Helper function to calculate the indices of the triangles in the mesh""" triangles = [] for i in range(nx-1): for j in range(ny-1): @@ -60,7 +59,8 @@ def __init__(self, self.colormap = ColorMap(colormap) - verts = [(float(x), float(y), float(z)) for x, y, z in zip(np.nditer(self.x_data), np.nditer(self.y_data), np.nditer(self.z_data))] + verts = [(float(x), float(y), float(z)) + for x, y, z in zip(np.nditer(self.x_data), np.nditer(self.y_data), np.nditer(self.z_data))] super().__init__( vertices=verts, @@ -74,8 +74,12 @@ def __init__(self, self.solid_render_enabled = True - # def _get_colors(self, z_values, colormap) -> Sequence[Color]: - # return [] - # - # def set_z_data(self, z_data): - # pass \ No newline at end of file + def set_z_data(self, z_data): + + "Set the z data on this surface plot" + + self.z_data = z_data + verts = [(float(x), float(y), float(z)) + for x, y, z in zip(np.nditer(self.x_data), np.nditer(self.y_data), np.nditer(self.z_data))] + + self.colors = self.colormap.color_array([z for _, _, z in verts]) diff --git a/src/sas/qtgui/GL/visual_checks/primitive_library.py b/src/sas/qtgui/GL/visual_checks/primitive_library.py index 595bd808ad..2fa89a37e0 100644 --- a/src/sas/qtgui/GL/visual_checks/primitive_library.py +++ b/src/sas/qtgui/GL/visual_checks/primitive_library.py @@ -1,9 +1,8 @@ """ As close a thing as there are to tests for GL""" -from typing import Optional, Tuple, List, Callable import numpy as np -from PyQt5 import QtWidgets, Qt, QtGui, QtOpenGL, QtCore +from PyQt5 import QtWidgets from sas.qtgui.GL.scene import GraphWidget from sas.qtgui.GL.models import ModelBase diff --git a/src/sas/qtgui/GL/visual_checks/transform_library.py b/src/sas/qtgui/GL/visual_checks/transform_library.py index 1eea2026b2..3a1d32c6b5 100644 --- a/src/sas/qtgui/GL/visual_checks/transform_library.py +++ b/src/sas/qtgui/GL/visual_checks/transform_library.py @@ -1,19 +1,13 @@ """ As close a thing as there are to tests for GL""" -from typing import Optional, Tuple, List, Callable -import numpy as np - -from PyQt5 import QtWidgets, Qt, QtGui, QtOpenGL, QtCore +from PyQt5 import QtWidgets from sas.qtgui.GL.scene import GraphWidget -from sas.qtgui.GL.models import ModelBase from sas.qtgui.GL.color import Color -from sas.qtgui.GL.surface import Surface from sas.qtgui.GL.cone import Cone from sas.qtgui.GL.cube import Cube from sas.qtgui.GL.cylinder import Cylinder from sas.qtgui.GL.icosahedron import Icosahedron -from sas.qtgui.GL.sphere import Sphere from sas.qtgui.GL.transforms import SceneGraphNode, Translation, Rotation, Scaling def transform_tests(): @@ -57,13 +51,14 @@ def transform_tests(): scaling_test = Scaling(0.5, 0.5, 0.5, *scaling_components) + + # Rotations cone_sphere = Scaling(0.5, 0.5, 0.5, cone, Translation(0, 1, 1, Scaling(0.5, 0.5, 0.5, icos))) - # Rotations scaling_components = [] for i in range(3): for j in range(3): @@ -78,6 +73,10 @@ def transform_tests(): rotation_test = Scaling(0.5, 0.5, 0.5, *scaling_components) + # + # Thing to iterate through the different tests + # + item_list = [ translate_test, scaling_test, @@ -111,6 +110,10 @@ def scan_states(): state = scan_states() next(state) + # + # Set up and show window + # + mainWindow = QtWidgets.QMainWindow() viewer = GraphWidget(parent=mainWindow) From 8ef5fb2f7649066cd907122caf0275b7be4c1a90 Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Fri, 25 Nov 2022 13:23:08 +0000 Subject: [PATCH 52/87] Added color_by_mesh cube example --- src/sas/qtgui/GL/visual_checks/primitive_library.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/sas/qtgui/GL/visual_checks/primitive_library.py b/src/sas/qtgui/GL/visual_checks/primitive_library.py index 2fa89a37e0..282f26a724 100644 --- a/src/sas/qtgui/GL/visual_checks/primitive_library.py +++ b/src/sas/qtgui/GL/visual_checks/primitive_library.py @@ -38,6 +38,14 @@ def primative_library(): item_list = [ mesh_example(), Cube(edge_colors=Color(1, 1, 1), face_colors=Color(0.7, 0.2, 0)), + Cube(edge_colors=Color(1, 1, 1), face_colors=[ + Color(1,0,0), + Color(0,1,0), + Color(0,0,1), + Color(1,1,0), + Color(0,1,1), + Color(1,0,1) + ], color_by_mesh=True), Cone(edge_colors=Color(1, 1, 1), vertex_colors=Color(0, 0.7, 0.2)), Cylinder(edge_colors=Color(1, 1, 1), vertex_colors=Color(0, 0.2, 0.7)), Icosahedron(edge_colors=Color(1, 1, 1), vertex_colors=Color(0.7, 0, 0.7)), From 6b16d5b13ea421de4f4e3e336e54645af9944085 Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Fri, 25 Nov 2022 13:53:21 +0000 Subject: [PATCH 53/87] Docs --- docs/sphinx-docs/source/dev/gl/opengl.rst | 12 ++++++++++++ src/sas/qtgui/GL/scene.py | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 docs/sphinx-docs/source/dev/gl/opengl.rst diff --git a/docs/sphinx-docs/source/dev/gl/opengl.rst b/docs/sphinx-docs/source/dev/gl/opengl.rst new file mode 100644 index 0000000000..75dd900b77 --- /dev/null +++ b/docs/sphinx-docs/source/dev/gl/opengl.rst @@ -0,0 +1,12 @@ +Open GL Subsystem +============== + +The SasView openGL subsystem is quite minimal, and works in the standard way though a scenegraph + +Within the `visual_checks` directory there are a couple of stand-alone python files that provide +a way of checking the rendering, and catalogue the available functions + + +Class Heirarchy +=============== + diff --git a/src/sas/qtgui/GL/scene.py b/src/sas/qtgui/GL/scene.py index 8f3fd40fa8..78ea04d467 100644 --- a/src/sas/qtgui/GL/scene.py +++ b/src/sas/qtgui/GL/scene.py @@ -190,7 +190,7 @@ def keyPressEvent(self, event: QtGui.QKeyEvent): def main(): - """ Show a demo of the opengl window """ + """ Show a demo of the opengl.rst window """ import os os.environ["QT_ENABLE_HIGHDPI_SCALING"] = "1" From 7b7b51287c3430e3d7fc2dcedaa48b79b43f6375 Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Fri, 25 Nov 2022 13:56:48 +0000 Subject: [PATCH 54/87] setup.py stuff --- setup.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index ff0a62e160..a62fd1c046 100644 --- a/setup.py +++ b/setup.py @@ -168,6 +168,12 @@ def run(self): "src", "sas", "qtgui", "UI") packages.append("sas.qtgui.UI") +## GL +package_dir["sas.qtgui.GL"] = os.path.join( + "src", "sas", "qtgui", "GL") +packages.append("sas.qtgui.GL") + + ## UnitTesting package_dir["sas.qtgui.UnitTesting"] = os.path.join( "src", "sas", "qtgui", "UnitTesting") @@ -188,13 +194,18 @@ def run(self): "src", "sas", "qtgui", "Utilities", "Reports", "UI") packages.append("sas.qtgui.Utilities.Reports.UI") -package_dir["sas.qtgui.Utilities.Preferences}"] = os.path.join( +package_dir["sas.qtgui.Utilities.Preferences"] = os.path.join( "src", "sas", "qtgui", "Utilities", "Preferences") packages.append("sas.qtgui.Utilities.Preferences") package_dir["sas.qtgui.Utilities.Preferences.UI"] = os.path.join( "src", "sas", "qtgui", "Utilities", "Preferences", "UI") packages.append("sas.qtgui.Utilities.Preferences.UI") +package_dir["sas.qtgui.Utilities.OrientationViewer"] = os.path.join( + "src", "sas", "qtgui", "Utilities", "OrientationViewer") +packages.append("sas.qtgui.Utilities.OrientationViewer") + + package_dir["sas.qtgui.Calculators"] = os.path.join( "src", "sas", "qtgui", "Calculators") package_dir["sas.qtgui.Calculators.UI"] = os.path.join( From 2c6bf271491cb4db46b3540114d1d718bf111224 Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Fri, 25 Nov 2022 15:57:38 +0000 Subject: [PATCH 55/87] Updating the orientation viewer --- src/sas/qtgui/GL/cone.py | 8 +- src/sas/qtgui/GL/cube.py | 2 +- src/sas/qtgui/GL/cylinder.py | 8 +- src/sas/qtgui/GL/icosahedron.py | 8 +- src/sas/qtgui/GL/models.py | 14 +- src/sas/qtgui/GL/scene.py | 4 +- src/sas/qtgui/GL/sphere.py | 10 +- src/sas/qtgui/GL/surface.py | 9 +- .../GL/visual_checks/primitive_library.py | 14 +- .../GL/visual_checks/transform_library.py | 10 +- .../OrientationViewer/OrientationViewer.py | 107 +++++---- .../OrientationViewer/OrientationViewerOld.py | 218 ------------------ 12 files changed, 112 insertions(+), 300 deletions(-) delete mode 100644 src/sas/qtgui/Utilities/OrientationViewer/OrientationViewerOld.py diff --git a/src/sas/qtgui/GL/cone.py b/src/sas/qtgui/GL/cone.py index 1ca5c5e52c..95475b198e 100644 --- a/src/sas/qtgui/GL/cone.py +++ b/src/sas/qtgui/GL/cone.py @@ -39,7 +39,7 @@ def cone_triangles(n) -> List[List[Tuple[int, int, int]]]: def __init__(self, n: int = 20, - vertex_colors: Optional[Union[Sequence[Color], Color]]=None, + colors: Optional[Union[Sequence[Color], Color]]=None, edge_colors: Optional[Union[Sequence[Color],Color]]=None): super().__init__( @@ -47,7 +47,7 @@ def __init__(self, edges=Cone.cone_edges(n), triangle_meshes=Cone.cone_triangles(n), edge_colors=edge_colors, - vertex_colors=vertex_colors) + colors=colors) if edge_colors is None: self.wireframe_render_enabled = False @@ -56,9 +56,9 @@ def __init__(self, self.wireframe_render_enabled = True self.edge_colors = edge_colors - if vertex_colors is None: + if colors is None: self.solid_render_enabled = False self.face_colors = [] else: self.solid_render_enabled = True - self.face_colors = vertex_colors + self.face_colors = colors diff --git a/src/sas/qtgui/GL/cube.py b/src/sas/qtgui/GL/cube.py index 791cf1118d..a08ff15284 100644 --- a/src/sas/qtgui/GL/cube.py +++ b/src/sas/qtgui/GL/cube.py @@ -58,7 +58,7 @@ def __init__(self, edges=Cube.cube_edges, triangle_meshes=Cube.cube_triangles, edge_colors=edge_colors, - vertex_colors=face_colors, + colors=face_colors, color_by_mesh=color_by_mesh) self.vertices = Cube.cube_vertices diff --git a/src/sas/qtgui/GL/cylinder.py b/src/sas/qtgui/GL/cylinder.py index 0dc99435db..3173c06827 100644 --- a/src/sas/qtgui/GL/cylinder.py +++ b/src/sas/qtgui/GL/cylinder.py @@ -54,7 +54,7 @@ def cylinder_triangles(n) -> List[List[Tuple[int, int, int]]]: def __init__(self, n: int = 20, - vertex_colors: Optional[Union[Sequence[Color], Color]]=None, + colors: Optional[Union[Sequence[Color], Color]]=None, edge_colors: Optional[Union[Sequence[Color],Color]]=None): super().__init__( @@ -62,7 +62,7 @@ def __init__(self, edges=Cylinder.cylinder_edges(n), triangle_meshes=Cylinder.cylinder_triangles(n), edge_colors=edge_colors, - vertex_colors=vertex_colors) + colors=colors) if edge_colors is None: self.wireframe_render_enabled = False @@ -71,9 +71,9 @@ def __init__(self, self.wireframe_render_enabled = True self.edge_colors = edge_colors - if vertex_colors is None: + if colors is None: self.solid_render_enabled = False self.face_colors = [] else: self.solid_render_enabled = True - self.face_colors = vertex_colors + self.face_colors = colors diff --git a/src/sas/qtgui/GL/icosahedron.py b/src/sas/qtgui/GL/icosahedron.py index dd997dc2b3..f12f03ef6c 100644 --- a/src/sas/qtgui/GL/icosahedron.py +++ b/src/sas/qtgui/GL/icosahedron.py @@ -77,7 +77,7 @@ class Icosahedron(FullModel): ]] def __init__(self, - vertex_colors: Optional[Union[Sequence[Color], Color]]=None, + colors: Optional[Union[Sequence[Color], Color]]=None, edge_colors: Optional[Union[Sequence[Color],Color]]=None): super().__init__( @@ -85,7 +85,7 @@ def __init__(self, edges=Icosahedron.ico_edges, triangle_meshes=Icosahedron.ico_triangles, edge_colors=edge_colors, - vertex_colors=vertex_colors) + colors=colors) if edge_colors is None: @@ -95,9 +95,9 @@ def __init__(self, self.wireframe_render_enabled = True self.edge_colors = edge_colors - if vertex_colors is None: + if colors is None: self.solid_render_enabled = False self.face_colors = [] else: self.solid_render_enabled = True - self.face_colors = vertex_colors + self.face_colors = colors diff --git a/src/sas/qtgui/GL/models.py b/src/sas/qtgui/GL/models.py index cccd83f7cd..2fa4aa9126 100644 --- a/src/sas/qtgui/GL/models.py +++ b/src/sas/qtgui/GL/models.py @@ -85,28 +85,28 @@ def __init__(self, self.color_by_mesh = color_by_mesh - self._vertex_colors = colors + self._colors = colors self._vertex_color_array = None if isinstance(colors, Color) or color_by_mesh else np.array([color.to_array() for color in colors], dtype=float) self.solid_render_enabled = self.colors is not None @property def colors(self): - return self._vertex_colors + return self._colors @colors.setter def colors(self, new_vertex_colors): - self.colors = new_vertex_colors + self._colors = new_vertex_colors self._vertex_color_array = None if isinstance(new_vertex_colors, Color) else np.array([color.to_array() for color in new_vertex_colors], dtype=float) self.solid_render_enabled = self.colors is not None def render_solid(self): if self.solid_render_enabled: - if isinstance(self._vertex_colors, Color): + if isinstance(self._colors, Color): glEnableClientState(GL_VERTEX_ARRAY) - self._vertex_colors.set() + self._colors.set() glVertexPointerf(self._vertex_array) @@ -205,7 +205,7 @@ def __init__(self, edges: Sequence[Tuple[int, int]], triangle_meshes: Sequence[Sequence[Tuple[int, int, int]]], edge_colors: Optional[Union[Sequence[Color], Color]], - vertex_colors: Optional[Union[Sequence[Color], Color]], + colors: Optional[Union[Sequence[Color], Color]], color_by_mesh: bool = False): @@ -213,7 +213,7 @@ def __init__(self, SolidVertexModel.__init__(self, vertices=vertices, triangle_meshes=triangle_meshes, - colors=vertex_colors, + colors=colors, color_by_mesh=color_by_mesh) WireModel.__init__(self, vertices=vertices, diff --git a/src/sas/qtgui/GL/scene.py b/src/sas/qtgui/GL/scene.py index 78ea04d467..054ddb6bc7 100644 --- a/src/sas/qtgui/GL/scene.py +++ b/src/sas/qtgui/GL/scene.py @@ -11,7 +11,7 @@ from sas.qtgui.GL.surface import Surface from sas.qtgui.GL.cone import Cone -class GraphWidget(QtOpenGL.QGLWidget): +class Scene(QtOpenGL.QGLWidget): def __init__(self, parent=None, on_key: Callable[[int], None] = lambda x: None): @@ -197,7 +197,7 @@ def main(): app = QtWidgets.QApplication([]) mainWindow = QtWidgets.QMainWindow() - viewer = GraphWidget(mainWindow) + viewer = Scene(mainWindow) x = np.linspace(-1, 1, 101) y = np.linspace(-1, 1, 101) diff --git a/src/sas/qtgui/GL/sphere.py b/src/sas/qtgui/GL/sphere.py index 0cfc50c271..b187d4f165 100644 --- a/src/sas/qtgui/GL/sphere.py +++ b/src/sas/qtgui/GL/sphere.py @@ -68,7 +68,7 @@ def __init__(self, n_horizontal: int = 21, n_segments: int = 28, grid_gap: int = 1, - vertex_colors: Optional[Union[Sequence[Color], Color]]=None, + colors: Optional[Union[Sequence[Color], Color]]=None, edge_colors: Optional[Union[Sequence[Color],Color]]=None): """ @@ -78,7 +78,7 @@ def __init__(self, :param n_horizontal: Number of horizontal bands :param n_segments: Number of segments (angular) :param grid_gap: Coarse grain the wireframe by skipping every 'grid_gap' coordinates - :param vertex_colors: List of colours for each vertex, or a single color for all + :param colors: List of colours for each vertex, or a single color for all :param edge_colors: List of colours for each edge, or a single color for all Note: For aesthetically pleasing results with `grid_gap`, `n_segments` should be a multiple @@ -97,7 +97,7 @@ def __init__(self, edges=Sphere.sphere_edges(n_horizontal, n_segments, grid_gap), triangle_meshes=Sphere.sphere_triangles(n_horizontal, n_segments), edge_colors=edge_colors, - vertex_colors=vertex_colors) + colors=colors) if edge_colors is None: self.wireframe_render_enabled = False @@ -106,9 +106,9 @@ def __init__(self, self.wireframe_render_enabled = True self.edge_colors = edge_colors - if vertex_colors is None: + if colors is None: self.solid_render_enabled = False self.face_colors = [] else: self.solid_render_enabled = True - self.face_colors = vertex_colors + self.face_colors = colors diff --git a/src/sas/qtgui/GL/surface.py b/src/sas/qtgui/GL/surface.py index 76dd468727..6e87c03e37 100644 --- a/src/sas/qtgui/GL/surface.py +++ b/src/sas/qtgui/GL/surface.py @@ -1,3 +1,5 @@ +from typing import Tuple + import logging import numpy as np @@ -38,6 +40,7 @@ def __init__(self, y_values: np.ndarray, z_data: np.ndarray, colormap: str= ColorMap._default_colormap, + c_range: Tuple[float, float] = (0, 1), edge_skip: int=1): """ Surface plot @@ -47,6 +50,7 @@ def __init__(self, :param y_values: 1D array of y values :param z_data: 2D array of z values :param colormap: name of a matplotlib colour map + :param c_range: min and max values for the color map to span :param edge_skip: skip every `edge_skip` index when drawing wireframe """ @@ -57,7 +61,7 @@ def __init__(self, self.n_x = len(x_values) self.n_y = len(y_values) - self.colormap = ColorMap(colormap) + self.colormap = ColorMap(colormap, min_value=c_range[0], max_value=c_range[1]) verts = [(float(x), float(y), float(z)) for x, y, z in zip(np.nditer(self.x_data), np.nditer(self.y_data), np.nditer(self.z_data))] @@ -67,7 +71,7 @@ def __init__(self, edges=Surface.calculate_edge_indices(self.n_x, self.n_y, edge_skip), triangle_meshes=[Surface.calculate_triangles(self.n_x, self.n_y)], edge_colors=Color(1.0,1.0,1.0), - vertex_colors=self.colormap.color_array([z for _, _, z in verts]) + colors=self.colormap.color_array([z for _, _, z in verts]) ) self.wireframe_render_enabled = True @@ -82,4 +86,5 @@ def set_z_data(self, z_data): verts = [(float(x), float(y), float(z)) for x, y, z in zip(np.nditer(self.x_data), np.nditer(self.y_data), np.nditer(self.z_data))] + self.vertices = verts self.colors = self.colormap.color_array([z for _, _, z in verts]) diff --git a/src/sas/qtgui/GL/visual_checks/primitive_library.py b/src/sas/qtgui/GL/visual_checks/primitive_library.py index 282f26a724..92331d3c8c 100644 --- a/src/sas/qtgui/GL/visual_checks/primitive_library.py +++ b/src/sas/qtgui/GL/visual_checks/primitive_library.py @@ -4,7 +4,7 @@ from PyQt5 import QtWidgets -from sas.qtgui.GL.scene import GraphWidget +from sas.qtgui.GL.scene import Scene from sas.qtgui.GL.models import ModelBase from sas.qtgui.GL.color import Color from sas.qtgui.GL.surface import Surface @@ -46,11 +46,11 @@ def primative_library(): Color(0,1,1), Color(1,0,1) ], color_by_mesh=True), - Cone(edge_colors=Color(1, 1, 1), vertex_colors=Color(0, 0.7, 0.2)), - Cylinder(edge_colors=Color(1, 1, 1), vertex_colors=Color(0, 0.2, 0.7)), - Icosahedron(edge_colors=Color(1, 1, 1), vertex_colors=Color(0.7, 0, 0.7)), - Sphere(edge_colors=Color(1, 1, 1), vertex_colors=Color(0.7, 0.7, 0.0)), - Sphere(edge_colors=Color(1, 1, 1), vertex_colors=Color(0.7, 0.4, 0.0), grid_gap=4) + Cone(edge_colors=Color(1, 1, 1), colors=Color(0, 0.7, 0.2)), + Cylinder(edge_colors=Color(1, 1, 1), colors=Color(0, 0.2, 0.7)), + Icosahedron(edge_colors=Color(1, 1, 1), colors=Color(0.7, 0, 0.7)), + Sphere(edge_colors=Color(1, 1, 1), colors=Color(0.7, 0.7, 0.0)), + Sphere(edge_colors=Color(1, 1, 1), colors=Color(0.7, 0.4, 0.0), grid_gap=4) ] # Turn off all of them @@ -92,7 +92,7 @@ def scan_states(): mainWindow = QtWidgets.QMainWindow() - viewer = GraphWidget(parent=mainWindow) + viewer = Scene(parent=mainWindow) # Keyboard callback def enable_disable(key): diff --git a/src/sas/qtgui/GL/visual_checks/transform_library.py b/src/sas/qtgui/GL/visual_checks/transform_library.py index 3a1d32c6b5..4132f152a4 100644 --- a/src/sas/qtgui/GL/visual_checks/transform_library.py +++ b/src/sas/qtgui/GL/visual_checks/transform_library.py @@ -2,7 +2,7 @@ from PyQt5 import QtWidgets -from sas.qtgui.GL.scene import GraphWidget +from sas.qtgui.GL.scene import Scene from sas.qtgui.GL.color import Color from sas.qtgui.GL.cone import Cone from sas.qtgui.GL.cube import Cube @@ -25,9 +25,9 @@ def transform_tests(): app = QtWidgets.QApplication([]) cube = Cube(edge_colors=Color(1, 1, 1), face_colors=Color(0.7, 0.2, 0)) - cone = Cone(edge_colors=Color(1, 1, 1), vertex_colors=Color(0, 0.7, 0.2)) - cylinder = Cylinder(edge_colors=Color(1, 1, 1), vertex_colors=Color(0, 0.2, 0.7)) - icos = Icosahedron(edge_colors=Color(1, 1, 1), vertex_colors=Color(0.7, 0, 0.7)) + cone = Cone(edge_colors=Color(1, 1, 1), colors=Color(0, 0.7, 0.2)) + cylinder = Cylinder(edge_colors=Color(1, 1, 1), colors=Color(0, 0.2, 0.7)) + icos = Icosahedron(edge_colors=Color(1, 1, 1), colors=Color(0.7, 0, 0.7)) # Translations @@ -116,7 +116,7 @@ def scan_states(): mainWindow = QtWidgets.QMainWindow() - viewer = GraphWidget(parent=mainWindow) + viewer = Scene(parent=mainWindow) # Keyboard callback def enable_disable(key): diff --git a/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py b/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py index 5897a264a3..8a85380943 100644 --- a/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py +++ b/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py @@ -11,7 +11,13 @@ from sasmodels.data import empty_data2D from sasmodels.direct_model import DirectModel -from sas.qtgui.GL.scene import GraphWidget +from sas.qtgui.GL.color import Color +from sas.qtgui.GL.scene import Scene +from sas.qtgui.GL.transforms import Rotation, Scaling, Translation +from sas.qtgui.GL.surface import Surface +from sas.qtgui.GL.cylinder import Cylinder +from sas.qtgui.GL.cone import Cone +from sas.qtgui.GL.cube import Cube from sas.qtgui.Utilities.OrientationViewer.OrientationViewerController import OrientationViewierController, Orientation from sas.qtgui.Utilities.OrientationViewer.OrientationViewerGraphics import OrientationViewerGraphics @@ -19,7 +25,6 @@ - class OrientationViewer(QtWidgets.QWidget): # Dimensions of scattering cuboid @@ -27,10 +32,13 @@ class OrientationViewer(QtWidgets.QWidget): b = 0.4 c = 1.0 + arrow_size = 0.2 + arrow_color = Color(0.9, 0.9, 0.9) + cuboid_scaling = [a, b, c] n_ghosts_per_perameter = 8 - n_q_samples = 256 + n_q_samples = 129 log_I_max = 10 log_I_min = -3 q_max = 0.5 @@ -46,35 +54,47 @@ def __init__(self, parent=None): # Put a barrier that will stop a flood of events going to the calculator self.set_image_data = FloodBarrier[Orientation](self._set_image_data, Orientation(), 0.5) - self.graph = GraphWidget() + self.scene = Scene() - self.graph.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + self.scene.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.controller = OrientationViewierController() layout = QtWidgets.QVBoxLayout() - layout.addWidget(self.graph) + layout.addWidget(self.scene) layout.addWidget(self.controller) self.setLayout(layout) - # - # self.arrow = OrientationViewerGraphics.create_arrow() - # self.image_plane_coordinate_points = np.linspace(-3, 3, 256) - # - # # temporary plot data - # x, y = np.meshgrid(self.image_plane_coordinate_points, self.image_plane_coordinate_points) - # self.image_plane_data = np.zeros_like(x) - # - # self.colormap = mpl.colormaps["viridis"] - # - # self.image_plane_colors = self.colormap(self.image_plane_data) - # - # self.image_plane = gl.GLSurfacePlotItem( - # self.image_plane_coordinate_points, - # self.image_plane_coordinate_points, - # self.image_plane_data, - # self.image_plane_colors - # ) - # + + self.arrow = Translation(0,0,1, + Rotation(180,0,1,0, + Scaling( + OrientationViewer.arrow_size, + OrientationViewer.arrow_size, + OrientationViewer.arrow_size, + Scaling(0.1, 0.1, 1, + Cylinder(colors=OrientationViewer.arrow_color)), + Translation(0,0,1.3, + Scaling(0.3, 0.3, 0.3, + Cone(colors=OrientationViewer.arrow_color)))))) + + self.scene.add(self.arrow) + + self.image_plane_coordinate_points = np.linspace(-3, 3, OrientationViewer.n_q_samples) + + # temporary plot data + x, y = np.meshgrid(self.image_plane_coordinate_points, self.image_plane_coordinate_points) + self.image_plane_data = np.zeros_like(x) + + self.surface = Surface( + self.image_plane_coordinate_points, + self.image_plane_coordinate_points, + self.image_plane_data, + edge_skip=8) + + self.image_plane = Translation(0,0,-1, Scaling(0.5, 0.5, 0.5, self.surface)) + + self.scene.add(self.image_plane) + # ghost_alpha = 1/(OrientationViewer.n_ghosts_per_perameter**3) # self.ghosts = [] # for a in np.linspace(-1, 1, OrientationViewer.n_ghosts_per_perameter): @@ -99,39 +119,44 @@ def __init__(self, parent=None): # ghost.setTransform(OrientationViewerGraphics.createCubeTransform(0, 0, 0, OrientationViewer.cuboid_scaling)) # # - # self.controller.valueEdited.connect(self.on_angle_change) - # - # self.calculator = OrientationViewer.create_calculator() - # self.on_angle_change(Orientation()) + self.controller.valueEdited.connect(self.on_angle_change) + + self.calculator = OrientationViewer.create_calculator() + self.on_angle_change(Orientation()) def _set_image_data(self, orientation: Orientation): + """ Set the data on the plot""" data = self.scatering_data(orientation) scaled_data = (np.log(data) - OrientationViewer.log_I_min) / OrientationViewer.log_I_range self.image_plane_data = np.clip(scaled_data, 0, 1) - self.image_plane_colors = self.colormap(self.image_plane_data) + print(self.image_plane_data) + + self.surface.set_z_data(self.image_plane_data) - self.image_plane.setData(z=self.image_plane_data, colors=self.image_plane_colors) + self.scene.update() def on_angle_change(self, orientation: Optional[Orientation]): + """ Response to angle change""" + if orientation is None: return - - for a, b, c, ghost in self.ghosts: - - ghost.setTransform( - OrientationViewerGraphics.createCubeTransform( - orientation.theta + 0.5*a*orientation.dtheta, - orientation.phi + 0.5*b*orientation.dphi, - orientation.psi + 0.5*c*orientation.dpsi, - OrientationViewer.cuboid_scaling)) + # + # for a, b, c, ghost in self.ghosts: + # + # ghost.setTransform( + # OrientationViewerGraphics.createCubeTransform( + # orientation.theta + 0.5*a*orientation.dtheta, + # orientation.phi + 0.5*b*orientation.dphi, + # orientation.psi + 0.5*c*orientation.dpsi, + # OrientationViewer.cuboid_scaling)) self.set_image_data(orientation) @@ -140,11 +165,11 @@ def create_calculator(): """ Make a parallelepiped model calculator for q range -qmax to qmax with n samples """ - model_info = load_model_info("parallelepiped") model = build_model(model_info) q = np.linspace(-OrientationViewer.q_max, OrientationViewer.q_max, OrientationViewer.n_q_samples) data = empty_data2D(q, q) + print(data.data.shape) calculator = DirectModel(data, model) return calculator diff --git a/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewerOld.py b/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewerOld.py deleted file mode 100644 index fbf2d85531..0000000000 --- a/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewerOld.py +++ /dev/null @@ -1,218 +0,0 @@ -from typing import Optional -import numpy as np - -from PyQt5 import QtWidgets -from PyQt5.QtWidgets import QSizePolicy -from PyQt5.QtCore import Qt - -import matplotlib as mpl - -import pyqtgraph.opengl as gl -from OpenGL.GL import * -from OpenGL.GLU import * -from PyQt5.QtOpenGL import QGLWidget - -from sasmodels.core import load_model_info, build_model -from sasmodels.data import empty_data2D -from sasmodels.direct_model import DirectModel - -from sas.qtgui.Utilities.OrientationViewer.OrientationViewerController import OrientationViewierController, Orientation -from sas.qtgui.Utilities.OrientationViewer.OrientationViewerGraphics import OrientationViewerGraphics -from sas.qtgui.Utilities.OrientationViewer.FloodBarrier import FloodBarrier - - - - - -class OrientationViewer(QtWidgets.QWidget): - - # Dimensions of scattering cuboid - a = 0.1 - b = 0.4 - c = 1.0 - - cuboid_scaling = [a, b, c] - - n_ghosts_per_perameter = 8 - n_q_samples = 256 - log_I_max = 10 - log_I_min = -3 - q_max = 0.5 - polydispersity_distribution = "gaussian" - - log_I_range = log_I_max - log_I_min - - def __init__(self, parent=None): - super().__init__() - - self.parent = parent - - # Put a barrier that will stop a flood of events going to the calculator - self.set_image_data = FloodBarrier[Orientation](self._set_image_data, Orientation(), 0.5) - - self.graph = gl.GLViewWidget() - - self.graph.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) - - self.controller = OrientationViewierController() - - layout = QtWidgets.QVBoxLayout() - layout.addWidget(self.graph) - layout.addWidget(self.controller) - self.setLayout(layout) - - self.arrow = OrientationViewerGraphics.create_arrow() - self.image_plane_coordinate_points = np.linspace(-3, 3, 256) - - # temporary plot data - x, y = np.meshgrid(self.image_plane_coordinate_points, self.image_plane_coordinate_points) - self.image_plane_data = np.zeros_like(x) - - self.colormap = mpl.colormaps["viridis"] - - self.image_plane_colors = self.colormap(self.image_plane_data) - - self.image_plane = gl.GLSurfacePlotItem( - self.image_plane_coordinate_points, - self.image_plane_coordinate_points, - self.image_plane_data, - self.image_plane_colors - ) - - ghost_alpha = 1/(OrientationViewer.n_ghosts_per_perameter**3) - self.ghosts = [] - for a in np.linspace(-1, 1, OrientationViewer.n_ghosts_per_perameter): - for b in np.linspace(-1, 1, OrientationViewer.n_ghosts_per_perameter): - for c in np.linspace(-1, 1, OrientationViewer.n_ghosts_per_perameter): - ghost = OrientationViewerGraphics.create_cube(ghost_alpha) - self.graph.addItem(ghost) - self.ghosts.append((a, b, c, ghost)) - - - - self.graph.addItem(self.arrow) - self.graph.addItem(self.image_plane) - - self.arrow.rotate(180, 1, 0, 0) - self.arrow.scale(0.05, 0.05, 0.05) - self.arrow.translate(0,0,1) - - self.image_plane.translate(0,0,-0.5) # It's 1 unit thick, so half way - - for _, _, _, ghost in self.ghosts: - ghost.setTransform(OrientationViewerGraphics.createCubeTransform(0, 0, 0, OrientationViewer.cuboid_scaling)) - - - self.controller.valueEdited.connect(self.on_angle_change) - - self.calculator = OrientationViewer.create_calculator() - self.on_angle_change(Orientation()) - - - - def _set_image_data(self, orientation: Orientation): - - data = self.scatering_data(orientation) - - scaled_data = (np.log(data) - OrientationViewer.log_I_min) / OrientationViewer.log_I_range - self.image_plane_data = np.clip(scaled_data, 0, 1) - - self.image_plane_colors = self.colormap(self.image_plane_data) - - self.image_plane.setData(z=self.image_plane_data, colors=self.image_plane_colors) - - - - def on_angle_change(self, orientation: Optional[Orientation]): - - if orientation is None: - return - - for a, b, c, ghost in self.ghosts: - - ghost.setTransform( - OrientationViewerGraphics.createCubeTransform( - orientation.theta + 0.5*a*orientation.dtheta, - orientation.phi + 0.5*b*orientation.dphi, - orientation.psi + 0.5*c*orientation.dpsi, - OrientationViewer.cuboid_scaling)) - - self.set_image_data(orientation) - - @staticmethod - def create_calculator(): - """ - Make a parallelepiped model calculator for q range -qmax to qmax with n samples - """ - - model_info = load_model_info("parallelepiped") - model = build_model(model_info) - q = np.linspace(-OrientationViewer.q_max, OrientationViewer.q_max, OrientationViewer.n_q_samples) - data = empty_data2D(q, q) - calculator = DirectModel(data, model) - - return calculator - - - def polydispersity_sample_count(self, orientation): - """ Work out how many samples to do for the polydispersity""" - polydispersity = [orientation.dtheta, orientation.dphi, orientation.dpsi] - is_polydisperse = [1 if x > 0 else 0 for x in polydispersity] - n_polydisperse = np.sum(is_polydisperse) - - samples = int(200 / (n_polydisperse**2.2 + 1)) # - - return (samples * x for x in is_polydisperse) - - def scatering_data(self, orientation: Orientation) -> np.ndarray: - - # add the orientation parameters to the model parameters - - theta_pd_n, phi_pd_n, psi_pd_n = self.polydispersity_sample_count(orientation) - - data = self.calculator( - theta=orientation.theta, - theta_pd=orientation.dtheta, - theta_pd_type=OrientationViewer.polydispersity_distribution, - theta_pd_n=theta_pd_n, - phi=orientation.phi, - phi_pd=orientation.dphi, - phi_pd_type=OrientationViewer.polydispersity_distribution, - phi_pd_n=phi_pd_n, - psi=orientation.psi, - psi_pd=orientation.dpsi, - psi_pd_type=OrientationViewer.polydispersity_distribution, - psi_pd_n=psi_pd_n, - a=OrientationViewer.a, - b=OrientationViewer.b, - c=OrientationViewer.c, - background=np.exp(OrientationViewer.log_I_min)) - - return np.reshape(data, (OrientationViewer.n_q_samples, OrientationViewer.n_q_samples)) - - - -def main(): - - import os - - os.environ["QT_ENABLE_HIGHDPI_SCALING"] = "1" - app = QtWidgets.QApplication([]) - - app.setAttribute(Qt.AA_EnableHighDpiScaling) - app.setAttribute(Qt.AA_ShareOpenGLContexts) - - - mainWindow = QtWidgets.QMainWindow() - viewer = OrientationViewer(mainWindow) - - mainWindow.setCentralWidget(viewer) - - mainWindow.show() - - mainWindow.resize(600, 600) - app.exec_() - - -if __name__ == "__main__": - main() \ No newline at end of file From 0aaaeaafb8a0303720f7c2b7d181b17fb8ad469a Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Fri, 25 Nov 2022 16:13:34 +0000 Subject: [PATCH 56/87] Fixed auto-refactor bug --- .../qtgui/Perspectives/Invariant/InvariantPerspective.py | 2 +- src/sas/qtgui/Plotting/Plotter.py | 2 +- src/sas/qtgui/Plotting/Plotter2D.py | 2 +- src/sas/qtgui/Plotting/Slicers/AnnulusSlicer.py | 2 +- src/sas/qtgui/Plotting/Slicers/BoxSlicer.py | 2 +- src/sas/qtgui/Plotting/Slicers/SectorSlicer.py | 2 +- test/sasinvariant/utest_data_handling.py | 6 +++--- 7 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/sas/qtgui/Perspectives/Invariant/InvariantPerspective.py b/src/sas/qtgui/Perspectives/Invariant/InvariantPerspective.py index c07420e329..d9bb712a1a 100644 --- a/src/sas/qtgui/Perspectives/Invariant/InvariantPerspective.py +++ b/src/sas/qtgui/Perspectives/Invariant/InvariantPerspective.py @@ -330,7 +330,7 @@ def calculateThread(self, extrapolation): # Pythonic name, typing # Update calculator with background, scale, and data values self._calculator.background = self._background - self._calculator.cuboid_scaling = self._scale + self._calculator.scale = self._scale self._calculator.set_data(temp_data) # Low Q extrapolation calculations diff --git a/src/sas/qtgui/Plotting/Plotter.py b/src/sas/qtgui/Plotting/Plotter.py index a3a1e643bc..2ce4908339 100644 --- a/src/sas/qtgui/Plotting/Plotter.py +++ b/src/sas/qtgui/Plotting/Plotter.py @@ -90,7 +90,7 @@ def data(self, value): else: self.yLabel = "%s"%(value._yaxis) - if value.cuboid_scaling == 'linear' or value.isSesans: + if value.scale == 'linear' or value.isSesans: self.xscale = 'linear' self.yscale = 'linear' self.title(title=value.name) diff --git a/src/sas/qtgui/Plotting/Plotter2D.py b/src/sas/qtgui/Plotting/Plotter2D.py index 72ebf3be51..a007260e26 100644 --- a/src/sas/qtgui/Plotting/Plotter2D.py +++ b/src/sas/qtgui/Plotting/Plotter2D.py @@ -290,7 +290,7 @@ def circularAverage(self): # Define axes if not done yet. new_plot.xaxis("\\rm{Q}", "A^{-1}") if hasattr(self.data0, "scale") and \ - self.data0.cuboid_scaling == 'linear': + self.data0.scale == 'linear': new_plot.ytransform = 'y' new_plot.yaxis("\\rm{Residuals} ", "normalized") else: diff --git a/src/sas/qtgui/Plotting/Slicers/AnnulusSlicer.py b/src/sas/qtgui/Plotting/Slicers/AnnulusSlicer.py index bd41690961..cd8696b6d6 100644 --- a/src/sas/qtgui/Plotting/Slicers/AnnulusSlicer.py +++ b/src/sas/qtgui/Plotting/Slicers/AnnulusSlicer.py @@ -129,7 +129,7 @@ def _post_data(self, nbins=None): # If the data file does not tell us what the axes are, just assume... new_plot.xaxis("\\rm{\phi}", 'degrees') new_plot.yaxis("\\rm{Intensity} ", "cm^{-1}") - if hasattr(data, "scale") and data.cuboid_scaling == 'linear' and \ + if hasattr(data, "scale") and data.scale == 'linear' and \ self.data.name.count("Residuals") > 0: new_plot.ytransform = 'y' new_plot.yaxis("\\rm{Residuals} ", "/") diff --git a/src/sas/qtgui/Plotting/Slicers/BoxSlicer.py b/src/sas/qtgui/Plotting/Slicers/BoxSlicer.py index deb44de727..272d16d6be 100644 --- a/src/sas/qtgui/Plotting/Slicers/BoxSlicer.py +++ b/src/sas/qtgui/Plotting/Slicers/BoxSlicer.py @@ -179,7 +179,7 @@ def post_data(self, new_slab=None, nbins=None, direction=None): new_plot.yaxis("\\rm{Intensity} ", "cm^{-1}") data = self.data - if hasattr(data, "scale") and data.cuboid_scaling == 'linear' and \ + if hasattr(data, "scale") and data.scale == 'linear' and \ self.data.name.count("Residuals") > 0: new_plot.ytransform = 'y' new_plot.yaxis("\\rm{Residuals} ", "/") diff --git a/src/sas/qtgui/Plotting/Slicers/SectorSlicer.py b/src/sas/qtgui/Plotting/Slicers/SectorSlicer.py index ef063e7f41..5270db60f7 100644 --- a/src/sas/qtgui/Plotting/Slicers/SectorSlicer.py +++ b/src/sas/qtgui/Plotting/Slicers/SectorSlicer.py @@ -160,7 +160,7 @@ def _post_data(self, nbins=None): # If the data file does not tell us what the axes are, just assume them. new_plot.xaxis("\\rm{Q}", "A^{-1}") new_plot.yaxis("\\rm{Intensity}", "cm^{-1}") - if hasattr(data, "scale") and data.cuboid_scaling == 'linear' and \ + if hasattr(data, "scale") and data.scale == 'linear' and \ self.data.name.count("Residuals") > 0: new_plot.ytransform = 'y' new_plot.yaxis("\\rm{Residuals} ", "/") diff --git a/test/sasinvariant/utest_data_handling.py b/test/sasinvariant/utest_data_handling.py index 972afb0dee..ecade8a754 100644 --- a/test/sasinvariant/utest_data_handling.py +++ b/test/sasinvariant/utest_data_handling.py @@ -425,7 +425,7 @@ def test_low_q(self): qmax=qmax, power=inv._low_extrapolation_power) self.assertAlmostEqual(self.scale, - inv._low_extrapolation_function.cuboid_scaling, 6) + inv._low_extrapolation_function.scale, 6) self.assertAlmostEqual(self.rg, inv._low_extrapolation_function.radius, 6) @@ -477,7 +477,7 @@ def test_low_q(self): power=inv._low_extrapolation_power) self.assertAlmostEqual(self.scale, - inv._low_extrapolation_function.cuboid_scaling, 6) + inv._low_extrapolation_function.scale, 6) self.assertAlmostEqual(self.m, inv._low_extrapolation_function.power, 6) @@ -568,7 +568,7 @@ def test_low_q(self): qmax=qmax, power=inv._low_extrapolation_power) self.assertAlmostEqual(self.scale, - inv._low_extrapolation_function.cuboid_scaling, 6) + inv._low_extrapolation_function.scale, 6) self.assertAlmostEqual(self.rg, inv._low_extrapolation_function.radius, 6) From c22ec4a2cfaf8790dce562bcc65104cf49267801 Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Fri, 25 Nov 2022 16:31:35 +0000 Subject: [PATCH 57/87] Explicitly added dependency to CI --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fdd02ddb71..9eb39840d1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -89,7 +89,7 @@ jobs: python -m pip install tinycc h5py pyparsing html5lib reportlab==3.6.6 pybind11 appdirs python -m pip install six numba mako ipython qtconsole xhtml2pdf pylint debugpy python -m pip install qt5reactor periodictable uncertainties dominate importlib_resources - python -m pip install html2text superqt + python -m pip install html2text superqt pyopengl - name: Install PyQt (Windows + Linux) if: ${{ !startsWith(matrix.os, 'macos') }} From d42ba77420165dd6b187b90f3b3ae81b7d11389b Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Fri, 25 Nov 2022 16:46:48 +0000 Subject: [PATCH 58/87] Removed reference to pyqtgraph --- .../OrientationViewerGraphics.py | 97 ------------------- 1 file changed, 97 deletions(-) diff --git a/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewerGraphics.py b/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewerGraphics.py index f8e446e83c..536c670e8e 100644 --- a/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewerGraphics.py +++ b/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewerGraphics.py @@ -1,110 +1,13 @@ from typing import List import numpy as np - -import pyqtgraph.opengl as gl from pyqtgraph.Transform3D import Transform3D -from OpenGL.GL import * from sasmodels.jitter import Rx, Ry, Rz class OrientationViewerGraphics: - @staticmethod - def hypercube(n): - """ Coordinates of a hypercube in with 'binary' ordering""" - if n <= 0: - return [[]] - else: - hypersquare = OrientationViewerGraphics.hypercube(n-1) - return [[0] + vert for vert in hypersquare] + [[1] + vert for vert in hypersquare] - - - @staticmethod - def create_cube(alpha=1.0): - """ Mesh for the main cuboid""" - # Sorry - vertices = OrientationViewerGraphics.hypercube(3) - - faces_and_colors = [] - - for fixed_dim in range(3): - for zero_one in range(2): - this_face = [(ind, v) for ind, v in enumerate(vertices) if v[fixed_dim] == zero_one] - - def sort_key(x): - _, v = x - other_dims = v[:fixed_dim] + v[fixed_dim+1:] - return (v[fixed_dim] - 0.5)*np.arctan2(other_dims[0]-0.5, other_dims[1]-0.5) - - this_face = sorted(this_face, key=sort_key) - - color = [255,255,255,255] #[0.6,0.6,0.6,alpha] - color[fixed_dim]=0.4 - - # faces_and_colors.append([[this_face[x][0] for x in range(4)], color]) - - faces_and_colors.append(([this_face[x][0] for x in (0,1,2)], color)) - faces_and_colors.append(([this_face[x][0] for x in (2,3,0)], color)) - - vertices = np.array(vertices, dtype=float) - 0.5 - faces = np.array([face for face, _ in faces_and_colors]) - colors = np.array([color for _, color in faces_and_colors]) - - return gl.GLMeshItem(vertexes=vertices, faces=faces, faceColors=colors, - drawEdges=False, edgeColor=(0, 0, 0, 1), smooth=False) - - @staticmethod - def create_arrow(n: int = 30, tail_length=10, tail_width=0.6): - """ Mesh for an arrow """ - # Thanks, I hate it. - - # top and tail - points = [[0, 0, 1], [0, 0, -tail_length]] - faces = [] - colors = [] - - # widest ring - for angle in 2 * np.pi * np.arange(0, n) / n: - points.append([np.sin(angle), np.cos(angle), 0]) - - # middle ring - for angle in 2 * np.pi * np.arange(0, n) / n: - points.append([tail_width*np.sin(angle), tail_width*np.cos(angle), 0]) - - # bottom ring - for angle in 2 * np.pi * np.arange(0, n) / n: - points.append([tail_width*np.sin(angle), tail_width*np.cos(angle), -tail_length]) - - for i in range(n): - # laterial indices - j = (i+1) % n - - # Pointy bit - faces.append([i + 2, j+2 + 2, 0]) - - # # Top ring - faces.append([i + n + 2, j + n + 2, j + 2]) - faces.append([i + n + 2, i + 2, j + 2]) - - # Cylinder sides - faces.append([i + 2 * n + 2, j + 2 * n + 2, j + n + 2]) - faces.append([i + 2 * n + 2, i + n + 2, j + n + 2]) - - # Cylinder base - faces.append([i+2*n + 2, j + 2 * n + 2, 1]) - - for i in range(6*n): - colors.append([0.6] * 3 + [1]) - - points = np.array(points) - np.array([0,0,-0.5*(tail_length-1)]) - faces = np.array(faces) - colors = np.array(colors) - - return gl.GLMeshItem(vertexes=points, faces=faces, faceColors=colors, - drawEdges=False, edgeColor=(0, 0, 0, 1), smooth=True) - @staticmethod def createCubeTransform(theta_deg: float, phi_deg: float, psi_deg: float, scaling: List[float]) -> Transform3D: From becc350a478aa0f2de4037e310e64222f1ffc760 Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Fri, 25 Nov 2022 17:01:33 +0000 Subject: [PATCH 59/87] Remove more pyqtgraph references --- .../Utilities/OrientationViewer/OrientationViewerGraphics.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewerGraphics.py b/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewerGraphics.py index 536c670e8e..1f866edc13 100644 --- a/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewerGraphics.py +++ b/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewerGraphics.py @@ -1,7 +1,6 @@ from typing import List import numpy as np -from pyqtgraph.Transform3D import Transform3D from sasmodels.jitter import Rx, Ry, Rz @@ -9,7 +8,7 @@ class OrientationViewerGraphics: @staticmethod - def createCubeTransform(theta_deg: float, phi_deg: float, psi_deg: float, scaling: List[float]) -> Transform3D: + def createCubeTransform(theta_deg: float, phi_deg: float, psi_deg: float, scaling: List[float]) -> np.ndarray: # Get rotation matrix r_mat = Rz(phi_deg)@Ry(theta_deg)@Rz(psi_deg)@np.diag(scaling) @@ -19,4 +18,4 @@ def createCubeTransform(theta_deg: float, phi_deg: float, psi_deg: float, scalin trans_mat[-1, -1] = 1 trans_mat[2, -1] = 2 - return Transform3D(trans_mat) \ No newline at end of file + return trans_mat \ No newline at end of file From 166033ec127ab8c6b24caa58932403b7777544d0 Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Fri, 25 Nov 2022 17:14:16 +0000 Subject: [PATCH 60/87] Moved transform to OrientationViewer, it should probably be deleted soon --- src/sas/qtgui/GL/transforms.py | 17 +++++++++++++++ .../OrientationViewer/OrientationViewer.py | 14 +++++++++++-- .../OrientationViewerGraphics.py | 21 ------------------- 3 files changed, 29 insertions(+), 23 deletions(-) delete mode 100644 src/sas/qtgui/Utilities/OrientationViewer/OrientationViewerGraphics.py diff --git a/src/sas/qtgui/GL/transforms.py b/src/sas/qtgui/GL/transforms.py index 6bf3321bad..1904bb7d6d 100644 --- a/src/sas/qtgui/GL/transforms.py +++ b/src/sas/qtgui/GL/transforms.py @@ -131,3 +131,20 @@ def __init__(self, x: float, y: float, z: float, *children: Renderable): def apply(self): glScale(self.x, self.y, self.z) +class MatrixTransform(SceneGraphNode): + + def __init__(self, matrix: np.ndarray, *children: Renderable): + """ + + Apply a 4x4 transformation matrix to the children of this node + + :param matrix: a 4x4 transformation matrix + + """ + + super().__init__(*children) + + self.matrix = matrix + + def apply(self): + glMultMatrixd(self.matrix) \ No newline at end of file diff --git a/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py b/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py index 8a85380943..8ec8883b08 100644 --- a/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py +++ b/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Optional, List import numpy as np from PyQt5 import QtWidgets @@ -10,6 +10,7 @@ from sasmodels.core import load_model_info, build_model from sasmodels.data import empty_data2D from sasmodels.direct_model import DirectModel +from sasmodels.jitter import Rx, Ry, Rz from sas.qtgui.GL.color import Color from sas.qtgui.GL.scene import Scene @@ -20,10 +21,19 @@ from sas.qtgui.GL.cube import Cube from sas.qtgui.Utilities.OrientationViewer.OrientationViewerController import OrientationViewierController, Orientation -from sas.qtgui.Utilities.OrientationViewer.OrientationViewerGraphics import OrientationViewerGraphics from sas.qtgui.Utilities.OrientationViewer.FloodBarrier import FloodBarrier +def createCubeTransform(theta_deg: float, phi_deg: float, psi_deg: float, scaling: List[float]) -> np.ndarray: + # Get rotation matrix + r_mat = Rz(phi_deg) @ Ry(theta_deg) @ Rz(psi_deg) @ np.diag(scaling) + + # Get the 4x4 transformation matrix, by (1) padding by zeros (2) setting the corner element to 1 + trans_mat = np.pad(r_mat, ((0, 1), (0, 1))) + trans_mat[-1, -1] = 1 + trans_mat[2, -1] = 2 + + return trans_mat class OrientationViewer(QtWidgets.QWidget): diff --git a/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewerGraphics.py b/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewerGraphics.py deleted file mode 100644 index 1f866edc13..0000000000 --- a/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewerGraphics.py +++ /dev/null @@ -1,21 +0,0 @@ -from typing import List - -import numpy as np - -from sasmodels.jitter import Rx, Ry, Rz - - -class OrientationViewerGraphics: - - @staticmethod - def createCubeTransform(theta_deg: float, phi_deg: float, psi_deg: float, scaling: List[float]) -> np.ndarray: - - # Get rotation matrix - r_mat = Rz(phi_deg)@Ry(theta_deg)@Rz(psi_deg)@np.diag(scaling) - - # Get the 4x4 transformation matrix, by (1) padding by zeros (2) setting the corner element to 1 - trans_mat = np.pad(r_mat, ((0, 1), (0, 1))) - trans_mat[-1, -1] = 1 - trans_mat[2, -1] = 2 - - return trans_mat \ No newline at end of file From a32562c1336d30d61569ee2be531ab738e0086cc Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Fri, 25 Nov 2022 18:29:56 +0000 Subject: [PATCH 61/87] Orientation viewer basics working --- src/sas/qtgui/GL/cube.py | 8 +- src/sas/qtgui/GL/models.py | 17 ++- src/sas/qtgui/GL/surface.py | 20 +++- .../GL/visual_checks/primitive_library.py | 4 +- .../GL/visual_checks/transform_library.py | 2 +- .../OrientationViewer/FloodBarrier.py | 34 ------ .../OrientationViewer/OrientationViewer.py | 111 +++++++++++++----- .../OrientationViewerController.py | 42 +++++-- 8 files changed, 150 insertions(+), 88 deletions(-) delete mode 100644 src/sas/qtgui/Utilities/OrientationViewer/FloodBarrier.py diff --git a/src/sas/qtgui/GL/cube.py b/src/sas/qtgui/GL/cube.py index a08ff15284..d4c5a515fc 100644 --- a/src/sas/qtgui/GL/cube.py +++ b/src/sas/qtgui/GL/cube.py @@ -49,7 +49,7 @@ class Cube(FullModel): ] def __init__(self, - face_colors: Optional[Union[Sequence[Color],Color]]=None, + colors: Optional[Union[Sequence[Color], Color]]=None, edge_colors: Optional[Union[Sequence[Color],Color]]=None, color_by_mesh: bool=False): @@ -58,7 +58,7 @@ def __init__(self, edges=Cube.cube_edges, triangle_meshes=Cube.cube_triangles, edge_colors=edge_colors, - colors=face_colors, + colors=colors, color_by_mesh=color_by_mesh) self.vertices = Cube.cube_vertices @@ -71,12 +71,12 @@ def __init__(self, self.wireframe_render_enabled = True self.edge_colors = edge_colors - if face_colors is None: + if colors is None: self.solid_render_enabled = False self.face_colors = [] else: self.solid_render_enabled = True - self.face_colors = face_colors + self.face_colors = colors diff --git a/src/sas/qtgui/GL/models.py b/src/sas/qtgui/GL/models.py index 2fa4aa9126..2866a0183f 100644 --- a/src/sas/qtgui/GL/models.py +++ b/src/sas/qtgui/GL/models.py @@ -86,7 +86,12 @@ def __init__(self, self.color_by_mesh = color_by_mesh self._colors = colors - self._vertex_color_array = None if isinstance(colors, Color) or color_by_mesh else np.array([color.to_array() for color in colors], dtype=float) + + if colors is None or isinstance(colors, Color) or color_by_mesh: + self._vertex_color_array = None + else: + self._vertex_color_array = np.array([color.to_array() for color in colors], dtype=float) + self.solid_render_enabled = self.colors is not None @@ -95,9 +100,13 @@ def colors(self): return self._colors @colors.setter - def colors(self, new_vertex_colors): - self._colors = new_vertex_colors - self._vertex_color_array = None if isinstance(new_vertex_colors, Color) else np.array([color.to_array() for color in new_vertex_colors], dtype=float) + def colors(self, new_colors): + self._colors = new_colors + if new_colors is None or isinstance(new_colors, Color) or self.color_by_mesh: + self._vertex_color_array = None + else: + self._vertex_color_array = np.array([color.to_array() for color in new_colors], dtype=float) + self.solid_render_enabled = self.colors is not None def render_solid(self): diff --git a/src/sas/qtgui/GL/surface.py b/src/sas/qtgui/GL/surface.py index 6e87c03e37..70faa52765 100644 --- a/src/sas/qtgui/GL/surface.py +++ b/src/sas/qtgui/GL/surface.py @@ -61,7 +61,9 @@ def __init__(self, self.n_x = len(x_values) self.n_y = len(y_values) - self.colormap = ColorMap(colormap, min_value=c_range[0], max_value=c_range[1]) + self.c_range = c_range + self._colormap_name = colormap + self._colormap = ColorMap(colormap, min_value=c_range[0], max_value=c_range[1]) verts = [(float(x), float(y), float(z)) for x, y, z in zip(np.nditer(self.x_data), np.nditer(self.y_data), np.nditer(self.z_data))] @@ -71,7 +73,7 @@ def __init__(self, edges=Surface.calculate_edge_indices(self.n_x, self.n_y, edge_skip), triangle_meshes=[Surface.calculate_triangles(self.n_x, self.n_y)], edge_colors=Color(1.0,1.0,1.0), - colors=self.colormap.color_array([z for _, _, z in verts]) + colors=self._colormap.color_array([z for _, _, z in verts]) ) self.wireframe_render_enabled = True @@ -87,4 +89,16 @@ def set_z_data(self, z_data): for x, y, z in zip(np.nditer(self.x_data), np.nditer(self.y_data), np.nditer(self.z_data))] self.vertices = verts - self.colors = self.colormap.color_array([z for _, _, z in verts]) + self.colors = self._colormap.color_array([z for _, _, z in verts]) + + @property + def colormap(self) -> str: + """ Name of the colormap""" + return self._colormap_name + + @colormap.setter + def colormap(self, colormap: str): + if self._colormap_name != colormap: + self._colormap = ColorMap(colormap, min_value=self.c_range[0], max_value=self.c_range[1]) + self.colors = self._colormap.color_array([z for _, _, z in self.vertices]) + self._colormap_name = colormap diff --git a/src/sas/qtgui/GL/visual_checks/primitive_library.py b/src/sas/qtgui/GL/visual_checks/primitive_library.py index 92331d3c8c..bb119453e9 100644 --- a/src/sas/qtgui/GL/visual_checks/primitive_library.py +++ b/src/sas/qtgui/GL/visual_checks/primitive_library.py @@ -37,8 +37,8 @@ def primative_library(): item_list = [ mesh_example(), - Cube(edge_colors=Color(1, 1, 1), face_colors=Color(0.7, 0.2, 0)), - Cube(edge_colors=Color(1, 1, 1), face_colors=[ + Cube(edge_colors=Color(1, 1, 1), colors=Color(0.7, 0.2, 0)), + Cube(edge_colors=Color(1, 1, 1), colors=[ Color(1,0,0), Color(0,1,0), Color(0,0,1), diff --git a/src/sas/qtgui/GL/visual_checks/transform_library.py b/src/sas/qtgui/GL/visual_checks/transform_library.py index 4132f152a4..10922b2c01 100644 --- a/src/sas/qtgui/GL/visual_checks/transform_library.py +++ b/src/sas/qtgui/GL/visual_checks/transform_library.py @@ -24,7 +24,7 @@ def transform_tests(): os.environ["QT_ENABLE_HIGHDPI_SCALING"] = "1" app = QtWidgets.QApplication([]) - cube = Cube(edge_colors=Color(1, 1, 1), face_colors=Color(0.7, 0.2, 0)) + cube = Cube(edge_colors=Color(1, 1, 1), colors=Color(0.7, 0.2, 0)) cone = Cone(edge_colors=Color(1, 1, 1), colors=Color(0, 0.7, 0.2)) cylinder = Cylinder(edge_colors=Color(1, 1, 1), colors=Color(0, 0.2, 0.7)) icos = Icosahedron(edge_colors=Color(1, 1, 1), colors=Color(0.7, 0, 0.7)) diff --git a/src/sas/qtgui/Utilities/OrientationViewer/FloodBarrier.py b/src/sas/qtgui/Utilities/OrientationViewer/FloodBarrier.py deleted file mode 100644 index a40b43b6b2..0000000000 --- a/src/sas/qtgui/Utilities/OrientationViewer/FloodBarrier.py +++ /dev/null @@ -1,34 +0,0 @@ -from typing import TypeVar, Generic, Callable, Any, List, Optional - -import time -import threading - -T = TypeVar('T') - -class FloodBarrier(Generic[T], threading.Event): - """ A single entry LIFO queue holds up a function call for a period - of time, waiting to see if another value comes along. If another value - does come along, then it relplaces it and waits. - - This should prevent a flood of messages being sent to a calculation.""" - def __init__(self, done_call: Callable[[T], Any], initial_value: T, wait_time_s: float = 0.05): - super().__init__() - - self.done_call = done_call - self.wait_time_s = wait_time_s - self.value = initial_value - - self.current_timer: Optional[threading.Timer] = None - - def on_timout(self): - self.done_call(self.value) - - def __call__(self, value: T): - - if self.current_timer is not None: - self.current_timer.cancel() - - self.value = value - - self.current_timer = threading.Timer(self.wait_time_s, self.on_timout) - self.current_timer.start() diff --git a/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py b/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py index 8ec8883b08..bb7d5b9769 100644 --- a/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py +++ b/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py @@ -21,7 +21,6 @@ from sas.qtgui.GL.cube import Cube from sas.qtgui.Utilities.OrientationViewer.OrientationViewerController import OrientationViewierController, Orientation -from sas.qtgui.Utilities.OrientationViewer.FloodBarrier import FloodBarrier def createCubeTransform(theta_deg: float, phi_deg: float, psi_deg: float, scaling: List[float]) -> np.ndarray: @@ -44,11 +43,12 @@ class OrientationViewer(QtWidgets.QWidget): arrow_size = 0.2 arrow_color = Color(0.9, 0.9, 0.9) + ghost_color = Color(0.6, 0.6, 0.6) cuboid_scaling = [a, b, c] n_ghosts_per_perameter = 8 - n_q_samples = 129 + n_q_samples = 128 log_I_max = 10 log_I_min = -3 q_max = 0.5 @@ -59,10 +59,9 @@ class OrientationViewer(QtWidgets.QWidget): def __init__(self, parent=None): super().__init__() - self.parent = parent + self._colormap_name = 'viridis' - # Put a barrier that will stop a flood of events going to the calculator - self.set_image_data = FloodBarrier[Orientation](self._set_image_data, Orientation(), 0.5) + self.parent = parent self.scene = Scene() @@ -75,7 +74,7 @@ def __init__(self, parent=None): layout.addWidget(self.controller) self.setLayout(layout) - self.arrow = Translation(0,0,1, + self.arrow = Translation(0,0,1.5, Rotation(180,0,1,0, Scaling( OrientationViewer.arrow_size, @@ -101,40 +100,59 @@ def __init__(self, parent=None): self.image_plane_data, edge_skip=8) + self.surface.wireframe_render_enabled = False + # self.surface.colormap = 'Greys' + + self.image_plane = Translation(0,0,-1, Scaling(0.5, 0.5, 0.5, self.surface)) self.scene.add(self.image_plane) - # ghost_alpha = 1/(OrientationViewer.n_ghosts_per_perameter**3) - # self.ghosts = [] - # for a in np.linspace(-1, 1, OrientationViewer.n_ghosts_per_perameter): - # for b in np.linspace(-1, 1, OrientationViewer.n_ghosts_per_perameter): - # for c in np.linspace(-1, 1, OrientationViewer.n_ghosts_per_perameter): - # ghost = OrientationViewerGraphics.create_cube(ghost_alpha) - # self.graph.addItem(ghost) - # self.ghosts.append((a, b, c, ghost)) - # - # - # - # self.graph.addItem(self.arrow) - # self.graph.addItem(self.image_plane) - # - # self.arrow.rotate(180, 1, 0, 0) - # self.arrow.scale(0.05, 0.05, 0.05) - # self.arrow.translate(0,0,1) + + for a in np.linspace(-1, 1, OrientationViewer.n_ghosts_per_perameter): + b_ghosts = [] + for b in np.linspace(-1, 1, OrientationViewer.n_ghosts_per_perameter): + c_ghosts = [] + for c in np.linspace(-1, 1, OrientationViewer.n_ghosts_per_perameter): + ghost = Cube(edge_colors=OrientationViewer.ghost_color) + + + + self.first_rotation = Rotation(0,0,0,1, + Scaling(OrientationViewer.a, + OrientationViewer.b, + OrientationViewer.c, + Cube(edge_colors=Color(1,1,1), colors=Color(0,1,0)) + )) + + self.second_rotation = Rotation(0,0,1,0,self.first_rotation) + self.third_rotation = Rotation(0,0,0,1,self.second_rotation) + + self.cubes = Translation(0,0,0.5,self.third_rotation) + + self.scene.add(self.cubes) + # - # self.image_plane.translate(0,0,-0.5) # It's 1 unit thick, so half way + # # for _, _, _, ghost in self.ghosts: # ghost.setTransform(OrientationViewerGraphics.createCubeTransform(0, 0, 0, OrientationViewer.cuboid_scaling)) # # - self.controller.valueEdited.connect(self.on_angle_change) + self.controller.sliderSet.connect(self.on_angle_changed) + self.controller.sliderMoved.connect(self.on_angle_changing) self.calculator = OrientationViewer.create_calculator() - self.on_angle_change(Orientation()) + self.on_angle_changed(Orientation()) + @property + def colormap(self) -> str: + return self._colormap_name + @colormap.setter + def colormap(self, colormap_name: str): + self._colormap_name = colormap_name + self.surface.colormap = self._colormap_name def _set_image_data(self, orientation: Orientation): """ Set the data on the plot""" @@ -144,21 +162,53 @@ def _set_image_data(self, orientation: Orientation): scaled_data = (np.log(data) - OrientationViewer.log_I_min) / OrientationViewer.log_I_range self.image_plane_data = np.clip(scaled_data, 0, 1) - print(self.image_plane_data) - self.surface.set_z_data(self.image_plane_data) + # self.surface.colormap = self.colormap + self.scene.update() - def on_angle_change(self, orientation: Optional[Orientation]): + def on_angle_changed(self, orientation: Optional[Orientation]): """ Response to angle change""" if orientation is None: return + + + #r_mat = Rz(phi_deg) @ Ry(theta_deg) @ Rz(psi_deg) @ np.diag(scaling) + self.first_rotation.angle = orientation.psi + self.second_rotation.angle = orientation.theta + self.third_rotation.angle = orientation.phi + + # for a, b, c, ghost in self.ghosts: # + # ghost.setTransform( + # OrientationViewerGraphics.createCubeTransform( + # orientation.theta + 0.5*a*orientation.dtheta, + # orientation.phi + 0.5*b*orientation.dphi, + # orientation.psi + 0.5*c*orientation.dpsi, + # OrientationViewer.cuboid_scaling)) + + self._set_image_data(orientation) + + + def on_angle_changing(self, orientation: Optional[Orientation]): + + """ Response to angle change""" + + if orientation is None: + return + + # self.surface.colormap = "Greys" + + #r_mat = Rz(phi_deg) @ Ry(theta_deg) @ Rz(psi_deg) @ np.diag(scaling) + self.first_rotation.angle = orientation.psi + self.second_rotation.angle = orientation.theta + self.third_rotation.angle = orientation.phi + # for a, b, c, ghost in self.ghosts: # # ghost.setTransform( @@ -168,7 +218,7 @@ def on_angle_change(self, orientation: Optional[Orientation]): # orientation.psi + 0.5*c*orientation.dpsi, # OrientationViewer.cuboid_scaling)) - self.set_image_data(orientation) + self.scene.update() @staticmethod def create_calculator(): @@ -179,7 +229,6 @@ def create_calculator(): model = build_model(model_info) q = np.linspace(-OrientationViewer.q_max, OrientationViewer.q_max, OrientationViewer.n_q_samples) data = empty_data2D(q, q) - print(data.data.shape) calculator = DirectModel(data, model) return calculator diff --git a/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewerController.py b/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewerController.py index ac66a6b415..e246db81ed 100644 --- a/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewerController.py +++ b/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewerController.py @@ -22,7 +22,8 @@ class OrientationViewierController(QtWidgets.QDialog, Ui_OrientationViewierContr """ Widget that controls the orientation viewer""" - valueEdited = pyqtSignal(Orientation, name='valueEdited') + sliderSet = pyqtSignal(Orientation, name='sliderSet') + sliderMoved = pyqtSignal(Orientation, name='sliderMoved') def __init__(self, parent=None): super().__init__() @@ -32,12 +33,19 @@ def __init__(self, parent=None): self.setLabels(Orientation()) # All sliders emit the same signal - the angular coordinates in degrees - self.thetaSlider.valueChanged.connect(self.onAngleChange) - self.phiSlider.valueChanged.connect(self.onAngleChange) - self.psiSlider.valueChanged.connect(self.onAngleChange) - self.deltaTheta.valueChanged.connect(self.onAngleChange) - self.deltaPhi.valueChanged.connect(self.onAngleChange) - self.deltaPsi.valueChanged.connect(self.onAngleChange) + self.thetaSlider.sliderReleased.connect(self.onSliderSet) + self.phiSlider.sliderReleased.connect(self.onSliderSet) + self.psiSlider.sliderReleased.connect(self.onSliderSet) + self.deltaTheta.sliderReleased.connect(self.onSliderSet) + self.deltaPhi.sliderReleased.connect(self.onSliderSet) + self.deltaPsi.sliderReleased.connect(self.onSliderSet) + + self.thetaSlider.valueChanged.connect(self.onSliderMoved) + self.phiSlider.valueChanged.connect(self.onSliderMoved) + self.psiSlider.valueChanged.connect(self.onSliderMoved) + self.deltaTheta.valueChanged.connect(self.onSliderMoved) + self.deltaPhi.valueChanged.connect(self.onSliderMoved) + self.deltaPsi.valueChanged.connect(self.onSliderMoved) def setLabels(self, orientation: Orientation): @@ -50,7 +58,7 @@ def setLabels(self, orientation: Orientation): self.deltaPsiNumber.setText(f"{orientation.dpsi}°") - def onAngleChange(self): + def onSliderSet(self): theta = self.thetaSlider.value() phi = self.phiSlider.value() psi = self.psiSlider.value() @@ -60,9 +68,25 @@ def onAngleChange(self): dpsi = self.deltaPsi.value() orientation = Orientation(theta, phi, psi, dtheta, dphi, dpsi) - self.valueEdited.emit(orientation) + self.sliderSet.emit(orientation) self.setLabels(orientation) + def onSliderMoved(self): + + theta = self.thetaSlider.value() + phi = self.phiSlider.value() + psi = self.psiSlider.value() + + dtheta = self.deltaTheta.value() + dphi = self.deltaPhi.value() + dpsi = self.deltaPsi.value() + + orientation = Orientation(theta, phi, psi, dtheta, dphi, dpsi) + self.sliderMoved.emit(orientation) + self.setLabels(orientation) + + + def main(): """ Show a demo of the slider """ From ca1c7cbdf0c14aff4246fc0a317e6260bca1f490 Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Fri, 25 Nov 2022 18:58:09 +0000 Subject: [PATCH 62/87] Ghosts work --- .../OrientationViewer/OrientationViewer.py | 64 ++++++++++++------- 1 file changed, 40 insertions(+), 24 deletions(-) diff --git a/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py b/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py index bb7d5b9769..99d3c76c86 100644 --- a/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py +++ b/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py @@ -34,7 +34,9 @@ def createCubeTransform(theta_deg: float, phi_deg: float, psi_deg: float, scalin return trans_mat + class OrientationViewer(QtWidgets.QWidget): + """ Orientation viewer widget """ # Dimensions of scattering cuboid a = 0.1 @@ -43,7 +45,8 @@ class OrientationViewer(QtWidgets.QWidget): arrow_size = 0.2 arrow_color = Color(0.9, 0.9, 0.9) - ghost_color = Color(0.6, 0.6, 0.6) + ghost_color = Color(0.0, 0.6, 0.2) + cube_color = Color(0.0, 0.8, 0.0) cuboid_scaling = [a, b, c] @@ -56,6 +59,14 @@ class OrientationViewer(QtWidgets.QWidget): log_I_range = log_I_max - log_I_min + @staticmethod + def create_ghost(): + """ Helper function: Create a ghost cube""" + return Scaling(OrientationViewer.a, + OrientationViewer.b, + OrientationViewer.c, + Cube(edge_colors=OrientationViewer.ghost_color)) + def __init__(self, parent=None): super().__init__() @@ -64,6 +75,7 @@ def __init__(self, parent=None): self.parent = parent self.scene = Scene() + self.scene.view_elevation = 20 self.scene.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) @@ -108,22 +120,31 @@ def __init__(self, parent=None): self.scene.add(self.image_plane) + self.ghost_index = np.linspace(-2, 2, OrientationViewer.n_ghosts_per_perameter) + # self.ghost_index = np.linspace(-1, 1, OrientationViewer.n_ghosts_per_perameter) - for a in np.linspace(-1, 1, OrientationViewer.n_ghosts_per_perameter): + self.all_ghosts = [] + for a in self.ghost_index: b_ghosts = [] - for b in np.linspace(-1, 1, OrientationViewer.n_ghosts_per_perameter): + for b in self.ghost_index: c_ghosts = [] - for c in np.linspace(-1, 1, OrientationViewer.n_ghosts_per_perameter): - ghost = Cube(edge_colors=OrientationViewer.ghost_color) - + for c in self.ghost_index: + ghost = Rotation(0, 0, 0, 1, OrientationViewer.create_ghost()) + c_ghosts.append(ghost) + ghosts = Rotation(0,0,1,0, *c_ghosts) + b_ghosts.append(ghosts) + ghosts = Rotation(0,0,0,1,*b_ghosts) + self.all_ghosts.append(ghosts) self.first_rotation = Rotation(0,0,0,1, Scaling(OrientationViewer.a, OrientationViewer.b, OrientationViewer.c, - Cube(edge_colors=Color(1,1,1), colors=Color(0,1,0)) - )) + Cube( + edge_colors=OrientationViewer.ghost_color, + colors=OrientationViewer.cube_color)), + *self.all_ghosts) self.second_rotation = Rotation(0,0,1,0,self.first_rotation) self.third_rotation = Rotation(0,0,0,1,self.second_rotation) @@ -169,6 +190,14 @@ def _set_image_data(self, orientation: Orientation): self.scene.update() + def orient_ghosts(self, orientation: Orientation): + + for a, a_ghosts in zip(self.ghost_index, self.all_ghosts): + a_ghosts.angle = 0.5*a*orientation.dtheta + for b, b_ghosts in zip(self.ghost_index, a_ghosts.children): + b_ghosts.angle = 0.5*b*orientation.dphi + for c, c_ghosts in zip(self.ghost_index, b_ghosts.children): + c_ghosts.angle = 0.5*c*orientation.dpsi def on_angle_changed(self, orientation: Optional[Orientation]): @@ -183,14 +212,8 @@ def on_angle_changed(self, orientation: Optional[Orientation]): self.second_rotation.angle = orientation.theta self.third_rotation.angle = orientation.phi - # for a, b, c, ghost in self.ghosts: - # - # ghost.setTransform( - # OrientationViewerGraphics.createCubeTransform( - # orientation.theta + 0.5*a*orientation.dtheta, - # orientation.phi + 0.5*b*orientation.dphi, - # orientation.psi + 0.5*c*orientation.dpsi, - # OrientationViewer.cuboid_scaling)) + self.orient_ghosts(orientation) + self._set_image_data(orientation) @@ -209,14 +232,7 @@ def on_angle_changing(self, orientation: Optional[Orientation]): self.second_rotation.angle = orientation.theta self.third_rotation.angle = orientation.phi - # for a, b, c, ghost in self.ghosts: - # - # ghost.setTransform( - # OrientationViewerGraphics.createCubeTransform( - # orientation.theta + 0.5*a*orientation.dtheta, - # orientation.phi + 0.5*b*orientation.dphi, - # orientation.psi + 0.5*c*orientation.dpsi, - # OrientationViewer.cuboid_scaling)) + self.orient_ghosts(orientation) self.scene.update() From f2bdf4cc7de5a89596dcfc2d66928e554cf5ad61 Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Tue, 3 Jan 2023 11:35:33 +0000 Subject: [PATCH 63/87] Refactoring of color classes --- src/sas/qtgui/GL/color.py | 92 ++++++++---- src/sas/qtgui/GL/models.py | 131 +++++++----------- .../qtgui/Plotting/UnitTesting/AddTextTest.py | 2 +- .../OrientationViewer/OrientationViewer.py | 2 +- 4 files changed, 121 insertions(+), 106 deletions(-) diff --git a/src/sas/qtgui/GL/color.py b/src/sas/qtgui/GL/color.py index 61c68c1441..cc219e3938 100644 --- a/src/sas/qtgui/GL/color.py +++ b/src/sas/qtgui/GL/color.py @@ -1,10 +1,10 @@ -from typing import Sequence +from typing import Sequence, Union import logging - import numpy as np - import matplotlib as mpl +from enum import Enum +from dataclasses import dataclass from OpenGL.GL import glColor4f @@ -12,24 +12,68 @@ logger = logging.getLogger("GL.Color") -class Color(): - """ A basic color class that makes it easy to set a colour""" - def __init__(self, r: float, g: float, b: float, alpha: float=1.0): - self.r = float(r) - self.g = float(g) - self.b = float(b) - self.alpha = float(alpha) +class ColorSpecificationMethod(Enum): + """ Specifies how to colour an object""" + UNIFORM = 1 # Whole object a single colour + BY_COMPONENT = 2 # Each mesh or edge within the object a single colour + BY_VERTEX = 3 # Vertex colouring for the whole object + +@dataclass +class ColorSpecification: + """ Specification of how to colour an object, and the data needed to do so""" + method: ColorSpecificationMethod + data: np.ndarray + + +def uniform_coloring(r, g, b, alpha=1.0): + """ Create a ColorSpecification for colouring with a single colour""" + return ColorSpecification( + method=ColorSpecificationMethod.UNIFORM, + data=np.array([r, b, g, alpha])) + + +def edge_coloring(data: Sequence[Union[Sequence[float], np.ndarray]]) -> ColorSpecification: + """ Create a ColorSpecification for colouring each edge within an object a single colour""" + return _component_coloring(data) + - def set(self): - """ Set the GL draw color to this color""" - glColor4f(self.r, self.g, self.b, self.alpha) +def mesh_coloring(data: Sequence[Union[Sequence[float], np.ndarray]]) -> ColorSpecification: + """ Create a ColorSpecification for colouring each mesh within an object a single colour""" + return _component_coloring(data) - def to_array(self): - """ length 4 array of values, r,g,b,alpha""" - return np.array([self.r, self.g, self.b, self.alpha]) - def __repr__(self): - return f"{self.__class__.__name__}({self.r}, {self.g}, {self.b}, {self.alpha})" +def _component_coloring(data: Sequence[Union[Sequence[float], np.ndarray]]) -> ColorSpecification: + """ Create a ColorSpecification for colouring each mesh/edge within an object a single colour""" + try: + data = np.array(data) + except: + raise ValueError("Colour data should be all n-by-3 or n-by-4") + + if data.shape[1] == 3: + data = np.concatenate((data, np.ones(data.shape[0], 1))) + elif data.shape[1] == 4: + pass + else: + raise ValueError("Colour data should be all n-by-3 or n-by-4") + + return ColorSpecification(ColorSpecificationMethod.BY_COMPONENT, data) + + +def vertex_coloring(data: np.ndarray) -> ColorSpecification: + """ Create a ColorSpecification for using vertex colouring""" + try: + data = np.array(data) + except: + raise ValueError("Colour data should be all n-by-3 or n-by-4") + + if data.shape[1] == 3: + data = np.concatenate((data, np.ones(data.shape[0], 1))) + elif data.shape[1] == 4: + pass + else: + raise ValueError("Colour data should be all n-by-3 or n-by-4") + + return ColorSpecification(ColorSpecificationMethod.BY_VERTEX, data) class ColorMap(): @@ -46,12 +90,8 @@ def __init__(self, colormap_name=_default_colormap, min_value=0.0, max_value=1.0 self.min_value = min_value self.max_value = max_value - def color(self, value: float): - """ Current colormap at this value""" - scaled = (value - self.min_value) / (self.max_value - self.min_value) + def vertex_coloring(self, values: np.ndarray): + """ Evaluate the color map and return a ColorSpecification object""" + scaled = (values - self.min_value) / (self.max_value - self.min_value) scaled = np.clip(scaled, 0, 1) - return Color(*self.colormap(scaled)) - - def color_array(self, values: Sequence[float]) -> Sequence[Color]: - """ Array of the right form for working with Renderables""" - return [self.color(value) for value in values] + return vertex_coloring(self.colormap(scaled)) diff --git a/src/sas/qtgui/GL/models.py b/src/sas/qtgui/GL/models.py index 2866a0183f..fb7376189f 100644 --- a/src/sas/qtgui/GL/models.py +++ b/src/sas/qtgui/GL/models.py @@ -11,19 +11,17 @@ from OpenGL.GL import * from sas.qtgui.GL.renderable import Renderable -from sas.qtgui.GL.color import Color +from sas.qtgui.GL.color import ColorSpecification, ColorSpecificationMethod -def color_sequence_to_array(colors: Union[Sequence[Color], Color]): - if isinstance(colors, Color): - return None - else: - return np.array([color.to_array for color in colors], dtype=float) +VertexData = Union[Sequence[Tuple[float, float, float]], np.ndarray] +EdgeData = Union[Sequence[Tuple[int, int]], np.ndarray] +TriangleMeshData = Union[Sequence[Tuple[int, int, int]], np.ndarray] class ModelBase(Renderable): """ Base class for all models""" - def __init__(self, vertices: Sequence[Tuple[float, float, float]]): + def __init__(self, vertices: VertexData): self._vertices = vertices self._vertex_array = np.array(vertices, dtype=float) @@ -33,7 +31,7 @@ def vertices(self): return self._vertices @vertices.setter - def vertices(self, new_vertices): + def vertices(self, new_vertices: VertexData): self._vertices = new_vertices self._vertex_array = np.array(new_vertices, dtype=float) @@ -42,8 +40,8 @@ def vertices(self, new_vertices): class SolidModel(ModelBase): """ Base class for the solid models""" def __init__(self, - vertices: Sequence[Tuple[float, float, float]], - triangle_meshes: Sequence[Sequence[Tuple[int, int, int]]]): + vertices: VertexData, + triangle_meshes: Sequence[TriangleMeshData]): ModelBase.__init__(self, vertices) @@ -53,21 +51,20 @@ def __init__(self, self._triangle_mesh_arrays = [np.array(x) for x in triangle_meshes] @property - def triangle_meshes(self) -> Sequence[Sequence[Tuple[int, int, int]]]: + def triangle_meshes(self) -> Sequence[TriangleMeshData]: return self._triangle_meshes @triangle_meshes.setter - def triangle_meshes(self, new_triangle_meshes: Sequence[Sequence[int]]): + def triangle_meshes(self, new_triangle_meshes: Sequence[TriangleMeshData]): self._triangle_meshes = new_triangle_meshes self._triangle_mesh_arrays = [np.array(x) for x in new_triangle_meshes] class SolidVertexModel(SolidModel): def __init__(self, - vertices: Sequence[Tuple[float, float, float]], - triangle_meshes: Sequence[Sequence[Tuple[int, int, int]]], - colors: Optional[Union[Sequence[Color], Color]], - color_by_mesh: bool = False): + vertices: VertexData, + triangle_meshes: Sequence[TriangleMeshData], + colors: Optional[ColorSpecification]): """ @@ -83,83 +80,59 @@ def __init__(self, super().__init__(vertices, triangle_meshes) - self.color_by_mesh = color_by_mesh - - self._colors = colors - - if colors is None or isinstance(colors, Color) or color_by_mesh: - self._vertex_color_array = None - else: - self._vertex_color_array = np.array([color.to_array() for color in colors], dtype=float) - + self.colors = colors self.solid_render_enabled = self.colors is not None - @property - def colors(self): - return self._colors - - @colors.setter - def colors(self, new_colors): - self._colors = new_colors - if new_colors is None or isinstance(new_colors, Color) or self.color_by_mesh: - self._vertex_color_array = None - else: - self._vertex_color_array = np.array([color.to_array() for color in new_colors], dtype=float) - - self.solid_render_enabled = self.colors is not None def render_solid(self): if self.solid_render_enabled: - if isinstance(self._colors, Color): + if self.colors.method == ColorSpecificationMethod.UNIFORM: glEnableClientState(GL_VERTEX_ARRAY) - self._colors.set() + glColor4f(*self.colors.data) glVertexPointerf(self._vertex_array) for triangle_mesh in self._triangle_mesh_arrays: glDrawElementsui(GL_TRIANGLES, triangle_mesh) - glDisableClientState(GL_VERTEX_ARRAY) + elif self.colors.method == ColorSpecificationMethod.BY_COMPONENT: - else: - - if self.color_by_mesh: + glEnableClientState(GL_VERTEX_ARRAY) - glEnableClientState(GL_VERTEX_ARRAY) + glVertexPointerf(self._vertex_array) - glVertexPointerf(self._vertex_array) + for triangle_mesh, color in zip(self._triangle_mesh_arrays, self.colors.data): + glColor4f(*color) + glDrawElementsui(GL_TRIANGLES, triangle_mesh) - for triangle_mesh, color in zip(self._triangle_mesh_arrays, self.colors): - color.set() - glDrawElementsui(GL_TRIANGLES, triangle_mesh) + glDisableClientState(GL_VERTEX_ARRAY) - glDisableClientState(GL_VERTEX_ARRAY) + elif self.colors.method == ColorSpecificationMethod.BY_VERTEX: - else: - glEnableClientState(GL_VERTEX_ARRAY) - glEnableClientState(GL_COLOR_ARRAY) + glEnableClientState(GL_VERTEX_ARRAY) + glEnableClientState(GL_COLOR_ARRAY) - glVertexPointerf(self._vertex_array) - glColorPointerf(self._vertex_color_array) + glVertexPointerf(self._vertex_array) + glColorPointerf(self.colors.data) - for triangle_mesh in self._triangle_mesh_arrays: - glDrawElementsui(GL_TRIANGLES, triangle_mesh) + for triangle_mesh in self._triangle_mesh_arrays: + glDrawElementsui(GL_TRIANGLES, triangle_mesh) - glDisableClientState(GL_COLOR_ARRAY) - glDisableClientState(GL_VERTEX_ARRAY) + glDisableClientState(GL_COLOR_ARRAY) + glDisableClientState(GL_VERTEX_ARRAY) class WireModel(ModelBase): def __init__(self, - vertices: Sequence[Tuple[float, float, float]], - edges: Sequence[Tuple[int, int]], - edge_colors: Optional[Union[Sequence[Color], Color]]): + vertices: VertexData, + edges: EdgeData, + edge_colors: Optional[ColorSpecification]): """ Wireframe Model @@ -171,7 +144,7 @@ def __init__(self, super().__init__(vertices) - self.wireframe_render_enabled = False + self.wireframe_render_enabled = edge_colors is not None self.edges = edges self.edge_colors = edge_colors @@ -180,10 +153,10 @@ def render_wireframe(self): vertices = self.vertices colors = self.edge_colors - if isinstance(colors, Color): + if colors.method == ColorSpecificationMethod.UNIFORM: glBegin(GL_LINES) - colors.set() + glColor4f(*colors.data) for edge in self.edges: glVertex3f(*vertices[edge[0]]) @@ -191,18 +164,24 @@ def render_wireframe(self): glEnd() - else: # Assume here that the correct type is supplied, thus colors are a sequence + elif colors.method == ColorSpecificationMethod.BY_COMPONENT: glBegin(GL_LINES) - for edge, color in zip(self.edges, colors): + for edge, color in zip(self.edges, colors.data): - color.set() + glColor4f(*color) glVertex3f(*vertices[edge[0]]) glVertex3f(*vertices[edge[1]]) glEnd() + elif colors.method == ColorSpecificationMethod.BY_VERTEX: + raise NotImplementedError("Vertex coloring of wireframe is currently not supported") + + else: + raise ValueError(f"Unknown coloring method: {ColorSpecification.method}") + class FullModel(SolidVertexModel, WireModel): """ Model that has both wireframe and solid, vertex coloured rendering enabled, @@ -210,20 +189,16 @@ class FullModel(SolidVertexModel, WireModel): See SolidVertexModel and WireModel """ def __init__(self, - vertices: Sequence[Tuple[float, float, float]], - edges: Sequence[Tuple[int, int]], - triangle_meshes: Sequence[Sequence[Tuple[int, int, int]]], - edge_colors: Optional[Union[Sequence[Color], Color]], - colors: Optional[Union[Sequence[Color], Color]], - color_by_mesh: bool = False): - - + vertices: VertexData, + edges: EdgeData, + triangle_meshes: TriangleMeshData, + edge_colors: ColorSpecification, + colors: ColorSpecification): SolidVertexModel.__init__(self, vertices=vertices, triangle_meshes=triangle_meshes, - colors=colors, - color_by_mesh=color_by_mesh) + colors=colors) WireModel.__init__(self, vertices=vertices, edges=edges, diff --git a/src/sas/qtgui/Plotting/UnitTesting/AddTextTest.py b/src/sas/qtgui/Plotting/UnitTesting/AddTextTest.py index f39dc048e9..398e5be86c 100644 --- a/src/sas/qtgui/Plotting/UnitTesting/AddTextTest.py +++ b/src/sas/qtgui/Plotting/UnitTesting/AddTextTest.py @@ -49,6 +49,6 @@ def testOnColorChange(self, widget, mocker): # Call the method widget.onColorChange(None) # Check that the text field got the new color info for text - assert widget.textEdit.palette().color(QtGui.QPalette.Text) == new_color + assert widget.textEdit.palette().vertex_coloring(QtGui.QPalette.Text) == new_color # ... and the hex value of this color is correct assert widget.color() == "#ff0000" diff --git a/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py b/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py index 99d3c76c86..2ec0b5a920 100644 --- a/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py +++ b/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py @@ -307,7 +307,7 @@ def main(): mainWindow.show() mainWindow.resize(600, 600) - app.exec_() + # app.exec_() if __name__ == "__main__": From 3865c8c1d43aba74a0be74917041910cf3c1ea78 Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Tue, 3 Jan 2023 13:25:59 +0000 Subject: [PATCH 64/87] Fixed dependencies on colour class --- src/sas/qtgui/GL/color.py | 4 +- src/sas/qtgui/GL/cone.py | 8 ++-- src/sas/qtgui/GL/cube.py | 12 +++--- src/sas/qtgui/GL/cylinder.py | 10 ++--- src/sas/qtgui/GL/icosahedron.py | 6 +-- src/sas/qtgui/GL/models.py | 6 +-- src/sas/qtgui/GL/scene.py | 2 - src/sas/qtgui/GL/sphere.py | 6 +-- src/sas/qtgui/GL/surface.py | 22 +++++----- .../GL/visual_checks/primitive_library.py | 42 ++++++++++++------- .../GL/visual_checks/transform_library.py | 10 ++--- 11 files changed, 68 insertions(+), 60 deletions(-) diff --git a/src/sas/qtgui/GL/color.py b/src/sas/qtgui/GL/color.py index cc219e3938..5d65e322c1 100644 --- a/src/sas/qtgui/GL/color.py +++ b/src/sas/qtgui/GL/color.py @@ -50,7 +50,7 @@ def _component_coloring(data: Sequence[Union[Sequence[float], np.ndarray]]) -> C raise ValueError("Colour data should be all n-by-3 or n-by-4") if data.shape[1] == 3: - data = np.concatenate((data, np.ones(data.shape[0], 1))) + data = np.concatenate((data, np.ones((data.shape[0], 1))), axis=1) elif data.shape[1] == 4: pass else: @@ -67,7 +67,7 @@ def vertex_coloring(data: np.ndarray) -> ColorSpecification: raise ValueError("Colour data should be all n-by-3 or n-by-4") if data.shape[1] == 3: - data = np.concatenate((data, np.ones(data.shape[0], 1))) + data = np.concatenate((data, np.ones((data.shape[0], 1))), axis=1) elif data.shape[1] == 4: pass else: diff --git a/src/sas/qtgui/GL/cone.py b/src/sas/qtgui/GL/cone.py index 95475b198e..ba2793a8b7 100644 --- a/src/sas/qtgui/GL/cone.py +++ b/src/sas/qtgui/GL/cone.py @@ -2,8 +2,8 @@ import numpy as np -from sas.qtgui.GL.models import FullModel, WireModel -from sas.qtgui.GL.color import Color +from sas.qtgui.GL.models import FullModel +from sas.qtgui.GL.color import ColorSpecification class Cone(FullModel): @@ -39,8 +39,8 @@ def cone_triangles(n) -> List[List[Tuple[int, int, int]]]: def __init__(self, n: int = 20, - colors: Optional[Union[Sequence[Color], Color]]=None, - edge_colors: Optional[Union[Sequence[Color],Color]]=None): + colors: Optional[ColorSpecification]=None, + edge_colors: Optional[ColorSpecification]=None): super().__init__( vertices=Cone.cone_vertices(n), diff --git a/src/sas/qtgui/GL/cube.py b/src/sas/qtgui/GL/cube.py index d4c5a515fc..8ffbf7dab4 100644 --- a/src/sas/qtgui/GL/cube.py +++ b/src/sas/qtgui/GL/cube.py @@ -1,7 +1,7 @@ from typing import Optional, Union, Sequence -from sas.qtgui.GL.models import FullModel, WireModel -from sas.qtgui.GL.color import Color +from sas.qtgui.GL.models import FullModel +from sas.qtgui.GL.color import ColorSpecification class Cube(FullModel): @@ -49,17 +49,15 @@ class Cube(FullModel): ] def __init__(self, - colors: Optional[Union[Sequence[Color], Color]]=None, - edge_colors: Optional[Union[Sequence[Color],Color]]=None, - color_by_mesh: bool=False): + colors: Optional[ColorSpecification]=None, + edge_colors: Optional[ColorSpecification]=None): super().__init__( vertices=Cube.cube_vertices, edges=Cube.cube_edges, triangle_meshes=Cube.cube_triangles, edge_colors=edge_colors, - colors=colors, - color_by_mesh=color_by_mesh) + colors=colors) self.vertices = Cube.cube_vertices self.edges = Cube.cube_edges diff --git a/src/sas/qtgui/GL/cylinder.py b/src/sas/qtgui/GL/cylinder.py index 3173c06827..effd1258f5 100644 --- a/src/sas/qtgui/GL/cylinder.py +++ b/src/sas/qtgui/GL/cylinder.py @@ -1,9 +1,9 @@ -from typing import Optional, Union, Sequence, List, Tuple +from typing import Optional, List, Tuple import numpy as np -from sas.qtgui.GL.models import FullModel, WireModel -from sas.qtgui.GL.color import Color +from sas.qtgui.GL.models import FullModel +from sas.qtgui.GL.color import ColorSpecification class Cylinder(FullModel): @@ -54,8 +54,8 @@ def cylinder_triangles(n) -> List[List[Tuple[int, int, int]]]: def __init__(self, n: int = 20, - colors: Optional[Union[Sequence[Color], Color]]=None, - edge_colors: Optional[Union[Sequence[Color],Color]]=None): + colors: Optional[ColorSpecification]=None, + edge_colors: Optional[ColorSpecification]=None): super().__init__( vertices=Cylinder.cylinder_vertices(n), diff --git a/src/sas/qtgui/GL/icosahedron.py b/src/sas/qtgui/GL/icosahedron.py index f12f03ef6c..9eb40a5af3 100644 --- a/src/sas/qtgui/GL/icosahedron.py +++ b/src/sas/qtgui/GL/icosahedron.py @@ -2,7 +2,7 @@ import numpy as np -from sas.qtgui.GL.color import Color +from sas.qtgui.GL.color import ColorSpecification from sas.qtgui.GL.models import FullModel ico_ring_h = np.sqrt(1/5) @@ -77,8 +77,8 @@ class Icosahedron(FullModel): ]] def __init__(self, - colors: Optional[Union[Sequence[Color], Color]]=None, - edge_colors: Optional[Union[Sequence[Color],Color]]=None): + colors: Optional[ColorSpecification]=None, + edge_colors: Optional[ColorSpecification]=None): super().__init__( vertices=Icosahedron.ico_vertices, diff --git a/src/sas/qtgui/GL/models.py b/src/sas/qtgui/GL/models.py index fb7376189f..a7c54a13bf 100644 --- a/src/sas/qtgui/GL/models.py +++ b/src/sas/qtgui/GL/models.py @@ -191,9 +191,9 @@ class FullModel(SolidVertexModel, WireModel): def __init__(self, vertices: VertexData, edges: EdgeData, - triangle_meshes: TriangleMeshData, - edge_colors: ColorSpecification, - colors: ColorSpecification): + triangle_meshes: Sequence[TriangleMeshData], + edge_colors: Optional[ColorSpecification], + colors: Optional[ColorSpecification]): SolidVertexModel.__init__(self, vertices=vertices, diff --git a/src/sas/qtgui/GL/scene.py b/src/sas/qtgui/GL/scene.py index 054ddb6bc7..3c0c92c6d0 100644 --- a/src/sas/qtgui/GL/scene.py +++ b/src/sas/qtgui/GL/scene.py @@ -7,9 +7,7 @@ from OpenGL.GLU import * from sas.qtgui.GL.renderable import Renderable -from sas.qtgui.GL.color import Color from sas.qtgui.GL.surface import Surface -from sas.qtgui.GL.cone import Cone class Scene(QtOpenGL.QGLWidget): diff --git a/src/sas/qtgui/GL/sphere.py b/src/sas/qtgui/GL/sphere.py index b187d4f165..f01ae0c37a 100644 --- a/src/sas/qtgui/GL/sphere.py +++ b/src/sas/qtgui/GL/sphere.py @@ -3,7 +3,7 @@ import numpy as np from sas.qtgui.GL.models import FullModel -from sas.qtgui.GL.color import Color +from sas.qtgui.GL.color import ColorSpecification class Sphere(FullModel): @@ -68,8 +68,8 @@ def __init__(self, n_horizontal: int = 21, n_segments: int = 28, grid_gap: int = 1, - colors: Optional[Union[Sequence[Color], Color]]=None, - edge_colors: Optional[Union[Sequence[Color],Color]]=None): + colors: Optional[ColorSpecification]=None, + edge_colors: Optional[ColorSpecification]=None): """ diff --git a/src/sas/qtgui/GL/surface.py b/src/sas/qtgui/GL/surface.py index 70faa52765..d88b5678c5 100644 --- a/src/sas/qtgui/GL/surface.py +++ b/src/sas/qtgui/GL/surface.py @@ -4,7 +4,7 @@ import numpy as np from sas.qtgui.GL.models import FullModel -from sas.qtgui.GL.color import Color, ColorMap +from sas.qtgui.GL.color import ColorMap, uniform_coloring logger = logging.getLogger("GL.Surface") @@ -65,15 +65,18 @@ def __init__(self, self._colormap_name = colormap self._colormap = ColorMap(colormap, min_value=c_range[0], max_value=c_range[1]) - verts = [(float(x), float(y), float(z)) - for x, y, z in zip(np.nditer(self.x_data), np.nditer(self.y_data), np.nditer(self.z_data))] + self.x_flat = self.x_data.flatten() + self.y_flat = self.y_data.flatten() + self.z_flat = self.z_data.flatten() + + verts = np.vstack((self.x_flat, self.y_flat, self.z_flat)).T super().__init__( vertices=verts, edges=Surface.calculate_edge_indices(self.n_x, self.n_y, edge_skip), triangle_meshes=[Surface.calculate_triangles(self.n_x, self.n_y)], - edge_colors=Color(1.0,1.0,1.0), - colors=self._colormap.color_array([z for _, _, z in verts]) + edge_colors=uniform_coloring(1.0,1.0,1.0), + colors=self._colormap.vertex_coloring(self.z_flat) ) self.wireframe_render_enabled = True @@ -85,11 +88,10 @@ def set_z_data(self, z_data): "Set the z data on this surface plot" self.z_data = z_data - verts = [(float(x), float(y), float(z)) - for x, y, z in zip(np.nditer(self.x_data), np.nditer(self.y_data), np.nditer(self.z_data))] + self.z_flat = z_data.flatten() - self.vertices = verts - self.colors = self._colormap.color_array([z for _, _, z in verts]) + self.vertices = np.vstack((self.x_flat, self.y_flat, self.z_flat)).T + self.colors = self._colormap.vertex_coloring(self.z_flat) @property def colormap(self) -> str: @@ -100,5 +102,5 @@ def colormap(self) -> str: def colormap(self, colormap: str): if self._colormap_name != colormap: self._colormap = ColorMap(colormap, min_value=self.c_range[0], max_value=self.c_range[1]) - self.colors = self._colormap.color_array([z for _, _, z in self.vertices]) + self.colors = self._colormap.vertex_coloring(self.z_flat) self._colormap_name = colormap diff --git a/src/sas/qtgui/GL/visual_checks/primitive_library.py b/src/sas/qtgui/GL/visual_checks/primitive_library.py index bb119453e9..9230fdbe30 100644 --- a/src/sas/qtgui/GL/visual_checks/primitive_library.py +++ b/src/sas/qtgui/GL/visual_checks/primitive_library.py @@ -6,7 +6,7 @@ from sas.qtgui.GL.scene import Scene from sas.qtgui.GL.models import ModelBase -from sas.qtgui.GL.color import Color +from sas.qtgui.GL.color import uniform_coloring, mesh_coloring, vertex_coloring from sas.qtgui.GL.surface import Surface from sas.qtgui.GL.cone import Cone from sas.qtgui.GL.cube import Cube @@ -29,7 +29,14 @@ def mesh_example(): def primative_library(): """ Shows all the existing primitives that can be rendered, press a key to go through them""" - import os + import sys, os, traceback + def excepthook(exc_type, exc_value, exc_tb): + tb = "".join(traceback.format_exception(exc_type, exc_value, exc_tb)) + print("error catched!:") + print("error message:\n", tb) + QtWidgets.QApplication.quit() + + sys.excepthook = excepthook os.environ["QT_ENABLE_HIGHDPI_SCALING"] = "1" app = QtWidgets.QApplication([]) @@ -37,20 +44,20 @@ def primative_library(): item_list = [ mesh_example(), - Cube(edge_colors=Color(1, 1, 1), colors=Color(0.7, 0.2, 0)), - Cube(edge_colors=Color(1, 1, 1), colors=[ - Color(1,0,0), - Color(0,1,0), - Color(0,0,1), - Color(1,1,0), - Color(0,1,1), - Color(1,0,1) - ], color_by_mesh=True), - Cone(edge_colors=Color(1, 1, 1), colors=Color(0, 0.7, 0.2)), - Cylinder(edge_colors=Color(1, 1, 1), colors=Color(0, 0.2, 0.7)), - Icosahedron(edge_colors=Color(1, 1, 1), colors=Color(0.7, 0, 0.7)), - Sphere(edge_colors=Color(1, 1, 1), colors=Color(0.7, 0.7, 0.0)), - Sphere(edge_colors=Color(1, 1, 1), colors=Color(0.7, 0.4, 0.0), grid_gap=4) + Cube(edge_colors=uniform_coloring(1, 1, 1), colors=uniform_coloring(0.7, 0.2, 0)), + Cube(edge_colors=uniform_coloring(1, 1, 1), colors=mesh_coloring([ + (1,0,0), + (0,1,0), + (0,0,1), + (1,1,0), + (0,1,1), + (1,0,1) + ])), + Cone(edge_colors=uniform_coloring(1, 1, 1), colors=uniform_coloring(0, 0.7, 0.2)), + Cylinder(edge_colors=uniform_coloring(1, 1, 1), colors=uniform_coloring(0, 0.2, 0.7)), + Icosahedron(edge_colors=uniform_coloring(1, 1, 1), colors=uniform_coloring(0.7, 0, 0.7)), + Sphere(edge_colors=uniform_coloring(1, 1, 1), colors=uniform_coloring(0.7, 0.7, 0.0)), + Sphere(edge_colors=uniform_coloring(1, 1, 1), colors=uniform_coloring(0.7, 0.4, 0.0), grid_gap=4) ] # Turn off all of them @@ -109,6 +116,9 @@ def enable_disable(key): mainWindow.show() mainWindow.resize(600, 600) + + + app.exec_() diff --git a/src/sas/qtgui/GL/visual_checks/transform_library.py b/src/sas/qtgui/GL/visual_checks/transform_library.py index 10922b2c01..08eec04574 100644 --- a/src/sas/qtgui/GL/visual_checks/transform_library.py +++ b/src/sas/qtgui/GL/visual_checks/transform_library.py @@ -3,7 +3,7 @@ from PyQt5 import QtWidgets from sas.qtgui.GL.scene import Scene -from sas.qtgui.GL.color import Color +from sas.qtgui.GL.color import uniform_coloring from sas.qtgui.GL.cone import Cone from sas.qtgui.GL.cube import Cube from sas.qtgui.GL.cylinder import Cylinder @@ -24,10 +24,10 @@ def transform_tests(): os.environ["QT_ENABLE_HIGHDPI_SCALING"] = "1" app = QtWidgets.QApplication([]) - cube = Cube(edge_colors=Color(1, 1, 1), colors=Color(0.7, 0.2, 0)) - cone = Cone(edge_colors=Color(1, 1, 1), colors=Color(0, 0.7, 0.2)) - cylinder = Cylinder(edge_colors=Color(1, 1, 1), colors=Color(0, 0.2, 0.7)) - icos = Icosahedron(edge_colors=Color(1, 1, 1), colors=Color(0.7, 0, 0.7)) + cube = Cube(edge_colors=uniform_coloring(1, 1, 1), colors=uniform_coloring(0.7, 0.2, 0)) + cone = Cone(edge_colors=uniform_coloring(1, 1, 1), colors=uniform_coloring(0, 0.7, 0.2)) + cylinder = Cylinder(edge_colors=uniform_coloring(1, 1, 1), colors=uniform_coloring(0, 0.2, 0.7)) + icos = Icosahedron(edge_colors=uniform_coloring(1, 1, 1), colors=uniform_coloring(0.7, 0, 0.7)) # Translations From 63ea97af89b412ec50d865757b838272fbcde788 Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Tue, 3 Jan 2023 13:29:45 +0000 Subject: [PATCH 65/87] Imports in OrientationViewer --- src/sas/qtgui/GL/color.py | 2 +- .../Utilities/OrientationViewer/OrientationViewer.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/sas/qtgui/GL/color.py b/src/sas/qtgui/GL/color.py index 5d65e322c1..026a0c0cc3 100644 --- a/src/sas/qtgui/GL/color.py +++ b/src/sas/qtgui/GL/color.py @@ -29,7 +29,7 @@ def uniform_coloring(r, g, b, alpha=1.0): """ Create a ColorSpecification for colouring with a single colour""" return ColorSpecification( method=ColorSpecificationMethod.UNIFORM, - data=np.array([r, b, g, alpha])) + data=np.array([r, g, b, alpha])) def edge_coloring(data: Sequence[Union[Sequence[float], np.ndarray]]) -> ColorSpecification: diff --git a/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py b/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py index 2ec0b5a920..b7ae3ac8e7 100644 --- a/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py +++ b/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py @@ -12,13 +12,13 @@ from sasmodels.direct_model import DirectModel from sasmodels.jitter import Rx, Ry, Rz -from sas.qtgui.GL.color import Color from sas.qtgui.GL.scene import Scene from sas.qtgui.GL.transforms import Rotation, Scaling, Translation from sas.qtgui.GL.surface import Surface from sas.qtgui.GL.cylinder import Cylinder from sas.qtgui.GL.cone import Cone from sas.qtgui.GL.cube import Cube +from sas.qtgui.GL.color import uniform_coloring from sas.qtgui.Utilities.OrientationViewer.OrientationViewerController import OrientationViewierController, Orientation @@ -44,9 +44,9 @@ class OrientationViewer(QtWidgets.QWidget): c = 1.0 arrow_size = 0.2 - arrow_color = Color(0.9, 0.9, 0.9) - ghost_color = Color(0.0, 0.6, 0.2) - cube_color = Color(0.0, 0.8, 0.0) + arrow_color = uniform_coloring(0.9, 0.9, 0.9) + ghost_color = uniform_coloring(0.0, 0.6, 0.2) + cube_color = uniform_coloring(0.0, 0.8, 0.0) cuboid_scaling = [a, b, c] @@ -307,7 +307,7 @@ def main(): mainWindow.show() mainWindow.resize(600, 600) - # app.exec_() + app.exec_() if __name__ == "__main__": From 87e4fb2d339b97c3a52ee091e85c0f77caf3f03c Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Tue, 3 Jan 2023 13:33:02 +0000 Subject: [PATCH 66/87] Moved orientation viewer UI file --- .../Utilities/OrientationViewer/OrientationViewerController.py | 2 +- .../{ => OrientationViewer}/UI/OrientationViewerControllerUI.ui | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename src/sas/qtgui/Utilities/{ => OrientationViewer}/UI/OrientationViewerControllerUI.ui (100%) diff --git a/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewerController.py b/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewerController.py index e246db81ed..3c464b18a1 100644 --- a/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewerController.py +++ b/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewerController.py @@ -5,7 +5,7 @@ -from sas.qtgui.Utilities.UI.OrientationViewerControllerUI import Ui_OrientationViewierControllerUI +from sas.qtgui.Utilities.OrientationViewer.UI.OrientationViewerControllerUI import Ui_OrientationViewierControllerUI class Orientation(NamedTuple): """ Data sent when updating the plot""" diff --git a/src/sas/qtgui/Utilities/UI/OrientationViewerControllerUI.ui b/src/sas/qtgui/Utilities/OrientationViewer/UI/OrientationViewerControllerUI.ui similarity index 100% rename from src/sas/qtgui/Utilities/UI/OrientationViewerControllerUI.ui rename to src/sas/qtgui/Utilities/OrientationViewer/UI/OrientationViewerControllerUI.ui From 21029fca80592f77e43b48ca3375e567b2e706a3 Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Tue, 3 Jan 2023 14:08:56 +0000 Subject: [PATCH 67/87] Reenabled some stuff commented for profiling --- src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py b/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py index b7ae3ac8e7..2bf0a1d7de 100644 --- a/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py +++ b/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py @@ -256,7 +256,7 @@ def polydispersity_sample_count(self, orientation): is_polydisperse = [1 if x > 0 else 0 for x in polydispersity] n_polydisperse = np.sum(is_polydisperse) - samples = int(200 / (n_polydisperse**2.2 + 1)) # + samples = int(200 / (n_polydisperse**2 + 1)) # return (samples * x for x in is_polydisperse) From d71ce9768c3718d02915cbc6491cb2806c47aa04 Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Tue, 3 Jan 2023 14:09:50 +0000 Subject: [PATCH 68/87] Cleaned up viewer code --- .../OrientationViewer/OrientationViewer.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py b/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py index 2bf0a1d7de..14f12daa25 100644 --- a/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py +++ b/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py @@ -5,12 +5,9 @@ from PyQt5.QtWidgets import QSizePolicy from PyQt5.QtCore import Qt -import matplotlib as mpl - from sasmodels.core import load_model_info, build_model from sasmodels.data import empty_data2D from sasmodels.direct_model import DirectModel -from sasmodels.jitter import Rx, Ry, Rz from sas.qtgui.GL.scene import Scene from sas.qtgui.GL.transforms import Rotation, Scaling, Translation @@ -23,18 +20,6 @@ from sas.qtgui.Utilities.OrientationViewer.OrientationViewerController import OrientationViewierController, Orientation -def createCubeTransform(theta_deg: float, phi_deg: float, psi_deg: float, scaling: List[float]) -> np.ndarray: - # Get rotation matrix - r_mat = Rz(phi_deg) @ Ry(theta_deg) @ Rz(psi_deg) @ np.diag(scaling) - - # Get the 4x4 transformation matrix, by (1) padding by zeros (2) setting the corner element to 1 - trans_mat = np.pad(r_mat, ((0, 1), (0, 1))) - trans_mat[-1, -1] = 1 - trans_mat[2, -1] = 2 - - return trans_mat - - class OrientationViewer(QtWidgets.QWidget): """ Orientation viewer widget """ From cabf58e8c613d3ea39216beb066590febd2bbd80 Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Tue, 3 Jan 2023 14:19:41 +0000 Subject: [PATCH 69/87] Modified docs a little --- docs/sphinx-docs/source/dev/gl/opengl.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/sphinx-docs/source/dev/gl/opengl.rst b/docs/sphinx-docs/source/dev/gl/opengl.rst index 75dd900b77..010c6a25c1 100644 --- a/docs/sphinx-docs/source/dev/gl/opengl.rst +++ b/docs/sphinx-docs/source/dev/gl/opengl.rst @@ -7,6 +7,7 @@ Within the `visual_checks` directory there are a couple of stand-alone python fi a way of checking the rendering, and catalogue the available functions -Class Heirarchy +Class Hierarchy =============== +TODO - data currently in PR. \ No newline at end of file From be603efff97c81e6657691e216b929f73660d4fa Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Tue, 3 Jan 2023 15:00:30 +0000 Subject: [PATCH 70/87] Dynamic loading and fixed closing issue --- src/sas/qtgui/MainWindow/GuiManager.py | 7 +++---- .../OrientationViewer/OrientationViewer.py | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/sas/qtgui/MainWindow/GuiManager.py b/src/sas/qtgui/MainWindow/GuiManager.py index 1ff1beee4c..fa9b04afd0 100644 --- a/src/sas/qtgui/MainWindow/GuiManager.py +++ b/src/sas/qtgui/MainWindow/GuiManager.py @@ -31,7 +31,7 @@ from sas.qtgui.Utilities.PluginManager import PluginManager from sas.qtgui.Utilities.GridPanel import BatchOutputPanel from sas.qtgui.Utilities.ResultPanel import ResultPanel -from sas.qtgui.Utilities.OrientationViewer.OrientationViewer import OrientationViewer +from sas.qtgui.Utilities.OrientationViewer.OrientationViewer import show_orientation_viewer from sas.qtgui.Utilities.HidableDialog import hidable_dialog from sas.qtgui.Utilities.Reports.ReportDialog import ReportDialog @@ -194,8 +194,6 @@ def addWidgets(self): self.DataOperation = DataOperationUtilityPanel(self) self.FileConverter = FileConverterWidget(self) - self.orientation_viewer = OrientationViewer() - def loadAllPerspectives(self): # Close any existing perspectives to prevent multiple open instances self.closeAllPerspectives() @@ -729,6 +727,7 @@ def addTriggers(self): self.communicate.sendDataToGridSignal.connect(self.showBatchOutput) self.communicate.resultPlotUpdateSignal.connect(self.showFitResults) + #============ FILE ================= def actionLoadData(self): """ @@ -1019,7 +1018,7 @@ def actionOrientation_Viewer(self): """ Make sasmodels orientation & jitter viewer available """ - self.orientation_viewer.show() + show_orientation_viewer() def actionImage_Viewer(self): """ diff --git a/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py b/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py index 14f12daa25..87c2a54e40 100644 --- a/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py +++ b/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py @@ -271,6 +271,24 @@ def scatering_data(self, orientation: Orientation) -> np.ndarray: return np.reshape(data, (OrientationViewer.n_q_samples, OrientationViewer.n_q_samples)) + def closeEvent(self, event): + try: + _orientation_viewers.remove(self) + except ValueError: # Not in list + pass + + event.accept() + + +# Code for handling multiple orientation viewers +_orientation_viewers = [] +def show_orientation_viewer(): + ov = OrientationViewer() + ov.show() + ov.resize(600, 600) + + _orientation_viewers.append(ov) + def main(): From bd52cbb2bbf25c1eadd5874a6fd751746d055f2a Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Tue, 3 Jan 2023 15:03:46 +0000 Subject: [PATCH 71/87] Window title --- .../Utilities/OrientationViewer/OrientationViewer.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py b/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py index 87c2a54e40..1925080757 100644 --- a/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py +++ b/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py @@ -23,6 +23,8 @@ class OrientationViewer(QtWidgets.QWidget): """ Orientation viewer widget """ + + # Dimensions of scattering cuboid a = 0.1 b = 0.4 @@ -55,10 +57,12 @@ def create_ghost(): def __init__(self, parent=None): super().__init__() - self._colormap_name = 'viridis' - self.parent = parent + self.setWindowTitle("Orientation Viewer") + + self._colormap_name = 'viridis' + self.scene = Scene() self.scene.view_elevation = 20 From d1aac40f7297293fe99e9a41ef8110e9e4b122ca Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Tue, 3 Jan 2023 15:52:13 +0000 Subject: [PATCH 72/87] Fixed ghost orientation, normally distruted them --- .../OrientationViewer/OrientationViewer.py | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py b/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py index 1925080757..082b9b4793 100644 --- a/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py +++ b/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py @@ -1,5 +1,7 @@ from typing import Optional, List + import numpy as np +from scipy.special import erfinv from PyQt5 import QtWidgets from PyQt5.QtWidgets import QSizePolicy @@ -46,6 +48,8 @@ class OrientationViewer(QtWidgets.QWidget): log_I_range = log_I_max - log_I_min + + @staticmethod def create_ghost(): """ Helper function: Create a ghost cube""" @@ -109,20 +113,19 @@ def __init__(self, parent=None): self.scene.add(self.image_plane) - self.ghost_index = np.linspace(-2, 2, OrientationViewer.n_ghosts_per_perameter) - # self.ghost_index = np.linspace(-1, 1, OrientationViewer.n_ghosts_per_perameter) + self.ghost_spacings = erfinv(np.linspace(-1, 1, OrientationViewer.n_ghosts_per_perameter+2)[1:-1])/np.sqrt(2) self.all_ghosts = [] - for a in self.ghost_index: + for a in self.ghost_spacings: b_ghosts = [] - for b in self.ghost_index: + for b in self.ghost_spacings: c_ghosts = [] - for c in self.ghost_index: + for c in self.ghost_spacings: ghost = Rotation(0, 0, 0, 1, OrientationViewer.create_ghost()) c_ghosts.append(ghost) ghosts = Rotation(0,0,1,0, *c_ghosts) b_ghosts.append(ghosts) - ghosts = Rotation(0,0,0,1,*b_ghosts) + ghosts = Rotation(0,1,0,0,*b_ghosts) self.all_ghosts.append(ghosts) @@ -181,12 +184,12 @@ def _set_image_data(self, orientation: Orientation): def orient_ghosts(self, orientation: Orientation): - for a, a_ghosts in zip(self.ghost_index, self.all_ghosts): - a_ghosts.angle = 0.5*a*orientation.dtheta - for b, b_ghosts in zip(self.ghost_index, a_ghosts.children): - b_ghosts.angle = 0.5*b*orientation.dphi - for c, c_ghosts in zip(self.ghost_index, b_ghosts.children): - c_ghosts.angle = 0.5*c*orientation.dpsi + for a, a_ghosts in zip(self.ghost_spacings, self.all_ghosts): + a_ghosts.angle = a*orientation.dtheta + for b, b_ghosts in zip(self.ghost_spacings, a_ghosts.children): + b_ghosts.angle = b*orientation.dphi + for c, c_ghosts in zip(self.ghost_spacings, b_ghosts.children): + c_ghosts.angle = c*orientation.dpsi def on_angle_changed(self, orientation: Optional[Orientation]): From f7afcf47737b8aabb7f8e50980a6822fadb2d9c3 Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Fri, 6 Jan 2023 15:03:27 +0000 Subject: [PATCH 73/87] Axis fix --- .../qtgui/Utilities/OrientationViewer/OrientationViewer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py b/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py index 082b9b4793..00290f5965 100644 --- a/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py +++ b/src/sas/qtgui/Utilities/OrientationViewer/OrientationViewer.py @@ -185,9 +185,9 @@ def _set_image_data(self, orientation: Orientation): def orient_ghosts(self, orientation: Orientation): for a, a_ghosts in zip(self.ghost_spacings, self.all_ghosts): - a_ghosts.angle = a*orientation.dtheta + a_ghosts.angle = a*orientation.dphi for b, b_ghosts in zip(self.ghost_spacings, a_ghosts.children): - b_ghosts.angle = b*orientation.dphi + b_ghosts.angle = b*orientation.dtheta for c, c_ghosts in zip(self.ghost_spacings, b_ghosts.children): c_ghosts.angle = c*orientation.dpsi From 3ee5171c3eac20a94003e0bd78b6bc52724056ff Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Mon, 16 Jan 2023 11:20:40 +0000 Subject: [PATCH 74/87] OrientationViewer.UI on setup path --- setup.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/setup.py b/setup.py index a62fd1c046..13b6ef9189 100644 --- a/setup.py +++ b/setup.py @@ -204,6 +204,9 @@ def run(self): package_dir["sas.qtgui.Utilities.OrientationViewer"] = os.path.join( "src", "sas", "qtgui", "Utilities", "OrientationViewer") packages.append("sas.qtgui.Utilities.OrientationViewer") +package_dir["sas.qtgui.Utilities.OrientationViewer.UI"] = os.path.join( + "src", "sas", "qtgui", "Utilities", "OrientationViewer", "UI") +packages.append("sas.qtgui.Utilities.OrientationViewer.UI") package_dir["sas.qtgui.Calculators"] = os.path.join( From c7d320308014ed878861999ef15dd3f62fb18cec Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Thu, 19 Jan 2023 11:58:05 +0000 Subject: [PATCH 75/87] Added pyopengl package path in .spec file --- installers/sasview.spec | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/installers/sasview.spec b/installers/sasview.spec index 8c9f770654..6be5152b01 100644 --- a/installers/sasview.spec +++ b/installers/sasview.spec @@ -20,8 +20,9 @@ datas = [ ('../docs/sphinx-docs/build/html','doc') ] #TODO: Hopefully we can get away from version specific packages -datas.append((os.path.join(PYTHON_PACKAGES, 'jedi'), 'jedi')) datas.append((os.path.join(PYTHON_PACKAGES, 'debugpy'), 'debugpy')) +datas.append((os.path.join(PYTHON_PACKAGES, 'jedi'), 'jedi')) +datas.append((os.path.join(PYTHON_PACKAGES, 'pyopengl', 'pyopengl'))) datas.append((os.path.join(PYTHON_PACKAGES, 'zmq'), 'zmq')) def add_data(data): From c18982747347996ccaba1d7a96aaf0e0eff9cfa0 Mon Sep 17 00:00:00 2001 From: Wojciech Potrzebowski Date: Thu, 19 Jan 2023 13:23:14 +0100 Subject: [PATCH 76/87] adding pyopengl_accelerate module --- .github/workflows/ci.yml | 2 +- build_tools/requirements.txt | 1 + installers/sasview.spec | 1 - 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9eb39840d1..63c2c7634c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -89,7 +89,7 @@ jobs: python -m pip install tinycc h5py pyparsing html5lib reportlab==3.6.6 pybind11 appdirs python -m pip install six numba mako ipython qtconsole xhtml2pdf pylint debugpy python -m pip install qt5reactor periodictable uncertainties dominate importlib_resources - python -m pip install html2text superqt pyopengl + python -m pip install html2text superqt pyopengl pyopengl_accelerate - name: Install PyQt (Windows + Linux) if: ${{ !startsWith(matrix.os, 'macos') }} diff --git a/build_tools/requirements.txt b/build_tools/requirements.txt index a460b76874..ce079c7497 100644 --- a/build_tools/requirements.txt +++ b/build_tools/requirements.txt @@ -36,3 +36,4 @@ html2text jsonschema superqt pyopengl +pyopengl_accelerate diff --git a/installers/sasview.spec b/installers/sasview.spec index 6be5152b01..7f7c43a0d0 100644 --- a/installers/sasview.spec +++ b/installers/sasview.spec @@ -22,7 +22,6 @@ datas = [ #TODO: Hopefully we can get away from version specific packages datas.append((os.path.join(PYTHON_PACKAGES, 'debugpy'), 'debugpy')) datas.append((os.path.join(PYTHON_PACKAGES, 'jedi'), 'jedi')) -datas.append((os.path.join(PYTHON_PACKAGES, 'pyopengl', 'pyopengl'))) datas.append((os.path.join(PYTHON_PACKAGES, 'zmq'), 'zmq')) def add_data(data): From fc9f461986dadd77b73656ff0c3be230aac46b64 Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Thu, 19 Jan 2023 13:49:50 +0000 Subject: [PATCH 77/87] typo fix --- installers/sasview.spec | 1 + 1 file changed, 1 insertion(+) diff --git a/installers/sasview.spec b/installers/sasview.spec index 7f7c43a0d0..4f922edd4e 100644 --- a/installers/sasview.spec +++ b/installers/sasview.spec @@ -21,6 +21,7 @@ datas = [ ] #TODO: Hopefully we can get away from version specific packages datas.append((os.path.join(PYTHON_PACKAGES, 'debugpy'), 'debugpy')) +datas.append((os.path.join(PYTHON_PACKAGES, 'pyopengl'), 'pyopengl')) datas.append((os.path.join(PYTHON_PACKAGES, 'jedi'), 'jedi')) datas.append((os.path.join(PYTHON_PACKAGES, 'zmq'), 'zmq')) From 71734447dece48ef0869eec5b913bb0b9e78d01e Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Thu, 19 Jan 2023 14:34:11 +0000 Subject: [PATCH 78/87] Removed pyopengl from datas --- installers/sasview.spec | 1 - 1 file changed, 1 deletion(-) diff --git a/installers/sasview.spec b/installers/sasview.spec index 4f922edd4e..7f7c43a0d0 100644 --- a/installers/sasview.spec +++ b/installers/sasview.spec @@ -21,7 +21,6 @@ datas = [ ] #TODO: Hopefully we can get away from version specific packages datas.append((os.path.join(PYTHON_PACKAGES, 'debugpy'), 'debugpy')) -datas.append((os.path.join(PYTHON_PACKAGES, 'pyopengl'), 'pyopengl')) datas.append((os.path.join(PYTHON_PACKAGES, 'jedi'), 'jedi')) datas.append((os.path.join(PYTHON_PACKAGES, 'zmq'), 'zmq')) From 49be2f9231aea1ef78ce6d7d80b96f48914e4dfa Mon Sep 17 00:00:00 2001 From: Wojciech Potrzebowski Date: Thu, 26 Jan 2023 06:05:36 +0100 Subject: [PATCH 79/87] Allowing signing of OSX bundle --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 63c2c7634c..31f3bb53e7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -267,7 +267,7 @@ jobs: if-no-files-found: error - name: Sign executable and create dmg (OSX) - if: ${{ env.RELEASE == 'true' && matrix.installer && startsWith(matrix.os, 'macos') }} + if: ${{ matrix.installer && startsWith(matrix.os, 'macos') }} env: MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }} MACOS_CERTIFICATE_PWD: ${{ secrets.MACOS_CERTIFICATE_PWD }} From caca6f9ad468690fd986003222c9b5992cb24bee Mon Sep 17 00:00:00 2001 From: Wojciech Potrzebowski Date: Thu, 26 Jan 2023 14:00:27 +0100 Subject: [PATCH 80/87] Relaxing PyQT version requirement for Mac (proper fix will come if it helps) --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 31f3bb53e7..985a8d28d0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -99,7 +99,7 @@ jobs: - name: Install lxml, matplotlib and PyQt (OSX) if: ${{ startsWith(matrix.os, 'macos') }} run: | - python -m pip install PyQt5==5.13 + python -m pip install PyQt5 python -m pip install matplotlib~=3.5.2 python -m pip install --no-binary lxml lxml From b26addf3a0d32e0082afffaa06d877a6df9c3fa7 Mon Sep 17 00:00:00 2001 From: Wojciech Potrzebowski Date: Thu, 26 Jan 2023 15:23:55 +0100 Subject: [PATCH 81/87] Reverting PyQt5 version to 5.13 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 985a8d28d0..31f3bb53e7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -99,7 +99,7 @@ jobs: - name: Install lxml, matplotlib and PyQt (OSX) if: ${{ startsWith(matrix.os, 'macos') }} run: | - python -m pip install PyQt5 + python -m pip install PyQt5==5.13 python -m pip install matplotlib~=3.5.2 python -m pip install --no-binary lxml lxml From 0e3a8af7c8b1ddbef3afafbacac4a68b1fcc5696 Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Mon, 30 Jan 2023 11:18:57 +0000 Subject: [PATCH 82/87] Use latest version of pyinstaller --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 559d23dcf1..76e6f3a025 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -213,7 +213,7 @@ jobs: - name: Install utilities to build installer if: ${{ matrix.installer }} run: | - python -m pip install pyinstaller + python -m pip install pyinstaller==5.7.0 - name: Build sasview with pyinstaller if: ${{ matrix.installer }} From 020e71c7edbbde53b43cae3a317ce73baf47a7bb Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Mon, 30 Jan 2023 13:13:15 +0000 Subject: [PATCH 83/87] Potential fix --- .github/workflows/ci.yml | 1 + .../fix_qt_folder_names_for_codesign.py | 120 ++++++++++++++++++ 2 files changed, 121 insertions(+) create mode 100644 build_tools/fix_qt_folder_names_for_codesign.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 76e6f3a025..17c7b1750f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -281,6 +281,7 @@ jobs: security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k DloaAcYP build.keychain cd installers/dist + python ../../build_tools/fix_qt_folder_names_for_codesign.py `pwd` python ../../build_tools/code_sign_osx.py codesign --verify --options=runtime --entitlements ../../build_tools/entitlements.plist --timestamp --deep --verbose=4 --force --sign "Developer ID Application: European Spallation Source Eric (W2AG9MPZ43)" SasView5.app hdiutil create SasView5.dmg -srcfolder SasView5.app -ov -format UDZO diff --git a/build_tools/fix_qt_folder_names_for_codesign.py b/build_tools/fix_qt_folder_names_for_codesign.py new file mode 100644 index 0000000000..75cac1f449 --- /dev/null +++ b/build_tools/fix_qt_folder_names_for_codesign.py @@ -0,0 +1,120 @@ +# -*- coding: utf-8 -*- +import os +import shutil +import sys +from pathlib import Path +from typing import Generator, List, Optional + +from macholib.MachO import MachO + + +def create_symlink(folder: Path) -> None: + """Create the appropriate symlink in the MacOS folder + pointing to the Resources folder. + """ + sibbling = Path(str(folder).replace("MacOS", "")) + + # PyQt5/Qt/qml/QtQml/Models.2 + root = str(sibbling).partition("Contents")[2].lstrip("/") + # ../../../../ + backward = "../" * (root.count("/") + 1) + # ../../../../Resources/PyQt5/Qt/qml/QtQml/Models.2 + good_path = f"{backward}Resources/{root}" + + folder.symlink_to(good_path) + + +def fix_dll(dll: Path) -> None: + """Fix the DLL lookup paths to use relative ones for Qt dependencies. + Inspiration: PyInstaller/depend/dylib.py:mac_set_relative_dylib_deps() + Currently one header is pointing to (we are in the Resources folder): + @loader_path/../../../../QtCore (it is referencing to the old MacOS folder) + It will be converted to: + @loader_path/../../../../../../MacOS/QtCore + """ + + def match_func(pth: str) -> Optional[str]: + """Callback function for MachO.rewriteLoadCommands() that is + called on every lookup path setted in the DLL headers. + By returning None for system libraries, it changes nothing. + Else we return a relative path pointing to the good file + in the MacOS folder. + """ + basename = os.path.basename(pth) + if not basename.startswith("Qt"): + return None + return f"@loader_path{good_path}/{basename}" + + # Resources/PyQt5/Qt/qml/QtQuick/Controls.2/Fusion + root = str(dll.parent).partition("Contents")[2][1:] + # /../../../../../../.. + backward = "/.." * (root.count("/") + 1) + # /../../../../../../../MacOS + good_path = f"{backward}/MacOS" + + # Rewrite Mach headers with corrected @loader_path + dll = MachO(dll) + dll.rewriteLoadCommands(match_func) + with open(dll.filename, "rb+") as f: + for header in dll.headers: + f.seek(0) + dll.write(f) + f.seek(0, 2) + f.flush() + + +def find_problematic_folders(folder: Path) -> Generator[Path, None, None]: + """Recursively yields problematic folders (containing a dot in their name).""" + for path in folder.iterdir(): + if not path.is_dir() or path.is_symlink(): + # Skip simlinks as they are allowed (even with a dot) + continue + if "." in path.name: + yield path + else: + yield from find_problematic_folders(path) + + +def move_contents_to_resources(folder: Path) -> Generator[Path, None, None]: + """Recursively move any non symlink file from a problematic folder + to the sibbling one in Resources. + """ + for path in folder.iterdir(): + if path.is_symlink(): + continue + if path.name == "qml": + yield from move_contents_to_resources(path) + else: + sibbling = Path(str(path).replace("MacOS", "Resources")) + sibbling.parent.mkdir(parents=True, exist_ok=True) + shutil.move(path, sibbling) + yield sibbling + + +def main(args: List[str]) -> int: + """ + Fix the application to allow codesign (NXDRIVE-1301). + Take one or more .app as arguments: "Nuxeo Drive.app". + To overall process will: + - move problematic folders from MacOS to Resources + - fix the DLLs lookup paths + - create the appropriate symbolic link + """ + for app in args: + name = os.path.basename(app) + print(f">>> [{name}] Fixing Qt folder names") + path = Path(app) / "Contents" / "MacOS" + for folder in find_problematic_folders(path): + for file in move_contents_to_resources(folder): + try: + fix_dll(file) + except (ValueError, IsADirectoryError): + continue + shutil.rmtree(folder) + create_symlink(folder) + print(f" !! Fixed {folder}") + print(f">>> [{name}] Application fixed.") + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) \ No newline at end of file From 564b5f54b1904b1afc247e2707c7e245aee0bc05 Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Mon, 30 Jan 2023 13:58:40 +0000 Subject: [PATCH 84/87] Was it just the wrong directory? --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 17c7b1750f..ecbc7e05cb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -281,7 +281,7 @@ jobs: security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k DloaAcYP build.keychain cd installers/dist - python ../../build_tools/fix_qt_folder_names_for_codesign.py `pwd` + python ../../build_tools/fix_qt_folder_names_for_codesign.py python ../../build_tools/code_sign_osx.py codesign --verify --options=runtime --entitlements ../../build_tools/entitlements.plist --timestamp --deep --verbose=4 --force --sign "Developer ID Application: European Spallation Source Eric (W2AG9MPZ43)" SasView5.app hdiutil create SasView5.dmg -srcfolder SasView5.app -ov -format UDZO From 547626a454f4a12cb5259500e9317383d99d7d9a Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Mon, 30 Jan 2023 14:33:18 +0000 Subject: [PATCH 85/87] Arguments --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ecbc7e05cb..d9abea4529 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -281,7 +281,7 @@ jobs: security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k DloaAcYP build.keychain cd installers/dist - python ../../build_tools/fix_qt_folder_names_for_codesign.py + python ../../build_tools/fix_qt_folder_names_for_codesign.py SasView5.app python ../../build_tools/code_sign_osx.py codesign --verify --options=runtime --entitlements ../../build_tools/entitlements.plist --timestamp --deep --verbose=4 --force --sign "Developer ID Application: European Spallation Source Eric (W2AG9MPZ43)" SasView5.app hdiutil create SasView5.dmg -srcfolder SasView5.app -ov -format UDZO From 77368080fbb4cf8821dcb81706e5ee05eee4c83f Mon Sep 17 00:00:00 2001 From: Wojciech Potrzebowski Date: Wed, 1 Feb 2023 15:25:32 +0100 Subject: [PATCH 86/87] Update ci.yml --- .github/workflows/ci.yml | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 596a2d0fad..86335aaecc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -233,17 +233,6 @@ jobs: installers/dist/sasview-pyinstaller-dist.tar.gz if-no-files-found: ignore - - name: Publish installer package - if: ${{ matrix.installer }} - uses: actions/upload-artifact@v3 - with: - name: SasView-Installer-${{ matrix.os }}-${{ matrix.python-version }} - path: | - installers/dist/setupSasView.exe - installers/dist/SasView5.dmg - installers/dist/sasview5.tar.gz - if-no-files-found: error - - name: Sign executable and create dmg (OSX) if: ${{ matrix.installer && startsWith(matrix.os, 'macos') }} env: @@ -263,6 +252,18 @@ jobs: codesign --verify --options=runtime --entitlements ../../build_tools/entitlements.plist --timestamp --deep --verbose=4 --force --sign "Developer ID Application: European Spallation Source Eric (W2AG9MPZ43)" SasView5.app hdiutil create SasView5.dmg -srcfolder SasView5.app -ov -format UDZO codesign -s "Developer ID Application: European Spallation Source Eric (W2AG9MPZ43)" SasView5.dmg + + - name: Publish installer package + if: ${{ matrix.installer }} + uses: actions/upload-artifact@v3 + with: + name: SasView-Installer-${{ matrix.os }}-${{ matrix.python-version }} + path: | + installers/dist/setupSasView.exe + installers/dist/SasView5.dmg + installers/dist/sasview5.tar.gz + if-no-files-found: error + # - name: Notarize Release Build (OSX) # if: ${{ env.RELEASE == 'true' && matrix.installer && startsWith(matrix.os, 'macos') }} From ab898a393235d07e1c6f58c9e37acf8c48d45671 Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Thu, 2 Feb 2023 12:22:45 +0000 Subject: [PATCH 87/87] Added __init__.py to OrientationViewer/UI --- src/sas/qtgui/Utilities/OrientationViewer/UI/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/sas/qtgui/Utilities/OrientationViewer/UI/__init__.py diff --git a/src/sas/qtgui/Utilities/OrientationViewer/UI/__init__.py b/src/sas/qtgui/Utilities/OrientationViewer/UI/__init__.py new file mode 100644 index 0000000000..e69de29bb2