Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

1763 implement the orientation viewer in 5x and GL Subsystem #2394

Merged
merged 101 commits into from
Feb 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
101 commits
Select commit Hold shift + click to select a range
b4f6201
Activated orientation viewer - currently calls external sasmodels window
lucas-wilkins Sep 28, 2022
c985d92
Orientation viewer stuff
lucas-wilkins Oct 10, 2022
5188623
Viewer .UI file
lucas-wilkins Oct 10, 2022
5d30f62
Qt stuff
lucas-wilkins Oct 11, 2022
d777fe2
Some progress, some issues Qt designer remain
lucas-wilkins Oct 13, 2022
8871c24
Merge branch 'main' into 1763-implement-the-orientation-viewer-in-5x
lucas-wilkins Oct 25, 2022
13702c0
Merge branch 'main' into 1763-implement-the-orientation-viewer-in-5x
lucas-wilkins Oct 25, 2022
c4a5618
Basics of the orientation viewer
lucas-wilkins Oct 25, 2022
8ad4d17
Test window
lucas-wilkins Oct 25, 2022
1f7f561
Added dependencies to requirements.txt
lucas-wilkins Oct 25, 2022
6fe330b
Basic layout
lucas-wilkins Oct 25, 2022
7b36954
Nicer cuboid
lucas-wilkins Oct 25, 2022
00f0941
3D elements, more gui
lucas-wilkins Oct 26, 2022
0c2e632
Merge branch 'high-dpi-madness' into 1763-implement-the-orientation-v…
lucas-wilkins Oct 26, 2022
17f65db
Work on ghosts
lucas-wilkins Oct 26, 2022
a4941c7
ughh
lucas-wilkins Oct 28, 2022
c3deb4b
Moved some of the old viewer over
lucas-wilkins Oct 29, 2022
c35694c
Basic OV done, just needs the correct cuboid coordinates and wiring up
lucas-wilkins Oct 29, 2022
9f01b71
queue bugfix
lucas-wilkins Oct 29, 2022
7c7e2b9
Label values
lucas-wilkins Oct 29, 2022
1b23d59
Translate everything up a bit
lucas-wilkins Oct 29, 2022
539de64
Updated slider appearance
lucas-wilkins Oct 29, 2022
8cd2971
Wired in OV window
lucas-wilkins Oct 29, 2022
7b5d56b
Merge branch 'main' into 1763-implement-the-orientation-viewer-in-5x
lucas-wilkins Oct 29, 2022
ed84cd5
Lets just make a gooder gl widget
lucas-wilkins Oct 30, 2022
497396a
Added some meat
lucas-wilkins Oct 30, 2022
1144a7d
More progress towards GL
lucas-wilkins Oct 31, 2022
57d2620
This is why working with opengl sucks
lucas-wilkins Oct 31, 2022
681d7e3
Mouse control kind of works
lucas-wilkins Oct 31, 2022
464623b
Minor changes
lucas-wilkins Nov 7, 2022
e4f5f46
GL rendering structure
lucas-wilkins Nov 8, 2022
3a2594d
Cube wireframe now vaguely functional
lucas-wilkins Nov 9, 2022
52143c9
Filled in cube
lucas-wilkins Nov 11, 2022
5de8eb9
Merge branch 'main' into 1763-implement-the-orientation-viewer-in-5x
lucas-wilkins Nov 11, 2022
f4ef7f3
New widget functioning in main GUI
lucas-wilkins Nov 11, 2022
fa5824d
Better mouse response
lucas-wilkins Nov 11, 2022
1a3c980
Rotations for the graph widget work, still need to set viewport
lucas-wilkins Nov 21, 2022
869c156
Viewport is correct
lucas-wilkins Nov 21, 2022
ab9a918
Work on surface plot
lucas-wilkins Nov 21, 2022
002bf63
Minor change
lucas-wilkins Nov 22, 2022
0e3031f
Wireframe surface works
lucas-wilkins Nov 22, 2022
1c05fc8
Scrolling working, solid meshes
lucas-wilkins Nov 23, 2022
10317f6
Merge branch 'main' into 1763-implement-the-orientation-viewer-in-5x
lucas-wilkins Nov 23, 2022
592a0a1
Surface looks good
lucas-wilkins Nov 24, 2022
78a09b4
Cone primative, refactored some things into a library for checking over
lucas-wilkins Nov 24, 2022
111fb08
Cylinder almost ready
lucas-wilkins Nov 24, 2022
96f5811
Cylinder works
lucas-wilkins Nov 24, 2022
2494d97
added icosahedron, and renamed primitives to lower case
lucas-wilkins Nov 24, 2022
7fe4449
moved ico
lucas-wilkins Nov 24, 2022
1555477
UV Sphere
lucas-wilkins Nov 24, 2022
89ea456
Filling in and refactoring transforms
lucas-wilkins Nov 24, 2022
132e916
minor change
lucas-wilkins Nov 24, 2022
010f84c
Merge branch 'main' into 1763-implement-the-orientation-viewer-in-5x
lucas-wilkins Nov 24, 2022
ef62f4b
translate test
lucas-wilkins Nov 24, 2022
412329e
Fixed nasty GL bug around matrix stack size limits
lucas-wilkins Nov 24, 2022
2634880
Some comments and clean up
lucas-wilkins Nov 24, 2022
a8a2c4d
Rotations + rotation check implemented, some comments added, cleanup
lucas-wilkins Nov 25, 2022
3ffa1ca
More documentation
lucas-wilkins Nov 25, 2022
8ef5fb2
Added color_by_mesh cube example
lucas-wilkins Nov 25, 2022
6b16d5b
Docs
lucas-wilkins Nov 25, 2022
7b7b512
setup.py stuff
lucas-wilkins Nov 25, 2022
2c6bf27
Updating the orientation viewer
lucas-wilkins Nov 25, 2022
0aaaeaa
Fixed auto-refactor bug
lucas-wilkins Nov 25, 2022
c22ec4a
Explicitly added dependency to CI
lucas-wilkins Nov 25, 2022
d42ba77
Removed reference to pyqtgraph
lucas-wilkins Nov 25, 2022
becc350
Remove more pyqtgraph references
lucas-wilkins Nov 25, 2022
166033e
Moved transform to OrientationViewer, it should probably be deleted soon
lucas-wilkins Nov 25, 2022
a32562c
Orientation viewer basics working
lucas-wilkins Nov 25, 2022
ca1c7cb
Ghosts work
lucas-wilkins Nov 25, 2022
7b15359
Merge branch 'main' into 1763-implement-the-orientation-viewer-in-5x
Dec 19, 2022
f2bdf4c
Refactoring of color classes
lucas-wilkins Jan 3, 2023
3865c8c
Fixed dependencies on colour class
lucas-wilkins Jan 3, 2023
63ea97a
Imports in OrientationViewer
lucas-wilkins Jan 3, 2023
87e4fb2
Moved orientation viewer UI file
lucas-wilkins Jan 3, 2023
21029fc
Reenabled some stuff commented for profiling
lucas-wilkins Jan 3, 2023
d71ce97
Cleaned up viewer code
lucas-wilkins Jan 3, 2023
cabf58e
Modified docs a little
lucas-wilkins Jan 3, 2023
be603ef
Dynamic loading and fixed closing issue
lucas-wilkins Jan 3, 2023
bd52cbb
Window title
lucas-wilkins Jan 3, 2023
d1aac40
Fixed ghost orientation, normally distruted them
lucas-wilkins Jan 3, 2023
f7afcf4
Axis fix
lucas-wilkins Jan 6, 2023
3ee5171
OrientationViewer.UI on setup path
lucas-wilkins Jan 16, 2023
c7d3203
Added pyopengl package path in .spec file
lucas-wilkins Jan 19, 2023
c189827
adding pyopengl_accelerate module
Jan 19, 2023
fc9f461
typo fix
lucas-wilkins Jan 19, 2023
ec9f366
Merge branch 'main' into 1763-implement-the-orientation-viewer-in-5x
lucas-wilkins Jan 19, 2023
7173444
Removed pyopengl from datas
lucas-wilkins Jan 19, 2023
fb418b7
Merge branch '1763-implement-the-orientation-viewer-in-5x' of https:/…
lucas-wilkins Jan 19, 2023
06d7ed2
Merge branch 'main' into 1763-implement-the-orientation-viewer-in-5x
lucas-wilkins Jan 20, 2023
49be2f9
Allowing signing of OSX bundle
Jan 26, 2023
caca6f9
Relaxing PyQT version requirement for Mac
Jan 26, 2023
b26addf
Reverting PyQt5 version to 5.13
Jan 26, 2023
81e457a
Merge branch '2419-setuppy-cleaning' into 1763-implement-the-orientat…
lucas-wilkins Jan 30, 2023
0e3a8af
Use latest version of pyinstaller
lucas-wilkins Jan 30, 2023
020e71c
Potential fix
lucas-wilkins Jan 30, 2023
564b5f5
Was it just the wrong directory?
lucas-wilkins Jan 30, 2023
547626a
Arguments
lucas-wilkins Jan 30, 2023
2ebbe99
Merge branch 'main' into 1763-implement-the-orientation-viewer-in-5x
lucas-wilkins Jan 31, 2023
7736808
Update ci.yml
Feb 1, 2023
9dad57b
Merge branch 'main' into 1763-implement-the-orientation-viewer-in-5x
lucas-wilkins Feb 2, 2023
ab898a3
Added __init__.py to OrientationViewer/UI
lucas-wilkins Feb 2, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 14 additions & 12 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,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 }}
Expand Down Expand Up @@ -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:
Expand All @@ -258,10 +247,23 @@ 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 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
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') }}
Expand Down
120 changes: 120 additions & 0 deletions build_tools/fix_qt_folder_names_for_codesign.py
Original file line number Diff line number Diff line change
@@ -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:]))
3 changes: 2 additions & 1 deletion build_tools/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,5 @@ bumps
html2text
jsonschema
superqt

