Skip to content

Commit

Permalink
Merge pull request #2394 from SasView/1763-implement-the-orientation-…
Browse files Browse the repository at this point in the history
…viewer-in-5x

1763 implement the orientation viewer in 5x and GL Subsystem
  • Loading branch information
lucas-wilkins authored Feb 2, 2023
2 parents 2c39565 + ab898a3 commit 655ca05
Show file tree
Hide file tree
Showing 28 changed files with 2,640 additions and 24 deletions.
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
===============

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

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

0 comments on commit 655ca05

Please sign in to comment.