-
Notifications
You must be signed in to change notification settings - Fork 43
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2394 from SasView/1763-implement-the-orientation-…
…viewer-in-5x 1763 implement the orientation viewer in 5x and GL Subsystem
- Loading branch information
Showing
28 changed files
with
2,640 additions
and
24 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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:])) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -36,4 +36,5 @@ bumps | |
html2text | ||
jsonschema | ||
superqt | ||
|
||
pyopengl | ||
pyopengl_accelerate |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.