pyopengl
pyopengl_accelerate
13 changes: 13 additions & 0 deletions docs/sphinx-docs/source/dev/gl/opengl.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
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 Hierarchy
===============

lucas-wilkins marked this conversation as resolved.
Show resolved Hide resolved
TODO - data currently in PR.
2 changes: 1 addition & 1 deletion installers/sasview.spec
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ 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, 'zmq'), 'zmq'))

def add_data(data):
Expand Down
Empty file added src/sas/qtgui/GL/__init__.py
Empty file.
97 changes: 97 additions & 0 deletions src/sas/qtgui/GL/color.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
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

"Helper classes for dealing with colours"

logger = logging.getLogger("GL.Color")

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, g, b, 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 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 _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))), axis=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))), axis=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():

_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:
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 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 vertex_coloring(self.colormap(scaled))
64 changes: 64 additions & 0 deletions src/sas/qtgui/GL/cone.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
from typing import Optional, Union, Sequence, List, Tuple
lucas-wilkins marked this conversation as resolved.
Show resolved Hide resolved

import numpy as np

from sas.qtgui.GL.models import FullModel
from sas.qtgui.GL.color import ColorSpecification


class Cone(FullModel):
""" Graphics primitive: Radius 1, Height 2 cone "centred" at (0,0,0)"""

@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)]

def __init__(self,
n: int = 20,
colors: Optional[ColorSpecification]=None,
edge_colors: Optional[ColorSpecification]=None):

super().__init__(
vertices=Cone.cone_vertices(n),
edges=Cone.cone_edges(n),
triangle_meshes=Cone.cone_triangles(n),
edge_colors=edge_colors,
colors=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 colors is None:
self.solid_render_enabled = False
self.face_colors = []
else:
self.solid_render_enabled = True
self.face_colors = colors
Loading