. A good place to start with pythonocc
@@ -60,4 +73,4 @@ Other pythonocc related resources
License
-------
-You can redistribute it and/or modify it under the terms of the GNU Lesser General Public License version 3 as published by the Free Software Foundation.
+pythonocc-core is licensed under the GNU Lesser General Public License version 3 as published by the Free Software Foundation. You can redistribute and/or modify it under the terms of this license.
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index c3c9aa3a7..5be7ad252 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -15,84 +15,105 @@ schedules:
jobs:
- template: conda-build.yml
parameters:
- name: Ubuntu_22_04_python39
- vmImage: 'ubuntu-22.04'
+ name: Ubuntu_24_04_python310
+ vmImage: 'ubuntu-24.04'
py_maj: 3
- py_min: 9
+ py_min: 10
- template: conda-build.yml
parameters:
- name: macOS_12_python39
+ name: macOS_12_python310
vmImage: 'macOS-latest'
py_maj: 3
- py_min: 9
+ py_min: 10
- template: conda-build.yml
parameters:
- name: Windows_VS2019_python39
- vmImage: 'windows-2019'
+ name: Windows_VS2022_python310
+ vmImage: 'windows-2022'
py_maj: 3
- py_min: 9
+ py_min: 10
- template: conda-build.yml
parameters:
- name: Ubuntu_22_04_python310
- vmImage: 'ubuntu-22.04'
+ name: Ubuntu_24_04_python311
+ vmImage: 'ubuntu-24.04'
py_maj: 3
- py_min: 10
+ py_min: 11
- template: conda-build.yml
parameters:
- name: macOS_12_python310
+ name: macOS_12_python311
vmImage: 'macOS-latest'
py_maj: 3
- py_min: 10
+ py_min: 11
- template: conda-build.yml
parameters:
- name: Windows_VS2019_python310
- vmImage: 'windows-2019'
+ name: Windows_VS2022_python311
+ vmImage: 'windows-2022'
py_maj: 3
- py_min: 10
+ py_min: 11
- template: conda-build.yml
parameters:
- name: Ubuntu_22_04_python311
- vmImage: 'ubuntu-22.04'
+ name: Ubuntu_24_04_python312
+ vmImage: 'ubuntu-24.04'
py_maj: 3
- py_min: 11
+ py_min: 12
- template: conda-build.yml
parameters:
- name: macOS_12_python311
+ name: macOS_12_python312
vmImage: 'macOS-latest'
py_maj: 3
- py_min: 11
+ py_min: 12
- template: conda-build.yml
parameters:
- name: Windows_VS2019_python311
- vmImage: 'windows-2019'
+ name: Windows_VS2022_python312
+ vmImage: 'windows-2022'
py_maj: 3
- py_min: 11
+ py_min: 12
- template: conda-build.yml
parameters:
- name: Ubuntu_22_04_python312
- vmImage: 'ubuntu-22.04'
+ name: Ubuntu_24_04_python313
+ vmImage: 'ubuntu-24.04'
py_maj: 3
- py_min: 12
+ py_min: 13
- template: conda-build.yml
parameters:
- name: macOS_12_python312
+ name: macOS_12_python313
vmImage: 'macOS-latest'
py_maj: 3
- py_min: 12
+ py_min: 13
- template: conda-build.yml
parameters:
- name: Windows_VS2019_python312
- vmImage: 'windows-2019'
+ name: Windows_VS2022_python313
+ vmImage: 'windows-2022'
py_maj: 3
- py_min: 12
+ py_min: 13
+
+- template: conda-build.yml
+ parameters:
+ name: Ubuntu_24_04_python314
+ vmImage: 'ubuntu-24.04'
+ py_maj: 3
+ py_min: 14
+
+- template: conda-build.yml
+ parameters:
+ name: macOS_12_python314
+ vmImage: 'macOS-latest'
+ py_maj: 3
+ py_min: 14
+
+- template: conda-build.yml
+ parameters:
+ name: Windows_VS2022_python314
+ vmImage: 'windows-2022'
+ py_maj: 3
+ py_min: 14
diff --git a/bandit.yml b/bandit.yml
new file mode 100644
index 000000000..ec2e3a917
--- /dev/null
+++ b/bandit.yml
@@ -0,0 +1 @@
+skips: ['B101']
\ No newline at end of file
diff --git a/ci/conda/build.sh b/ci/conda/build.sh
index 5a9b913dd..662ef3e55 100644
--- a/ci/conda/build.sh
+++ b/ci/conda/build.sh
@@ -1,7 +1,11 @@
#!/bin/bash
-# make an in source build do to some problems with install
# Configure step
+EXTRA_CMAKE_ARGS=""
+if [ "$(uname)" == "Darwin" ]; then
+ EXTRA_CMAKE_ARGS="-DCMAKE_OSX_DEPLOYMENT_TARGET=10.13"
+fi
+
cmake -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_PREFIX_PATH=$PREFIX \
@@ -12,17 +16,11 @@ cmake -G Ninja \
-DPython3_FIND_STRATEGY=LOCATION \
-DPython3_FIND_FRAMEWORK=NEVER \
-DSWIG_HIDE_WARNINGS=ON \
- -DPYTHONOCC_MESHDS_NUMPY=ON
+ -DPYTHONOCC_MESHDS_NUMPY=ON \
+ $EXTRA_CMAKE_ARGS
# Build step
ninja
# Install step
ninja install
-
-# fix rpaths
-#if [ $(uname) == Darwin ]; then
-# for lib in $(ls $SP_DIR/OCC/_*.so); do
-# install_name_tool -rpath $PREFIX/lib @loader_path/../../../ $lib
-# done
-#fi
diff --git a/ci/conda/meta.yaml b/ci/conda/meta.yaml
index 054db095f..8777f1332 100644
--- a/ci/conda/meta.yaml
+++ b/ci/conda/meta.yaml
@@ -1,4 +1,4 @@
-{% set version = "7.8.1" %}
+{% set version = "7.9.3" %}
package:
name: pythonocc-core
@@ -22,31 +22,34 @@ requirements:
- {{ cdt('libxi-devel') }} # [linux]
- ninja
- cmake
- - swig ==4.2.1
+ - swig ==4.4.1
+ - sysroot_linux-64 >=2.28 # [linux]
host:
- python {{ python }}
- - occt ==7.8.1
+ - python_abi * *_cp313 [py==313]
+ - python_abi * *_cp314 [py==314]
+ - occt ==7.9.3
- numpy >=1.17
run:
- - occt ==7.8.1
- - six
+ - occt ==7.9.3
- numpy >=1.17
+ - python_abi * *_cp313 [py==313]
+ - python_abi * *_cp314 [py==314]
test:
imports:
- OCC
- OCC.Core.BRepPrimAPI
- - OCC.Core.MeshDS
- OCC.Core.Tesselator
requires:
- - pyqt >=5
+ - pyqt5-sip # [win]
+ - pyside6 >=6.10 # [win]
+ - wxpython >=4.2 # [win]
+ - svgwrite >=1.4
- mypy
- pytest
- - svgwrite
- - wxpython >=4
- - pyside6
about:
home: https://github.com/tpaviot/pythonocc-core
diff --git a/conda-build.yml b/conda-build.yml
index 6f4ff69a1..287a74ad8 100644
--- a/conda-build.yml
+++ b/conda-build.yml
@@ -1,9 +1,8 @@
parameters:
name: 'Conda build job'
- vmImage: 'Ubuntu-18.04'
+ vmImage: 'ubuntu-22.04'
py_maj: '3'
- py_min: '6'
-
+ py_min: '9'
jobs:
- job: ${{ parameters.name }}
timeoutInMinutes: 360
@@ -12,55 +11,66 @@ jobs:
vmImage: ${{ parameters.vmImage }}
steps:
-
+ # install conda on osx
+ - ${{ if contains(parameters.vmImage, 'macOS') }}:
+ - bash: |
+ curl -O https://repo.anaconda.com/miniconda/Miniconda3-latest-MacOSX-x86_64.sh
+ bash Miniconda3-latest-MacOSX-x86_64.sh -b -p $HOME/miniconda
+ echo "##vso[task.prependpath]$HOME/miniconda/bin"
+ displayName: 'Install Miniconda on macOS'
#activate conda
- - ${{ if or(contains(parameters.vmImage, 'macOS'),contains(parameters.vmImage, 'Ubuntu')) }}:
+ - ${{ if contains(parameters.vmImage, 'macOS') }}:
+ - bash: |
+ source $HOME/miniconda/bin/activate
+ conda init bash
+ displayName: 'Add conda to PATH'
+ - ${{ if contains(parameters.vmImage, 'ubuntu') }}:
- bash: echo "##vso[task.prependpath]$CONDA/bin"
displayName: 'Add conda to PATH'
- ${{ if contains(parameters.vmImage, 'win') }}:
- powershell: Write-Host "##vso[task.prependpath]$env:CONDA\Scripts"
displayName: 'Add conda to PATH'
-
# Ubuntu install opengl items and remove swig packages that conflict with anaconda
- - ${{ if contains(parameters.vmImage, 'Ubuntu') }}:
+ - ${{ if contains(parameters.vmImage, 'ubuntu') }}:
- bash: |
sudo apt-get update && \
sudo apt-get -q -y install libglu1-mesa-dev libgl1-mesa-dev libxmu-dev libxi-dev && \
sudo apt list --installed && \
sudo apt remove swig swig4.0
displayName: 'Install OpenGL headers'
-
- bash: |
conda config --set always_yes yes --set changeps1 no && \
conda update -q conda && \
conda info -a && \
conda config --add channels https://conda.anaconda.org/conda-forge
displayName: 'Conda config and info'
-
- - ${{ if eq(parameters.vmImage, 'windows-2019') }}:
- - script: |
- call "C:\\Program Files (x86)\\Microsoft Visual Studio\\2019\\Enterprise\\VC\\Auxiliary\\Build\\vcvars64.bat"
- displayName: 'Set Windows environment'
- env:
- CXX: "cl.exe"
-
+ - bash: conda create --yes --quiet --name build_env conda-build conda-verify libarchive python=3.12 anaconda-client
+ displayName: 'Create Anaconda environment'
- ${{ if eq(parameters.vmImage, 'windows-2022') }}:
- script: |
call "C:\\Program Files\\Microsoft Visual Studio\\2022\\Enterprise\\VC\\Auxiliary\\Build\\vcvars64.bat"
- displayName: 'Set Windows environment'
+ echo "PATH=%PATH%"
+ where cl.exe
+ set CC=cl.exe
+ set CXX=cl.exe
+ call activate build_env
+ conda-build --python ${{ parameters.py_maj }}.${{ parameters.py_min }} --no-remove-work-dir --dirty ci/conda
+ displayName: 'Set Windows environment and build'
env:
CXX: "cl.exe"
-
- - bash: conda create --yes --quiet --name build_env conda-build conda-verify libarchive python=${{ parameters.py_maj }}.${{ parameters.py_min }} anaconda-client
- displayName: 'Create Anaconda environment'
-
- - bash: |
- source activate build_env && \
- conda-build --no-remove-work-dir --dirty ci/conda
- displayName: 'Run conda build'
- failOnStderr: false
- env:
- PYTHONBUFFERED: 1
- PYTHON_VERSION: ${{ parameters.py_maj }}.${{ parameters.py_min }}
- PACKAGE_VERSION: $(Build.SourceBranchName)
- TOKEN: $(anaconda.TOKEN)
+ CC: "cl.exe"
+ PYTHONBUFFERED: 1
+ PYTHON_VERSION: ${{ parameters.py_maj }}.${{ parameters.py_min }}
+ PACKAGE_VERSION: $(Build.SourceBranchName)
+ TOKEN: $(anaconda.TOKEN)
+ - ${{ if not(contains(parameters.vmImage, 'win')) }}:
+ - bash: |
+ source activate build_env && \
+ conda-build --python ${{ parameters.py_maj }}.${{ parameters.py_min }} --no-remove-work-dir --dirty ci/conda
+ displayName: 'Run conda build'
+ failOnStderr: false
+ env:
+ PYTHONBUFFERED: 1
+ PYTHON_VERSION: ${{ parameters.py_maj }}.${{ parameters.py_min }}
+ PACKAGE_VERSION: $(Build.SourceBranchName)
+ TOKEN: $(anaconda.TOKEN)
diff --git a/src/Display/OCCViewer.py b/src/Display/OCCViewer.py
index e773960ac..674ec7cfd 100644
--- a/src/Display/OCCViewer.py
+++ b/src/Display/OCCViewer.py
@@ -22,6 +22,7 @@
import os
import sys
import time
+from typing import Any, Callable, List, Optional, Tuple, Union
import OCC
from OCC.Core.Aspect import Aspect_GFM_VER
@@ -39,12 +40,14 @@
BRepBuilderAPI_MakeFace,
)
from OCC.Core.TopAbs import (
- TopAbs_FACE,
- TopAbs_EDGE,
TopAbs_VERTEX,
+ TopAbs_EDGE,
+ TopAbs_WIRE,
+ TopAbs_FACE,
TopAbs_SHELL,
TopAbs_SOLID,
)
+from OCC.Core.GeomAbs import GeomAbs_G2
from OCC.Core.Geom import Geom_Curve, Geom_Surface
from OCC.Core.Geom2d import Geom2d_Curve
from OCC.Core.Visualization import Display3d
@@ -87,7 +90,12 @@
Graphic3d_GraduatedTrihedron,
Graphic3d_NameOfMaterial,
)
-from OCC.Core.Aspect import Aspect_TOTP_RIGHT_LOWER, Aspect_FM_STRETCH, Aspect_FM_NONE
+from OCC.Core.Aspect import (
+ Aspect_TOTP_RIGHT_LOWER,
+ Aspect_FM_STRETCH,
+ Aspect_FM_NONE,
+ Aspect_FillMethod,
+)
if sys.platform == "win32":
if "CASROOT" in os.environ:
@@ -113,11 +121,11 @@
os.environ["CASROOT"] = casroot_path
-def rgb_color(r, g, b):
+def rgb_color(r: float, g: float, b: float) -> Quantity_Color:
return Quantity_Color(r, g, b, Quantity_TOC_RGB)
-def get_color_from_name(color_name):
+def get_color_from_name(color_name: str) -> Quantity_Color:
"""from the string 'WHITE', returns Quantity_Color
WHITE.
color_name is the color name, case insensitive.
@@ -134,14 +142,24 @@ def get_color_from_name(color_name):
return Quantity_Color(color_num)
-# some thing we'll need later
-modes = itertools.cycle(
- [TopAbs_FACE, TopAbs_EDGE, TopAbs_VERTEX, TopAbs_SHELL, TopAbs_SOLID]
+TOPOLOGY_MODES = itertools.cycle(
+ [TopAbs_SOLID, TopAbs_SHELL, TopAbs_FACE, TopAbs_WIRE, TopAbs_EDGE, TopAbs_VERTEX]
)
class Viewer3d(Display3d):
- def __init__(self):
+ """
+ A 3D viewer for pythonOCC.
+
+ This class provides a 3D viewer for displaying OpenCASCADE shapes.
+ It is based on the `OCC.Core.Visualization.Display3d` class and provides
+ a higher-level API for interacting with the viewer.
+ """
+
+ def __init__(self) -> None:
+ """
+ Initializes the Viewer3d.
+ """
Display3d.__init__(self)
self._parent = None # the parent opengl GUI container
@@ -157,48 +175,93 @@ def __init__(self):
self.default_drawer = None
self._is_offscreen = None
- self.selected_shapes = []
- self._select_callbacks = []
- self._overlay_items = []
+ self.selected_shapes: List[AIS_Shape] = []
+ self._select_callbacks: List[Callable] = []
+ self._overlay_items: List[Any] = []
+
+ self._window_handle: Optional[Any] = None
- self._window_handle = None
+ def get_parent(self) -> Any:
+ """
+ Returns the parent of the viewer.
- def get_parent(self):
+ Returns:
+ The parent of the viewer.
+ """
return self._parent
- def register_overlay_item(self, overlay_item):
+ def register_overlay_item(self, overlay_item: Any) -> None:
+ """
+ Registers an overlay item to be drawn on top of the 3D view.
+
+ Args:
+ overlay_item: The overlay item to register.
+ """
self._overlay_items.append(overlay_item)
self.View.MustBeResized()
self.View.Redraw()
- def register_select_callback(self, callback):
- """Adds a callback that will be called each time a shape s selected"""
+ def register_select_callback(self, callback: Callable) -> None:
+ """
+ Adds a callback that will be called each time a shape is selected.
+
+ Args:
+ callback: The callback function to register. The callback will be
+ called with the selected shapes as argument.
+ """
if not callable(callback):
raise AssertionError("You must provide a callable to register the callback")
self._select_callbacks.append(callback)
- def unregister_callback(self, callback):
- """Remove a callback from the callback list"""
+ def unregister_callback(self, callback: Callable) -> None:
+ """
+ Remove a callback from the callback list.
+
+ Args:
+ callback: The callback function to unregister.
+ """
if callback not in self._select_callbacks:
raise AssertionError("This callback is not registered")
self._select_callbacks.remove(callback)
- def MoveTo(self, X, Y):
+ def MoveTo(self, X: int, Y: int) -> None:
+ """
+ Moves the mouse to the given coordinates.
+
+ Args:
+ X (int): The x-coordinate.
+ Y (int): The y-coordinate.
+ """
self.Context.MoveTo(X, Y, self.View, True)
- def FitAll(self):
+ def FitAll(self) -> None:
+ """
+ Fits all objects in the view.
+ """
self.View.ZFitAll()
self.View.FitAll()
def Create(
self,
- window_handle=None,
- parent=None,
- create_default_lights=True,
- draw_face_boundaries=True,
- phong_shading=True,
- display_glinfo=True,
- ):
+ window_handle: Optional[Any] = None,
+ parent: Optional[Any] = None,
+ create_default_lights: bool = True,
+ draw_face_boundaries: bool = True,
+ phong_shading: bool = True,
+ display_glinfo: bool = True,
+ ) -> None:
+ """
+ Creates the viewer.
+
+ Args:
+ window_handle: The handle of the window to create the viewer in.
+ If None, an offscreen renderer will be created.
+ parent: The parent of the viewer.
+ create_default_lights (bool): Whether to create default lights.
+ draw_face_boundaries (bool): Whether to draw face boundaries.
+ phong_shading (bool): Whether to use Phong shading.
+ display_glinfo (bool): Whether to display OpenGL information.
+ """
self._window_handle = window_handle
self._parent = parent
@@ -222,6 +285,8 @@ def Create(
# draw black contour edges, like other famous CAD packages
if draw_face_boundaries:
self.default_drawer.SetFaceBoundaryDraw(True)
+ # Don't draw seam edges
+ self.default_drawer.SetFaceBoundaryUpperContinuity(GeomAbs_G2)
# turn up tessellation defaults, which are too conversative...
chord_dev = self.default_drawer.MaximalChordialDeviation() / 10.0
@@ -234,70 +299,123 @@ def Create(
# turn self._inited flag to True
self._inited = True
- def OnResize(self):
+ def OnResize(self) -> None:
+ """
+ Called when the view is resized.
+ """
self.View.MustBeResized()
- def ResetView(self):
+ def ResetView(self) -> None:
+ """
+ Resets the view.
+ """
self.View.Reset()
- def Repaint(self):
+ def Repaint(self) -> None:
+ """
+ Repaints the view.
+ """
self.Viewer.Redraw()
- def SetModeWireFrame(self):
+ def SetModeWireFrame(self) -> None:
+ """
+ Sets the display mode to wireframe.
+ """
self.View.SetComputedMode(False)
self.Context.SetDisplayMode(AIS_WireFrame, True)
- def SetModeShaded(self):
+ def SetModeShaded(self) -> None:
+ """
+ Sets the display mode to shaded.
+ """
self.View.SetComputedMode(False)
self.Context.SetDisplayMode(AIS_Shaded, True)
- def SetModeHLR(self):
+ def SetModeHLR(self) -> None:
+ """
+ Sets the display mode to hidden line removal.
+ """
self.View.SetComputedMode(True)
- def SetOrthographicProjection(self):
+ def SetOrthographicProjection(self) -> None:
+ """
+ Sets the projection to orthographic.
+ """
self.camera.SetProjectionType(Graphic3d_Camera.Projection_Orthographic)
- def SetPerspectiveProjection(self):
+ def SetPerspectiveProjection(self) -> None:
+ """
+ Sets the projection to perspective.
+ """
self.camera.SetProjectionType(Graphic3d_Camera.Projection_Perspective)
- def View_Top(self):
+ def View_Top(self) -> None:
+ """
+ Sets the view to top.
+ """
self.View.SetProj(V3d_Zpos)
- def View_Bottom(self):
+ def View_Bottom(self) -> None:
+ """
+ Sets the view to bottom.
+ """
self.View.SetProj(V3d_Zneg)
- def View_Left(self):
+ def View_Left(self) -> None:
+ """
+ Sets the view to left.
+ """
self.View.SetProj(V3d_Xneg)
- def View_Right(self):
+ def View_Right(self) -> None:
+ """
+ Sets the view to right.
+ """
self.View.SetProj(V3d_Xpos)
- def View_Front(self):
+ def View_Front(self) -> None:
+ """
+ Sets the view to front.
+ """
self.View.SetProj(V3d_Yneg)
- def View_Rear(self):
+ def View_Rear(self) -> None:
+ """
+ Sets the view to rear.
+ """
self.View.SetProj(V3d_Ypos)
- def View_Iso(self):
+ def View_Iso(self) -> None:
+ """
+ Sets the view to isometric.
+ """
self.View.SetProj(V3d_XposYnegZpos)
- def EnableTextureEnv(self, name_of_texture=Graphic3d_NOT_ENV_CLOUDS):
- """enable environment mapping. Possible modes are
- Graphic3d_NOT_ENV_CLOUDS
- Graphic3d_NOT_ENV_CV
- Graphic3d_NOT_ENV_MEDIT
- Graphic3d_NOT_ENV_PEARL
- Graphic3d_NOT_ENV_SKY1
- Graphic3d_NOT_ENV_SKY2
- Graphic3d_NOT_ENV_LINES
- Graphic3d_NOT_ENV_ROAD
- Graphic3d_NOT_ENV_UNKNOWN
+ def EnableTextureEnv(self, name_of_texture: int = Graphic3d_NOT_ENV_CLOUDS) -> None:
+ """
+ Enables environment mapping.
+
+ Args:
+ name_of_texture: The name of the texture to use. Possible values
+ are:
+ - Graphic3d_NOT_ENV_CLOUDS
+ - Graphic3d_NOT_ENV_CV
+ - Graphic3d_NOT_ENV_MEDIT
+ - Graphic3d_NOT_ENV_PEARL
+ - Graphic3d_NOT_ENV_SKY1
+ - Graphic3d_NOT_ENV_SKY2
+ - Graphic3d_NOT_ENV_LINES
+ - Graphic3d_NOT_ENV_ROAD
+ - Graphic3d_NOT_ENV_UNKNOWN
"""
texture_env = Graphic3d_TextureEnv(name_of_texture)
self.View.SetTextureEnv(texture_env)
self.View.Redraw()
- def DisableTextureEnv(self):
+ def DisableTextureEnv(self) -> None:
+ """
+ Disables environment mapping.
+ """
a_null_texture = Handle_Graphic3d_TextureEnv_Create()
self.View.SetTextureEnv(
a_null_texture
@@ -306,26 +424,29 @@ def DisableTextureEnv(self):
def SetRenderingParams(
self,
- Method=Graphic3d_RM_RASTERIZATION,
- RaytracingDepth=3,
- IsShadowEnabled=True,
- IsReflectionEnabled=False,
- IsAntialiasingEnabled=False,
- IsTransparentShadowEnabled=False,
- StereoMode=Graphic3d_StereoMode_QuadBuffer,
- AnaglyphFilter=Graphic3d_RenderingParams.Anaglyph_RedCyan_Optimized,
- ToReverseStereo=False,
- ):
- """Default values are :
- Method=Graphic3d_RM_RASTERIZATION,
- RaytracingDepth=3,
- IsShadowEnabled=True,
- IsReflectionEnabled=False,
- IsAntialiasingEnabled=False,
- IsTransparentShadowEnabled=False,
- StereoMode=Graphic3d_StereoMode_QuadBuffer,
- AnaglyphFilter=Graphic3d_RenderingParams.Anaglyph_RedCyan_Optimized,
- ToReverseStereo=False)
+ Method: int = Graphic3d_RM_RASTERIZATION,
+ RaytracingDepth: int = 3,
+ IsShadowEnabled: bool = True,
+ IsReflectionEnabled: bool = False,
+ IsAntialiasingEnabled: bool = False,
+ IsTransparentShadowEnabled: bool = False,
+ StereoMode: int = Graphic3d_StereoMode_QuadBuffer,
+ AnaglyphFilter: "Graphic3d_RenderingParams.Anaglyph" = Graphic3d_RenderingParams.Anaglyph_RedCyan_Optimized,
+ ToReverseStereo: bool = False,
+ ) -> None:
+ """
+ Sets the rendering parameters.
+
+ Args:
+ Method: The rendering method to use.
+ RaytracingDepth (int): The ray tracing depth.
+ IsShadowEnabled (bool): Whether to enable shadows.
+ IsReflectionEnabled (bool): Whether to enable reflections.
+ IsAntialiasingEnabled (bool): Whether to enable anti-aliasing.
+ IsTransparentShadowEnabled (bool): Whether to enable transparent shadows.
+ StereoMode: The stereo mode to use.
+ AnaglyphFilter: The anaglyph filter to use.
+ ToReverseStereo (bool): Whether to reverse stereo.
"""
self.ChangeRenderingParams(
Method,
@@ -339,14 +460,19 @@ def SetRenderingParams(
ToReverseStereo,
)
- def SetRasterizationMode(self):
- """to enable rasterization mode, just call the SetRenderingParams
- with default values
+ def SetRasterizationMode(self) -> None:
+ """
+ Sets the rendering mode to rasterization.
"""
self.SetRenderingParams()
- def SetRaytracingMode(self, depth=3):
- """enables the raytracing mode"""
+ def SetRaytracingMode(self, depth: int = 3) -> None:
+ """
+ Enables the raytracing mode.
+
+ Args:
+ depth (int): The ray tracing depth.
+ """
self.SetRenderingParams(
Method=Graphic3d_RM_RAYTRACING,
RaytracingDepth=depth,
@@ -356,15 +482,26 @@ def SetRaytracingMode(self, depth=3):
IsTransparentShadowEnabled=True,
)
- def ExportToImage(self, image_filename):
+ def ExportToImage(self, image_filename: str) -> None:
+ """
+ Exports the view to an image file.
+
+ Args:
+ image_filename (str): The name of the image file.
+ """
self.View.Dump(image_filename)
- def display_graduated_trihedron(self):
+ def display_graduated_trihedron(self) -> None:
+ """
+ Displays a graduated trihedron.
+ """
a_trihedron_data = Graphic3d_GraduatedTrihedron()
self.View.GraduatedTrihedronDisplay(a_trihedron_data)
- def display_triedron(self):
- """Show a black triedron in lower right corner"""
+ def display_triedron(self) -> None:
+ """
+ Shows a black triedron in lower right corner.
+ """
self.View.TriedronDisplay(
Aspect_TOTP_RIGHT_LOWER,
Quantity_Color(Quantity_NOC_BLACK),
@@ -372,17 +509,36 @@ def display_triedron(self):
V3d_ZBUFFER,
)
- def hide_triedron(self):
- """Show a black triedron in lower right corner"""
+ def hide_triedron(self) -> None:
+ """
+ Hides the triedron.
+ """
self.View.TriedronErase()
- def set_bg_gradient_color(self, color1, color2, fill_method=Aspect_GFM_VER):
- """set a bg vertical gradient color.
- color1 is [R1, G1, B1], each being bytes or an instance of Quantity_Color
- color2 is [R2, G2, B2], each being bytes or an instance of Quantity_Color
- fill_method is one of Aspect_GFM_VER value Aspect_GFM_NONE, Aspect_GFM_HOR,
- Aspect_GFM_VER, Aspect_GFM_DIAG1, Aspect_GFM_DIAG2, Aspect_GFM_CORNER1, Aspect_GFM_CORNER2,
- Aspect_GFM_CORNER3, Aspect_GFM_CORNER4
+ def set_bg_gradient_color(
+ self,
+ color1: Union[List[float], Quantity_Color],
+ color2: Union[List[float], Quantity_Color],
+ fill_method: Aspect_FillMethod = Aspect_GFM_VER,
+ ) -> None:
+ """
+ Sets a background vertical gradient color.
+
+ Args:
+ color1: The first color. Can be a list of 3 floats (R, G, B) or a
+ Quantity_Color.
+ color2: The second color. Can be a list of 3 floats (R, G, B) or a
+ Quantity_Color.
+ fill_method: The fill method to use. Can be one of:
+ - Aspect_GFM_NONE
+ - Aspect_GFM_HOR
+ - Aspect_GFM_VER
+ - Aspect_GFM_DIAG1
+ - Aspect_GFM_DIAG2
+ - Aspect_GFM_CORNER1
+ - Aspect_GFM_CORNER2
+ - Aspect_GFM_CORNER3
+ - Aspect_GFM_CORNER4
"""
if isinstance(color1, list) and isinstance(color2, list):
R1, G1, B1 = color1
@@ -397,8 +553,14 @@ def set_bg_gradient_color(self, color1, color2, fill_method=Aspect_GFM_VER):
)
self.View.SetBgGradientColors(color1, color2, fill_method, True)
- def SetBackgroundImage(self, image_filename, stretch=True):
- """displays a background image (jpg, png etc.)"""
+ def SetBackgroundImage(self, image_filename: str, stretch: bool = True) -> None:
+ """
+ Displays a background image (jpg, png etc.).
+
+ Args:
+ image_filename (str): The name of the image file.
+ stretch (bool): Whether to stretch the image to fit the view.
+ """
if not os.path.isfile(image_filename):
raise IOError(f"image file {image_filename} not found.")
if stretch:
@@ -406,8 +568,20 @@ def SetBackgroundImage(self, image_filename, stretch=True):
else:
self.View.SetBackgroundImage(image_filename, Aspect_FM_NONE, True)
- def DisplayVector(self, vec, pnt, update=False):
- """displays a vector as an arrow"""
+ def DisplayVector(
+ self, vec: gp_Vec, pnt: gp_Pnt, update: bool = False
+ ) -> Optional[Graphic3d_Structure]:
+ """
+ Displays a vector as an arrow.
+
+ Args:
+ vec (gp_Vec): The vector to display.
+ pnt (gp_Pnt): The starting point of the vector.
+ update (bool): Whether to update the view.
+
+ Returns:
+ The created structure.
+ """
if self._inited:
aStructure = Graphic3d_Structure(self.struc_mgr)
@@ -428,20 +602,30 @@ def DisplayVector(self, vec, pnt, update=False):
if update:
self.Repaint()
return aStructure
+ return None
def DisplayMessage(
self,
- point,
- text_to_write,
- height=14.0,
- message_color=(0.0, 0.0, 0.0),
- update=False,
- ):
- """
- :point: a gp_Pnt or gp_Pnt2d instance
- :text_to_write: a string
- :height: font height, 12 by defaults
- :message_color: triple with the range 0-1, default to black
+ point: Union[gp_Pnt, gp_Pnt2d],
+ text_to_write: str,
+ height: float = 14.0,
+ message_color: Tuple[float, float, float] = (0.0, 0.0, 0.0),
+ update: bool = False,
+ ) -> Graphic3d_Structure:
+ """
+ Displays a message at the given point.
+
+ Args:
+ point: The point where to display the message. Can be a gp_Pnt or
+ a gp_Pnt2d.
+ text_to_write (str): The text to display.
+ height (float): The font height.
+ message_color (tuple): The color of the message, as a tuple of 3
+ floats (R, G, B).
+ update (bool): Whether to update the view.
+
+ Returns:
+ The created structure.
"""
aStructure = Graphic3d_Structure(self.struc_mgr)
@@ -461,15 +645,29 @@ def DisplayMessage(
def DisplayShape(
self,
- shapes,
- material=None,
- texture=None,
- color=None,
- transparency=None,
- update=False,
- ):
- """display one or a set of displayable objects"""
- ais_shapes = [] # the list of all displayed shapes
+ shapes: Any,
+ material: Optional[Any] = None,
+ texture: Optional[Any] = None,
+ color: Optional[Union[str, int, Quantity_Color]] = None,
+ transparency: Optional[float] = None,
+ update: bool = False,
+ ) -> List[AIS_Shape]:
+ """
+ Displays one or a set of displayable objects.
+
+ Args:
+ shapes: The shape(s) to display. Can be a single shape or a list of
+ shapes.
+ material: The material to use for the shape.
+ texture: The texture to use for the shape.
+ color: The color to use for the shape.
+ transparency (float): The transparency to use for the shape (0.0 to 1.0).
+ update (bool): Whether to update the view.
+
+ Returns:
+ A list of the displayed AIS_Shape objects.
+ """
+ ais_shapes: List[AIS_Shape] = [] # the list of all displayed shapes
if issubclass(shapes.__class__, gp_Pnt):
# if a gp_Pnt is passed, first convert to vertex
@@ -566,10 +764,21 @@ def DisplayShape(
def DisplayColoredShape(
self,
- shapes,
- color="YELLOW",
- update=False,
- ):
+ shapes: Any,
+ color: Union[str, Quantity_Color] = "YELLOW",
+ update: bool = False,
+ ) -> List[AIS_Shape]:
+ """
+ Displays a shape with the given color.
+
+ Args:
+ shapes: The shape(s) to display.
+ color (str or Quantity_Color): The color to use.
+ update (bool): Whether to update the view.
+
+ Returns:
+ A list of the displayed AIS_Shape objects.
+ """
if isinstance(color, str):
dict_color = {
"WHITE": Quantity_NOC_WHITE,
@@ -591,52 +800,136 @@ def DisplayColoredShape(
return self.DisplayShape(shapes, color=clr, update=update)
- def EnableAntiAliasing(self):
+ def EnableAntiAliasing(self) -> None:
+ """
+ Enables anti-aliasing.
+ """
self.SetNbMsaaSample(4)
- def DisableAntiAliasing(self):
+ def DisableAntiAliasing(self) -> None:
+ """
+ Disables anti-aliasing.
+ """
self.SetNbMsaaSample(0)
- def EraseAll(self):
+ def EraseAll(self) -> None:
+ """
+ Erases all objects from the view.
+ """
self.Context.EraseAll(True)
- def Tumble(self, num_images, animation=True):
+ def Tumble(self, num_images: int, animation: bool = True) -> None:
+ """
+ Tumbles the view.
+
+ Args:
+ num_images (int): The number of images to generate.
+ animation (bool): Whether to animate the tumble.
+ """
self.View.Tumble(num_images, animation)
- def Pan(self, dx, dy):
+ def Pan(self, dx: int, dy: int) -> None:
+ """
+ Pans the view.
+
+ Args:
+ dx (int): The horizontal panning distance.
+ dy (int): The vertical panning distance.
+ """
self.View.Pan(dx, dy)
- def SetSelectionMode(self, mode=None):
+ def SetSelectionMode(self, mode: Optional[int] = None) -> None:
+ """
+ Sets the selection mode.
+
+ Args:
+ mode: The selection mode to use. If None, cycles through the
+ available modes.
+ """
self.Context.Deactivate()
- topo_level = next(modes)
if mode is None:
+ topo_level = next(TOPOLOGY_MODES)
self.Context.Activate(AIS_Shape.SelectionMode(topo_level), True)
else:
self.Context.Activate(AIS_Shape.SelectionMode(mode), True)
self.Context.UpdateSelected(True)
- def SetSelectionModeVertex(self):
+ def SetSelectionModeVertex(self) -> None:
+ """
+ Sets the selection mode to vertex.
+ """
self.SetSelectionMode(TopAbs_VERTEX)
- def SetSelectionModeEdge(self):
+ def SetSelectionModeEdge(self) -> None:
+ """
+ Sets the selection mode to edge.
+ """
self.SetSelectionMode(TopAbs_EDGE)
- def SetSelectionModeFace(self):
+ def SetSelectionModeWire(self) -> None:
+ """
+ Sets the selection mode to wire.
+ """
+ self.SetSelectionMode(TopAbs_WIRE)
+
+ def SetSelectionModeFace(self) -> None:
+ """
+ Sets the selection mode to face.
+ """
self.SetSelectionMode(TopAbs_FACE)
- def SetSelectionModeShape(self):
+ def SetSelectionModeShell(self) -> None:
+ """
+ Sets the selection mode to shell.
+ """
+ self.SetSelectionMode(TopAbs_SHELL)
+
+ def SetSelectionModeSolid(self) -> None:
+ """
+ Sets the selection mode to solid.
+ """
+ self.SetSelectionMode(TopAbs_SOLID)
+
+ def SetSelectionModeShape(self) -> None:
+ """
+ Sets the selection mode to shape.
+ """
self.Context.Deactivate()
- def SetSelectionModeNeutral(self):
+ def SetSelectionModeNeutral(self) -> None:
+ """
+ Sets the selection mode to neutral.
+ """
self.Context.Deactivate()
- def GetSelectedShapes(self):
+ def GetSelectedShapes(self) -> List[AIS_Shape]:
+ """
+ Returns the selected shapes.
+
+ Returns:
+ A list of the selected shapes.
+ """
return self.selected_shapes
- def GetSelectedShape(self):
+ def GetSelectedShape(self) -> AIS_Shape:
+ """
+ Returns the selected shape.
+
+ Returns:
+ The selected shape.
+ """
return self.Context.SelectedShape()
- def SelectArea(self, Xmin, Ymin, Xmax, Ymax):
+ def SelectArea(self, Xmin: int, Ymin: int, Xmax: int, Ymax: int) -> None:
+ """
+ Selects objects within the given area.
+
+ Args:
+ Xmin (int): The minimum x-coordinate of the selection area.
+ Ymin (int): The minimum y-coordinate of the selection area.
+ Xmax (int): The maximum x-coordinate of the selection area.
+ Ymax (int): The maximum y-coordinate of the selection area.
+ """
self.Context.Select(Xmin, Ymin, Xmax, Ymax, self.View, True)
self.Context.InitSelected()
# reinit the selected_shapes list
@@ -649,7 +942,14 @@ def SelectArea(self, Xmin, Ymin, Xmax, Ymax):
for callback in self._select_callbacks:
callback(self.selected_shapes, Xmin, Ymin, Xmax, Ymax)
- def Select(self, X, Y):
+ def Select(self, X: int, Y: int) -> None:
+ """
+ Selects the object at the given coordinates.
+
+ Args:
+ X (int): The x-coordinate.
+ Y (int): The y-coordinate.
+ """
self.Context.Select(True)
self.Context.InitSelected()
@@ -661,7 +961,14 @@ def Select(self, X, Y):
for callback in self._select_callbacks:
callback(self.selected_shapes, X, Y)
- def ShiftSelect(self, X, Y):
+ def ShiftSelect(self, X: int, Y: int) -> None:
+ """
+ Adds the object at the given coordinates to the selection.
+
+ Args:
+ X (int): The x-coordinate.
+ Y (int): The y-coordinate.
+ """
self.Context.ShiftSelect(True)
self.Context.InitSelected()
@@ -676,32 +983,86 @@ def ShiftSelect(self, X, Y):
for callback in self._select_callbacks:
callback(self.selected_shapes, X, Y)
- def Rotation(self, X, Y):
+ def Rotation(self, X: int, Y: int) -> None:
+ """
+ Rotates the view.
+
+ Args:
+ X (int): The x-coordinate.
+ Y (int): The y-coordinate.
+ """
self.View.Rotation(X, Y)
- def DynamicZoom(self, X1, Y1, X2, Y2):
+ def DynamicZoom(self, X1: int, Y1: int, X2: int, Y2: int) -> None:
+ """
+ Zooms the view dynamically.
+
+ Args:
+ X1 (int): The first x-coordinate.
+ Y1 (int): The first y-coordinate.
+ X2 (int): The second x-coordinate.
+ Y2 (int): The second y-coordinate.
+ """
self.View.Zoom(X1, Y1, X2, Y2)
- def ZoomFactor(self, zoom_factor):
+ def ZoomFactor(self, zoom_factor: float) -> None:
+ """
+ Sets the zoom factor.
+
+ Args:
+ zoom_factor (float): The zoom factor.
+ """
self.View.SetZoom(zoom_factor)
- def ZoomArea(self, X1, Y1, X2, Y2):
+ def ZoomArea(self, X1: int, Y1: int, X2: int, Y2: int) -> None:
+ """
+ Zooms to the given area.
+
+ Args:
+ X1 (int): The first x-coordinate.
+ Y1 (int): The first y-coordinate.
+ X2 (int): The second x-coordinate.
+ Y2 (int): The second y-coordinate.
+ """
self.View.WindowFit(X1, Y1, X2, Y2)
- def Zoom(self, X, Y):
+ def Zoom(self, X: int, Y: int) -> None:
+ """
+ Zooms the view.
+
+ Args:
+ X (int): The x-coordinate.
+ Y (int): The y-coordinate.
+ """
self.View.Zoom(X, Y)
- def StartRotation(self, X, Y):
+ def StartRotation(self, X: int, Y: int) -> None:
+ """
+ Starts the rotation.
+
+ Args:
+ X (int): The x-coordinate.
+ Y (int): The y-coordinate.
+ """
self.View.StartRotation(X, Y)
class OffscreenRenderer(Viewer3d):
- """The offscreen renderer is inherited from Viewer3d.
+ """
+ An offscreen renderer for pythonOCC.
+
+ The offscreen renderer is inherited from Viewer3d.
The DisplayShape method is overridden to export to image
each time it is called.
"""
- def __init__(self, screen_size=(640, 480)):
+ def __init__(self, screen_size: Tuple[int, int] = (640, 480)) -> None:
+ """
+ Initializes the OffscreenRenderer.
+
+ Args:
+ screen_size (tuple): The size of the screen (width, height).
+ """
Viewer3d.__init__(self)
# create the renderer
self.Create()
@@ -713,16 +1074,33 @@ def __init__(self, screen_size=(640, 480)):
def DisplayShape(
self,
- shapes,
- material=None,
- texture=None,
- color=None,
- transparency=None,
- update=True,
- dump_image=True,
- dump_image_path=None,
- dump_image_filename=None,
- ):
+ shapes: Any,
+ material: Optional[Any] = None,
+ texture: Optional[Any] = None,
+ color: Optional[Union[str, int, Quantity_Color]] = None,
+ transparency: Optional[float] = None,
+ update: bool = True,
+ dump_image: bool = True,
+ dump_image_path: Optional[str] = None,
+ dump_image_filename: Optional[str] = None,
+ ) -> List[AIS_Shape]:
+ """
+ Displays a shape and dumps the view to an image file.
+
+ Args:
+ shapes: The shape(s) to display.
+ material: The material to use for the shape.
+ texture: The texture to use for the shape.
+ color: The color to use for the shape.
+ transparency (float): The transparency to use for the shape (0.0 to 1.0).
+ update (bool): Whether to update the view.
+ dump_image (bool): Whether to dump the view to an image file.
+ dump_image_path (str): The path where to save the image file.
+ dump_image_filename (str): The name of the image file.
+
+ Returns:
+ A list of the displayed AIS_Shape objects.
+ """
# call the "original" DisplayShape method
r = super(OffscreenRenderer, self).DisplayShape(
shapes, material, texture, color, transparency, update
diff --git a/src/Display/OCCViewer.pyi b/src/Display/OCCViewer.pyi
new file mode 100644
index 000000000..3540d004e
--- /dev/null
+++ b/src/Display/OCCViewer.pyi
@@ -0,0 +1,136 @@
+from typing import Any, Callable, List, Optional, Tuple, Union
+
+from OCC.Core.AIS import AIS_Shape
+from OCC.Core.Graphic3d import Graphic3d_Structure
+from OCC.Core.gp import gp_Pnt, gp_Pnt2d, gp_Vec
+from OCC.Core.Quantity import Quantity_Color
+from OCC.Core.Visualization import Display3d
+
+def rgb_color(r: float, g: float, b: float) -> Quantity_Color: ...
+def get_color_from_name(color_name: str) -> Quantity_Color: ...
+
+class Viewer3d(Display3d):
+ def __init__(self) -> None: ...
+ def get_parent(self) -> Any: ...
+ def register_overlay_item(self, overlay_item: Any) -> None: ...
+ def register_select_callback(self, callback: Callable) -> None: ...
+ def unregister_callback(self, callback: Callable) -> None: ...
+ def MoveTo(self, X: int, Y: int) -> None: ...
+ def FitAll(self) -> None: ...
+ def Create(
+ self,
+ window_handle: Optional[Any] = None,
+ parent: Optional[Any] = None,
+ create_default_lights: bool = True,
+ draw_face_boundaries: bool = True,
+ phong_shading: bool = True,
+ display_glinfo: bool = True,
+ ) -> None: ...
+ def OnResize(self) -> None: ...
+ def ResetView(self) -> None: ...
+ def Repaint(self) -> None: ...
+ def SetModeWireFrame(self) -> None: ...
+ def SetModeShaded(self) -> None: ...
+ def SetModeHLR(self) -> None: ...
+ def SetOrthographicProjection(self) -> None: ...
+ def SetPerspectiveProjection(self) -> None: ...
+ def View_Top(self) -> None: ...
+ def View_Bottom(self) -> None: ...
+ def View_Left(self) -> None: ...
+ def View_Right(self) -> None: ...
+ def View_Front(self) -> None: ...
+ def View_Rear(self) -> None: ...
+ def View_Iso(self) -> None: ...
+ def EnableTextureEnv(self, name_of_texture: int = ...) -> None: ...
+ def DisableTextureEnv(self) -> None: ...
+ def SetRenderingParams(
+ self,
+ Method: int = ...,
+ RaytracingDepth: int = 3,
+ IsShadowEnabled: bool = True,
+ IsReflectionEnabled: bool = False,
+ IsAntialiasingEnabled: bool = False,
+ IsTransparentShadowEnabled: bool = False,
+ StereoMode: int = ...,
+ AnaglyphFilter: int = ...,
+ ToReverseStereo: bool = False,
+ ) -> None: ...
+ def SetRasterizationMode(self) -> None: ...
+ def SetRaytracingMode(self, depth: int = 3) -> None: ...
+ def ExportToImage(self, image_filename: str) -> None: ...
+ def display_graduated_trihedron(self) -> None: ...
+ def display_triedron(self) -> None: ...
+ def hide_triedron(self) -> None: ...
+ def set_bg_gradient_color(
+ self,
+ color1: Union[List[float], Quantity_Color],
+ color2: Union[List[float], Quantity_Color],
+ fill_method: int = ...,
+ ) -> None: ...
+ def SetBackgroundImage(self, image_filename: str, stretch: bool = True) -> None: ...
+ def DisplayVector(
+ self, vec: gp_Vec, pnt: gp_Pnt, update: bool = False
+ ) -> Optional[Graphic3d_Structure]: ...
+ def DisplayMessage(
+ self,
+ point: Union[gp_Pnt, gp_Pnt2d],
+ text_to_write: str,
+ height: float = 14.0,
+ message_color: Tuple[float, float, float] = ...,
+ update: bool = False,
+ ) -> Graphic3d_Structure: ...
+ def DisplayShape(
+ self,
+ shapes: Any,
+ material: Optional[Any] = None,
+ texture: Optional[Any] = None,
+ color: Optional[Union[str, int, Quantity_Color]] = None,
+ transparency: Optional[float] = None,
+ update: bool = False,
+ ) -> List[AIS_Shape]: ...
+ def DisplayColoredShape(
+ self,
+ shapes: Any,
+ color: Union[str, Quantity_Color] = "YELLOW",
+ update: bool = False,
+ ) -> List[AIS_Shape]: ...
+ def EnableAntiAliasing(self) -> None: ...
+ def DisableAntiAliasing(self) -> None: ...
+ def EraseAll(self) -> None: ...
+ def Tumble(self, num_images: int, animation: bool = True) -> None: ...
+ def Pan(self, dx: int, dy: int) -> None: ...
+ def SetSelectionMode(self, mode: Optional[int] = None) -> None: ...
+ def SetSelectionModeVertex(self) -> None: ...
+ def SetSelectionModeEdge(self) -> None: ...
+ def SetSelectionModeWire(self) -> None: ...
+ def SetSelectionModeFace(self) -> None: ...
+ def SetSelectionModeShell(self) -> None: ...
+ def SetSelectionModeSolid(self) -> None: ...
+ def SetSelectionModeShape(self) -> None: ...
+ def SetSelectionModeNeutral(self) -> None: ...
+ def GetSelectedShapes(self) -> List[AIS_Shape]: ...
+ def GetSelectedShape(self) -> AIS_Shape: ...
+ def SelectArea(self, Xmin: int, Ymin: int, Xmax: int, Ymax: int) -> None: ...
+ def Select(self, X: int, Y: int) -> None: ...
+ def ShiftSelect(self, X: int, Y: int) -> None: ...
+ def Rotation(self, X: int, Y: int) -> None: ...
+ def DynamicZoom(self, X1: int, Y1: int, X2: int, Y2: int) -> None: ...
+ def ZoomFactor(self, zoom_factor: float) -> None: ...
+ def ZoomArea(self, X1: int, Y1: int, X2: int, Y2: int) -> None: ...
+ def Zoom(self, X: int, Y: int) -> None: ...
+ def StartRotation(self, X: int, Y: int) -> None: ...
+
+class OffscreenRenderer(Viewer3d):
+ def __init__(self, screen_size: Tuple[int, int] = ...) -> None: ...
+ def DisplayShape(
+ self,
+ shapes: Any,
+ material: Optional[Any] = None,
+ texture: Optional[Any] = None,
+ color: Optional[Union[str, int, Quantity_Color]] = None,
+ transparency: Optional[float] = None,
+ update: bool = True,
+ dump_image: bool = True,
+ dump_image_path: Optional[str] = None,
+ dump_image_filename: Optional[str] = None,
+ ) -> List[AIS_Shape]: ...
diff --git a/src/Display/SimpleGui.py b/src/Display/SimpleGui.py
index 033dbf8a8..659865abe 100644
--- a/src/Display/SimpleGui.py
+++ b/src/Display/SimpleGui.py
@@ -23,13 +23,22 @@
from typing import Any, Callable, List, Optional, Tuple
from OCC import VERSION
-from OCC.Display.backend import load_backend, get_qt_modules
-from OCC.Display.OCCViewer import OffscreenRenderer
+from OCC.Display.backend import get_qt_modules, load_backend
+from OCC.Display.OCCViewer import OffscreenRenderer, Viewer3d
log = logging.getLogger(__name__)
def check_callable(_callable: Callable) -> None:
+ """
+ Checks if the given object is callable.
+
+ Args:
+ _callable: The object to check.
+
+ Raises:
+ AssertionError: If the object is not callable.
+ """
if not callable(_callable):
raise AssertionError("The function supplied is not callable")
@@ -40,21 +49,31 @@ def init_display(
display_triedron: Optional[bool] = True,
background_gradient_color1: Optional[List[int]] = [206, 215, 222],
background_gradient_color2: Optional[List[int]] = [128, 128, 128],
-):
- """This function loads and initialize a GUI using either wx, pyqt5, pyqt6, pyside2 or pyside6.
- If ever the environment variable PYTHONOCC_OFFSCREEN_RENDERER, then the GUI is simply
- ignored and an offscreen renderer is returned.
- init_display returns 4 objects :
- * display : an instance of Viewer3d ;
- * start_display : a function (the GUI mainloop) ;
- * add_menu : a function that creates a menu in the GUI
- * add_function_to_menu : adds a menu option
-
- In case an offscreen renderer is returned, start_display and add_menu are ignored, i.e.
- an empty function is returned (named do_nothing). add_function_to_menu just execute the
- function taken as a parameter.
-
- Note : the offscreen renderer is used on the travis side.
+) -> Tuple[Viewer3d, Callable, Callable, Callable]:
+ """
+ Initializes a GUI for the 3D viewer.
+
+ This function loads and initializes a GUI using either wx, pyqt5, pyqt6,
+ pyside2 or pyside6. If the environment variable
+ PYTHONOCC_OFFSCREEN_RENDERER is set to "1", the GUI is ignored and an
+ offscreen renderer is returned instead.
+
+ Args:
+ backend_str (str, optional): The backend to use. If not specified,
+ it will be automatically detected.
+ size (tuple, optional): The size of the window.
+ display_triedron (bool, optional): Whether to display the triedron.
+ background_gradient_color1 (list, optional): The first color of the
+ background gradient.
+ background_gradient_color2 (list, optional): The second color of the
+ background gradient.
+
+ Returns:
+ A tuple containing:
+ - display: An instance of Viewer3d.
+ - start_display: A function to start the GUI main loop.
+ - add_menu: A function to add a menu to the GUI.
+ - add_function_to_menu: A function to add a function to a menu.
"""
if size is None: # prevent size to being None (mypy)
raise AssertionError("window size cannot be None")
@@ -63,11 +82,11 @@ def init_display(
# create the offscreen renderer
offscreen_renderer = OffscreenRenderer()
- def do_nothing(*kargs: Any, **kwargs: Any) -> None:
+ def do_nothing(*args: Any, **kwargs: Any) -> None:
"""takes as many parameters as you want, and does nothing"""
return None
- def call_function(s, func: Callable) -> None:
+ def call_function(s: str, func: Callable) -> None:
"""A function that calls another function.
Helpful to bypass add_function_to_menu. s should be a string
"""
@@ -86,6 +105,7 @@ def call_function(s, func: Callable) -> None:
# tkinter SimpleGui
if used_backend == "tk":
import tkinter as tk
+
from OCC.Display.tkDisplay import tkViewer3d
root = tk.Tk()
@@ -121,7 +141,7 @@ def add_function_to_menu(menu_name: str, _callable: Callable) -> None:
print("wxPython backend - ", wx.version())
class AppFrame(wx.Frame):
- def __init__(self, parent):
+ def __init__(self, parent: Any) -> None:
wx.Frame.__init__(
self,
parent,
@@ -145,7 +165,7 @@ def add_function_to_menu(self, menu_name: str, _callable: Callable) -> None:
# point on curve
_id = wx.NewId()
check_callable(_callable)
- if not menu_name in self._menus:
+ if menu_name not in self._menus:
raise ValueError(f"the menu item {menu_name} does not exist")
self._menus[menu_name].Append(
_id, _callable.__name__.replace("_", " ").lower()
@@ -160,10 +180,10 @@ def add_function_to_menu(self, menu_name: str, _callable: Callable) -> None:
app.SetTopWindow(win)
display = win.canva._display
- def add_menu(*args, **kwargs) -> None:
+ def add_menu(*args: Any, **kwargs: Any) -> None:
win.add_menu(*args, **kwargs)
- def add_function_to_menu(*args, **kwargs) -> None:
+ def add_function_to_menu(*args: Any, **kwargs: Any) -> None:
win.add_function_to_menu(*args, **kwargs)
def start_display() -> None:
@@ -215,7 +235,7 @@ def add_menu(self, menu_name: str) -> None:
def add_function_to_menu(self, menu_name: str, _callable: Callable) -> None:
check_callable(_callable)
- if not menu_name in self._menus:
+ if menu_name not in self._menus:
raise ValueError(f"the menu item {menu_name} does not exist")
qaction = (
QtGui.QAction
@@ -227,7 +247,7 @@ def add_function_to_menu(self, menu_name: str, _callable: Callable) -> None:
self._menus[menu_name].addAction(_action)
# following couple of lines is a tweak to enable ipython --gui='qt'
- app = QtWidgets.QApplication(sys.argv)
+ app = QtWidgets.QApplication.instance() or QtWidgets.QApplication(sys.argv)
win = MainWindow()
win.resize(size[0] - 1, size[1] - 1)
win.show()
@@ -238,10 +258,10 @@ def add_function_to_menu(self, menu_name: str, _callable: Callable) -> None:
win.canva.qApp = app
display = win.canva._display
- def add_menu(*args, **kwargs) -> None:
+ def add_menu(*args: Any, **kwargs: Any) -> None:
win.add_menu(*args, **kwargs)
- def add_function_to_menu(*args, **kwargs) -> None:
+ def add_function_to_menu(*args: Any, **kwargs: Any) -> None:
win.add_function_to_menu(*args, **kwargs)
def start_display() -> None:
diff --git a/src/Display/SimpleGui.pyi b/src/Display/SimpleGui.pyi
new file mode 100644
index 000000000..01df7cd2d
--- /dev/null
+++ b/src/Display/SimpleGui.pyi
@@ -0,0 +1,12 @@
+from typing import Callable, List, Optional, Tuple
+
+from OCC.Display.OCCViewer import Viewer3d
+
+def check_callable(_callable: Callable) -> None: ...
+def init_display(
+ backend_str: Optional[str] = None,
+ size: Optional[Tuple[int, int]] = (1024, 768),
+ display_triedron: Optional[bool] = True,
+ background_gradient_color1: Optional[List[int]] = [206, 215, 222],
+ background_gradient_color2: Optional[List[int]] = [128, 128, 128],
+) -> Tuple[Viewer3d, Callable, Callable, Callable]: ...
diff --git a/src/Display/WebGl/flask_server.py b/src/Display/WebGl/flask_server.py
index 1a7f86760..c85a3fb91 100644
--- a/src/Display/WebGl/flask_server.py
+++ b/src/Display/WebGl/flask_server.py
@@ -1,9 +1,10 @@
-""" A flask webserver. """
+"""A flask webserver."""
import sys
import uuid
+from typing import Any, Dict, Optional, Tuple
-from OCC.Display.WebGl.threejs_renderer import (
+from threejs_renderer import (
ThreejsRenderer,
OCC_VERSION,
THREEJS_RELEASE,
@@ -14,7 +15,7 @@
from OCC.Core.Tesselator import ShapeTesselator
# Import following for building vertex (or point cloud) in WebGL
-from OCC.Core.gp import gp_Pnt, gp_Vec
+from OCC.Core.gp import gp_Pnt
from OCC.Core.BRep import BRep_Builder
from OCC.Core.TopoDS import TopoDS_Compound
from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_MakeVertex
@@ -22,37 +23,79 @@
from flask import Flask, render_template
-def format_color(r, g, b):
+def format_color(r: int, g: int, b: int) -> str:
+ """
+ Formats a color from RGB to a hex string.
+
+ Args:
+ r (int): The red component (0-255).
+ g (int): The green component (0-255).
+ b (int): The blue component (0-255).
+
+ Returns:
+ str: The color as a hex string.
+ """
return "0x%02x%02x%02x" % (r, g, b)
class RenderWraper(ThreejsRenderer):
+ """
+ A wrapper for the ThreejsRenderer that adds support for Flask.
+ """
+
def __init__(
self,
- path=None,
- default_shape_color=format_color(166, 166, 166), # light grey
- default_edge_color=format_color(32, 32, 32), # dark grey
- default_vertex_color=format_color(8, 8, 8),
- ): # darker gray
+ path: Optional[str] = None,
+ default_shape_color: str = format_color(166, 166, 166), # light grey
+ default_edge_color: str = format_color(32, 32, 32), # dark grey
+ default_vertex_color: str = format_color(8, 8, 8),
+ ) -> None: # darker gray
+ """
+ Initializes the RenderWraper.
+
+ Args:
+ path (str, optional): The path to the templates.
+ default_shape_color (str, optional): The default color for shapes.
+ default_edge_color (str, optional): The default color for edges.
+ default_vertex_color (str, optional): The default color for vertices.
+ """
super().__init__(path)
- self._3js_vertex = {}
+ self._3js_vertex: Dict[str, Any] = {}
self._default_shape_color = default_shape_color
self._default_edge_color = default_edge_color
self._default_vertex_color = default_vertex_color
def convert_shape(
self,
- shape,
- export_edges=False,
- color=(0.65, 0.65, 0.7),
- specular_color=(0.2, 0.2, 0.2),
- shininess=0.9,
- transparency=0.0,
- line_color=(0, 0.0, 0.0),
- line_width=1.0,
- point_size=1.0,
- mesh_quality=1.0,
- ):
+ shape: Any,
+ export_edges: bool = False,
+ color: Tuple[float, float, float] = (0.65, 0.65, 0.7),
+ specular_color: Tuple[float, float, float] = (0.2, 0.2, 0.2),
+ shininess: float = 0.9,
+ transparency: float = 0.0,
+ line_color: Tuple[float, float, float] = (0, 0.0, 0.0),
+ line_width: float = 1.0,
+ point_size: float = 1.0,
+ mesh_quality: float = 1.0,
+ ) -> Tuple[Dict[str, Any], Dict[str, Any], Dict[str, Any]]:
+ """
+ Converts a shape to a format that can be rendered by Three.js.
+
+ Args:
+ shape: The shape to convert.
+ export_edges (bool, optional): Whether to export the edges of the shape.
+ color (tuple, optional): The color of the shape.
+ specular_color (tuple, optional): The specular color of the shape.
+ shininess (float, optional): The shininess of the shape.
+ transparency (float, optional): The transparency of the shape.
+ line_color (tuple, optional): The color of the lines.
+ line_width (float, optional): The width of the lines.
+ point_size (float, optional): The size of the points.
+ mesh_quality (float, optional): The quality of the mesh.
+
+ Returns:
+ A tuple containing the shapes, edges, and vertices.
+ """
# if the shape is an edge or a wire, use the related functions
color = color_to_hex(color)
specular_color = color_to_hex(specular_color)
@@ -140,14 +183,28 @@ def convert_shape(
class RenderConfig:
+ """
+ Configuration for the renderer.
+ """
+
def __init__(
self,
- bg_gradient_color1="#ced7de",
- bg_gradient_color2="#808080",
- vertex_shader=None,
- fragment_shader=None,
- uniforms=None,
- ):
+ bg_gradient_color1: str = "#ced7de",
+ bg_gradient_color2: str = "#808080",
+ vertex_shader: Optional[str] = None,
+ fragment_shader: Optional[str] = None,
+ uniforms: Optional[Dict[str, Any]] = None,
+ ) -> None:
+ """
+ Initializes the RenderConfig.
+
+ Args:
+ bg_gradient_color1 (str, optional): The first color of the background gradient.
+ bg_gradient_color2 (str, optional): The second color of the background gradient.
+ vertex_shader (str, optional): The vertex shader to use.
+ fragment_shader (str, optional): The fragment shader to use.
+ uniforms (dict, optional): The uniforms to use.
+ """
self._occ_version = OCC_VERSION
self._3js_version = THREEJS_RELEASE
self._bg_gradient_color1 = bg_gradient_color1
@@ -166,7 +223,7 @@ def __init__(
@app.route("/")
@app.route("/index")
- def index():
+ def index() -> str:
"""PythonOCC Demo Page"""
# remove shapes from previous (avoid duplicate shape after F5 refresh)
my_ren._3js_shapes = {}
@@ -176,10 +233,13 @@ def index():
# import additional modules for building a box and a torus.
from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeBox, BRepPrimAPI_MakeTorus
from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_Transform
- from OCC.Core.gp import gp_Trsf
+ from OCC.Core.gp import gp_Trsf, gp_Vec
+ from OCC.Core.TopoDS import TopoDS_Shape
import time
- def translate_shp(shp, vec, copy=False):
+ def translate_shp(
+ shp: TopoDS_Shape, vec: gp_Vec, copy: bool = False
+ ) -> TopoDS_Shape:
trns = gp_Trsf()
trns.SetTranslation(vec)
brep_trns = BRepBuilderAPI_Transform(shp, trns, copy)
diff --git a/src/Display/WebGl/flask_server.pyi b/src/Display/WebGl/flask_server.pyi
new file mode 100644
index 000000000..f33aac813
--- /dev/null
+++ b/src/Display/WebGl/flask_server.pyi
@@ -0,0 +1,37 @@
+from typing import Any, Dict, Optional, Tuple
+
+from threejs_renderer import ThreejsRenderer
+
+def format_color(r: int, g: int, b: int) -> str: ...
+
+class RenderWraper(ThreejsRenderer):
+ def __init__(
+ self,
+ path: Optional[str] = None,
+ default_shape_color: str = "0xa6a6a6",
+ default_edge_color: str = "0x202020",
+ default_vertex_color: str = "0x80808",
+ ) -> None: ...
+ def convert_shape(
+ self,
+ shape: Any,
+ export_edges: bool = False,
+ color: Tuple[float, float, float] = (0.65, 0.65, 0.7),
+ specular_color: Tuple[float, float, float] = (0.2, 0.2, 0.2),
+ shininess: float = 0.9,
+ transparency: float = 0.0,
+ line_color: Tuple[float, float, float] = (0.0, 0.0, 0.0),
+ line_width: float = 1.0,
+ point_size: float = 1.0,
+ mesh_quality: float = 1.0,
+ ) -> Tuple[Dict[str, Any], Dict[str, Any], Dict[str, Any]]: ...
+
+class RenderConfig:
+ def __init__(
+ self,
+ bg_gradient_color1: str = "#ced7de",
+ bg_gradient_color2: str = "#808080",
+ vertex_shader: Optional[str] = None,
+ fragment_shader: Optional[str] = None,
+ uniforms: Optional[Dict[str, Any]] = None,
+ ) -> None: ...
diff --git a/src/Display/WebGl/jupyter_renderer.py b/src/Display/WebGl/jupyter_renderer.py
index 254acd556..29a664dc9 100644
--- a/src/Display/WebGl/jupyter_renderer.py
+++ b/src/Display/WebGl/jupyter_renderer.py
@@ -21,6 +21,7 @@
import math
import uuid
import sys
+from typing import Any, Callable, Dict, List, Optional, Tuple, Union
# pythreejs
try:
@@ -88,31 +89,38 @@
#
# Util mathematical functions
#
-def _add(vec1, vec2):
+def _add(vec1: List[float], vec2: List[float]) -> List[float]:
+ """Adds two vectors."""
return [v1 + v2 for v1, v2 in zip(vec1, vec2)]
-def _explode(edge_list):
+def _explode(edge_list: List[List[float]]) -> List[List[List[float]]]:
+ """Explodes a list of edges into a list of segments."""
return [[edge_list[i], edge_list[i + 1]] for i in range(len(edge_list) - 1)]
-def _flatten(nested_dict):
+def _flatten(nested_dict: Dict[Any, Any]) -> List[Any]:
+ """Flattens a nested dictionary."""
return [y for x in nested_dict for y in x]
-def format_color(r, g, b):
+def format_color(r: int, g: int, b: int) -> str:
+ """Formats a color from RGB to a hex string."""
return "#%02x%02x%02x" % (r, g, b)
-def _distance(v1, v2):
+def _distance(v1: List[float], v2: List[float]) -> float:
+ """Computes the distance between two vectors."""
return np.linalg.norm([x - y for x, y in zip(v1, v2)])
-def _bool_or_new(val):
+def _bool_or_new(val: Union[bool, Dict[str, Any]]) -> bool:
+ """Returns the value of a boolean or a new value."""
return val if isinstance(val, bool) else val["new"]
-def _opt(b1, b2):
+def _opt(b1: Tuple[float, ...], b2: Tuple[float, ...]) -> Tuple[float, ...]:
+ """Returns the union of two bounding boxes."""
return (
min(b1[0], b2[0]),
max(b1[1], b2[1]),
@@ -123,12 +131,23 @@ def _opt(b1, b2):
)
-def _shift(v, offset):
+def _shift(v: List[float], offset: List[float]) -> List[float]:
+ """Shifts a vector by an offset."""
return [x + o for x, o in zip(v, offset)]
# https://stackoverflow.com/questions/4947682/intelligently-calculating-chart-tick-positions
-def _nice_number(value, round_=False):
+def _nice_number(value: float, round_: bool = False) -> float:
+ """
+ Returns a "nice" number approximately equal to value.
+
+ Args:
+ value (float): The value to make nice.
+ round_ (bool, optional): Whether to round the number. Defaults to False.
+
+ Returns:
+ float: The nice number.
+ """
exponent = math.floor(math.log(value, 10))
fraction = value / 10**exponent
@@ -153,7 +172,20 @@ def _nice_number(value, round_=False):
return nice_fraction * 10**exponent
-def _nice_bounds(axis_start, axis_end, num_ticks=10):
+def _nice_bounds(
+ axis_start: float, axis_end: float, num_ticks: int = 10
+) -> Tuple[float, float, float]:
+ """
+ Returns "nice" bounds for a given axis.
+
+ Args:
+ axis_start (float): The start of the axis.
+ axis_end (float): The end of the axis.
+ num_ticks (int, optional): The number of ticks. Defaults to 10.
+
+ Returns:
+ A tuple containing the nice start, nice end, and nice tick.
+ """
axis_width = axis_end - axis_start
if axis_width == 0:
nice_tick = 0
@@ -170,20 +202,57 @@ def _nice_bounds(axis_start, axis_end, num_ticks=10):
# Helpers
#
class Helpers:
- def __init__(self, bb_center):
+ """
+ A base class for helpers.
+ """
+
+ def __init__(self, bb_center: Tuple[float, float, float]) -> None:
+ """
+ Initializes the Helpers.
+
+ Args:
+ bb_center: The center of the bounding box.
+ """
self.bb_center = bb_center
self.center = (0, 0, 0)
- def _center(self, zero=True):
+ def _center(self, zero: bool = True) -> Tuple[float, float, float]:
+ """
+ Returns the center of the bounding box.
+
+ Args:
+ zero (bool, optional): Whether to return the origin. Defaults to True.
+
+ Returns:
+ The center of the bounding box.
+ """
return self.center if zero else self.bb_center
- def set_position(self, position):
+ def set_position(self, position: Tuple[float, float, float]) -> None:
+ """
+ Sets the position of the helper.
+
+ Args:
+ position: The position to set.
+ """
raise NotImplementedError()
- def set_visibility(self, change):
+ def set_visibility(self, change: bool) -> None:
+ """
+ Sets the visibility of the helper.
+
+ Args:
+ change: The visibility to set.
+ """
raise NotImplementedError()
- def set_center(self, change):
+ def set_center(self, change: bool) -> None:
+ """
+ Sets the center of the helper.
+
+ Args:
+ change: The center to set.
+ """
self.set_position(self._center(change))
@@ -191,14 +260,28 @@ def set_center(self, change):
# Grid helper
#
class Grid(Helpers):
+ """
+ A grid helper.
+ """
+
def __init__(
self,
- bb_center=None,
- maximum=5,
- ticks=10,
- colorCenterLine="#aaa",
- colorGrid="#ddd",
- ):
+ bb_center: Optional[Tuple[float, float, float]] = None,
+ maximum: int = 5,
+ ticks: int = 10,
+ colorCenterLine: str = "#aaa",
+ colorGrid: str = "#ddd",
+ ) -> None:
+ """
+ Initializes the Grid.
+
+ Args:
+ bb_center (tuple, optional): The center of the bounding box.
+ maximum (int, optional): The maximum size of the grid.
+ ticks (int, optional): The number of ticks in the grid.
+ colorCenterLine (str, optional): The color of the center line.
+ colorGrid (str, optional): The color of the grid.
+ """
Helpers.__init__(self, bb_center)
axis_start, axis_end, nice_tick = _nice_bounds(-maximum, maximum, 2 * ticks)
self.step = nice_tick
@@ -211,13 +294,31 @@ def __init__(
)
self.set_center(True)
- def set_position(self, position):
+ def set_position(self, position: Tuple[float, float, float]) -> None:
+ """
+ Sets the position of the grid.
+
+ Args:
+ position: The position to set.
+ """
self.grid.position = position
- def set_visibility(self, change):
+ def set_visibility(self, change: bool) -> None:
+ """
+ Sets the visibility of the grid.
+
+ Args:
+ change: The visibility to set.
+ """
self.grid.visible = change
- def set_rotation(self, rotation):
+ def set_rotation(self, rotation: Tuple[float, float, float, str]) -> None:
+ """
+ Sets the rotation of the grid.
+
+ Args:
+ rotation: The rotation to set.
+ """
self.grid.rotation = rotation
@@ -225,16 +326,33 @@ def set_rotation(self, rotation):
# Axes helper
#
class Axes(Helpers):
- """X, Y and Z axis
- X is red
- Y is green
- Z is blue
"""
+ An axes helper.
+
+ - X is red
+ - Y is green
+ - Z is blue
+ """
+
+ def __init__(
+ self,
+ bb_center: Tuple[float, float, float],
+ length: int = 1,
+ width: int = 3,
+ display_labels: bool = False,
+ ) -> None:
+ """
+ Initializes the Axes.
- def __init__(self, bb_center, length=1, width=3, display_labels=False):
+ Args:
+ bb_center: The center of the bounding box.
+ length (int, optional): The length of the axes. Defaults to 1.
+ width (int, optional): The width of the axes. Defaults to 3.
+ display_labels (bool, optional): Whether to display labels.
+ """
Helpers.__init__(self, bb_center)
- self.axes = []
+ self.axes: List[Any] = []
self.axes.extend(
LineSegments2(
LineSegmentsGeometry(
@@ -257,11 +375,23 @@ def __init__(self, bb_center, length=1, width=3, display_labels=False):
self.axes.append(y_text)
self.axes.append(z_text)
- def set_position(self, position):
+ def set_position(self, position: Tuple[float, float, float]) -> None:
+ """
+ Sets the position of the axes.
+
+ Args:
+ position: The position to set.
+ """
for i in range(3):
self.axes[i].position = position
- def set_visibility(self, change):
+ def set_visibility(self, change: bool) -> None:
+ """
+ Sets the visibility of the axes.
+
+ Args:
+ change: The visibility to set.
+ """
for i in range(3):
self.axes[i].visible = change
@@ -270,7 +400,17 @@ def set_visibility(self, change):
# Custom Material helper
#
class CustomMaterial(ShaderMaterial):
- def __init__(self, typ):
+ """
+ A custom material helper.
+ """
+
+ def __init__(self, typ: str) -> None:
+ """
+ Initializes the CustomMaterial.
+
+ Args:
+ typ: The type of the material.
+ """
self.types = {
"diffuse": "c",
"uvTransform": "m3",
@@ -306,22 +446,35 @@ def __init__(self, typ):
self.lights = True
@property
- def color(self):
+ def color(self) -> str:
+ """
+ The color of the material.
+ """
return self.uniforms["diffuse"]["value"]
@color.setter
- def color(self, value):
+ def color(self, value: str) -> None:
self.update("diffuse", value)
@property
- def alpha(self):
+ def alpha(self) -> float:
+ """
+ The alpha of the material.
+ """
return self.uniforms["alpha"]["value"]
@alpha.setter
- def alpha(self, value):
+ def alpha(self, value: float) -> None:
self.update("alpha", value)
- def update(self, key, value):
+ def update(self, key: str, value: Any) -> None:
+ """
+ Updates a uniform.
+
+ Args:
+ key: The key of the uniform to update.
+ value: The value to set.
+ """
uniforms = dict(**self.uniforms)
if self.types.get(key) is None:
uniforms[key] = {"value": value}
@@ -335,7 +488,18 @@ def update(self, key, value):
# Bounding Box
#
class BoundingBox:
- def __init__(self, objects, tol=1e-5):
+ """
+ A bounding box helper.
+ """
+
+ def __init__(self, objects: List[Any], tol: float = 1e-5) -> None:
+ """
+ Initializes the BoundingBox.
+
+ Args:
+ objects: The objects to compute the bounding box for.
+ tol (float, optional): The tolerance. Defaults to 1e-5.
+ """
self.tol = tol
bbox = reduce(_opt, [self._bbox(obj) for obj in objects])
@@ -350,7 +514,10 @@ def __init__(self, objects, tol=1e-5):
)
self.max = reduce(lambda a, b: max(abs(a), abs(b)), bbox)
- def _max_dist_from_center(self):
+ def _max_dist_from_center(self) -> float:
+ """
+ Returns the maximum distance from the center.
+ """
return max(
_distance(self.center, v)
for v in itertools.product(
@@ -360,7 +527,10 @@ def _max_dist_from_center(self):
)
)
- def _max_dist_from_origin(self):
+ def _max_dist_from_origin(self) -> float:
+ """
+ Returns the maximum distance from the origin.
+ """
return max(
np.linalg.norm(v)
for v in itertools.product(
@@ -370,17 +540,23 @@ def _max_dist_from_origin(self):
)
)
- def _bounding_box(self, obj, tol=1e-5):
+ def _bounding_box(self, obj: Any, tol: float = 1e-5) -> Tuple[float, ...]:
+ """
+ Computes the bounding box of an object.
+ """
bbox = Bnd_Box()
bbox.SetGap(self.tol)
brepbndlib.Add(obj, bbox, True)
values = bbox.Get()
return (values[0], values[3], values[1], values[4], values[2], values[5])
- def _bbox(self, objects):
+ def _bbox(self, objects: List[Any]) -> Tuple[float, ...]:
+ """
+ Computes the bounding box of a list of objects.
+ """
return reduce(_opt, [self._bounding_box(obj) for obj in objects])
- def __repr__(self):
+ def __repr__(self) -> str:
return "[x(%f .. %f), y(%f .. %f), z(%f .. %f)]" % (
self.xmin,
self.xmax,
@@ -397,32 +573,31 @@ class NORMAL(enum.Enum):
class JupyterRenderer:
+ """
+ A renderer for Jupyter notebooks.
+ """
+
def __init__(
self,
- size=(640, 480),
- compute_normals_mode=NORMAL.SERVER_SIDE,
- default_shape_color=format_color(166, 166, 166), # light grey
- default_edge_color=format_color(32, 32, 32), # dark grey
- default_vertex_color=format_color(8, 8, 8), # darker grey
- pick_color=format_color(232, 176, 36), # orange
- background_color="white",
- ):
- """Creates a jupyter renderer.
- size: a tuple (width, height). Must be a square, or shapes will look like deformed
- compute_normals_mode: optional, set to SERVER_SIDE by default. This flag lets you choose the
- way normals are computed. If SERVER_SIDE is selected (default value), then normals
- will be computed by the Tesselator, packed as a python tuple, and send as a json structure
- to the client. If, on the other hand, CLIENT_SIDE is chose, then the computer only compute vertex
- indices, and let the normals be computed by the client (the web js machine embedded in the webrowser).
-
- * SERVER_SIDE: higher server load, loading time increased, lower client load. Poor performance client will
- choose this option (mobile terminals for instance)
- * CLIENT_SIDE: lower server load, loading time decreased, higher client load. Higher performance clients will
- choose this option (laptops, desktop machines).
- * default_shape_color
- * default_e1dge_color:
- * default_pick_color:
- * background_color:
+ size: Tuple[int, int] = (640, 480),
+ compute_normals_mode: int = NORMAL.SERVER_SIDE,
+ default_shape_color: str = format_color(166, 166, 166), # light grey
+ default_edge_color: str = format_color(32, 32, 32), # dark grey
+ default_vertex_color: str = format_color(8, 8, 8), # darker grey
+ pick_color: str = format_color(232, 176, 36), # orange
+ background_color: str = "white",
+ ) -> None:
+ """
+ Initializes the JupyterRenderer.
+
+ Args:
+ size (tuple, optional): The size of the renderer.
+ compute_normals_mode (NORMAL, optional): The mode for computing normals.
+ default_shape_color (str, optional): The default color for shapes.
+ default_edge_color (str, optional): The default color for edges.
+ default_vertex_color (str, optional): The default color for vertices.
+ pick_color (str, optional): The color for picked objects.
+ background_color (str, optional): The background color.
"""
self._default_shape_color = default_shape_color
self._default_edge_color = default_edge_color
@@ -434,7 +609,9 @@ def __init__(
self._size = size
self._compute_normals_mode = compute_normals_mode
- self._bb = None # the bounding box, necessary to compute camera position
+ self._bb: Optional[BoundingBox] = (
+ None # the bounding box, necessary to compute camera position
+ )
# the default camera object
self._camera_target = [0.0, 0.0, 0.0] # the point to look at
@@ -445,10 +622,10 @@ def __init__(
# a dictionary of all the shapes belonging to the renderer
# each element is a key 'mesh_id:shape'
- self._shapes = {}
+ self._shapes: Dict[str, Any] = {}
# we save the renderer so that is can be accessed
- self._renderer = None
+ self._renderer: Optional[Renderer] = None
# the group of 3d and 2d objects to render
self._displayed_pickable_objects = Group()
@@ -457,15 +634,15 @@ def __init__(
self._displayed_non_pickable_objects = Group()
# event manager/selection manager
- self._picker = None
+ self._picker: Optional[Picker] = None
- self._current_shape_selection = None
- self._current_mesh_selection = None
- self._savestate = None
+ self._current_shape_selection: Optional[Any] = None
+ self._current_mesh_selection: Optional[Mesh] = None
+ self._savestate: Optional[Tuple[Any, Any]] = None
self._selection_color = format_color(232, 176, 36)
- self._select_callbacks = (
+ self._select_callbacks: List[Callable] = (
[]
) # a list of all functions called after an object is selected
@@ -506,7 +683,21 @@ def __init__(
]
self.html = HTML("")
- def create_button(self, description, tooltip, disabled, handler):
+ def create_button(
+ self, description: str, tooltip: str, disabled: bool, handler: Callable
+ ) -> Button:
+ """
+ Creates a button.
+
+ Args:
+ description (str): The description of the button.
+ tooltip (str): The tooltip of the button.
+ disabled (bool): Whether the button is disabled.
+ handler: The handler for the button.
+
+ Returns:
+ The created button.
+ """
button = Button(
disabled=disabled,
tooltip=tooltip,
@@ -516,20 +707,40 @@ def create_button(self, description, tooltip, disabled, handler):
button.on_click(handler)
return button
- def create_checkbox(self, kind, description, value, handler):
+ def create_checkbox(
+ self, kind: str, description: str, value: bool, handler: Callable
+ ) -> Checkbox:
+ """
+ Creates a checkbox.
+
+ Args:
+ kind (str): The kind of the checkbox.
+ description (str): The description of the checkbox.
+ value (bool): The value of the checkbox.
+ handler: The handler for the checkbox.
+
+ Returns:
+ The created checkbox.
+ """
checkbox = Checkbox(value=value, description=description, layout=self.layout)
checkbox.observe(handler, "value")
checkbox.add_class(f"view_{kind}")
return checkbox
- def remove_shape(self, *kargs):
+ def remove_shape(self, *kargs: Any) -> None:
+ """
+ Removes the selected shape.
+ """
self.clicked_obj.visible = not self.clicked_obj.visible
# remove shape from the mapping dict
cur_id = self.clicked_obj.name
del self._shapes[cur_id]
self._remove_shp_button.disabled = True
- def on_compute_change(self, change):
+ def on_compute_change(self, change: Dict[str, Any]) -> None:
+ """
+ Called when the compute dropdown changes.
+ """
if change["type"] != "change" or change["name"] != "value":
return
selection = change["new"]
@@ -617,18 +828,32 @@ def on_compute_change(self, change):
)
self.html.value = output
- def toggle_shape_visibility(self, *kargs):
+ def toggle_shape_visibility(self, *kargs: Any) -> None:
+ """
+ Toggles the visibility of the selected shape.
+ """
self.clicked_obj.visible = not self.clicked_obj.visible
- def toggle_axes_visibility(self, change):
+ def toggle_axes_visibility(self, change: Dict[str, Any]) -> None:
+ """
+ Toggles the visibility of the axes.
+ """
self.axes.set_visibility(_bool_or_new(change))
- def toggle_grid_visibility(self, change):
+ def toggle_grid_visibility(self, change: Dict[str, Any]) -> None:
+ """
+ Toggles the visibility of the grid.
+ """
self.horizontal_grid.set_visibility(_bool_or_new(change))
self.vertical_grid.set_visibility(_bool_or_new(change))
- def click(self, value):
- """called whenever a shape or edge is clicked"""
+ def click(self, value: Any) -> None:
+ """
+ Called whenever a shape or edge is clicked.
+
+ Args:
+ value: The clicked object.
+ """
obj = value.owner.object
self.clicked_obj = obj
if self._current_mesh_selection != obj:
@@ -669,33 +894,56 @@ def click(self, value):
for callback in self._select_callbacks:
callback(self._current_shape_selection)
- def register_select_callback(self, callback):
- """Adds a callback that will be called each time a shape is selected"""
+ def register_select_callback(self, callback: Callable) -> None:
+ """
+ Adds a callback that will be called each time a shape is selected.
+
+ Args:
+ callback: The callback to add.
+ """
if not callable(callback):
raise AssertionError("You must provide a callable to register the callback")
else:
self._select_callbacks.append(callback)
- def unregister_callback(self, callback):
- """Remove a callback from the callback list"""
+ def unregister_callback(self, callback: Callable) -> None:
+ """
+ Removes a callback from the callback list.
+
+ Args:
+ callback: The callback to remove.
+ """
if callback not in self._select_callbacks:
raise AssertionError("This callback is not registered")
else:
self._select_callbacks.remove(callback)
- def GetSelectedShape(self):
- """Returns the selected shape"""
+ def GetSelectedShape(self) -> Any:
+ """
+ Returns the selected shape.
+ """
return self._current_shape_selection
def DisplayShapeAsSVG(
self,
- shp,
- export_hidden_edges=True,
- location=gp_Pnt(0, 0, 0),
- direction=gp_Dir(1, 1, 1),
- color="black",
- line_width=0.5,
- ):
+ shp: Any,
+ export_hidden_edges: bool = True,
+ location: gp_Pnt = gp_Pnt(0, 0, 0),
+ direction: gp_Dir = gp_Dir(1, 1, 1),
+ color: str = "black",
+ line_width: float = 0.5,
+ ) -> None:
+ """
+ Displays a shape as an SVG.
+
+ Args:
+ shp: The shape to display.
+ export_hidden_edges (bool, optional): Whether to export hidden edges.
+ location (gp_Pnt, optional): The location of the camera.
+ direction (gp_Dir, optional): The direction of the camera.
+ color (str, optional): The color of the shape.
+ line_width (float, optional): The width of the lines.
+ """
svg_string = export_shape_to_svg(
shp,
export_hidden_edges=export_hidden_edges,
@@ -711,37 +959,35 @@ def DisplayShapeAsSVG(
def DisplayShape(
self,
- shp,
- shape_color=None,
- render_edges=False,
- edge_color=None,
- edge_deflection=0.05,
- vertex_color=None,
- quality=1.0,
- transparency=False,
- opacity=1.0,
- topo_level="default",
- update=False,
- selectable=True,
- ):
- """Displays a topods_shape in the renderer instance.
- shp: the TopoDS_Shape to render
- shape_color: the shape color, in html corm, eg '#abe000'
- render_edges: optional, False by default. If True, compute and display all
- edges as a linear interpolation of segments.
- edge_color: optional, black by default. The color used for edge rendering,
- in html form eg '#ff00ee'
- edge_deflection: optional, 0.05 by default
- vertex_color: optional
- quality: optional, 1.0 by default. If set to something lower than 1.0,
- mesh will be more precise. If set to something higher than 1.0,
- mesh will be less precise, i.e. lower number of triangles.
- transparency: optional, False by default (opaque).
- opacity: optional, float, by default to 1 (opaque). if transparency is set to True,
- 1. is fully opaque, 0. is fully transparent.
- topo_level: "default" by default. The value should be either "compound", "shape", "vertex".
- update: optional, False by default. If True, render all the shapes.
- selectable: if True, can be doubleclicked from the 3d window
+ shp: Any,
+ shape_color: Optional[str] = None,
+ render_edges: bool = False,
+ edge_color: Optional[str] = None,
+ edge_deflection: float = 0.05,
+ vertex_color: Optional[str] = None,
+ quality: float = 1.0,
+ transparency: bool = False,
+ opacity: float = 1.0,
+ topo_level: str = "default",
+ update: bool = False,
+ selectable: bool = True,
+ ) -> None:
+ """
+ Displays a shape in the renderer.
+
+ Args:
+ shp: The shape to display.
+ shape_color (str, optional): The color of the shape.
+ render_edges (bool, optional): Whether to render the edges.
+ edge_color (str, optional): The color of the edges.
+ edge_deflection (float, optional): The deflection of the edges.
+ vertex_color (str, optional): The color of the vertices.
+ quality (float, optional): The quality of the mesh.
+ transparency (bool, optional): Whether the shape is transparent.
+ opacity (float, optional): The opacity of the shape.
+ topo_level (str, optional): The topological level to display.
+ update (bool, optional): Whether to update the renderer.
+ selectable (bool, optional): Whether the shape is selectable.
"""
if edge_color is None:
edge_color = self._default_edge_color
@@ -750,7 +996,7 @@ def DisplayShape(
if vertex_color is None:
vertex_color = self._default_vertex_color
- output = [] # a list of all geometries created from the shape
+ output: List[Any] = [] # a list of all geometries created from the shape
# is it list of gp_Pnt ?
if isinstance(shp, list) and isinstance(shp[0], gp_Pnt):
result = self.AddVerticesToScene(shp, vertex_color)
@@ -800,9 +1046,21 @@ def DisplayShape(
if update:
self.Display()
- def AddVerticesToScene(self, pnt_list, vertex_color, vertex_width=5):
- """shp is a list of gp_Pnt"""
- vertices_list = [] # will be passed to pythreejs
+ def AddVerticesToScene(
+ self, pnt_list: List[gp_Pnt], vertex_color: str, vertex_width: int = 5
+ ) -> Points:
+ """
+ Adds a list of vertices to the scene.
+
+ Args:
+ pnt_list (list): A list of gp_Pnt objects.
+ vertex_color (str): The color of the vertices.
+ vertex_width (int, optional): The width of the vertices. Defaults to 5.
+
+ Returns:
+ The created Points object.
+ """
+ vertices_list: List[List[float]] = [] # will be passed to pythreejs
BB = BRep_Builder()
compound = TopoDS_Compound()
BB.MakeCompound(compound)
@@ -817,16 +1075,26 @@ def AddVerticesToScene(self, pnt_list, vertex_color, vertex_width=5):
point_cloud_id = f"{uuid.uuid4().hex}"
self._shapes[point_cloud_id] = compound
- vertices_list = np.array(vertices_list, dtype=np.float32)
- attributes = {"position": BufferAttribute(vertices_list, normalized=False)}
+ np_vertices_list = np.array(vertices_list, dtype=np.float32)
+ attributes = {"position": BufferAttribute(np_vertices_list, normalized=False)}
mat = PointsMaterial(
color=vertex_color, sizeAttenuation=True, size=vertex_width
)
geom = BufferGeometry(attributes=attributes)
return Points(geometry=geom, material=mat, name=point_cloud_id)
- def AddCurveToScene(self, shp, edge_color, deflection):
- """shp is either a TopoDS_Wire or a TopodS_Edge."""
+ def AddCurveToScene(self, shp: Any, edge_color: str, deflection: float) -> Line:
+ """
+ Adds a curve to the scene.
+
+ Args:
+ shp: The curve to add.
+ edge_color (str): The color of the curve.
+ deflection (float): The deflection of the curve.
+
+ Returns:
+ The created Line object.
+ """
if is_edge(shp):
pnts = discretize_edge(shp, deflection)
elif is_wire(shp):
@@ -851,15 +1119,31 @@ def AddCurveToScene(self, shp, edge_color, deflection):
def AddShapeToScene(
self,
- shp,
- shape_color=None, # the default
- render_edges=False,
- edge_color=None,
- vertex_color=None,
- quality=1.0,
- transparency=False,
- opacity=1.0,
- ):
+ shp: Any,
+ shape_color: Optional[str] = None, # the default
+ render_edges: bool = False,
+ edge_color: Optional[str] = None,
+ vertex_color: Optional[str] = None,
+ quality: float = 1.0,
+ transparency: bool = False,
+ opacity: float = 1.0,
+ ) -> Any:
+ """
+ Adds a shape to the scene.
+
+ Args:
+ shp: The shape to add.
+ shape_color (str, optional): The color of the shape.
+ render_edges (bool, optional): Whether to render the edges.
+ edge_color (str, optional): The color of the edges.
+ vertex_color (str, optional): The color of the vertices.
+ quality (float, optional): The quality of the mesh.
+ transparency (bool, optional): Whether the shape is transparent.
+ opacity (float, optional): The opacity of the shape.
+
+ Returns:
+ The created Mesh object.
+ """
# first, compute the tessellation
tess = ShapeTesselator(shp)
tess.Compute(compute_edges=render_edges, mesh_quality=quality, parallel=True)
@@ -937,12 +1221,20 @@ def AddShapeToScene(
return shape_mesh
- def _scale(self, vec):
+ def _scale(self, vec: List[float]) -> List[float]:
+ """
+ Scales a vector.
+ """
r = self._bb._max_dist_from_center() * self._camera_distance_factor
n = np.linalg.norm(vec)
return [v / n * r for v in vec]
- def _material(self, color, transparent=False, opacity=1.0):
+ def _material(
+ self, color: str, transparent: bool = False, opacity: float = 1.0
+ ) -> CustomMaterial:
+ """
+ Creates a material.
+ """
# material = MeshPhongMaterial()
material = CustomMaterial("standard")
material.color = color
@@ -958,7 +1250,10 @@ def _material(self, color, transparent=False, opacity=1.0):
material.update("roughness", 0.8)
return material
- def EraseAll(self):
+ def EraseAll(self) -> None:
+ """
+ Erases all shapes from the renderer.
+ """
self._shapes = {}
self._displayed_pickable_objects = Group()
self._current_shape_selection = None
@@ -966,12 +1261,23 @@ def EraseAll(self):
self._current_selection_material = None
self._renderer.scene = Scene(children=[])
- def Display(self, position=None, rotation=None):
+ def Display(
+ self,
+ position: Optional[Tuple[float, float, float]] = None,
+ rotation: Optional[Tuple[float, float, float]] = None,
+ ) -> None:
+ """
+ Displays the renderer.
+
+ Args:
+ position (tuple, optional): The position of the camera.
+ rotation (tuple, optional): The rotation of the camera.
+ """
# Get the overall bounding box
if self._shapes:
- self._bb = BoundingBox([self._shapes.values()])
+ self._bb = BoundingBox(list(self._shapes.values()))
else: # if nothing registered yet, create a fake bb
- self._bb = BoundingBox([[BRepPrimAPI_MakeSphere(5.0).Shape()]])
+ self._bb = BoundingBox([BRepPrimAPI_MakeSphere(5.0).Shape()])
bb_max = self._bb.max
orbit_radius = 1.5 * self._bb._max_dist_from_center()
@@ -1068,18 +1374,30 @@ def Display(self, position=None, rotation=None):
# then display both 3d widgets and webui
display(HBox([VBox([HBox(self._controls), self._renderer]), self.html]))
- def ExportToHTML(self, filename):
+ def ExportToHTML(self, filename: str) -> None:
+ """
+ Exports the renderer to an HTML file.
+
+ Args:
+ filename (str): The name of the file to export to.
+ """
embed.embed_minimal_html(filename, views=self._renderer, title="pythonocc")
- def _reset(self, *kargs):
+ def _reset(self, *kargs: Any) -> None:
+ """
+ Resets the camera.
+ """
self._camera.rotation, self._controller.target = self._savestate
self._camera.position = _add(self._bb.center, self._scale((1, 1, 1)))
self._camera.zoom = self._camera_initial_zoom
self._update()
- def _update(self):
+ def _update(self) -> None:
+ """
+ Updates the controller.
+ """
self._controller.exec_three_obj_method("update")
- def __repr__(self):
+ def __repr__(self) -> str:
self.Display()
return ""
diff --git a/src/Display/WebGl/jupyter_renderer.pyi b/src/Display/WebGl/jupyter_renderer.pyi
new file mode 100644
index 000000000..e1e167108
--- /dev/null
+++ b/src/Display/WebGl/jupyter_renderer.pyi
@@ -0,0 +1,158 @@
+from typing import Any, Callable, Dict, List, Optional, Tuple, Union
+
+from OCC.Core.gp import gp_Dir, gp_Pnt
+from pythreejs import (
+ Button,
+ Checkbox,
+ Line,
+ Points,
+ ShaderMaterial,
+)
+
+def _add(vec1: List[float], vec2: List[float]) -> List[float]: ...
+def _explode(edge_list: List[List[float]]) -> List[List[List[float]]]: ...
+def _flatten(nested_dict: Dict[Any, Any]) -> List[Any]: ...
+def format_color(r: int, g: int, b: int) -> str: ...
+def _distance(v1: List[float], v2: List[float]) -> float: ...
+def _bool_or_new(val: Union[bool, Dict[str, Any]]) -> bool: ...
+def _opt(b1: Tuple[float, ...], b2: Tuple[float, ...]) -> Tuple[float, ...]: ...
+def _shift(v: List[float], offset: List[float]) -> List[float]: ...
+def _nice_number(value: float, round_: bool = False) -> float: ...
+def _nice_bounds(
+ axis_start: float, axis_end: float, num_ticks: int = 10
+) -> Tuple[float, float, float]: ...
+
+class Helpers:
+ def __init__(self, bb_center: Tuple[float, float, float]) -> None: ...
+ def _center(self, zero: bool = True) -> Tuple[float, float, float]: ...
+ def set_position(self, position: Tuple[float, float, float]) -> None: ...
+ def set_visibility(self, change: bool) -> None: ...
+ def set_center(self, change: bool) -> None: ...
+
+class Grid(Helpers):
+ def __init__(
+ self,
+ bb_center: Optional[Tuple[float, float, float]] = None,
+ maximum: int = 5,
+ ticks: int = 10,
+ colorCenterLine: str = "#aaa",
+ colorGrid: str = "#ddd",
+ ) -> None: ...
+ def set_position(self, position: Tuple[float, float, float]) -> None: ...
+ def set_visibility(self, change: bool) -> None: ...
+ def set_rotation(self, rotation: Tuple[float, float, float, str]) -> None: ...
+
+class Axes(Helpers):
+ def __init__(
+ self,
+ bb_center: Tuple[float, float, float],
+ length: int = 1,
+ width: int = 3,
+ display_labels: bool = False,
+ ) -> None: ...
+ def set_position(self, position: Tuple[float, float, float]) -> None: ...
+ def set_visibility(self, change: bool) -> None: ...
+
+class CustomMaterial(ShaderMaterial):
+ def __init__(self, typ: str) -> None: ...
+ @property
+ def color(self) -> str: ...
+ @color.setter
+ def color(self, value: str) -> None: ...
+ @property
+ def alpha(self) -> float: ...
+ @alpha.setter
+ def alpha(self, value: float) -> None: ...
+ def update(self, key: str, value: Any) -> None: ...
+
+class BoundingBox:
+ def __init__(self, objects: List[Any], tol: float = 1e-5) -> None: ...
+ def _max_dist_from_center(self) -> float: ...
+ def _max_dist_from_origin(self) -> float: ...
+ def _bounding_box(self, obj: Any, tol: float = 1e-5) -> Tuple[float, ...]: ...
+ def _bbox(self, objects: List[Any]) -> Tuple[float, ...]: ...
+ def __repr__(self) -> str: ...
+
+class NORMAL:
+ SERVER_SIDE: int
+ CLIENT_SIDE: int
+
+class JupyterRenderer:
+ def __init__(
+ self,
+ size: Tuple[int, int] = (640, 480),
+ compute_normals_mode: int = ...,
+ default_shape_color: str = ...,
+ default_edge_color: str = ...,
+ default_vertex_color: str = ...,
+ pick_color: str = ...,
+ background_color: str = "white",
+ ) -> None: ...
+ def create_button(
+ self, description: str, tooltip: str, disabled: bool, handler: Callable
+ ) -> Button: ...
+ def create_checkbox(
+ self, kind: str, description: str, value: bool, handler: Callable
+ ) -> Checkbox: ...
+ def remove_shape(self, *kargs: Any) -> None: ...
+ def on_compute_change(self, change: Dict[str, Any]) -> None: ...
+ def toggle_shape_visibility(self, *kargs: Any) -> None: ...
+ def toggle_axes_visibility(self, change: Dict[str, Any]) -> None: ...
+ def toggle_grid_visibility(self, change: Dict[str, Any]) -> None: ...
+ def click(self, value: Any) -> None: ...
+ def register_select_callback(self, callback: Callable) -> None: ...
+ def unregister_callback(self, callback: Callable) -> None: ...
+ def GetSelectedShape(self) -> Any: ...
+ def DisplayShapeAsSVG(
+ self,
+ shp: Any,
+ export_hidden_edges: bool = True,
+ location: gp_Pnt = ...,
+ direction: gp_Dir = ...,
+ color: str = "black",
+ line_width: float = 0.5,
+ ) -> None: ...
+ def DisplayShape(
+ self,
+ shp: Any,
+ shape_color: Optional[str] = None,
+ render_edges: bool = False,
+ edge_color: Optional[str] = None,
+ edge_deflection: float = 0.05,
+ vertex_color: Optional[str] = None,
+ quality: float = 1.0,
+ transparency: bool = False,
+ opacity: float = 1.0,
+ topo_level: str = "default",
+ update: bool = False,
+ selectable: bool = True,
+ ) -> None: ...
+ def AddVerticesToScene(
+ self, pnt_list: List[gp_Pnt], vertex_color: str, vertex_width: int = 5
+ ) -> Points: ...
+ def AddCurveToScene(self, shp: Any, edge_color: str, deflection: float) -> Line: ...
+ def AddShapeToScene(
+ self,
+ shp: Any,
+ shape_color: Optional[str] = None,
+ render_edges: bool = False,
+ edge_color: Optional[str] = None,
+ vertex_color: Optional[str] = None,
+ quality: float = 1.0,
+ transparency: bool = False,
+ opacity: float = 1.0,
+ ) -> Any: ...
+ def _scale(self, vec: List[float]) -> List[float]: ...
+ def _material(
+ self, color: str, transparent: bool = False, opacity: float = 1.0
+ ) -> CustomMaterial: ...
+ def EraseAll(self) -> None: ...
+ def Display(
+ self,
+ position: Optional[Tuple[float, float, float]] = None,
+ rotation: Optional[Tuple[float, float, float]] = None,
+ ) -> None: ...
+ def ExportToHTML(self, filename: str) -> None: ...
+ def _reset(self, *kargs: Any) -> None: ...
+ def _update(self) -> None: ...
+ def __repr__(self) -> str: ...
diff --git a/src/Display/WebGl/simple_server.py b/src/Display/WebGl/simple_server.py
index e78a41575..cdaff653c 100644
--- a/src/Display/WebGl/simple_server.py
+++ b/src/Display/WebGl/simple_server.py
@@ -15,7 +15,7 @@
##You should have received a copy of the GNU Lesser General Public License
##along with pythonOCC. If not, see .
-""" A very simple webserver. """
+"""A very simple webserver."""
import os
import socket
@@ -23,14 +23,23 @@
import errno
-def get_available_port(port):
- """sometimes, the python webserver is closed but the
- port is not made available for a further call. So let's find
- any available port to prevent such issue. This function:
- * takes a port number (an integer), above 1024
- * check if it is available
- * if not, take another one
- * returns the port number
+def get_available_port(port: int) -> int:
+ """
+ Gets an available port.
+
+ Sometimes, the python webserver is closed but the port is not made
+ available for a further call. So let's find any available port to
+ prevent such issue. This function:
+ - takes a port number (an integer), above 1024
+ - check if it is available
+ - if not, take another one
+ - returns the port number
+
+ Args:
+ port (int): The port to check.
+
+ Returns:
+ int: An available port.
"""
if port <= 1024:
raise AssertionError("port number should be > 1024")
@@ -52,10 +61,17 @@ def get_available_port(port):
def get_interface_ip(family: socket.AddressFamily) -> str:
- """Get the IP address of an external interface. Used when binding to
- 0.0.0.0 or ::1 to show a more useful URL. Inspire of `werkzeug`.
+ """
+ Get the IP address of an external interface.
- :meta private:
+ Used when binding to 0.0.0.0 or ::1 to show a more useful URL.
+ Inspired by `werkzeug`.
+
+ Args:
+ family (socket.AddressFamily): The address family.
+
+ Returns:
+ str: The IP address.
"""
# arbitrary private address
host = "2001:db8::1" if family == socket.AF_INET6 else "192.0.2.1"
@@ -69,12 +85,23 @@ def get_interface_ip(family: socket.AddressFamily) -> str:
return s.getsockname()[0] # type: ignore
-def start_server(addr="127.0.0.1", port=8080, x3d_path=".", open_webbrowser=False):
- """starts the server if the PYTHONOCC_SHUNT_WEB_SERVER
- env var is not set
- * port: the port number to use (if available) ;
- * path: where thehtml files are located
- * open_webbrower: if True, open the web browser to the correct url
+def start_server(
+ addr: str = "127.0.0.1",
+ port: int = 8080,
+ x3d_path: str = ".",
+ open_webbrowser: bool = False,
+) -> None:
+ """
+ Starts a simple web server.
+
+ The server is started if the PYTHONOCC_SHUNT_WEB_SERVER environment
+ variable is not set.
+
+ Args:
+ addr (str, optional): The address to bind to.
+ port (int, optional): The port to use.
+ x3d_path (str, optional): The path to the HTML files.
+ open_webbrowser (bool, optional): Whether to open a web browser.
"""
if os.getenv("PYTHONOCC_SHUNT_WEB_SERVER") == "1":
return False
@@ -93,10 +120,15 @@ def start_server(addr="127.0.0.1", port=8080, x3d_path=".", open_webbrowser=Fals
httpd = HTTPServer((addr, port), SimpleHTTPRequestHandler)
print(f"\n## Serving {x3d_path} using SimpleHTTPServer")
display_hostname = "localhost"
- if addr == "0.0.0.0": # Did not consider ipv6 `::` because httpd does not support it
+ if (
+ addr == "0.0.0.0"
+ ): # Did not consider ipv6 `::` because httpd does not support it
display_hostname = get_interface_ip(socket.AF_INET)
print(f"## Running on all addresses ({addr})")
- print("## Open your webbrowser at the URL: http://%s:%i" % (display_hostname, port))
+ print(
+ "## Open your webbrowser at the URL: http://%s:%i"
+ % (display_hostname, port)
+ )
# open webbrowser
if open_webbrowser:
webbrowser.open("http://%s:%i" % (display_hostname, port), new=2)
diff --git a/src/Display/WebGl/simple_server.pyi b/src/Display/WebGl/simple_server.pyi
new file mode 100644
index 000000000..6cf35c6d3
--- /dev/null
+++ b/src/Display/WebGl/simple_server.pyi
@@ -0,0 +1,10 @@
+import socket
+
+def get_available_port(port: int) -> int: ...
+def get_interface_ip(family: socket.AddressFamily) -> str: ...
+def start_server(
+ addr: str = "127.0.0.1",
+ port: int = 8080,
+ x3d_path: str = ".",
+ open_webbrowser: bool = False,
+) -> None: ...
diff --git a/src/Display/WebGl/templates/index.html b/src/Display/WebGl/templates/index.html
index ad576aab3..758baa921 100644
--- a/src/Display/WebGl/templates/index.html
+++ b/src/Display/WebGl/templates/index.html
@@ -56,8 +56,6 @@
t view/hide shape
diff --git a/src/Display/WebGl/threejs_renderer.py b/src/Display/WebGl/threejs_renderer.py
index b5422b66f..167905b82 100644
--- a/src/Display/WebGl/threejs_renderer.py
+++ b/src/Display/WebGl/threejs_renderer.py
@@ -1,4 +1,4 @@
-##Copyright 2011-2019 Thomas Paviot (tpaviot@gmail.com)
+##Copyright 2011-2024 Thomas Paviot (tpaviot@gmail.com)
##
##This file is part of pythonOCC.
##
@@ -21,6 +21,7 @@
import sys
import tempfile
import uuid
+from typing import Any, Dict, Generator, List, Optional, Tuple
from OCC.Core.gp import gp_Vec
from OCC.Core.Tesselator import ShapeTesselator
@@ -30,14 +31,23 @@
from OCC.Display.WebGl.simple_server import start_server
-def spinning_cursor():
+def spinning_cursor() -> Generator[str, None, None]:
+ """
+ A spinning cursor generator.
+ """
while True:
yield from "|/-\\"
-def color_to_hex(rgb_color):
- """Takes a tuple with 3 floats between 0 and 1.
- Returns a hex. Useful to convert occ colors to web color code
+def color_to_hex(rgb_color: Tuple[float, float, float]) -> str:
+ """
+ Converts a color from RGB to a hex string.
+
+ Args:
+ rgb_color (tuple): A tuple of 3 floats (R, G, B) between 0 and 1.
+
+ Returns:
+ str: The color as a hex string.
"""
r, g, b = rgb_color
if not (0 <= r <= 1.0 and 0 <= g <= 1.0 and 0 <= b <= 1.0):
@@ -48,8 +58,17 @@ def color_to_hex(rgb_color):
return "0x%.02x%.02x%.02x" % (rh, gh, bh)
-def export_edgedata_to_json(edge_hash, point_set):
- """Export a set of points to a LineSegment buffergeometry"""
+def export_edgedata_to_json(edge_hash: str, point_set: List[List[float]]) -> str:
+ """
+ Exports a set of points to a LineSegment buffergeometry.
+
+ Args:
+ edge_hash (str): The hash of the edge.
+ point_set (list): A list of points.
+
+ Returns:
+ str: The JSON string.
+ """
# first build the array of point coordinates
# edges are built as follows:
# points_coordinates =[P0x, P0y, P0z, P1x, P1y, P1z, P2x, P2y, etc.]
@@ -168,9 +187,8 @@ def export_edgedata_to_json(edge_hash, point_set):
"""
import * as THREE from 'three';
import { TrackballControls } from 'three/addons/controls/TrackballControls.js';
-import Stats from 'three/addons/libs/stats.module.js'
-var camera, scene, renderer, object, stats, container, shape_material;
+var camera, scene, renderer, object, container, shape_material;
var controls;
var directionalLight;
var axisHelper, gridHelper;
@@ -202,21 +220,21 @@ def export_edgedata_to_json(edge_hash, point_set):
camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 1, 200);
camera.position.z = 100;
- //controls = new THREE.OrbitControls(camera);
- //controls = new THREE.OrbitControls(camera);
- // for selection
+
raycaster = new THREE.Raycaster();
mouse = new THREE.Vector2();
- // create scene
+
scene = new THREE.Scene();
- scene.add(new THREE.AmbientLight(0x101010));
- directionalLight = new THREE.DirectionalLight(0xffffff);
- directionalLight.position.x = 1;
- directionalLight.position.y = -1;
- directionalLight.position.z = 2;
- directionalLight.position.normalize();
+ scene.background = new THREE.Color(0xf0f0f0);
+ const ambientLight = new THREE.AmbientLight(0x404040, 1.5);
+ scene.add(ambientLight);
+
+ directionalLight = new THREE.DirectionalLight(0xffffff, 1);
+ directionalLight.position.set(1, 1, 1).normalize();
scene.add(directionalLight);
- light1 = new THREE.PointLight(0xffffff);
+
+ light1 = new THREE.PointLight(0xffffff, 0.8);
+ light1.position.set(50, 50, 50);
scene.add(light1);
$Uniforms
@@ -231,39 +249,41 @@ def export_edgedata_to_json(edge_hash, point_set):
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio( window.devicePixelRatio );
container.appendChild(renderer.domElement);
- //renderer.gammaInput = true;
- //renderer.gammaOutput = true;
- // for shadow rendering
+
+ // shadow rendering
renderer.shadowMap.enabled = true;
- renderer.shadowMap.type = THREE.PCFShadowMap;
+ renderer.shadowMap.type = THREE.PCFSoftShadowMap;
+
+ // tone mapping
+ renderer.toneMapping = THREE.ACESFilmicToneMapping;
+ renderer.toneMappingExposure = 1.0;
+ renderer.outputColorSpace = THREE.SRGBColorSpace;
+
controls = new TrackballControls(camera, renderer.domElement);
- // show stats, is it really useful ?
- stats = new Stats();
- stats.domElement.style.position = 'absolute';
- stats.domElement.style.top = '2%';
- stats.domElement.style.left = '1%';
- container.appendChild(stats.domElement);
- // add events
+
document.addEventListener('keypress', onDocumentKeyPress, false);
document.addEventListener('click', onDocumentMouseClick, false);
window.addEventListener('resize', onWindowResize, false);
}
+
function animate() {
requestAnimationFrame(animate);
controls.update();
render();
- stats.update();
}
+
function update_lights() {
if (directionalLight != undefined) {
directionalLight.position.copy(camera.position);
}
}
+
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
+
function onDocumentKeyPress(event) {
event.preventDefault();
if (event.key=="t") { // t key
@@ -283,6 +303,7 @@ def export_edgedata_to_json(edge_hash, point_set):
}
}
}
+
function onDocumentMouseClick(event) {
event.preventDefault();
mouse.x = ( event.clientX / window.innerWidth ) * 2 - 1;
@@ -306,11 +327,13 @@ def export_edgedata_to_json(edge_hash, point_set):
selected_target = target;
}
}
+
function fit_to_scene() {
// compute bounding sphere of whole scene
var center = new THREE.Vector3(0,0,0);
var radiuses = new Array();
var positions = new Array();
+
// compute center of all objects
scene.traverse(function(child) {
if (child instanceof THREE.Mesh) {
@@ -323,9 +346,11 @@ def export_edgedata_to_json(edge_hash, point_set):
radiuses.push(radius);
}
});
+
if (radiuses.length > 0) {
center.divideScalar(radiuses.length*0.7);
}
+
var maxRad = 1.;
// compute bounding radius
for (var ichild = 0; ichild < radiuses.length; ++ichild) {
@@ -335,6 +360,7 @@ def export_edgedata_to_json(edge_hash, point_set):
maxRad = totalDist;
}
}
+
maxRad = maxRad * 0.7; // otherwise the scene seems to be too far away
camera.lookAt(center);
var direction = new THREE.Vector3().copy(camera.position).sub(controls.target);
@@ -348,18 +374,21 @@ def export_edgedata_to_json(edge_hash, point_set):
var pnew = new THREE.Vector3().copy(center).add(direction);
// change near far values to avoid culling of objects
camera.position.set(pnew.x, pnew.y, pnew.z);
- camera.far = lnew*50;
- camera.near = lnew*50*0.001;
+ camera.far = lnew * 50;
+ camera.near = lnew * 50 * 0.001;
camera.updateProjectionMatrix();
controls.target = center;
controls.update();
+
// adds and adjust a grid helper if needed
gridHelper = new THREE.GridHelper(maxRad*4, 10)
scene.add(gridHelper);
+
// axisHelper
axisHelper = new THREE.AxesHelper(maxRad);
scene.add(axisHelper);
}
+
function render() {
//@IncrementTime@ TODO UNCOMMENT
update_lights();
@@ -370,11 +399,27 @@ def export_edgedata_to_json(edge_hash, point_set):
class HTMLHeader:
- def __init__(self, bg_gradient_color1="#ced7de", bg_gradient_color2="#808080"):
+ """
+ A class to generate the HTML header.
+ """
+
+ def __init__(
+ self, bg_gradient_color1: str = "#ced7de", bg_gradient_color2: str = "#808080"
+ ) -> None:
+ """
+ Initializes the HTMLHeader.
+
+ Args:
+ bg_gradient_color1 (str, optional): The first color of the background gradient.
+ bg_gradient_color2 (str, optional): The second color of the background gradient.
+ """
self._bg_gradient_color1 = bg_gradient_color1
self._bg_gradient_color2 = bg_gradient_color2
- def get_str(self):
+ def get_str(self) -> str:
+ """
+ Returns the HTML header as a string.
+ """
return HEADER_TEMPLATE.substitute(
{
"bg_gradient_color1": f"{self._bg_gradient_color1}",
@@ -384,79 +429,57 @@ def get_str(self):
)
-# class HTMLBody_Part1:
-# def __init__(self, vertex_shader=None, fragment_shader=None, uniforms=None):
-# self._vertex_shader = vertex_shader
-# self._fragment_shader = fragment_shader
-# self._uniforms = uniforms
-
-# def get_str(self):
-# global BODY_TEMPLATE_PART2
-# # get the location where pythonocc is running from
-# body_str = BODY_TEMPLATE_PART1.replace("@VERSION@", VERSION)
-# if self._fragment_shader is not None:
-# vertex_shader_string_definition = f''
-# fragment_shader_string_definition = f''
-# shader_material_definition = """
-# var vertexShader = document.getElementById('vertexShader').textContent;
-# var fragmentShader = document.getElementById('fragmentShader').textContent;
-# var shader_material = new THREE.ShaderMaterial({uniforms: uniforms,
-# vertexShader: vertexShader,
-# fragmentShader: fragmentShader});
-# """
-# if self._uniforms is None:
-# body_str = body_str.replace("@Uniforms@", "uniforms ={};\n")
-# BODY_TEMPLATE_PART2 = BODY_TEMPLATE_PART2.replace("@IncrementTime@", "")
-# else:
-# body_str = body_str.replace("@Uniforms@", self._uniforms)
-# if "time" in self._uniforms:
-# BODY_TEMPLATE_PART2 = BODY_TEMPLATE_PART2.replace(
-# "@IncrementTime@", "uniforms.time.value += 0.05;"
-# )
-# else:
-# BODY_TEMPLATE_PART2 = BODY_TEMPLATE_PART2.replace("@IncrementTime@", "")
-# body_str = body_str.replace(
-# "@VertexShaderDefinition@", vertex_shader_string_definition
-# )
-# body_str = body_str.replace(
-# "@FragmentShaderDefinition@", fragment_shader_string_definition
-# )
-# body_str = body_str.replace(
-# "@ShaderMaterialDefinition@", shader_material_definition
-# )
-# body_str = body_str.replace("@ShapeMaterial@", "shader_material")
-# else:
-# body_str = body_str.replace("@Uniforms@", "")
-# body_str = body_str.replace("@VertexShaderDefinition@", "")
-# body_str = body_str.replace("@FragmentShaderDefinition@", "")
-# body_str = body_str.replace("@ShaderMaterialDefinition@", "")
-# body_str = body_str.replace("@ShapeMaterial@", "phong_material")
-# body_str = body_str.replace("@IncrementTime@", "")
-# return body_str
+class ThreejsRenderer:
+ """
+ A renderer that uses three.js to display shapes in a web browser.
+ """
+ def __init__(self, path: Optional[str] = None) -> None:
+ """
+ Initializes the ThreejsRenderer.
-class ThreejsRenderer:
- def __init__(self, path=None):
+ Args:
+ path (str, optional): The path to the directory where the HTML
+ and JavaScript files will be created. If not specified, a
+ temporary directory will be created.
+ """
self._path = tempfile.mkdtemp() if not path else path
self._html_filename = os.path.join(self._path, "index.html")
self._main_js_filename = os.path.join(self._path, "main.js")
- self._3js_shapes = {}
- self._3js_edges = {}
+ self._3js_shapes: Dict[str, Any] = {}
+ self._3js_edges: Dict[str, Any] = {}
self.spinning_cursor = spinning_cursor()
- print(f"## threejs renderer")
+ print("## threejs renderer")
def DisplayShape(
self,
- shape,
- export_edges=False,
- color=(0.65, 0.65, 0.7),
- specular_color=(0.2, 0.2, 0.2),
- shininess=0.9,
- transparency=0.0,
- line_color=(0, 0.0, 0.0),
- line_width=1.0,
- mesh_quality=1.0,
- ):
+ shape: Any,
+ export_edges: bool = False,
+ color: Tuple[float, float, float] = (0.65, 0.65, 0.7),
+ specular_color: Tuple[float, float, float] = (0.2, 0.2, 0.2),
+ shininess: float = 0.9,
+ transparency: float = 0.0,
+ line_color: Tuple[float, float, float] = (0, 0.0, 0.0),
+ line_width: float = 1.0,
+ mesh_quality: float = 1.0,
+ ) -> Tuple[Dict[str, Any], Dict[str, Any]]:
+ """
+ Displays a shape.
+
+ Args:
+ shape: The shape to display.
+ export_edges (bool, optional): Whether to export the edges of the shape.
+ color (tuple, optional): The color of the shape.
+ specular_color (tuple, optional): The specular color of the shape.
+ shininess (float, optional): The shininess of the shape.
+ transparency (float, optional): The transparency of the shape.
+ line_color (tuple, optional): The color of the lines.
+ line_width (float, optional): The width of the lines.
+ mesh_quality (float, optional): The quality of the mesh.
+
+ Returns:
+ A tuple containing the shapes and edges.
+ """
# if the shape is an edge or a wire, use the related functions
if is_edge(shape):
print("discretize an edge")
@@ -506,7 +529,6 @@ def DisplayShape(
line_width,
]
# generate the mesh
- # tess.ExportShapeToThreejs(shape_hash, shape_full_path)
# and also to JSON
with open(shape_full_path, "w") as json_file:
json_file.write(tess.ExportShapeToThreejsJSONString(shape_uuid))
@@ -533,8 +555,10 @@ def DisplayShape(
self._3js_edges[edge_hash] = [(0, 0, 0), line_width]
return self._3js_shapes, self._3js_edges
- def generate_html_file(self):
- """Generate the HTML file to be rendered by the web browser"""
+ def generate_html_file(self) -> None:
+ """
+ Generates the HTML file to be rendered by the web browser.
+ """
global BODY_TEMPLATE
# loop over shapes to generate html shapes stuff
# the following line is a list that will help generating the string
@@ -559,6 +583,7 @@ def generate_html_file(self):
f"specular:{color_to_hex(specular_color)},",
"shininess:%g," % shininess,
"side: THREE.DoubleSide,",
+ "flatShading:false,",
)
)
if transparency > 0.0:
@@ -606,11 +631,6 @@ def generate_html_file(self):
"ShaderMaterialDefinition": "",
}
)
- # fp.write(HTMLBody_Part1().get_str())
- # fp.write("".join(shape_string_list))
- # fp.write("".join(edge_string_list))
- ## then write header part 2
- # fp.write(BODY_TEMPLATE_PART2)
fp.write(main_js)
# write the index.html file
@@ -630,8 +650,20 @@ def generate_html_file(self):
fp.write(body)
fp.write("