diff --git a/README.md b/README.md index 1517e222..e1a1bae7 100644 --- a/README.md +++ b/README.md @@ -39,14 +39,14 @@ pip install -U vtkplotter *Windows-10 users* can place this file [vtkplotter.bat](https://github.com/marcomusy/vtkplotter-examples/blob/master/vtkplotter.bat) on the desktop to *drag&drop* files to visualize. -(Need to edit the path of the local Anaconda installation). +(Need to edit the path of the local python installation). ## 📙 Documentation Automatically generated documentation can be found [**here**](https://vtkplotter.embl.es). -#### 📌 Need help? +##### 📌 Need help? Have any question, or wish to suggest or ask for a missing feature? Do not hesitate to open a [**issue**](https://github.com/marcomusy/vtkplotter-examples/issues) or send an [email](mailto:marco.musy@embl.es). @@ -56,12 +56,13 @@ or send an [email](mailto:marco.musy@embl.es). Intuitive and straightforward API which can be combined with VTK seamlessly in a program, whilst mantaining access to the full range of VTK native classes. -It includes a [**large set of working examples**](https://github.com/marcomusy/vtkplotter-examples/tree/master/vtkplotter_examples) -for all the following functionalities: - - - Import meshes from VTK format, STL, Wavefront OBJ, 3DS, Dolfin-XML, Neutral, GMSH, OFF, PCD (PointCloud), volumetric TIFF stacks, DICOM, SLC, MHD, 2D images PNG, JPEG. - - Export meshes as ASCII or binary to VTK, STL, OBJ, PLY formats. - - Mesh analysis through the built-in methods of VTK. Additional analysis tools like *Moving Least Squares*, mesh morphing and more.. +It includes a **[large set of working examples](https://github.com/marcomusy/vtkplotter-examples/tree/master/vtkplotter_examples)** +for a wide range of functionalities *(click triangle to expand...)*: +
+*working with polygonal meshes and point clouds* + - Import meshes from VTK format, STL, Wavefront OBJ, 3DS, Dolfin-XML, Neutral, GMSH, OFF, PCD (PointCloud), + - Export meshes as ASCII or binary to VTK, STL, OBJ, PLY ... formats. + - Analysis tools like *Moving Least Squares*, mesh morphing and more.. - Tools to visualize and edit meshes (cutting a mesh with another mesh, slicing, normalizing, moving vertex positions, etc..). - Split mesh based on surface connectivity. Extract the largest connected area. - Calculate areas, volumes, center of mass, average sizes etc. @@ -79,32 +80,53 @@ for all the following functionalities: - Generate meshes by joining nearby lines in space. - Find the closest path from one point to another, travelling along the edges of a mesh. - Find the intersection of a mesh with lines, planes or other meshes. + - Interpolate scalar and vectorial fields with *Radial Basis Functions* and *Thin Plate Splines*. + - Add sliders and buttons to interact with the scene and the individual objects. + - Visualization of tensors. - Analysis of *Point Clouds*: - *Moving Least Squares* smoothing of 2D, 3D and 4D clouds - Fit lines, planes, spheres and ellipses in space - Identify outliers in a distribution of points - Decimate a cloud to a uniform distribution. - - Histogramming and function plotting in 1D and 2D. - - Interpolate scalar and vectorial fields with *Radial Basis Functions* and *Thin Plate Splines*. - - Analysis of volumetric datasets: - - Isosurfacing of volumes - - Composite and maximum projection volumetric rendering - - Generate volumetric signed-distance data from an input surface mesh - - Probe a volume with lines and planes - - Generate stream-lines and stream-tubes from vectorial fields - - Add sliders and buttons to interact with the scene and the individual objects. - - Fully customizable axis style. - - Visualization of tensors. - - Draw `latex`-formatted formulas in the rending window. - - Integration with the *Qt5* framework. - - Examples using [SHTools](https://shtools.oca.eu/shtools) package for *spherical harmonics* expansion of a mesh shape. - - Support for [FEniCS/Dolfin](https://fenicsproject.org/) platform for visualization of finite-element calculations. - - Interoperability with the [trimesh](https://trimsh.org/) library. - - Export a 3D scene and embed it into a [web page](https://vtkplotter.embl.es/examples/fenics_elasticity.html). - - Embed the 3D rendering in a *jupyter* notebook with [K3D](https://github.com/K3D-tools/K3D-jupyter) (can export an interactive 3D-snapshot page [here](https://vtkplotter.embl.es/examples/K3D_snapshot.html)). - - -### ➜ Command Line Interface +
+
+*working with volumetric data* + - Import data from VTK format volumetric TIFF stacks, DICOM, SLC, MHD and more + - Import 2D images as PNG, JPEG, BMP + - Isosurfacing of volumes + - Composite and maximum projection volumetric rendering + - Generate volumetric signed-distance data from an input surface mesh + - Probe a volume with lines and planes + - Generate stream-lines and stream-tubes from vectorial fields + - Slice and crop volumes + - Support for other volumetric structures (structured and grid data) +
+
+*plotting and histogramming* + - Fully customizable axis styles + - 'donut' plots and pie charts + - Scatter plots in 2D and 3D + - Surface function plotting + - 1D customizable histograms + - 2D hexagonal histograms + - Polar plots and histogramming + - Draw `latex`-formatted formulas in the rendering window. + - Quiver plots + - Point markers analogous to `matplotlib` + +
+ + +Moreover: +- Integration with the *Qt5* framework. +- Examples using [SHTools](https://shtools.oca.eu/shtools) package for *spherical harmonics* expansion of a mesh shape. +- Support for [FEniCS/Dolfin](https://fenicsproject.org/) platform for visualization of finite-element calculations. +- Interoperability with the [trimesh](https://trimsh.org/) library. +- Export a 3D scene and embed it into a [web page](https://vtkplotter.embl.es/examples/fenics_elasticity.html). +- Embed the 3D rendering in a *jupyter* notebook with [K3D](https://github.com/K3D-tools/K3D-jupyter) (can export an interactive 3D-snapshot page [here](https://vtkplotter.embl.es/examples/K3D_snapshot.html)). + + +## ⌨ Command Line Interface Visualize a mesh from a terminal window with: ```bash vtkplotter mesh.obj @@ -112,7 +134,7 @@ vtkplotter mesh.obj # pcd,xyz,txt,byu,tif,off,slc,vti,mhd,dcm,dem,nrrd,nii,bmp,png,jpg] ``` Voxel-data (_mhd, vti, slc, tiff, dicom etc.._) files can be visualized with options `-g`. E.g.:
-`vtkplotter -g examples/data/embryo.slc`
+`vtkplotter -g embryo.slc`
![isohead](https://user-images.githubusercontent.com/32848391/58336107-5a09a180-7e43-11e9-8c4e-b50e4e95ae71.gif) @@ -123,7 +145,7 @@ To visualize multiple files or files time-sequences try `-n` or `-s` options. Us |`vtkplotter head.vti` |`vtkplotter -s *.vtk` |`vtkplotter `
`--slicer embr.slc` | `vtkplotter --lego embryo.slc`| |![isohead](https://user-images.githubusercontent.com/32848391/56972083-a7f3f800-6b6a-11e9-9cb3-1047b69dcad2.gif)| ![viz_raycast](https://user-images.githubusercontent.com/32848391/58336919-f7b1a080-7e44-11e9-9106-f574371093a8.gif) | ![viz_slicer](https://user-images.githubusercontent.com/32848391/56972084-a7f3f800-6b6a-11e9-98c4-dc4ffec70a5e.png) |![lego](https://user-images.githubusercontent.com/32848391/56969949-71b47980-6b66-11e9-8251-4bbdb275cb22.jpg) | -### ➜ Graphic User Interface +### 🖥 Graphic User Interface A Graphic User Interface is available (mainly useful to *Windows 10* users): ![gui](https://user-images.githubusercontent.com/32848391/63259840-c861d280-c27f-11e9-9c2a-99d0fae85313.png) @@ -135,7 +157,7 @@ pip install -U git+https://github.com/marcomusy/vtkplotter-examples vtkplotter --list vtkplotter --run tube.py ``` -**More than 280 working examples can be found in directories** _(scroll down to see thumbnails):_
+**More than 300 working examples can be found in directories** _(scroll down to see thumbnails):_
[**examples/basic**](https://github.com/marcomusy/vtkplotter-examples/blob/master/vtkplotter_examples/basic)
[**examples/advanced**](https://github.com/marcomusy/vtkplotter-examples/blob/master/vtkplotter_examples/advanced)
[**examples/volumetric**](https://github.com/marcomusy/vtkplotter-examples/blob/master/vtkplotter_examples/volumetric)
diff --git a/bin/vtkplotter b/bin/vtkplotter index d82e3d7e..35468d66 100755 --- a/bin/vtkplotter +++ b/bin/vtkplotter @@ -1044,6 +1044,22 @@ else: ######################################################################### scs.append(lb+bn.replace('.py','')) printc("".join(scs), c='y', bold=0) + printc("Simulation examples:", c='m', bold=1, underline=1) + scs = [] + for f,bn in exfiles: + if "simulation" in f: + lb = ' ' if (len(scs)+1)%nl else '\n' + scs.append(lb+bn.replace('.py','')) + printc("".join(scs), c='m', bold=0) + + printc("Plotting 2D examples:", c='w', bold=1, underline=1) + scs = [] + for f,bn in exfiles: + if "plotting2d" in f: + lb = ' ' if (len(scs)+1)%nl else '\n' + scs.append(lb+bn.replace('.py','')) + printc("".join(scs), c='w', bold=0) + printc("Volumetric examples:", c='b', bold=1, underline=1) scs = [] for f,bn in exfiles: diff --git a/docs/README.rst b/docs/README.rst index b732c9ac..af1bd038 100644 --- a/docs/README.rst +++ b/docs/README.rst @@ -20,13 +20,13 @@ .. image:: https://img.shields.io/badge/docs%20by-gendocs-blue.svg :target: https://gendocs.readthedocs.io/en/latest/ :alt: Documentation Built by gendocs - + .. image:: https://zenodo.org/badge/DOI/10.5281/zenodo.2561402.svg :target: https://doi.org/10.5281/zenodo.2561402 - + --------------------- -A lightweight python module for scientific visualization, analysis and animation of 3D objects +A lightweight python module for scientific visualization, analysis and animation of 3D objects and `point clouds` based on `VTK `_ and `numpy `_. @@ -52,18 +52,18 @@ Check out the **Git repository** here: https://github.com/marcomusy/vtkplotter-e *Windows-10 users* can manually place this file `vtkplotter.bat `_ -on the desktop to *drag&drop* files to visualize. +on the desktop to *drag&drop* files to visualize. (Need to edit the path of their local Anaconda installation). Features: --------- -Intuitive and straightforward API which can be combined with VTK seamlessly +Intuitive and straightforward API which can be combined with VTK seamlessly in a program, whilst mantaining access to the full range of VTK native classes. -It includes a -`large set of working examples `_ +It includes a +`large set of working examples `_ for the all following functionalities: - Import meshes from VTK format, STL, Wavefront OBJ, 3DS, XML, Neutral, GMSH, PCD (PointCloud), volumetric TIFF stacks, SLC, MHD, 2D images PNG, JPEG. @@ -76,7 +76,7 @@ for the all following functionalities: - Subdivide faces of a mesh, increasing the number of vertex points. Mesh simplification. - Coloring and thresholding of meshes based on associated scalar or vectorial data. - Point-surface operations: find nearest points, check if a point lies inside or outside a mesh. -- Create primitive objects like: spheres, arrows, cubes, torus, ellipsoids... +- Create primitive objects like: spheres, arrows, cubes, torus, ellipsoids... - Generate *glyphs* (associating a mesh to each vertex of a source mesh). - Create animations easily by just defining the position of the displayed objects in the 3D scene. Add trailing lines to moving objects automatically. - Straightforward support for multiple `sync-ed` or independent renderers in the same window. @@ -87,7 +87,7 @@ for the all following functionalities: - Find the closest path from one point to another, travelling along the edges of a mesh. - Find the intersection of a mesh with a line (or with another mesh). - Analysis of `Point Clouds`: - + - `Moving Least Squares` smoothing of 2D, 3D and 4D clouds - Fit lines, planes and spheres in space - Perform PCA (Principal Component Analysis) on point coordinates @@ -122,14 +122,14 @@ In your python script, load a simple ``3DS`` file and display it: .. code-block:: python from vtkplotter import show - - show('flamingo.3ds') + + show('flamingo.3ds') .. image:: https://user-images.githubusercontent.com/32848391/50738813-58af4380-11d8-11e9-84ce-53579c1dba65.png :alt: flam Allowed input objects to the ``show()`` command are: \ :raw-html-m2r:`
` -``filename``, ``vtkPolyData``, ``vtkActor``, +``filename``, ``vtkPolyData``, ``vtkActor``, ``vtkActor2D``, ``vtkImageActor``, ``vtkAssembly`` or ``vtkVolume``. @@ -167,7 +167,7 @@ Load and browse a sequence of meshes: .. code-block:: bash - vtkplotter -s examples/data/timecourse1d/*.vtk + vtkplotter -s examples/data/timecourse1d/*.vtk .. image:: https://user-images.githubusercontent.com/32848391/58336919-f7b1a080-7e44-11e9-9106-f574371093a8.gif @@ -203,7 +203,7 @@ More than 280 examples can be found in directories: Apply a *Moving Least Squares* algorithm to obtain a smooth surface from a to a -large cloud of scattered points in space +large cloud of scattered points in space (`moving_least_squares2D.py `_): .. image:: https://user-images.githubusercontent.com/32848391/50738808-5816ad00-11d8-11e9-9854-c952be6fb941.jpg @@ -211,7 +211,7 @@ large cloud of scattered points in space :alt: rabbit -Simulation of a gyroscope hanging from a spring +Simulation of a gyroscope hanging from a spring (`gyroscope1.py `_): .. image:: https://user-images.githubusercontent.com/32848391/39766016-85c1c1d6-52e3-11e8-8575-d167b7ce5217.gif @@ -219,7 +219,7 @@ Simulation of a gyroscope hanging from a spring :alt: gyro -Quantum-tunnelling effect integrating the Schroedinger equation with 4th order Runge-Kutta method. +Quantum-tunnelling effect integrating the Schroedinger equation with 4th order Runge-Kutta method. The animation shows the evolution of a particle in a box hitting a sinusoidal potential barrier (`tunnelling2.py `_): @@ -238,7 +238,7 @@ Visualizing a Turing system of reaction-diffusion between two molecules -Support for the `FEniCS/dolfin `_ platform for visualization of PDE and +Support for the `FEniCS/dolfin `_ platform for visualization of PDE and finite element solutions (`see here `_. @@ -249,13 +249,11 @@ finite element solutions Mesh format conversion ^^^^^^^^^^^^^^^^^^^^^^ -The command ``vtkconvert`` can be used to convert multiple files from a format to a different one: +The command ``vtkplotter-convert`` can be used to convert multiple files from a format to a different one: .. code-block:: bash - Usage: vtkconvert [-h] [-to] [files [files ...]] + Usage: vtkplotter-convert [-h] [-to] [files [files ...]] allowed targets formats: [vtk, vtp, vtu, vts, ply, stl, byu, xml] - Example: > vtkconvert myfile.vtk -to ply - - + Example: > vtkplotter-convert myfile.vtk -to ply diff --git a/docs/logos/fenics_mesh.py b/docs/logos/fenics_mesh.py index 8ea4d4c2..b4ff43d8 100644 --- a/docs/logos/fenics_mesh.py +++ b/docs/logos/fenics_mesh.py @@ -9,12 +9,12 @@ show(tf, bx) printc('implicitModeller', mytext, "this takes time") -imp = implicitModeller(mergeActors(tf, bx), +imp = implicitModeller(mergeActors(tf, bx), distance=0.04, - outer=True, -# res=[50,20,10], + outer=True, +# res=[50,20,10], res=[110,40,20], - bounds=[-1.0, 10.0, -1.0, 2.0, -.5, .5], + bounds=[-1.0, 10.0, -1.0, 2.0, -.5, .5], maxdist=0.25, ) diff --git a/docs/news.txt b/docs/news.txt index b74734e7..bb84ca13 100644 --- a/docs/news.txt +++ b/docs/news.txt @@ -1,3 +1,22 @@ +2020/01/20 +- added shapes Arrow2D and Arrows2D, SphericGrid, CubicGrid +- completely revised customizable axes=1 type + - axes can be created for each object independently +- quiver plots, scatter plots, error bands plots +- plotting in spherical coordinates +- '2d' backend to plot static images in notebooks. +Available backends: +```python +from vtkplotter import * +# embedWindow('2d') +# embedWindow('k3d') +# embedWindow('itk') +# embedWindow('panel') +# embedWindow(False) +``` +- Added wavefront format .OBJ writer + + 2020/01/08 - added type axes=11 (horizontal grid) - fix bug by @m-albert on Volume from numpy object @@ -15,6 +34,7 @@ - added advanced/centerline.py examples - bump version to 2020.0.1 + 2019/11/19 - added PDV paraview file reading. - PDB protein data bank file reader diff --git a/setup.py b/setup.py index eff0bd9a..2895d5f5 100644 --- a/setup.py +++ b/setup.py @@ -36,8 +36,8 @@ 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7' - 'Programming Language :: Python :: 3.8' + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', ], include_package_data=True ) @@ -56,10 +56,14 @@ # cd ~/Projects/vtkplotter-examples/vtkplotter_examples # ./run_all.sh +# cd ~/Projects/vtkplotter/ # python prove/test_filetypes.py +# cd ~/Projects/vtkplotter/tests/common +# ./run_all.sh + # check vtkconvert: -# vtkconvert data/290.vtk -to ply; vtkplotter data/290.ply +# vtkplotter-convert data/290.vtk -to ply; vtkplotter data/290.ply # check on python2 the same stuff is ok # cd ~/Projects/vtkplotter/ @@ -119,7 +123,7 @@ # git clone https://github.com/marcomusy/vtkplotter.git # cd vtkplotter # pip3 -v install . --user -# +# # cd # pip3 install git+https://github.com/FEniCS/fiat.git --upgrade # pip3 install git+https://github.com/FEniCS/ufl.git --upgrade diff --git a/tests/dolfin/test_demo_cahn-hilliard.py b/tests/dolfin/test_demo_cahn-hilliard.py index aad5afbb..3827059b 100644 --- a/tests/dolfin/test_demo_cahn-hilliard.py +++ b/tests/dolfin/test_demo_cahn-hilliard.py @@ -85,7 +85,7 @@ def J(self, A, x): assemble(self.a, tensor=A) solver.parameters["convergence_criterion"] = "incremental" solver.parameters["relative_tolerance"] = 1e-6 -# Step in time +# Step in time t = 0 T = 10*dt scalarbar = False @@ -108,7 +108,7 @@ def J(self, A, x): assemble(self.a, tensor=A) lighting='plastic', offscreen=1) break - + ################################################################################# actor = settings.plotter_instance.actors[0] diff --git a/vtkplotter/__init__.py b/vtkplotter/__init__.py index 2349e3cf..3f76fd23 100644 --- a/vtkplotter/__init__.py +++ b/vtkplotter/__init__.py @@ -8,8 +8,8 @@ A full list of examples can be found in directories: - - `examples/basic `_ - - `examples/advanced `_ + - `examples/basic `_ + - `examples/advanced `_ - `examples/volumetric `_ - `examples/simulations `_ - `examples/plotting2d `_ @@ -43,6 +43,7 @@ from vtkplotter.utils import * from vtkplotter.colors import * import vtkplotter.settings as settings +import vtkplotter.addons as addons from vtkplotter.settings import datadir, embedWindow # hack: need to uncomment this to generate documentation html diff --git a/vtkplotter/addons.py b/vtkplotter/addons.py index c087595c..54d38927 100644 --- a/vtkplotter/addons.py +++ b/vtkplotter/addons.py @@ -5,7 +5,7 @@ from vtkplotter.colors import printc, getColor from vtkplotter.assembly import Assembly from vtkplotter.mesh import Mesh -from vtkplotter.utils import precision, mag, isSequence +from vtkplotter.utils import precision, mag, isSequence, linInterpolate import vtkplotter.shapes as shapes import vtkplotter.settings as settings import vtkplotter.docs as docs @@ -28,7 +28,6 @@ "addButton", "addCutterTool", "addIcon", - "addAxes", "addLegend", ] @@ -235,7 +234,7 @@ def addScalarBar3D( return None else: lut = cmap - vmin,vmax = obj.mapper().GetScalarRange() + vmin, vmax = obj.mapper().GetScalarRange() elif isSequence(obj): vmin, vmax = np.min(obj), np.max(obj) @@ -694,8 +693,564 @@ def computeVisibleBounds(): sizes = np.array([max_bns[1]-min_bns[0], max_bns[3]-min_bns[2], max_bns[5]-min_bns[4]]) return vbb, sizes, min_bns, max_bns + ##################################################################### -def addAxes(axtype=None, c=None): +def buildAxes(obj, + xtitle="x", ytitle="y", ztitle="z", + c=None, + numberOfDivisions=None, + limitRatio=0.04, + axesLineWidth=1, + gridLineWidth=1, + reorientShortTitle=True, + titleDepth=0, + xTitlePosition=0.95, yTitlePosition=0.95, zTitlePosition=0.95, + xTitleOffset=0.05, yTitleOffset=0.05, zTitleOffset=0.05, + xTitleJustify="top-right", yTitleJustify="bottom-right", zTitleJustify="bottom-right", + xTitleRotation=0, yTitleRotation=90, zTitleRotation=135, + xTitleSize=0.025, yTitleSize=0.025, zTitleSize=0.025, + xTitleColor=None, yTitleColor=None, zTitleColor=None, + xTitleBackfaceColor=None, yTitleBackfaceColor=None, zTitleBackfaceColor=None, + xKeepAspectRatio=True, yKeepAspectRatio=True, zKeepAspectRatio=True, + xyGrid=True, yzGrid=True, zxGrid=False, + xyGrid2=False, yzGrid2=False, zxGrid2=False, + xyGridTransparent=False, yzGridTransparent=False, zxGridTransparent=False, + xyGrid2Transparent=False, yzGrid2Transparent=False, zxGrid2Transparent=False, + xyPlaneColor=None, yzPlaneColor=None, zxPlaneColor=None, + xyGridColor=None, yzGridColor=None, zxGridColor=None, + xyAlpha=0.05, yzAlpha=0.05, zxAlpha=0.05, + xLineColor=None, yLineColor=None, zLineColor=None, + xOriginMarkerSize=0, yOriginMarkerSize=0, zOriginMarkerSize=0, + xHighlightZero=False, yHighlightZero=False, zHighlightZero=False, + xHighlightZeroColor="red", yHighlightZeroColor="green", zHighlightZeroColor="blue", + showTicks=True, + xTickRadius=0.005, yTickRadius=0.005, zTickRadius=0.005, + xTickThickness=0.0025, yTickThickness=0.0025, zTickThickness=0.0025, + xTickColor=None, yTickColor=None, zTickColor=None, + xMinorTicks=True, yMinorTicks=True, zMinorTicks=True, + tipSize=0.01, + xLabelSize=0.0175, yLabelSize=0.0175, zLabelSize=0.0175, + xLabelOffset=0.015, yLabelOffset=0.015, zLabelOffset=0.01, + xPositionsAndLabels=None, yPositionsAndLabels=None, zPositionsAndLabels=None, + useGlobal=False, + ): + """Draw axes on a ``Mesh`` or ``Volume``. + Returns a ``Assembly`` object. + + - `xtitle`, ['x'], x-axis title text. + - `ytitle`, ['y'], y-axis title text. + - `ztitle`, ['z'], z-axis title text. + - `numberOfDivisions`,[None], approximate number of divisions on the longest axis + - `axesLineWidth`, [1], width of the axes lines + - `gridLineWidth`, [1], width of the grid lines + - `reorientShortTitle`, [True], titles shorter than 2 letter are placed horizontally + - `originMarkerSize`, [0.01], draw a small cube on the axis where the origin is + - `titleDepth`, [0], extrusion fractional depth of title text + - `xyGrid`, [True], show a gridded wall on plane xy + - `yzGrid`, [True], show a gridded wall on plane yz + - `zxGrid`, [True], show a gridded wall on plane zx + - `zxGrid2`, [False], show zx plane on opposite side of the bounding box + - `xyGridTransparent` [False], make grid plane completely transparent + - `xyGrid2Transparent` [False], make grid plane completely transparent on opposite side box + - `xyPlaneColor`, ['gray'], color of the plane + - `xyGridColor`, ['gray'], grid line color + - `xyAlpha`, [0.15], grid plane opacity + - `showTicks`, [True], show major ticks + - `xTitlePosition`, [0.32], title fractional positions along axis + - `xTitleOffset`, [0.05], title fractional offset distance from axis line + - `xTitleJustify`, ["top-right"], title justification + - `xTitleRotation`, [0], add a rotation of the axis title + - `xLineColor`, [automatic], color of the x-axis + - `xTitleColor`, [automatic], color of the axis title + - `xTitleBackfaceColor`, [None], color of axis title on its backface + - `xTitleSize`, [0.025], size of the axis title + - `xHighlightZero`, [True], draw a line highlighting zero position if in range + - `xHighlightZeroColor`, [automatic], color of the line highlighting the zero position + - `xTickRadius`, [0.005], radius of the major ticks + - `xTickThickness`, [0.0025], thickness of the major ticks along their axis + - `xTickColor`, [automatic], color of major ticks + - `xMinorTicks`, [1], number of minor ticks between two major ticks + - `tipSize`, [0.01], size of the arrow tip + - `xPositionsAndLabels` [], assign custom tick positions and labels [(pos1, label1), ...] + - `xLabelPrecision`, [2], nr. of significative digits to be shown + - `xLabelSize`, [0.015], size of the numeric labels along axis + - `xLabelOffset`, [0.025], offset of numeric labels + - `limitRatio`, [0.04], below this ratio don't plot small axis + + :Example: + + .. code-block:: python + + from vtkplotter import Box, show + + b = Box(pos=(1,2,3), length=8, width=9, height=7).alpha(0) + bax = buildAxes(b, c='white') # returns Assembly object + + show(b, bax) + + |customAxes| |customAxes.py|_ + """ + if c is None: # automatic black or white + c = (0.9, 0.9, 0.9) + vp = settings.plotter_instance + if vp and vp.renderer: + bgcol = vp.renderer.GetBackground() + else: + bgcol = obj.color() + if np.sum(bgcol) > 1.5: + c = (0.1, 0.1, 0.1) + else: + c = getColor(c) + + if useGlobal: + vbb, ss, min_bns, max_bns = computeVisibleBounds() + else: + vbb = obj.GetBounds() + ss = np.array([vbb[1]-vbb[0], vbb[3]-vbb[2], vbb[5]-vbb[4]]) + min_bns = vbb + max_bns = vbb + + ssmax = max(ss) + if not ssmax: + return + if ss[0]/ssmax < limitRatio: + ss[0] = 0 + xtitle = '' + if ss[1]/ssmax < limitRatio: + ss[1] = 0 + ytitle = '' + if ss[2]/ssmax < limitRatio: + ss[2] = 0 + ztitle = '' + + if not xTitleColor: xTitleColor = c + if not yTitleColor: yTitleColor = c + if not zTitleColor: zTitleColor = c + if not xyPlaneColor: xyPlaneColor = c + if not yzPlaneColor: yzPlaneColor = c + if not zxPlaneColor: zxPlaneColor = c + if not xyGridColor: xyGridColor = c + if not yzGridColor: yzGridColor = c + if not zxGridColor: zxGridColor = c + if not xLineColor: xLineColor = c + if not yLineColor: yLineColor = c + if not zLineColor: zLineColor = c + if not xTickColor: xTickColor = xLineColor + if not yTickColor: yTickColor = yLineColor + if not zTickColor: zTickColor = zLineColor + if settings.useDepthPeeling: + xyGrid = False + yzGrid = False + zxGrid = False + xyGrid2 = False + yzGrid2 = False + zxGrid2 = False + + ####################################### + def make_ticks(x0, x1, N, labels=None): + + if x1<=x0: + return [], [] + + ticks_str, ticks_float = [], [] + + if labels is not None: + # user is passing custom labels + ticks_float.append(0) + ticks_str.append('') + for tp, ts in labels: + if tp == x1: + continue + ticks_str.append(ts) + tickn = linInterpolate(tp, [x0,x1], [0,1]) + ticks_float.append(tickn) + + else: + # automatic + if 0<=x0<10: + lowBound = 0 + elif -10 limitRatio or sizes[2]/sizes[0] > limitRatio): - sizes[0] = 0 - xtitle = '' - if sizes[1] and (sizes[0]/sizes[1] > limitRatio or sizes[2]/sizes[1] > limitRatio): - sizes[1] = 0 - ytitle = '' - if sizes[2] and (sizes[0]/sizes[2] > limitRatio or sizes[1]/sizes[2] > limitRatio): - sizes[2] = 0 - ztitle = '' - rats = [] - if sizes[0]: rats += [sizes[1]/sizes[0], sizes[2]/sizes[0]] - if sizes[1]: rats += [sizes[0]/sizes[1], sizes[2]/sizes[1]] - if sizes[2]: rats += [sizes[0]/sizes[2], sizes[1]/sizes[2]] - if not len(rats): - return - rats = max(rats) - if rats == 0: - return - - nrDiv = max(1, int(6.5/rats)) - - numberOfDivisions = axes.pop('numberOfDivisions', nrDiv) # number of divisions on the shortest axis - - axesLineWidth = axes.pop('axesLineWidth', 1) - gridLineWidth = axes.pop('gridLineWidth', 1) - reorientShortTitle = axes.pop('reorientShortTitle', True) - originMarkerSize = axes.pop('originMarkerSize', 0) - enableLastLabel = axes.pop('enableLastLabel', False) - titleDepth = axes.pop('titleDepth', 0) - - xTitlePosition = axes.pop('xTitlePosition', 0.95) # title fractional positions along axis [0->1] - yTitlePosition = axes.pop('yTitlePosition', 0.95) - zTitlePosition = axes.pop('zTitlePosition', 0.95) - - xTitleOffset = axes.pop('xTitleOffset', 0.05) # title fractional offsets - yTitleOffset = axes.pop('yTitleOffset', 0.05) # - zTitleOffset = axes.pop('zTitleOffset', 0.05) # - - xTitleJustify = axes.pop('xTitleJustify', "top-right") - yTitleJustify = axes.pop('yTitleJustify', "bottom-right") - zTitleJustify = axes.pop('zTitleJustify', "bottom-right") - - xTitleRotation = axes.pop('xTitleRotation', 0) - yTitleRotation = axes.pop('yTitleRotation', 90) - zTitleRotation = axes.pop('zTitleRotation', 135) - - xTitleSize = axes.pop('xTitleSize', 0.025) - yTitleSize = axes.pop('yTitleSize', 0.025) - zTitleSize = axes.pop('zTitleSize', 0.025) - - xTitleColor = axes.pop('xTitleColor', c) - yTitleColor = axes.pop('yTitleColor', c) - zTitleColor = axes.pop('zTitleColor', c) - - xTitleBackfaceColor = axes.pop('xTitleBackfaceColor', None) - yTitleBackfaceColor = axes.pop('yTitleBackfaceColor', None) - zTitleBackfaceColor = axes.pop('zTitleBackfaceColor', None) - - xKeepAspectRatio = axes.pop('xKeepAspectRatio', True) - yKeepAspectRatio = axes.pop('yKeepAspectRatio', True) - zKeepAspectRatio = axes.pop('zKeepAspectRatio', True) - - xyGrid = axes.pop('xyGrid', True) - yzGrid = axes.pop('yzGrid', True) - zxGrid = axes.pop('zxGrid', False) - xyGrid2 = axes.pop('xyGrid2', False) # opposite side grid - yzGrid2 = axes.pop('yzGrid2', False) - zxGrid2 = axes.pop('zxGrid2', False) - if settings.plotter_instance and settings.plotter_instance.renderer.GetUseDepthPeeling(): - xyGrid = False - yzGrid = False - zxGrid = False - xyGrid2 = False - yzGrid2 = False - zxGrid2 = False - - xyGridTransparent = axes.pop('xyGridTransparent', False) - yzGridTransparent = axes.pop('yzGridTransparent', False) - zxGridTransparent = axes.pop('zxGridTransparent', False) - xyGrid2Transparent = axes.pop('xyGrid2Transparent', False) - yzGrid2Transparent = axes.pop('yzGrid2Transparent', False) - zxGrid2Transparent = axes.pop('zxGrid2Transparent', False) - - xyPlaneColor = axes.pop('xyPlaneColor', c) - yzPlaneColor = axes.pop('yzPlaneColor', c) - zxPlaneColor = axes.pop('zxPlaneColor', c) - xyGridColor = axes.pop('xyGridColor', c) - yzGridColor = axes.pop('yzGridColor', c) - zxGridColor = axes.pop('zxGridColor', c) - xyAlpha = axes.pop('xyAlpha', 0.05) - yzAlpha = axes.pop('yzAlpha', 0.05) - zxAlpha = axes.pop('zxAlpha', 0.05) - - xLineColor = axes.pop('xLineColor', c) - yLineColor = axes.pop('yLineColor', c) - zLineColor = axes.pop('zLineColor', c) - - xHighlightZero = axes.pop('xHighlightZero', False) - yHighlightZero = axes.pop('yHighlightZero', False) - zHighlightZero = axes.pop('zHighlightZero', False) - xHighlightZeroColor = axes.pop('xHighlightZeroColor', 'red') - yHighlightZeroColor = axes.pop('yHighlightZeroColor', 'green') - zHighlightZeroColor = axes.pop('zHighlightZeroColor', 'blue') - - showTicks = axes.pop('showTicks', True) - xTickRadius = axes.pop('xTickRadius', 0.005) - yTickRadius = axes.pop('yTickRadius', 0.005) - zTickRadius = axes.pop('zTickRadius', 0.005) - - xTickThickness = axes.pop('xTickThickness', 0.0025) - yTickThickness = axes.pop('yTickThickness', 0.0025) - zTickThickness = axes.pop('zTickThickness', 0.0025) - - xTickColor = axes.pop('xTickColor', xLineColor) - yTickColor = axes.pop('yTickColor', yLineColor) - zTickColor = axes.pop('zTickColor', zLineColor) - - xMinorTicks = axes.pop('xMinorTicks', 1) - yMinorTicks = axes.pop('yMinorTicks', 1) - zMinorTicks = axes.pop('zMinorTicks', 1) - - tipSize = axes.pop('tipSize', 0.01) - - xLabelPrecision = axes.pop('xLabelPrecision', 2) # nr. of significant digits - yLabelPrecision = axes.pop('yLabelPrecision', 2) - zLabelPrecision = axes.pop('zLabelPrecision', 2) - - xLabelSize = axes.pop('xLabelSize', 0.0175) - yLabelSize = axes.pop('yLabelSize', 0.0175) - zLabelSize = axes.pop('zLabelSize', 0.0175) - - xLabelOffset = axes.pop('xLabelOffset', 0.015) - yLabelOffset = axes.pop('yLabelOffset', 0.015) - zLabelOffset = axes.pop('zLabelOffset', 0.01) - - ######################## - step = np.min(sizes[np.nonzero(sizes)]) / numberOfDivisions - rx, ry, rz = np.rint(sizes / step).astype(int) - if rx==0: xtitle='' - if ry==0: ytitle='' - if rz==0: ztitle='' - - if enableLastLabel: - enableLastLabel = 1 + vp.axes.update({'useGlobal':True}) + asse = buildAxes(None, **vp.axes) else: - enableLastLabel = 0 - - ################################################ axes lines - lines = [] - if xtitle: lines.append(shapes.Line([0, 0, 0], [1, 0, 0], c=xLineColor, lw=axesLineWidth)) - if ytitle: lines.append(shapes.Line([0, 0, 0], [0, 1, 0], c=yLineColor, lw=axesLineWidth)) - if ztitle: lines.append(shapes.Line([0, 0, 0], [0, 0, 1], c=zLineColor, lw=axesLineWidth)) - - ################################################ grid planes - grids = [] - if xyGrid and xtitle and ytitle: - gxy = shapes.Grid(pos=(0.5, 0.5, 0), normal=[0, 0, 1], resx=rx, resy=ry) - gxy.alpha(xyAlpha).wireframe(xyGridTransparent).c(xyPlaneColor).lw(gridLineWidth).lc(xyGridColor) - grids.append(gxy) - if yzGrid and ytitle and ztitle: - gyz = shapes.Grid(pos=(0, 0.5, 0.5), normal=[1, 0, 0], resx=rz, resy=ry) - gyz.alpha(yzAlpha).wireframe(yzGridTransparent).c(yzPlaneColor).lw(gridLineWidth).lc(yzGridColor) - grids.append(gyz) - if zxGrid and ztitle and xtitle: - gzx = shapes.Grid(pos=(0.5, 0, 0.5), normal=[0, 1, 0], resx=rz, resy=rx) - gzx.alpha(zxAlpha).wireframe(zxGridTransparent).c(zxPlaneColor).lw(gridLineWidth).lc(zxGridColor) - grids.append(gzx) - - grids2 = [] - if xyGrid2 and xtitle and ytitle: - gxy2 = shapes.Grid(pos=(0.5, 0.5, 1), normal=[0, 0, 1], resx=rx, resy=ry) - gxy2.alpha(xyAlpha).wireframe(xyGrid2Transparent).c(xyPlaneColor).lw(gridLineWidth).lc(xyGridColor) - grids2.append(gxy2) - if yzGrid2 and ytitle and ztitle: - gyz2 = shapes.Grid(pos=(1, 0.5, 0.5), normal=[1, 0, 0], resx=rz, resy=ry) - gyz2.alpha(yzAlpha).wireframe(yzGrid2Transparent).c(yzPlaneColor).lw(gridLineWidth).lc(yzGridColor) - grids2.append(gyz2) - if zxGrid2 and ztitle and xtitle: - gzx2 = shapes.Grid(pos=(0.5, 1, 0.5), normal=[0, 1, 0], resx=rz, resy=rx) - gzx2.alpha(zxAlpha).wireframe(zxGrid2Transparent).c(zxPlaneColor).lw(gridLineWidth).lc(zxGridColor) - grids2.append(gzx2) - - - ################################################ zero lines highlights - highlights = [] - if xyGrid and xtitle and ytitle: - if xHighlightZero and min_bns[0] <= 0 and max_bns[1] > 0: - xhl = -min_bns[0] / sizes[0] - hxy = shapes.Line([xhl,0,0], [xhl,1,0], c=xHighlightZeroColor) - hxy.alpha(np.sqrt(xyAlpha)).lw(gridLineWidth*2) - highlights.append(hxy) - if yHighlightZero and min_bns[2] <= 0 and max_bns[3] > 0: - yhl = -min_bns[2] / sizes[1] - hyx = shapes.Line([0,yhl,0], [1,yhl,0], c=yHighlightZeroColor) - hyx.alpha(np.sqrt(yzAlpha)).lw(gridLineWidth*2) - highlights.append(hyx) - - if yzGrid and ytitle and ztitle: - if yHighlightZero and min_bns[2] <= 0 and max_bns[3] > 0: - yhl = -min_bns[2] / sizes[1] - hyz = shapes.Line([0,yhl,0], [0,yhl,1], c=yHighlightZeroColor) - hyz.alpha(np.sqrt(yzAlpha)).lw(gridLineWidth*2) - highlights.append(hyz) - if zHighlightZero and min_bns[4] <= 0 and max_bns[5] > 0: - zhl = -min_bns[4] / sizes[2] - hzy = shapes.Line([0,0,zhl], [0,1,zhl], c=zHighlightZeroColor) - hzy.alpha(np.sqrt(yzAlpha)).lw(gridLineWidth*2) - highlights.append(hzy) - - if zxGrid and ztitle and xtitle: - if zHighlightZero and min_bns[4] <= 0 and max_bns[5] > 0: - zhl = -min_bns[4] / sizes[2] - hzx = shapes.Line([0,0,zhl], [1,0,zhl], c=zHighlightZeroColor) - hzx.alpha(np.sqrt(zxAlpha)).lw(gridLineWidth*2) - highlights.append(hzx) - if xHighlightZero and min_bns[0] <= 0 and max_bns[1] > 0: - xhl = -min_bns[0] / sizes[0] - hxz = shapes.Line([xhl,0,0], [xhl,0,1], c=xHighlightZeroColor) - hxz.alpha(np.sqrt(zxAlpha)).lw(gridLineWidth*2) - highlights.append(hxz) - - ################################################ aspect ratio scales - x_aspect_ratio_scale=1 - y_aspect_ratio_scale=1 - z_aspect_ratio_scale=1 - if xtitle: - if sizes[0] > sizes[1]: - x_aspect_ratio_scale = (1, sizes[0]/sizes[1], 1) - else: - x_aspect_ratio_scale = (sizes[1]/sizes[0], 1, 1) - - if ytitle: - if sizes[0] > sizes[1]: - y_aspect_ratio_scale = (sizes[0]/sizes[1], 1, 1) - else: - y_aspect_ratio_scale = (1, sizes[1]/sizes[0], 1) - - if ztitle: - smean = (sizes[0]+sizes[1])/2 - if smean: - if sizes[2] > smean: - zarfact = smean/sizes[2] - z_aspect_ratio_scale = (zarfact, zarfact*sizes[2]/smean, zarfact) - else: - z_aspect_ratio_scale = (smean/sizes[2], 1, 1) - - ################################################ axes titles - titles = [] - if xtitle: - xt = shapes.Text(xtitle, pos=(0,0,0), s=xTitleSize, bc=xTitleBackfaceColor, - c=xTitleColor, justify=xTitleJustify, depth=titleDepth) - if reorientShortTitle and len(ytitle) < 3: # title is short - wpos = [xTitlePosition, -xTitleOffset +0.02, 0] - else: - wpos = [xTitlePosition, -xTitleOffset, 0] - if xKeepAspectRatio: xt.SetScale(x_aspect_ratio_scale) - xt.RotateX(xTitleRotation) - xt.pos(wpos) - titles.append(xt.lighting(specular=0, diffuse=0, ambient=1)) + asse = buildAxes(None, useGlobal=True) - if ytitle: - yt = shapes.Text(ytitle, pos=(0, 0, 0), s=yTitleSize, bc=yTitleBackfaceColor, - c=yTitleColor, justify=yTitleJustify, depth=titleDepth) - if reorientShortTitle and len(ytitle) < 3: # title is short - wpos = [-yTitleOffset +0.03-0.01*len(ytitle), yTitlePosition, 0] - if yKeepAspectRatio: yt.SetScale(x_aspect_ratio_scale) #x! - else: - wpos = [-yTitleOffset, yTitlePosition, 0] - if yKeepAspectRatio: yt.SetScale(y_aspect_ratio_scale) - yt.RotateZ(yTitleRotation) - yt.pos(wpos) - titles.append(yt.lighting(specular=0, diffuse=0, ambient=1)) - - if ztitle: - zt = shapes.Text(ztitle, pos=(0, 0, 0), s=zTitleSize, bc=zTitleBackfaceColor, - c=zTitleColor, justify=zTitleJustify, depth=titleDepth) - if reorientShortTitle and len(ztitle) < 3: # title is short - wpos = [(-zTitleOffset+0.02-0.003*len(ztitle))/1.42, - (-zTitleOffset+0.02-0.003*len(ztitle))/1.42, zTitlePosition] - if zKeepAspectRatio: - zr2 = (z_aspect_ratio_scale[1], z_aspect_ratio_scale[0], z_aspect_ratio_scale[2]) - zt.SetScale(zr2) - zt.RotateX(90) - zt.RotateY(45) - zt.pos(wpos) - else: - if zKeepAspectRatio: zt.SetScale(z_aspect_ratio_scale) - wpos = [-zTitleOffset/1.42, -zTitleOffset/1.42, zTitlePosition] - zt.RotateY(-90) - zt.RotateX(zTitleRotation) - zt.pos(wpos) - titles.append(zt.lighting(specular=0, diffuse=0, ambient=1)) - - ################################################ cube origin ticks - originmarks = [] - if originMarkerSize: - if xtitle: - if min_bns[0] <= 0 and max_bns[1] > 0: # mark x origin - ox = shapes.Cube([-min_bns[0] / sizes[0], 0, 0], side=originMarkerSize, c=xLineColor) - originmarks.append(ox.lighting(specular=0, diffuse=0, ambient=1)) - - if ytitle: - if min_bns[2] <= 0 and max_bns[3] > 0: # mark y origin - oy = shapes.Cube([0, -min_bns[2] / sizes[1], 0], side=originMarkerSize, c=yLineColor) - originmarks.append(oy.lighting(specular=0, diffuse=0, ambient=1)) - - if ztitle: - if min_bns[4] <= 0 and max_bns[5] > 0: # mark z origin - oz = shapes.Cube([0, 0, -min_bns[4] / sizes[2]], side=originMarkerSize, c=zLineColor) - originmarks.append(oz.lighting(specular=0, diffuse=0, ambient=1)) - - ################################################ arrow cone - cones = [] - if tipSize: - if xtitle: - cx = shapes.Cone((1,0,0), r=tipSize, height=tipSize*2, axis=(1,0,0), c=xLineColor, res=10) - cones.append(cx.lighting(specular=0, diffuse=0, ambient=1)) - if ytitle: - cy = shapes.Cone((0,1,0), r=tipSize, height=tipSize*2, axis=(0,1,0), c=yLineColor, res=10) - cones.append(cy.lighting(specular=0, diffuse=0, ambient=1)) - if ztitle: - cz = shapes.Cone((0,0,1), r=tipSize, height=tipSize*2, axis=(0,0,1), c=zLineColor, res=10) - cones.append(cz.lighting(specular=0, diffuse=0, ambient=1)) - - ################################################ cylindrical ticks - ticks = [] - if showTicks: - if xtitle: - for coo in range(1 ,rx): - v = [coo/rx,0,0] - xds = shapes.Cylinder(v, r=xTickRadius, height=xTickThickness, axis=(1,0,0), res=10) - ticks.append(xds.c(xTickColor).lighting(specular=0, ambient=1)) - if ytitle: - for coo in range(1 ,ry): - v = [0,coo/ry,0] - yds = shapes.Cylinder(v, r=yTickRadius, height=yTickThickness, axis=(0,1,0), res=10) - ticks.append(yds.c(yTickColor).lighting(specular=0, ambient=1)) - if ztitle: - for coo in range(1 ,rz): - v = [0,0,coo/rz] - zds = shapes.Cylinder(v, r=zTickRadius, height=zTickThickness, axis=(0,0,1), res=10) - ticks.append(zds.c(zTickColor).lighting(specular=0, ambient=1)) - - ################################################ MINOR cylindrical ticks - minorticks = [] - if xMinorTicks and xtitle: - xMinorTicks += 1 - for coo in range(1, rx*xMinorTicks): - v = [coo/rx/xMinorTicks,0,0] - mxds = shapes.Cylinder(v, r=xTickRadius/1.5, height=xTickThickness, axis=(1,0,0), res=6) - minorticks.append(mxds.c(xTickColor).lighting(specular=0, ambient=1)) - if yMinorTicks and ytitle: - yMinorTicks += 1 - for coo in range(1, ry*yMinorTicks): - v = [0, coo/ry/yMinorTicks,0] - myds = shapes.Cylinder(v, r=yTickRadius/1.5, height=yTickThickness, axis=(0,1,0), res=6) - minorticks.append(myds.c(yTickColor).lighting(specular=0, ambient=1)) - if zMinorTicks and ztitle: - zMinorTicks += 1 - for coo in range(1, rz*zMinorTicks): - v = [0, 0, coo/rz/zMinorTicks] - mzds = shapes.Cylinder(v, r=zTickRadius/1.5, height=zTickThickness, axis=(0,0,1), res=6) - minorticks.append(mzds.c(zTickColor).lighting(specular=0, ambient=1)) - - ################################################ axes tick NUMERIC labels - labels = [] - if xLabelSize: - if xtitle: - if rx > 12: rx = int(rx/2) - for ic in range(1, rx+enableLastLabel): - v = (ic/rx, -xLabelOffset, 0) - val = v[0]*sizes[0]+min_bns[0] - if abs(val)>1 and sizes[0]<1: xLabelPrecision = int(xLabelPrecision-np.log10(sizes[0])) - tval = precision(val, xLabelPrecision, vrange=sizes[0]) - xlab = shapes.Text(tval, pos=v, s=xLabelSize, justify="center-top", depth=0) - if xKeepAspectRatio: xlab.SetScale(x_aspect_ratio_scale) - labels.append(xlab.c(xTickColor).lighting(specular=0, ambient=1)) - if yLabelSize: - if ytitle: - if ry > 12: ry = int(ry/2) - for ic in range(1, ry+enableLastLabel): - v = (-yLabelOffset, ic/ry, 0) - val = v[1]*sizes[1]+min_bns[2] - if abs(val)>1 and sizes[1]<1: yLabelPrecision = int(yLabelPrecision-np.log10(sizes[1])) - tval = precision(val, yLabelPrecision, vrange=sizes[1]) - ylab = shapes.Text(tval, pos=(0,0,0), s=yLabelSize, justify="center-bottom", depth=0) - if yKeepAspectRatio: ylab.SetScale(y_aspect_ratio_scale) - ylab.RotateZ(yTitleRotation) - ylab.pos(v) - labels.append(ylab.c(yTickColor).lighting(specular=0, ambient=1)) - if zLabelSize: - if ztitle: - if rz > 12: rz = int(rz/2) - for ic in range(1, rz+enableLastLabel): - v = (-zLabelOffset, -zLabelOffset, ic/rz) - val = v[2]*sizes[2]+min_bns[4] - tval = precision(val, zLabelPrecision, vrange=sizes[2]) - if abs(val)>1 and sizes[2]<1: zLabelPrecision = int(zLabelPrecision-np.log10(sizes[2])) - zlab = shapes.Text(tval, pos=(0,0,0), s=zLabelSize, justify="center-bottom", depth=0) - if zKeepAspectRatio: zlab.SetScale(z_aspect_ratio_scale) - zlab.RotateY(-90) - zlab.RotateX(zTitleRotation) - zlab.pos(v) - labels.append(zlab.c(zTickColor).lighting(specular=0, ambient=1)) - - acts = grids + grids2 + lines + highlights + titles - acts += minorticks + originmarks + ticks + cones + labels - for a in acts: - a.PickableOff() - asse = Assembly(acts) - asse.pos(min_bns[0], min_bns[2], min_bns[4]) - asse.SetScale(sizes) - asse.PickableOff() vp.renderer.AddActor(asse) vp.axes_instances[r] = asse - vp.axes = axes_copy #un-pop elif vp.axes == 2 or vp.axes == 3: @@ -1470,10 +1593,10 @@ def addAxes(axtype=None, c=None): vp.axes_instances[r] = ca elif vp.axes == 11: - vbb, sizes = computeVisibleBounds()[0:2] + vbb, ss = computeVisibleBounds()[0:2] xpos, ypos = (vbb[1] + vbb[0]) /2, (vbb[3] + vbb[2]) /2 gr = shapes.Grid((xpos, ypos, vbb[4]), - sx=sizes[0]*8, sy=sizes[1]*8, + sx=ss[0]*8, sy=ss[1]*8, resx=7, resy=7, c=c, alpha=0.2) gr.lighting('ambient').PickableOff() diff --git a/vtkplotter/analysis.py b/vtkplotter/analysis.py index 76263621..15859b10 100644 --- a/vtkplotter/analysis.py +++ b/vtkplotter/analysis.py @@ -89,7 +89,7 @@ def delaunay2D(plist, mode='xy', tol=None): """ pd = vtk.vtkPolyData() vpts = vtk.vtkPoints() - vpts.SetData(numpy_to_vtk(plist, deep=True)) + vpts.SetData(numpy_to_vtk(np.ascontiguousarray(plist), deep=True)) pd.SetPoints(vpts) delny = vtk.vtkDelaunay2D() delny.SetInputData(pd) @@ -442,9 +442,9 @@ def pcaEllipsoid(points, pvalue=0.95, pcaAxes=False): ``mesh.info['va']``, ``mesh.info['vb']``, ``mesh.info['vc']`` (sphericity is equal to 0 for a perfect sphere). - .. hint:: Examples: |pca.py|_ |cell_main.py|_ + .. hint:: Examples: |pca.py|_ |cell_colony.py|_ - |pca| |cell_main| + |pca| |cell_colony| """ try: from scipy.stats import f @@ -505,6 +505,7 @@ def pcaEllipsoid(points, pvalue=0.95, pcaAxes=False): finact.info["va"] = ua finact.info["vb"] = ub finact.info["vc"] = uc + finact.name = "pcaEllipsoid" return finact diff --git a/vtkplotter/assembly.py b/vtkplotter/assembly.py index b05c3f51..44a48a59 100644 --- a/vtkplotter/assembly.py +++ b/vtkplotter/assembly.py @@ -52,13 +52,13 @@ def __add__(self, meshs): self.AddPart(meshs) return self - + def getActors(self): """Obsolete, use getMeshes() instead.""" print("WARNING: getActors() is obsolete, use getMeshes() instead.") return self.getMeshes() - + def getMeshes(self): """Unpack the list of ``Mesh`` objects from a ``Assembly``.""" return self.actors diff --git a/vtkplotter/backends.py b/vtkplotter/backends.py index 75777403..f6abde82 100644 --- a/vtkplotter/backends.py +++ b/vtkplotter/backends.py @@ -4,7 +4,6 @@ import os import vtkplotter.colors as colors -from vtkplotter.assembly import Assembly from vtkplotter.mesh import Mesh from vtkplotter.volume import Volume import vtkplotter.settings as settings @@ -28,57 +27,10 @@ def getNotebookBackend(actors2show, zoom, viewup): if sum(vp.shape) != 2: colors.printc("Warning: multirendering is not supported in jupyter.", c=1) -# settings.notebook_plotter = view(actors=actors2show, -# cmap='jet', ui_collapsed=True, -# gradient_opacity=False) + settings.notebook_plotter = view(actors=actors2show, + cmap='jet', ui_collapsed=True, + gradient_opacity=False) - polys2show = [] - points2show = [] - imgs2show = [] - polycols, polyalphas, pointcols, pointalphas = [],[],[],[] - - for ia in actors2show: - - if isinstance(ia, Assembly): #unpack assemblies - acass = ia.getMeshes() - for ac in acass: - if ac.polydata().GetNumberOfPolys(): - polys2show.append(ac.polydata()) - polycols.append(ac.color()) - polyalphas.append(ac.alpha()) - else: - points2show.append(ac.polydata()) - pointcols.append(ac.color()) - pointalphas.append(ac.alpha()) - - elif isinstance(ia, Mesh): - if ia.polydata().GetNumberOfPolys(): - polys2show.append(ia.polydata()) - polycols.append(ia.color()) - polyalphas.append(ia.alpha()) - else: - points2show.append(ia.polydata()) - pointcols.append(ia.color()) - pointalphas.append(ia.alpha()) - - elif isinstance(ia, Volume): - imgs2show.append(ia.imagedata()) - - img = None - if len(imgs2show): - img = imgs2show[0] - - settings.notebook_plotter = view(image=img, - geometries=polys2show, - geometry_colors=polycols, - geometry_opacities=polyalphas, - point_sets=points2show, - point_set_colors=pointcols, - point_set_opacities=pointalphas, - cmap='jet', - ui_collapsed=True, - gradient_opacity=False, - ) #################################################################################### elif settings.notebookBackend == 'k3d': @@ -263,4 +215,21 @@ def getNotebookBackend(actors2show, zoom, viewup): settings.notebook_plotter = panel.pane.VTK(vp.window, width=int(vp.size[0]/1.5), height=int(vp.size[1]/2)) + + + #################################################################################### + elif '2d' in settings.notebookBackend.lower() and hasattr(vp, 'window') and vp.window: + import PIL.Image + try: + import IPython + except ImportError: + raise Exception('IPython not available.') + return + + from vtkplotter.vtkio import screenshot + settings.screeshotLargeImage = True + nn = screenshot(returnNumpy=True, scale=settings.screeshotScale+2) + pil_img = PIL.Image.fromarray(nn) + settings.notebook_plotter = IPython.display.display(pil_img) + return settings.notebook_plotter diff --git a/vtkplotter/base.py b/vtkplotter/base.py index 24e04f32..c39e6aa5 100644 --- a/vtkplotter/base.py +++ b/vtkplotter/base.py @@ -70,6 +70,68 @@ def inputdata(self): return self._mapper.GetInput() return self.GetMapper().GetInput() + def buildAxes(self, **kargs): + """Draw axes on a ``Mesh`` or ``Volume``. + Returns a ``Assembly`` object. + + - `xtitle`, ['x'], x-axis title text. + - `ytitle`, ['y'], y-axis title text. + - `ztitle`, ['z'], z-axis title text. + - `numberOfDivisions`,[None], approximate number of divisions on the longest axis + - `axesLineWidth`, [1], width of the axes lines + - `gridLineWidth`, [1], width of the grid lines + - `reorientShortTitle`, [True], titles shorter than 2 letter are placed horizontally + - `originMarkerSize`, [0.01], draw a small cube on the axis where the origin is + - `titleDepth`, [0], extrusion fractional depth of title text + - `xyGrid`, [True], show a gridded wall on plane xy + - `yzGrid`, [True], show a gridded wall on plane yz + - `zxGrid`, [True], show a gridded wall on plane zx + - `zxGrid2`, [False], show zx plane on opposite side of the bounding box + - `xyGridTransparent` [False], make grid plane completely transparent + - `xyGrid2Transparent` [False], make grid plane completely transparent on opposite side box + - `xyPlaneColor`, ['gray'], color of the plane + - `xyGridColor`, ['gray'], grid line color + - `xyAlpha`, [0.15], grid plane opacity + - `showTicks`, [True], show major ticks + - `xTitlePosition`, [0.32], title fractional positions along axis + - `xTitleOffset`, [0.05], title fractional offset distance from axis line + - `xTitleJustify`, ["top-right"], title justification + - `xTitleRotation`, [0], add a rotation of the axis title + - `xLineColor`, [automatic], color of the x-axis + - `xTitleColor`, [automatic], color of the axis title + - `xTitleBackfaceColor`, [None], color of axis title on its backface + - `xTitleSize`, [0.025], size of the axis title + - `xHighlightZero`, [True], draw a line highlighting zero position if in range + - `xHighlightZeroColor`, [automatic], color of the line highlighting the zero position + - `xTickRadius`, [0.005], radius of the major ticks + - `xTickThickness`, [0.0025], thickness of the major ticks along their axis + - `xTickColor`, [automatic], color of major ticks + - `xMinorTicks`, [1], number of minor ticks between two major ticks + - `tipSize`, [0.01], size of the arrow tip + - `xPositionsAndLabels` [], assign custom tick positions and labels [(pos1, label1), ...] + - `xLabelPrecision`, [2], nr. of significative digits to be shown + - `xLabelSize`, [0.015], size of the numeric labels along axis + - `xLabelOffset`, [0.025], offset of numeric labels + - `limitRatio`, [0.04], below this ratio don't plot small axis + + :Example: + + .. code-block:: python + + from vtkplotter import Box, show + + b = Box(pos=(1,2,3), length=8, width=9, height=7).alpha(0) + bax = buildAxes(b, c='white') # returns Assembly object + + show(b, bax) + + |customAxes| |customAxes.py|_ + + |customIndividualAxes| |customIndividualAxes.py|_ + """ + from vtkplotter.addons import buildAxes + return buildAxes(self, **kargs) + def show(self, **options): """ diff --git a/vtkplotter/docs.py b/vtkplotter/docs.py index 39e60d5b..b788e39a 100644 --- a/vtkplotter/docs.py +++ b/vtkplotter/docs.py @@ -349,7 +349,7 @@ def tips(): .. |histoHexagonal.py| replace:: histoHexagonal.py .. _histoHexagonal.py: https://github.com/marcomusy/vtkplotter-examples/blob/master/vtkplotter_examples/plotting2d/histoHexagonal.py -.. |histoHexagonal| image:: https://user-images.githubusercontent.com/32848391/50738861-bfccf800-11d8-11e9-9698-c0b9dccdba4d.jpg +.. |histoHexagonal| image:: https://user-images.githubusercontent.com/32848391/72434748-b471bc80-379c-11ea-95d7-d70333770582.png :width: 250 px :target: histoHexagonal.py_ :alt: histoHexagonal.py @@ -375,12 +375,12 @@ def tips(): :target: pca.py_ :alt: pca.py -.. |cell_main.py| replace:: cell_main.py -.. _cell_main.py: https://github.com/marcomusy/vtkplotter-examples/blob/master/vtkplotter_examples/simulations/cell_main.py -.. |cell_main| image:: https://user-images.githubusercontent.com/32848391/50738947-7335ec80-11d9-11e9-9a45-6053b4eaf9f9.jpg +.. |cell_colony.py| replace:: cell_colony.py +.. _cell_colony.py: https://github.com/marcomusy/vtkplotter-examples/blob/master/vtkplotter_examples/simulations/cell_colony.py +.. |cell_colony| image:: https://user-images.githubusercontent.com/32848391/50738947-7335ec80-11d9-11e9-9a45-6053b4eaf9f9.jpg :width: 250 px - :target: cell_main.py_ - :alt: cell_main.py + :target: cell_colony.py_ + :alt: cell_colony.py .. |mesh_smoothers.py| replace:: mesh_smoothers.py .. _mesh_smoothers.py: https://github.com/marcomusy/vtkplotter-examples/blob/master/vtkplotter_examples/advanced/mesh_smoothers.py @@ -889,8 +889,8 @@ def tips(): .. |isolines.py| replace:: isolines.py .. _isolines.py: https://github.com/marcomusy/vtkplotter-examples/blob/master/vtkplotter_examples/basic/isolines.py -.. |isolines| image:: https://user-images.githubusercontent.com/32848391/56752570-de0b3380-6788-11e9-8679-6697c6fa7e5a.png - :width: 250 px +.. |isolines| image:: https://user-images.githubusercontent.com/32848391/72433087-f00a8780-3798-11ea-9778-991f0abeca70.png + :width: 350 px :target: isolines.py_ :alt: isolines.py @@ -1194,6 +1194,13 @@ def tips(): .. |warpto| image:: https://user-images.githubusercontent.com/32848391/69259878-3c817e80-0bbf-11ea-9025-03b9f6affccc.png :width: 350 px +.. |linInterpolate.py| replace:: linInterpolate.py +.. _linInterpolate.py: https://github.com/marcomusy/vtkplotter-examples/blob/master/vtkplotter_examples/basic/linInterpolate.py +.. |linInterpolate| image:: https://user-images.githubusercontent.com/32848391/70559826-a621f680-1b87-11ea-89f3-e6b74d8953d9.png + :width: 350 px + :target: linInterpolate.py_ + :alt: linInterpolate.py + .. |plotxy.py| replace:: plotxy.py .. _plotxy.py: https://github.com/marcomusy/vtkplotter-examples/blob/master/vtkplotter_examples/plotting2d/plotxy.py .. |plotxy| image:: https://user-images.githubusercontent.com/32848391/69158509-d6c1c380-0ae6-11ea-9dbf-ff5cd396a9a6.png @@ -1201,12 +1208,57 @@ def tips(): :target: plotxy.py_ :alt: plotxy.py -.. |linInterpolate.py| replace:: linInterpolate.py -.. _linInterpolate.py: https://github.com/marcomusy/vtkplotter-examples/blob/master/vtkplotter_examples/basic/linInterpolate.py -.. |linInterpolate| image:: https://user-images.githubusercontent.com/32848391/70559826-a621f680-1b87-11ea-89f3-e6b74d8953d9.png +.. |quiverPlot.py| replace:: quiverPlot.py +.. _quiverPlot.py: https://github.com/marcomusy/vtkplotter-examples/blob/master/vtkplotter_examples/plotting2d/quiverPlot.py +.. |quiverPlot| image:: https://user-images.githubusercontent.com/32848391/72261438-199aa600-3615-11ea-870e-e44ca4c4b8d3.png + :width: 250 px + :target: quiverPlot.py_ + :alt: quiverPlot.py + +.. |sphericPlot.py| replace:: sphericPlot.py +.. _sphericPlot.py: https://github.com/marcomusy/vtkplotter-examples/blob/master/vtkplotter_examples/plotting2d/sphericPlot.py +.. |sphericPlot| image:: https://user-images.githubusercontent.com/32848391/72433091-f0a31e00-3798-11ea-86bd-6c522e23ec61.png :width: 350 px - :target: linInterpolate.py_ - :alt: linInterpolate.py + :target: sphericPlot.py_ + :alt: sphericPlot.py + +.. |sphericgrid| image:: https://user-images.githubusercontent.com/32848391/72433092-f0a31e00-3798-11ea-85f7-b2f5fcc31568.png + :width: 250 px + +.. |histogram2D.py| replace:: histogram2D.py +.. _histogram2D.py: https://github.com/marcomusy/vtkplotter-examples/blob/master/vtkplotter_examples/plotting2d/histogram2D.py +.. |histogram2D| image:: https://user-images.githubusercontent.com/32848391/72452359-b5671600-37bd-11ea-8b1d-c44d884496ed.png + :width: 350 px + :target: histogram2D.py_ + :alt: histogram2D.py + +.. |plot_errband.py| replace:: plot_errband.py +.. _plot_errband.py: https://github.com/marcomusy/vtkplotter-examples/blob/master/vtkplotter_examples/plotting2d/plot_errband.py +.. |plot_errband| image:: https://user-images.githubusercontent.com/32848391/72461569-970a1600-37cf-11ea-8060-a8a6cf657b95.png + :width: 350 px + :target: plot_errband.py_ + :alt: plot_errband.py + +.. |scatter2.py| replace:: scatter2.py +.. _scatter2.py: https://github.com/marcomusy/vtkplotter-examples/blob/master/vtkplotter_examples/plotting2d/scatter2.py +.. |scatter2| image:: https://user-images.githubusercontent.com/32848391/72615028-013bcb80-3934-11ea-8ab8-823f1916bc6c.png + :width: 350 px + :target: scatter2.py_ + :alt: scatter2.py + +.. |scatter3.py| replace:: scatter3.py +.. _scatter3.py: https://github.com/marcomusy/vtkplotter-examples/blob/master/vtkplotter_examples/plotting2d/scatter3.py +.. |scatter3| image:: https://user-images.githubusercontent.com/32848391/72446102-2d7c0e80-37b3-11ea-8fe4-b27526af574f.png + :width: 350 px + :target: scatter3.py_ + :alt: scatter3.py + +.. |customIndividualAxes.py| replace:: customIndividualAxes.py +.. _customIndividualAxes.py: https://github.com/marcomusy/vtkplotter-examples/blob/master/vtkplotter_examples/plotting2d/customIndividualAxes.py +.. |customIndividualAxes| image:: https://user-images.githubusercontent.com/32848391/72752870-ab7d5280-3bc3-11ea-8911-9ace00211e23.png + :width: 350 px + :target: customIndividualAxes.py_ + :alt: customIndividualAxes.py """ diff --git a/vtkplotter/dolfin.py b/vtkplotter/dolfin.py index 57a80a0b..4f1fc380 100644 --- a/vtkplotter/dolfin.py +++ b/vtkplotter/dolfin.py @@ -379,46 +379,47 @@ def plot(*inputobj, **options): - 11, show a large grid on the x-y plane (use with zoom=8) - 12, show polar axes. - Axes type-1 can be fully customized by passing a dictionary ``axes=dict()`` where: - - - `xtitle`, ['x'], x-axis title text. - - `ytitle`, ['y'], y-axis title text. - - `ztitle`, ['z'], z-axis title text. - - `numberOfDivisions`, [automatic], number of divisions on the shortest axis - - `axesLineWidth`, [1], width of the axes lines - - `gridLineWidth`, [1], width of the grid lines - - `reorientShortTitle`, [True], titles shorter than 2 letter are placed horizontally - - `originMarkerSize`, [0.01], draw a small cube on the axis where the origin is - - `enableLastLabel`, [False], show last numeric label on axes - - `titleDepth`, [0], extrusion fractional depth of title text - - `xyGrid`, [True], show a gridded wall on plane xy - - `yzGrid`, [True], show a gridded wall on plane yz - - `zxGrid`, [True], show a gridded wall on plane zx - - `zxGrid2`, [False], show zx plane on opposite side of the bounding box - - `xyGridTransparent` [False], make grid plane completely transparent - - `xyGrid2Transparent` [False], make grid plane completely transparent on opposite side box - - `xyPlaneColor`, ['gray'], color of the plane - - `xyGridColor`, ['gray'], grid line color - - `xyAlpha`, [0.15], grid plane opacity - - `showTicks`, [True], show major ticks - - `xTitlePosition`, [0.32], title fractional positions along axis - - `xTitleOffset`, [0.05], title fractional offset distance from axis line - - `xTitleJustify`, ["top-right"], title justification - - `xTitleRotation`, [0], add a rotation of the axis title - - `xLineColor`, [automatic], color of the x-axis - - `xTitleColor`, [automatic], color of the axis title - - `xTitleBackfaceColor`, [None], color of axis title on its backface - - `xTitleSize`, [0.025], size of the axis title - - `xHighlightZero`, [True], draw a line highlighting zero position if in range - - `xHighlightZeroColor`, [automatic], color of the line highlighting the zero position - - `xTickRadius`, [0.005], radius of the major ticks - - `xTickThickness`, [0.0025], thickness of the major ticks along their axis - - `xTickColor`, [automatic], color of major ticks - - `xMinorTicks`, [1], number of minor ticks between two major ticks - - `tipSize`, [0.01], size of the arrow tip - - `xLabelPrecision`, [2], nr. of significative digits to be shown - - `xLabelSize`, [0.015], size of the numeric labels along axis - - `xLabelOffset`, [0.025], offset of numeric labels + Axes type-1 can be fully customized by passing a dictionary ``axes=dict()`` where: + + - `xtitle`, ['x'], x-axis title text. + - `ytitle`, ['y'], y-axis title text. + - `ztitle`, ['z'], z-axis title text. + - `numberOfDivisions`, [automatic], approximate number of divisions on the longest axis + - `axesLineWidth`, [1], width of the axes lines + - `gridLineWidth`, [1], width of the grid lines + - `reorientShortTitle`, [True], titles shorter than 2 letter are placed horizontally + - `originMarkerSize`, [0.01], draw a small cube on the axis where the origin is + - `titleDepth`, [0], extrusion fractional depth of title text + - `xyGrid`, [True], show a gridded wall on plane xy + - `yzGrid`, [True], show a gridded wall on plane yz + - `zxGrid`, [True], show a gridded wall on plane zx + - `zxGrid2`, [False], show zx plane on opposite side of the bounding box + - `xyGridTransparent` [False], make grid plane completely transparent + - `xyGrid2Transparent` [False], make grid plane completely transparent on opposite side box + - `xyPlaneColor`, ['gray'], color of the plane + - `xyGridColor`, ['gray'], grid line color + - `xyAlpha`, [0.15], grid plane opacity + - `showTicks`, [True], show major ticks + - `xTitlePosition`, [0.32], title fractional positions along axis + - `xTitleOffset`, [0.05], title fractional offset distance from axis line + - `xTitleJustify`, ["top-right"], title justification + - `xTitleRotation`, [0], add a rotation of the axis title + - `xLineColor`, [automatic], color of the x-axis + - `xTitleColor`, [automatic], color of the axis title + - `xTitleBackfaceColor`, [None], color of axis title on its backface + - `xTitleSize`, [0.025], size of the axis title + - `xHighlightZero`, [True], draw a line highlighting zero position if in range + - `xHighlightZeroColor`, [automatic], color of the line highlighting the zero position + - `xTickRadius`, [0.005], radius of the major ticks + - `xTickThickness`, [0.0025], thickness of the major ticks along their axis + - `xTickColor`, [automatic], color of major ticks + - `xMinorTicks`, [1], number of minor ticks between two major ticks + - `tipSize`, [0.01], size of the arrow tip + - `xPositionsAndLabels` [], assign custom tick positions and labels [(pos1, label1), ...] + - `xLabelPrecision`, [2], nr. of significative digits to be shown + - `xLabelSize`, [0.015], size of the numeric labels along axis + - `xLabelOffset`, [0.025], offset of numeric labels + - `limitRatio`, [0.04], below this ratio don't plot small axis :param bool infinity: if True fugue point is set at infinity (no perspective effects) :param bool sharecam: if False each renderer will have an independent vtkCamera @@ -831,7 +832,7 @@ def move(self, u=None, deltas=None): movedpts = coords + deltas if movedpts.shape[1] == 2: #2d movedpts = np.c_[movedpts, np.zeros(movedpts.shape[0])] - self.polydata(False).GetPoints().SetData(numpy_to_vtk(movedpts)) + self.polydata(False).GetPoints().SetData(numpy_to_vtk(np.ascontiguousarray(movedpts))) self._polydata.GetPoints().Modified() diff --git a/vtkplotter/mesh.py b/vtkplotter/mesh.py index 85e56f21..43e20ca6 100644 --- a/vtkplotter/mesh.py +++ b/vtkplotter/mesh.py @@ -284,21 +284,6 @@ def _update(self, polydata): self._mapper.Modified() return self - def getPoints(self, transformed=True, copy=False): - """Obsolete, use points() instead.""" - colors.printc("WARNING: getPoints() is obsolete, use points() instead.", box='=', c=1) - return self.points(transformed=transformed, copy=copy) - - def setPoints(self, pts): - """Obsolete, use points(pts) instead.""" - colors.printc("WARNING: setPoints(pts) is obsolete, use points(pts) instead.", box='=', c=1) - return self.points(pts) - - def coordinates(self, transformed=True, copy=False): - """Obsolete, use points() instead.""" - colors.printc("WARNING: coordinates() is obsolete, use points() instead.", box='=', c=1) - return self.points(transformed=transformed, copy=copy) - def points(self, pts=None, transformed=True, copy=False): """ Set/Get the vertex coordinates of the mesh. @@ -310,18 +295,27 @@ def points(self, pts=None, transformed=True, copy=False): :param bool copy: if `False` return the reference to the points so that they can be modified in place, otherwise a copy is built. """ - if pts is None: # getter + if pts is None: ### getter + poly = self.polydata(transformed) - if copy: - return np.array(vtk_to_numpy(poly.GetPoints().GetData())) + vpts = poly.GetPoints() + if vpts: + if copy: + return np.array(vtk_to_numpy(vpts.GetData())) + else: + return vtk_to_numpy(vpts.GetData()) else: - return vtk_to_numpy(poly.GetPoints().GetData()) + return np.array([]) elif (utils.isSequence(pts) and not utils.isSequence(pts[0])) or isinstance(pts, (int, np.integer)): #passing a list of indices or a single index return vtk_to_numpy(self.polydata(transformed).GetPoints().GetData())[pts] - else: # setter + else: ### setter + + if len(pts) == 3 and len(pts[0]) != 3: + # assume plist is in the format [all_x, all_y, all_z] + pts = np.stack((pts[0], pts[1], pts[2]), axis=1) vpts = self._polydata.GetPoints() vpts.SetData(numpy_to_vtk(np.ascontiguousarray(pts), deep=True)) self._polydata.GetPoints().Modified() @@ -329,6 +323,7 @@ def points(self, pts=None, transformed=True, copy=False): self.PokeMatrix(vtk.vtkMatrix4x4()) return self + def faces(self): """Get cell polygonal connectivity ids as a python ``list``. The output format is: [[id0 ... idn], [id0 ... idm], etc]. @@ -358,6 +353,30 @@ def faces(self): return conn # cannot always make a numpy array of it! + def lines(self): + """Get lines connectivity ids as a numpy array.""" + #Get cell connettivity ids as a 1D array. The vtk format is: + # [nids1, id0 ... idn, niids2, id0 ... idm, etc]. + arr1d = vtk_to_numpy(self.polydata(False).GetLines().GetData()) + return arr1d + + # obsolete stuff: + def getPoints(self, transformed=True, copy=False): + """Obsolete, use points() instead.""" + colors.printc("WARNING: getPoints() is obsolete, use points() instead.", box='=', c=1) + return self.points(transformed=transformed, copy=copy) + + def setPoints(self, pts): + """Obsolete, use points(pts) instead.""" + colors.printc("WARNING: setPoints(pts) is obsolete, use points(pts) instead.", box='=', c=1) + return self.points(pts) + + def coordinates(self, transformed=True, copy=False): + """Obsolete, use points() instead.""" + colors.printc("WARNING: coordinates() is obsolete, use points() instead.", box='=', c=1) + return self.points(transformed=transformed, copy=copy) + + def addScalarBar(self, pos=(0.8,0.05), title="", @@ -463,7 +482,7 @@ def texture(self, tname, return self if tcoords.shape[1] != 2: colors.printc('Error in texture(): vector must have 2 components', c=1) - tarr = numpy_to_vtk(tcoords) + tarr = numpy_to_vtk(np.ascontiguousarray(tcoords), deep=True) tarr.SetName('TCoordinates') pd.GetPointData().SetTCoords(tarr) pd.GetPointData().Modified() @@ -1029,7 +1048,7 @@ def distanceToMesh(self, mesh, signed=False, negate=False): def clone(self, transformed=True): """ - Clone a ``Mesh(vtkActor)`` and make an exact copy of it. + Clone a ``Mesh`` object to make an exact copy of it. :param transformed: if `False` ignore any previous transformation applied to the mesh. @@ -1039,14 +1058,21 @@ def clone(self, transformed=True): polyCopy = vtk.vtkPolyData() polyCopy.DeepCopy(poly) - cloned = Mesh() + cloned = Mesh(polyCopy) cloned._polydata = polyCopy - cloned._mapper.SetInputData(polyCopy) - cloned._mapper.SetScalarVisibility(self._mapper.GetScalarVisibility()) - cloned._mapper.SetScalarRange(self._mapper.GetScalarRange()) pr = vtk.vtkProperty() pr.DeepCopy(self.GetProperty()) cloned.SetProperty(pr) + + cloned._mapper.SetScalarVisibility(self._mapper.GetScalarVisibility()) + cloned._mapper.SetScalarRange(self._mapper.GetScalarRange()) + cloned._mapper.SetColorMode(self._mapper.GetColorMode()) + cloned._mapper.SetUseLookupTableScalarRange(self._mapper.GetUseLookupTableScalarRange()) + cloned._mapper.SetScalarMode(self._mapper.GetScalarMode()) + lut = self._mapper.GetLookupTable() + if lut: + cloned._mapper.SetLookupTable(lut) + cloned.base = self.base cloned.top = self.top if self.trail: @@ -1260,7 +1286,7 @@ def crop(self, top=None, bottom=None, right=None, left=None, front=None, back=No self._update(clipper.GetOutput()) return self - def cutWithPlane(self, origin=(0, 0, 0), normal=(1, 0, 0), showcut=False): + def cutWithPlane(self, origin=(0, 0, 0), normal=(1, 0, 0), returnCut=False): """ Cut the mesh with the plane defined by a point and a normal. @@ -1295,7 +1321,7 @@ def cutWithPlane(self, origin=(0, 0, 0), normal=(1, 0, 0), showcut=False): clipper = vtk.vtkClipPolyData() clipper.SetInputData(poly) clipper.SetClipFunction(plane) - if showcut: + if returnCut: clipper.GenerateClippedOutputOn() else: clipper.GenerateClippedOutputOff() @@ -1305,15 +1331,12 @@ def cutWithPlane(self, origin=(0, 0, 0), normal=(1, 0, 0), showcut=False): self._update(clipper.GetOutput()).computeNormals() - if showcut: - from vtkplotter.assembly import Assembly + if returnCut: c = self.GetProperty().GetColor() cpoly = clipper.GetClippedOutput() - restmesh = Mesh(cpoly, c, 0.05).wireframe(True) + restmesh = Mesh(cpoly, c, 0.1).wireframe(True) restmesh.SetUserMatrix(self.GetMatrix()) - asse = Assembly([self, restmesh]) - self = asse - return asse + return restmesh else: return self @@ -2131,7 +2154,8 @@ def normals(self, cells=False): vtknormals = self.polydata().GetCellData().GetNormals() else: vtknormals = self.polydata().GetPointData().GetNormals() - + if not vtknormals: + return np.array([]) return vtk_to_numpy(vtknormals) def polydata(self, transformed=True): diff --git a/vtkplotter/plot2d.py b/vtkplotter/plot2d.py index e990f91a..0457c3ea 100644 --- a/vtkplotter/plot2d.py +++ b/vtkplotter/plot2d.py @@ -24,9 +24,12 @@ "cornerPlot", "cornerHistogram", "histogram", + "histogram2D", "hexHistogram", "polarHistogram", "polarPlot", + "sphericPlot", + "quiverPlot", "donutPlot", ] @@ -49,21 +52,26 @@ def plotxy( titleSize=None, ec=None, lc="k", + la=1, lw=2, - line=True, + line=False, dashed=False, splined=False, - marker=None, + errorBand=False, + marker='.', ms=None, mc=None, ma=None, ): - """Draw a 2D plot of variable x vs y. + """Draw a 2D line plot, or scatter plot, of variable x vs variable y. :param list data: input format can be [allx, ally] or [(x1,y1), (x2,y2), ...] :param list xerrors: set uncertainties for the x variable, shown as error bars. :param list yerrors: set uncertainties for the y variable, shown as error bars. + :param bool errorBand: represent errors on y as a filled error band. + Use ``ec`` keyword to modify its color. + :param list xlimits: set limits to the range for the x variable :param list ylimits: set limits to the range for the y variable :param float xscale: set scaling factor in x. Default is 1. @@ -80,6 +88,7 @@ def plotxy( :param float titleSize: size of title :param str ec: color of error bar, by default the same as marker color :param str lc: color of line + :param float la: transparency of line :param float lw: width of line :param bool line: join points with line :param bool dashed: use a dashed line style @@ -90,31 +99,54 @@ def plotxy( :param float ma: opacity of marker |plotxy| |plotxy.py|_ + + |plot_errband| |plot_errband.py|_ + + |scatter2| |scatter2.py|_ + + |scatter3| |scatter3.py|_ """ if len(data) == 2 and len(data[0])>1 and len(data[0]) == len(data[1]): #format is [allx, ally], convert it: data = np.c_[data[0], data[1]] + # crop arrays in x and y if xlimits is not None: - cdata = [] + cdata, cxerrors, cyerrors = [], [], [] x0lim = xlimits[0] x1lim = xlimits[1] - for d in data: - if d[0] > x0lim and d[0] < x1lim: + for i,d in enumerate(data): + if d[0] >= x0lim and d[0] <= x1lim: cdata.append(d) + if xerrors is not None: + cxerrors.append(xerrors[i]) + if yerrors is not None: + cyerrors.append(yerrors[i]) data = cdata + if xerrors is not None: + xerrors = np.array(cxerrors) + if yerrors is not None: + yerrors = np.array(cyerrors) if not len(data): colors.printc("Error in plotxy(): no points within xlimits", c=1) return None if ylimits is not None: - cdata = [] + cdata, cxerrors, cyerrors = [], [], [] y0lim = ylimits[0] y1lim = ylimits[1] - for d in data: - if d[1] > y0lim and d[1] < y1lim: + for i,d in enumerate(data): + if d[1] >= y0lim and d[1] <= y1lim: cdata.append(d) + if xerrors is not None: + cxerrors.append(xerrors[i]) + if yerrors is not None: + cyerrors.append(yerrors[i]) data = cdata + if xerrors is not None: + xerrors = np.array(cxerrors) + if yerrors is not None: + yerrors = np.array(cyerrors) if not len(data): colors.printc("Error in plotxy(): no points within ylimits", c=1) return None @@ -145,33 +177,59 @@ def plotxy( acts = [] if dashed: - l = shapes.DashedLine(data, lw=lw, spacing=20) + l = shapes.DashedLine(data, c=lc, alpha=la, lw=lw, spacing=20) acts.append(l) elif splined: - l = shapes.KSpline(data).lw(lw).c(lc) + l = shapes.KSpline(data).lw(lw).c(lc).alpha(la) acts.append(l) elif line: - l = shapes.Line(data, lw=lw, c=lc) + l = shapes.Line(data, lw=lw, c=lc, alpha=la) acts.append(l) + if marker: - if ms is None: - ms = (x1 - x0) / 75.0 + + pts = shapes.Points(data) if mc is None: mc = lc - mk = shapes.Marker(marker, s=ms, alpha=ma) - pts = shapes.Points(data) - marked = shapes.Glyph(pts, glyphObj=mk, c=mc) + + if utils.isSequence(ms): ### variable point size + mk = shapes.Marker(marker, s=1) + msv = np.zeros_like(pts.points()) + msv[:,0] = ms + marked = shapes.Glyph(pts, glyphObj=mk, c=mc, + orientationArray=msv, scaleByVectorSize=True) + + else: ### fixed point size + + if ms is None: + ms = np.sqrt((x1-x0)**2+(y1-y0)**2) / 100.0 + #print('automatic ms =', ms) + + if utils.isSequence(mc): + #print('mc is sequence') + mk = shapes.Marker(marker, s=ms) + msv = np.zeros_like(pts.points()) + msv[:,0] = 1 + marked = shapes.Glyph(pts, glyphObj=mk, c=mc, + orientationArray=msv, scaleByVectorSize=True) + else: + #print('mc is fixed color') + mk = shapes.Marker(marker, s=ms) + marked = shapes.Glyph(pts, glyphObj=mk, c=mc) + + marked.lighting('ambient').alpha(ma) acts.append(marked) + if ec is None: if mc is not None: ec = mc else: ec = lc - offs = (x1-x0)/1000 + offs = (x1-x0)/1000 - if yerrors is not None: + if yerrors is not None and not errorBand: if len(yerrors) != len(data): colors.printc("Error in plotxy(yerrors=...): mismatched array length.", c=1) return None @@ -183,7 +241,7 @@ def plotxy( myerrs = merge(errs).c(ec).lw(lw).alpha(alpha) acts.append(myerrs) - if xerrors is not None: + if xerrors is not None and not errorBand: if len(xerrors) != len(data): colors.printc("Error in plotxy(xerrors=...): mismatched array length.", c=1) return None @@ -195,6 +253,21 @@ def plotxy( mxerrs = merge(errs).c(ec).lw(lw).alpha(alpha) acts.append(mxerrs) + if errorBand: + epsy = np.zeros_like(data) + epsy[:,1] = yerrors/2*yscale + data3dup = data+epsy + data3dup = np.c_[data3dup, np.zeros_like(yerrors)] + data3d_down = data-epsy + data3d_down = np.c_[data3d_down, np.zeros_like(yerrors)] + band = shapes.Ribbon(data3dup, data3d_down).z(-offs) + if ec is None: + band.c(lc) + else: + band.c(ec) + band.alpha(la) + acts.append(band) + x0lim = x0 x1lim = x1 y0lim = y0*yscale @@ -345,6 +418,144 @@ def cornerHistogram( return plot +def fxy( + z="sin(3*x)*log(x-y)/3", + x=(0, 3), + y=(0, 3), + zlimits=(None, None), + showNan=True, + zlevels=10, + c="b", + bc="aqua", + alpha=1, + texture="paper", + res=(100, 100), +): + """ + Build a surface representing the function :math:`f(x,y)` specified as a string + or as a reference to an external function. + + :param float x: x range of values. + :param float y: y range of values. + :param float zlimits: limit the z range of the independent variable. + :param int zlevels: will draw the specified number of z-levels contour lines. + :param bool showNan: show where the function does not exist as red points. + :param list res: resolution in x and y. + + |fxy| |fxy.py|_ + + Function is: :math:`f(x,y)=\sin(3x) \cdot \log(x-y)/3` in range :math:`x=[0,3], y=[0,3]`. + """ + if isinstance(z, str): + try: + z = z.replace("math.", "").replace("np.", "") + namespace = locals() + code = "from math import*\ndef zfunc(x,y): return " + z + exec(code, namespace) + z = namespace["zfunc"] + except: + colors.printc("Syntax Error in fxy()", c=1) + return None + + ps = vtk.vtkPlaneSource() + ps.SetResolution(res[0], res[1]) + ps.SetNormal([0, 0, 1]) + ps.Update() + poly = ps.GetOutput() + dx = x[1] - x[0] + dy = y[1] - y[0] + todel, nans = [], [] + + for i in range(poly.GetNumberOfPoints()): + px, py, _ = poly.GetPoint(i) + xv = (px + 0.5) * dx + x[0] + yv = (py + 0.5) * dy + y[0] + try: + zv = z(xv, yv) + except: + zv = 0 + todel.append(i) + nans.append([xv, yv, 0]) + poly.GetPoints().SetPoint(i, [xv, yv, zv]) + + if len(todel): + cellIds = vtk.vtkIdList() + poly.BuildLinks() + for i in todel: + poly.GetPointCells(i, cellIds) + for j in range(cellIds.GetNumberOfIds()): + poly.DeleteCell(cellIds.GetId(j)) # flag cell + poly.RemoveDeletedCells() + cl = vtk.vtkCleanPolyData() + cl.SetInputData(poly) + cl.Update() + poly = cl.GetOutput() + + if not poly.GetNumberOfPoints(): + colors.printc("Function is not real in the domain", c=1) + return None + + if zlimits[0]: + tmpact1 = Mesh(poly) + a = tmpact1.cutWithPlane((0, 0, zlimits[0]), (0, 0, 1)) + poly = a.polydata() + if zlimits[1]: + tmpact2 = Mesh(poly) + a = tmpact2.cutWithPlane((0, 0, zlimits[1]), (0, 0, -1)) + poly = a.polydata() + + if c is None: + elev = vtk.vtkElevationFilter() + elev.SetInputData(poly) + elev.Update() + poly = elev.GetOutput() + + mesh = Mesh(poly, c, alpha).computeNormals().lighting("plastic") + if c is None: + mesh.getPointArray("Elevation") + + if bc: + mesh.bc(bc) + + mesh.texture(texture) + #mesh.mapper().SetResolveCoincidentTopologyToPolygonOffset() + #mesh.mapper().SetResolveCoincidentTopologyPolygonOffsetParameters(0.1, 0.1) + + acts = [mesh] + if zlevels: + elevation = vtk.vtkElevationFilter() + elevation.SetInputData(poly) + bounds = poly.GetBounds() + elevation.SetLowPoint(0, 0, bounds[4]) + elevation.SetHighPoint(0, 0, bounds[5]) + elevation.Update() + bcf = vtk.vtkBandedPolyDataContourFilter() + bcf.SetInputData(elevation.GetOutput()) + bcf.SetScalarModeToValue() + bcf.GenerateContourEdgesOn() + bcf.GenerateValues(zlevels, elevation.GetScalarRange()) + bcf.Update() + zpoly = bcf.GetContourEdgesOutput() + zbandsact = Mesh(zpoly, "k", alpha).lw(2).lighting('ambient') + acts.append(zbandsact) + + if showNan and len(todel): + bb = mesh.GetBounds() + if bb[4] <= 0 and bb[5] >= 0: + zm = 0.0 + else: + zm = (bb[4] + bb[5]) / 2 + nans = np.array(nans) + [0, 0, zm] + nansact = shapes.Points(nans, r=2, c="red", alpha=alpha) + nansact.GetProperty().RenderPointsAsSpheresOff() + acts.append(nansact) + + if len(acts) > 1: + return Assembly(acts) + else: + return mesh + + def histogram( values, xtitle="", @@ -363,7 +574,7 @@ def histogram( errors=False, ): """ - Build a histogram from a list of values in n bins. + Build a 1D histogram from a list of values in n bins. The resulting object is an ``Assembly``. :param int bins: number of bins. @@ -429,9 +640,61 @@ def histogram( if yscale is None: yscale = 10 / np.sum(fs) * (maxe - mine) asse.scale([1, yscale, 1]) + asse.base = np.array([0, 0, 0]) + asse.top = np.array([0, 0, 1]) return asse +def histogram2D(xvalues, yvalues=None, + xtitle="", + ytitle="", + bins=(20, 20), + cmap="viridis", + alpha=1, + lw=0.1, + scalarbar=True, +): + """Build a 2D Histogram. Returns a ``Mesh`` object. + + Single input data format [(x1,y1), (x2,y2) ...] is also accepted. + + |histogram2D| |histogram2D.py|_ + """ + if xtitle: + from vtkplotter import settings + settings.xtitle = xtitle + if ytitle: + from vtkplotter import settings + settings.ytitle = ytitle + + if isinstance(xvalues, Mesh): + xvalues = xvalues.points() + + if yvalues is None: + # assume [(x1,y1), (x2,y2) ...] format + yvalues = xvalues[:,1] + xvalues = xvalues[:,0] + + xmin, xmax = np.min(xvalues), np.max(xvalues) + ymin, ymax = np.min(yvalues), np.max(yvalues) + dx, dy = xmax - xmin, ymax - ymin + + xedges = np.linspace(xmin, xmax, num=bins[0]+1, endpoint=True) + yedges = np.linspace(ymin, ymax, num=bins[1]+1, endpoint=True) + H, xedges, yedges = np.histogram2d(xvalues, yvalues, bins=(xedges, yedges)) + + g = shapes.Grid(pos=[(xmin+xmax)/2, (ymin+ymax)/2, 0], + sx=dx, sy=dy, resx=bins[0], resy=bins[1]) + g.alpha(alpha).lw(lw).wireframe(0) + g.cellColors(np.ravel(H.T, order='C'), cmap=cmap) + if scalarbar: + g.addScalarBar() + g.base = np.array([0, 0, 0]) + g.top = np.array([0, 0, 1]) + return g + + + def hexHistogram( xvalues, yvalues, @@ -457,15 +720,12 @@ def hexHistogram( """ if xtitle: from vtkplotter import settings - settings.xtitle = xtitle if ytitle: from vtkplotter import settings - settings.ytitle = ytitle if ztitle: from vtkplotter import settings - settings.ztitle = ztitle xmin, xmax = np.min(xvalues), np.max(xvalues) @@ -544,6 +804,8 @@ def hexHistogram( asse = Assembly(hexs) asse.SetScale(1.2 / n * dx, 1 / m * dy, norm / binmax * (dx + dy) / 4) asse.SetPosition(xmin, ymin, 0) + asse.base = np.array([0, 0, 0]) + asse.top = np.array([0, 0, 1]) return asse @@ -936,137 +1198,135 @@ def polarPlot( return rh -def fxy( - z="sin(3*x)*log(x-y)/3", - x=(0, 3), - y=(0, 3), - zlimits=(None, None), - showNan=True, - zlevels=10, - c="b", - bc="aqua", - alpha=1, - texture="paper", - res=(100, 100), -): - """ - Build a surface representing the function :math:`f(x,y)` specified as a string - or as a reference to an external function. +def sphericPlot(rfunc, normalize=True, res=25, + scalarbar=True, c='grey', alpha=0.1, cmap='jet'): + """Surface plotting in spherical coordinates. - :param float x: x range of values. - :param float y: y range of values. - :param float zlimits: limit the z range of the independent variable. - :param int zlevels: will draw the specified number of z-levels contour lines. - :param bool showNan: show where the function does not exist as red points. - :param list res: resolution in x and y. + Return an ``Assembly`` of 2 objects, the unit grid + sphere (in wireframe representation) and the surface `rho(theta, phi)`. - |fxy| |fxy.py|_ + :param function rfunc: handle to a user defined function. + :param bool normalize: scale surface to fit inside the unit sphere + :param int res: grid resolution + :param bool scalarbar: add a 3D scalarbar to the plot for radius + :param c: color of the unit grid + :param alpha: transparency of the unit grid + :param str cmap: color map of the surface - Function is: :math:`f(x,y)=\sin(3x) \cdot \log(x-y)/3` in range :math:`x=[0,3], y=[0,3]`. + |sphericPlot| |sphericPlot.py|_ """ - if isinstance(z, str): - try: - z = z.replace("math.", "").replace("np.", "") - namespace = locals() - code = "from math import*\ndef zfunc(x,y): return " + z - exec(code, namespace) - z = namespace["zfunc"] - except: - colors.printc("Syntax Error in fxy()", c=1) - return None + sg = shapes.SphericGrid(res=res) + sg.alpha(alpha).c(c).wireframe() - ps = vtk.vtkPlaneSource() - ps.SetResolution(res[0], res[1]) - ps.SetNormal([0, 0, 1]) - ps.Update() - poly = ps.GetOutput() - dx = x[1] - x[0] - dy = y[1] - y[0] - todel, nans = [], [] + cgpts = sg.points() + x, y, z = cgpts[:,0], cgpts[:,1], cgpts[:,2] + r, theta, phi = utils.cart2spher(x, y, z) - for i in range(poly.GetNumberOfPoints()): - px, py, _ = poly.GetPoint(i) - xv = (px + 0.5) * dx + x[0] - yv = (py + 0.5) * dy + y[0] + newr, inans = [], [] + for i in range(len(r)): try: - zv = z(xv, yv) + ri = rfunc(theta[i], phi[i]) + if np.isnan(ri): + inans.append(i) + newr.append(1) + else: + newr.append(ri) except: - zv = 0 - todel.append(i) - nans.append([xv, yv, 0]) - poly.GetPoints().SetPoint(i, [xv, yv, zv]) + inans.append(i) + newr.append(1) - if len(todel): - cellIds = vtk.vtkIdList() - poly.BuildLinks() - for i in todel: - poly.GetPointCells(i, cellIds) - for j in range(cellIds.GetNumberOfIds()): - poly.DeleteCell(cellIds.GetId(j)) # flag cell - poly.RemoveDeletedCells() - cl = vtk.vtkCleanPolyData() - cl.SetInputData(poly) - cl.Update() - poly = cl.GetOutput() + newr = np.array(newr) + if normalize: + newr = newr/np.max(newr) + newr[inans] = 1 - if not poly.GetNumberOfPoints(): - colors.printc("Function is not real in the domain", c=1) - return None + nanpts = [] + if len(inans): + redpts = utils.spher2cart(newr[inans], theta[inans], phi[inans]) + nanpts.append(shapes.Points(redpts, r=3, c='r')) - if zlimits[0]: - tmpact1 = Mesh(poly) - a = tmpact1.cutWithPlane((0, 0, zlimits[0]), (0, 0, 1)) - poly = a.polydata() - if zlimits[1]: - tmpact2 = Mesh(poly) - a = tmpact2.cutWithPlane((0, 0, zlimits[1]), (0, 0, -1)) - poly = a.polydata() + pts = utils.spher2cart(newr, theta, phi) - if c is None: - elev = vtk.vtkElevationFilter() - elev.SetInputData(poly) - elev.Update() - poly = elev.GetOutput() + ssurf = sg.clone().points(pts) + if len(inans): + ssurf.deletePoints(inans) - mesh = Mesh(poly, c, alpha).computeNormals().lighting("plastic") - if c is None: - mesh.getPointArray("Elevation") + ssurf.alpha(1).wireframe(0) - if bc: - mesh.bc(bc) + if scalarbar: + xm = np.max([np.max(pts[0]), 1]) + ym = np.max([np.abs(np.max(pts[1])), 1]) + ssurf.mapper().SetScalarRange(np.min(newr), np.max(newr)) + sb3d = ssurf.addScalarBar3D(cmap=cmap, sx=xm*0.07, sy=ym) + sb3d.rotateX(90).pos(xm*1.1, 0, -0.5) + else: + sb3d = None - mesh.texture(texture) + ssurf.pointColors(newr, cmap=cmap) + ssurf.computeNormals() + sg.pickable(False) + return Assembly([ssurf, sg] + nanpts + [sb3d]) - acts = [mesh] - if zlevels: - elevation = vtk.vtkElevationFilter() - elevation.SetInputData(poly) - bounds = poly.GetBounds() - elevation.SetLowPoint(0, 0, bounds[4]) - elevation.SetHighPoint(0, 0, bounds[5]) - elevation.Update() - bcf = vtk.vtkBandedPolyDataContourFilter() - bcf.SetInputData(elevation.GetOutput()) - bcf.SetScalarModeToValue() - bcf.GenerateContourEdgesOn() - bcf.GenerateValues(zlevels, elevation.GetScalarRange()) - bcf.Update() - zpoly = bcf.GetContourEdgesOutput() - zbandsact = Mesh(zpoly, "k", alpha).lw(0.5) - acts.append(zbandsact) - if showNan and len(todel): - bb = mesh.GetBounds() - if bb[4] <= 0 and bb[5] >= 0: - zm = 0.0 - else: - zm = (bb[4] + bb[5]) / 2 - nans = np.array(nans) + [0, 0, zm] - nansact = shapes.Points(nans, r=2, c="red", alpha=alpha) - nansact.GetProperty().RenderPointsAsSpheresOff() - acts.append(nansact) +def quiverPlot(points, vectors, + cmap='jet', alpha=1, + shaftLength=0.8, + shaftWidth=0.05, + headLength=0.25, + headWidth=0.2, + fill=True, + scale=1 + ): + """Quiver Plot, display `vectors` at `points` locations. - if len(acts) > 1: - return Assembly(acts) + Color can be specified as a colormap which maps the size of the arrows. + + :param float shaftLength: fractional shaft length + :param float shaftWidth: fractional shaft width + :param float headLength: fractional head length + :param float headWidth: fractional head width + :param bool fill: if False only generate the outline + + :param float scale: apply a rescaling factor to the length + + |quiverPlot| |quiverPlot.py|_ + """ + if isinstance(points, Mesh): + points = points.points() else: - return mesh + points = np.array(points) + vectors = np.array(vectors)/2 + + spts = points - vectors + epts = points + vectors + + arrs2d = shapes.Arrows2D(spts, epts, c=cmap, + shaftLength=shaftLength, + shaftWidth=shaftWidth, + headLength=headLength, + headWidth=headWidth, + fill=fill, + scale=scale, + alpha=alpha, + ) + arrs2d.pickable(False) + return arrs2d + + + + + + + + + + + + + + + + + + + diff --git a/vtkplotter/plotter.py b/vtkplotter/plotter.py index 0d31be0a..34655a4b 100644 --- a/vtkplotter/plotter.py +++ b/vtkplotter/plotter.py @@ -74,7 +74,6 @@ def show(*actors, **options): - `gridLineWidth`, [1], width of the grid lines - `reorientShortTitle`, [True], titles shorter than 2 letter are placed horizontally - `originMarkerSize`, [0.01], draw a small cube on the axis where the origin is - - `enableLastLabel`, [False], show last numeric label on axes - `titleDepth`, [0], extrusion fractional depth of title text - `xyGrid`, [True], show a gridded wall on plane xy - `yzGrid`, [True], show a gridded wall on plane yz @@ -101,6 +100,7 @@ def show(*actors, **options): - `xTickColor`, [automatic], color of major ticks - `xMinorTicks`, [1], number of minor ticks between two major ticks - `tipSize`, [0.01], size of the arrow tip + - `xPositionsAndLabels` [], assign custom tick positions and labels [(pos1, label1), ...] - `xLabelPrecision`, [2], nr. of significative digits to be shown - `xLabelSize`, [0.015], size of the numeric labels along axis - `xLabelOffset`, [0.025], offset of numeric labels @@ -195,6 +195,9 @@ def show(*actors, **options): size = options.pop("size", "auto") screensize = options.pop("screensize", "auto") title = options.pop("title", "") + #xtitle = options.pop("xtitle", "x") + #ytitle = options.pop("ytitle", "y") + #ztitle = options.pop("ztitle", "z") bg = options.pop("bg", "blackboard") bg2 = options.pop("bg2", None) axes = options.pop("axes", 4) @@ -215,7 +218,7 @@ def show(*actors, **options): if len(options): for op in options: - colors.printc("Warning: unknown keyword in shdow():", op, c=5) + colors.printc("Warning: unknown keyword in show():", op, c=5) if len(actors) == 0: actors = None @@ -259,6 +262,9 @@ def show(*actors, **options): interactive=interactive, offscreen=offscreen, ) + #vp.xtitle = xtitle + #vp.ytitle = ytitle + #vp.ztitle = ztitle # use _vp_to_return because vp.show() can return a k3d/panel plot if utils.isSequence(at): @@ -374,7 +380,7 @@ class Plotter: - `xtitle`, ['x'], x-axis title text. - `ytitle`, ['y'], y-axis title text. - `ztitle`, ['z'], z-axis title text. - - `numberOfDivisions`, [automatic], number of divisions on the shortest axis + - `numberOfDivisions`, [automatic], number of divisions on the longest axis - `axesLineWidth`, [1], width of the axes lines - `gridLineWidth`, [1], width of the grid lines - `reorientShortTitle`, [True], titles shorter than 2 letter are placed horizontally @@ -519,12 +525,19 @@ def __init__( self.window = vtk.vtkRenderWindow() ############################################################ - if settings.notebookBackend and settings.notebookBackend != "panel": + notebookBackend = settings.notebookBackend + if notebookBackend and notebookBackend.lower() == '2d': + self.offscreen = True + if self.size == "auto": + self.size = (900, 700) + if (notebookBackend + and notebookBackend != "panel" + and notebookBackend.lower() != "2d"): self.interactive = False self.interactor = None self.window = None self.camera = None # let the backend choose - if size == "auto": + if self.size == "auto": self.size = (1000, 1000) ######################################################## return ################################################# @@ -580,13 +593,15 @@ def __init__( if isinstance(shape, str): if '|' in shape: - if size == "auto": self.size = (800, 1200) + if self.size == "auto": + self.size = (800, 1200) n = int(shape.split('|')[0]) m = int(shape.split('|')[1]) rangen = reversed(range(n)) rangem = reversed(range(m)) else: - if size == "auto": self.size = (1200, 800) + if self.size == "auto": + self.size = (1200, 800) m = int(shape.split('/')[0]) n = int(shape.split('/')[1]) rangen = range(n) @@ -620,6 +635,7 @@ def __init__( r.SetLightFollowCamera(settings.lightFollowsCamera) if settings.useFXAA is not None: r.SetUseFXAA(settings.useFXAA) + self.window.SetMultiSamples(settings.multiSamples) if settings.useDepthPeeling: r.SetUseDepthPeeling(True) r.SetMaximumNumberOfPeels(settings.maxNumberOfPeels) @@ -631,7 +647,7 @@ def __init__( else: - if size == "auto": # figure out a reasonable window size + if self.size == "auto": # figure out a reasonable window size f = 1.5 xs = y / f * shape[1] # because y pip install itkwidgets # and if necessary:') print('> conda install nodejs') + elif backend.lower() == '2d': + verbose=False + elif backend=='panel': try: import panel @@ -317,8 +321,9 @@ def _init(): collectable_actors = [] for f in os.listdir(textures_path): - textures.append(f.split(".")[0]) - textures.remove("earth") + tfn = f.split(".")[0] + if 'earth' in tfn: continue + textures.append(tfn) textures = list(sorted(textures)) for f in os.listdir(fonts_path): diff --git a/vtkplotter/shapes.py b/vtkplotter/shapes.py index 7191a675..5c2c4234 100644 --- a/vtkplotter/shapes.py +++ b/vtkplotter/shapes.py @@ -27,6 +27,8 @@ "Ribbon", "Arrow", "Arrows", + "Arrow2D", + "Arrows2D", "FlatArrow", "Polygon", "Rectangle", @@ -36,9 +38,11 @@ "Star", "Sphere", "Spheres", + "SphericGrid", "Earth", "Ellipsoid", "Grid", + "CubicGrid", "Plane", "Box", "Cube", @@ -167,7 +171,7 @@ def __init__(self, plist, r=5, c="gold", alpha=1): vgf.SetInputData(src.GetOutput()) vgf.Update() pd = vgf.GetOutput() - pd.GetPoints().SetData(numpy_to_vtk(plist, deep=True)) + pd.GetPoints().SetData(numpy_to_vtk(np.ascontiguousarray(plist), deep=True)) ucols = vtk.vtkUnsignedCharArray() ucols.SetNumberOfComponents(4) @@ -223,7 +227,7 @@ def __init__(self, plist, r=5, c="gold", alpha=1): if n == 1: # passing just one point pd.GetPoints().SetPoint(0, [0, 0, 0]) else: - pd.GetPoints().SetData(numpy_to_vtk(plist, deep=True)) + pd.GetPoints().SetData(numpy_to_vtk(np.ascontiguousarray(plist), deep=True)) Mesh.__init__(self, pd, c, alpha) self.GetProperty().SetPointSize(r) if n == 1: @@ -238,6 +242,8 @@ class Glyph(Mesh): At each vertex of a mesh, another mesh - a `'glyph'` - is shown with various orientation options and coloring. + The input ``mesh`` can also be a simple list of 2D or 3D coordinates. + Color can be specified as a colormap which maps the size of the orientation vectors in `orientationArray`. @@ -261,6 +267,10 @@ def __init__(self, mesh, glyphObj, orientationArray=None, cmap = c c = None + if utils.isSequence(mesh): + # create a cloud of points + mesh = Points(mesh) + if tol: mesh = mesh.clone().clean(tol) poly = mesh.polydata() @@ -458,7 +468,7 @@ def __init__(self, p0, p1=None, c="r", alpha=1, lw=1, dotted=False, res=None): for i, p in enumerate(p0): ppoints.InsertPoint(i, p[0], p[1], 0) else: - ppoints.SetData(numpy_to_vtk(p0, deep=True)) + ppoints.SetData(numpy_to_vtk(np.ascontiguousarray(p0), deep=True)) lines = vtk.vtkCellArray() # Create the polyline. lines.InsertNextCell(len(p0)) for i in range(len(p0)): @@ -613,6 +623,9 @@ class Spline(Mesh): |tutorial_spline| |tutorial.py|_ """ def __init__(self, points, smooth=0.5, degree=2, s=2, res=None): + + if isinstance(points, Mesh): points = points.points() + from scipy.interpolate import splprep, splev if res is None: res = len(points)*20 @@ -711,7 +724,7 @@ class Tube(Mesh): def __init__(self, points, r=1, cap=True, c=None, alpha=1, res=12): ppoints = vtk.vtkPoints() # Generate the polyline - ppoints.SetData(numpy_to_vtk(points, deep=True)) + ppoints.SetData(numpy_to_vtk(np.ascontiguousarray(points), deep=True)) lines = vtk.vtkCellArray() lines.InsertNextCell(len(points)) for i in range(len(points)): @@ -773,7 +786,7 @@ def __init__(self, line1, line2, c="m", alpha=1, res=(200,5)): line2 = line2.points() ppoints1 = vtk.vtkPoints() # Generate the polyline1 - ppoints1.SetData(numpy_to_vtk(line1, deep=True)) + ppoints1.SetData(numpy_to_vtk(np.ascontiguousarray(line1), deep=True)) lines1 = vtk.vtkCellArray() lines1.InsertNextCell(len(line1)) for i in range(len(line1)): @@ -783,7 +796,7 @@ def __init__(self, line1, line2, c="m", alpha=1, res=(200,5)): poly1.SetLines(lines1) ppoints2 = vtk.vtkPoints() # Generate the polyline2 - ppoints2.SetData(numpy_to_vtk(line2, deep=True)) + ppoints2.SetData(numpy_to_vtk(np.ascontiguousarray(line2), deep=True)) lines2 = vtk.vtkCellArray() lines2.InsertNextCell(len(line2)) for i in range(len(line2)): @@ -827,36 +840,6 @@ def __init__(self, line1, line2, c="m", alpha=1, res=(200,5)): self.name = "Ribbon" -def FlatArrow(line1, line2, c="m", alpha=1, tipSize=1, tipWidth=1): - """Build a 2D arrow in 3D space by joining two close lines. - - |flatarrow| |flatarrow.py|_ - """ - if isinstance(line1, Mesh): line1 = line1.points() - if isinstance(line2, Mesh): line2 = line2.points() - - sm1, sm2 = np.array(line1[-1]), np.array(line2[-1]) - - v = (sm1-sm2)/3*tipWidth - p1 = sm1+v - p2 = sm2-v - pm1 = (sm1+sm2)/2 - pm2 = (np.array(line1[-2])+np.array(line2[-2]))/2 - pm12 = pm1-pm2 - tip = pm12/np.linalg.norm(pm12)*np.linalg.norm(v)*3*tipSize/tipWidth + pm1 - - line1.append(p1) - line1.append(tip) - line2.append(p2) - line2.append(tip) - resm = max(100, len(line1)) - - mesh = Ribbon(line1, line2, alpha=alpha, c=c, res=(resm, 1)).phong() - settings.collectable_actors.pop() - settings.collectable_actors.append(mesh) - mesh.name = "FlatArrow" - return mesh - class Arrow(Mesh): """ Build a 3D arrow from `startPoint` to `endPoint` of section size `s`, @@ -954,6 +937,175 @@ def Arrows(startPoints, endPoints=None, s=None, scale=1, c="r", alpha=1, res=12) return arrg +class Arrow2D(Mesh): + """ + Build a 2D arrow from `startPoint` to `endPoint`. + + :param float shaftLength: fractional shaft length + :param float shaftWidth: fractional shaft width + :param float headLength: fractional head length + :param float headWidth: fractional head width + :param bool fill: if False only generate the outline + """ + def __init__(self, startPoint, endPoint, + shaftLength=0.8, + shaftWidth=0.09, + headLength=None, + headWidth=0.2, + fill=True, + c="r", + alpha=1): + + # in case user is passing meshs + if isinstance(startPoint, vtk.vtkActor): startPoint = startPoint.GetPosition() + if isinstance(endPoint, vtk.vtkActor): endPoint = endPoint.GetPosition() + if len(startPoint) == 2: + startPoint = [startPoint[0], startPoint[1], 0] + if len(endPoint) == 2: + endPoint = [endPoint[0], endPoint[1], 0] + + headBase = 1 - headLength + if headWidth < shaftWidth: + headWidth = shaftWidth + if headLength is None or headBase > shaftLength: + headBase = shaftLength + + verts = [] + verts.append([0, -shaftWidth/2, 0]) + verts.append([shaftLength,-shaftWidth/2, 0]) + verts.append([headBase, -headWidth/2, 0]) + verts.append([1,0,0]) + verts.append([headBase, headWidth/2, 0]) + verts.append([shaftLength, shaftWidth/2, 0]) + verts.append([0, shaftWidth/2, 0]) + if fill: + faces = ((0,1,3,5,6), (5,3,4), (1,2,3)) + poly = utils.buildPolyData(verts, faces) + else: + lines = ((0,1,2,3,4,5,6,0)) + poly = utils.buildPolyData(verts, [], lines=lines) + + axis = np.array(endPoint) - np.array(startPoint) + length = np.linalg.norm(axis) + if length: + axis = axis / length + theta = np.arccos(axis[2]) + phi = np.arctan2(axis[1], axis[0]) + t = vtk.vtkTransform() + t.RotateZ(np.rad2deg(phi)) + t.RotateY(np.rad2deg(theta)) + t.RotateY(-90) # put it along Z + t.Scale(length, length, length) + tf = vtk.vtkTransformPolyDataFilter() + tf.SetInputData(poly) + tf.SetTransform(t) + tf.Update() + + Mesh.__init__(self, tf.GetOutput(), c, alpha) + self.flat().lighting('ambient') + self.DragableOff() + self.PickableOff() + self.base = np.array(startPoint) + self.top = np.array(endPoint) + settings.collectable_actors.append(self) + self.name = "Arrow2D" + +def Arrows2D(startPoints, endPoints=None, + shaftLength=0.8, + shaftWidth=0.09, + headLength=None, + headWidth=0.2, + fill=True, + scale=1, + c="r", alpha=1): + """ + Build 2D arrows between two lists of points `startPoints` and `endPoints`. + `startPoints` can be also passed in the form ``[[point1, point2], ...]``. + + Color can be specified as a colormap which maps the size of the arrows. + + :param float shaftLength: fractional shaft length + :param float shaftWidth: fractional shaft width + :param float headLength: fractional head length + :param float headWidth: fractional head width + :param bool fill: if False only generate the outline + + :param float scale: apply a rescaling factor to the length + :param c: color or array of colors, can also be a color map name. + :param float alpha: set transparency + + :Example: + .. code-block:: python + + from vtkplotter import Grid, Arrows2D + g1 = Grid(sx=1, sy=1) + g2 = Grid(sx=1.2, sy=1.2).rotateZ(4) + arrs2d = Arrows2D(g1, g2, c='jet') + arrs2d.show(axes=1, bg='white') + + |quiverPlot| + """ + if isinstance(startPoints, Mesh): startPoints = startPoints.points() + if isinstance(endPoints, Mesh): endPoints = endPoints.points() + startPoints = np.array(startPoints) + if endPoints is None: + strt = startPoints[:,0] + endPoints = startPoints[:,1] + startPoints = strt + else: + endPoints = np.array(endPoints) + + if headLength is None: + headLength = 1 - shaftLength + + arr = Arrow2D((0,0,0), (1,0,0), + shaftLength, shaftWidth, + headLength, headWidth, fill) + pts = Points(startPoints, r=0.001, c=c, alpha=alpha).off() + + orients = (endPoints - startPoints) * scale + if orients.shape[1] == 2: # make it 3d + orients = np.c_[np.array(orients), np.zeros(len(orients))] + + arrg = Glyph(pts, arr.polydata(False), + orientationArray=orients, scaleByVectorSize=True, + c=c, alpha=alpha).flat().lighting('ambient') + settings.collectable_actors.append(arrg) + arrg.name = "Arrows2D" + return arrg + + +def FlatArrow(line1, line2, c="m", alpha=1, tipSize=1, tipWidth=1): + """Build a 2D arrow in 3D space by joining two close lines. + + |flatarrow| |flatarrow.py|_ + """ + if isinstance(line1, Mesh): line1 = line1.points() + if isinstance(line2, Mesh): line2 = line2.points() + + sm1, sm2 = np.array(line1[-1]), np.array(line2[-1]) + + v = (sm1-sm2)/3*tipWidth + p1 = sm1+v + p2 = sm2-v + pm1 = (sm1+sm2)/2 + pm2 = (np.array(line1[-2])+np.array(line2[-2]))/2 + pm12 = pm1-pm2 + tip = pm12/np.linalg.norm(pm12)*np.linalg.norm(v)*3*tipSize/tipWidth + pm1 + + line1.append(p1) + line1.append(tip) + line2.append(p2) + line2.append(tip) + resm = max(100, len(line1)) + + mesh = Ribbon(line1, line2, alpha=alpha, c=c, res=(resm, 1)).phong() + settings.collectable_actors.pop() + settings.collectable_actors.append(mesh) + mesh.name = "FlatArrow" + return mesh + + class Polygon(Mesh): """ Build a polygon in the `xy` plane of `nsides` of radius `r`. @@ -1206,12 +1358,35 @@ def __init__(self, centers, r=1, c="r", alpha=1, res=8): self.name = "Spheres" +def SphericGrid(pos=(0, 0, 0), r=1, c="t", alpha=1, res=12): + """Build a sphere made of quads. + + |sphericgrid| + """ + sg = CubicGrid(n=(res,res,res)) + + cgpts = sg.points()-(.5,.5,.5) + + x, y, z = cgpts[:,0], cgpts[:,1], cgpts[:,2] + x = x*(1+x*x)/2 + y = y*(1+y*y)/2 + z = z*(1+z*z)/2 + _, theta, phi = utils.cart2spher(x, y, z) + + pts = utils.spher2cart(np.ones_like(phi)*r, theta, phi) + + sg.points(pts).computeNormals().c(c).alpha(alpha) + sg.SetPosition(pos) + sg.name = "SphericGrid" + return sg + + class Earth(Mesh): """Build a textured mesh representing the Earth. |geodesic| |geodesic.py|_ """ - def __init__(self, pos=(0, 0, 0), r=1): + def __init__(self, pos=(0, 0, 0), r=1, style=1): tss = vtk.vtkTexturedSphereSource() tss.SetRadius(r) tss.SetThetaResolution(72) @@ -1220,8 +1395,8 @@ def __init__(self, pos=(0, 0, 0), r=1): Mesh.__init__(self, tss, c="w") atext = vtk.vtkTexture() - pnmReader = vtk.vtkPNMReader() - fn = settings.textures_path + "earth.ppm" + pnmReader = vtk.vtkJPEGReader() + fn = settings.textures_path + "earth" + str(style) +".jpg" pnmReader.SetFileName(fn) atext.SetInputConnection(pnmReader.GetOutputPort()) atext.InterpolateOn() @@ -1238,6 +1413,8 @@ class Ellipsoid(Mesh): .. note:: `axis1` and `axis2` are only used to define sizes and one azimuth angle. |projectsphere| + + |pca| |pca.py|_ """ def __init__(self, pos=(0, 0, 0), axis1=(1, 0, 0), axis2=(0, 2, 0), axis3=(0, 0, 3), c="c", alpha=1, res=24): @@ -1273,7 +1450,7 @@ def __init__(self, pos=(0, 0, 0), axis1=(1, 0, 0), axis2=(0, 2, 0), axis3=(0, 0, self.GetProperty().BackfaceCullingOn() self.SetPosition(pos) - self.base = -np.array(axis1) / 2 + pos + self.Length = -np.array(axis1) / 2 + pos self.top = np.array(axis1) / 2 + pos settings.collectable_actors.append(self) self.name = "Ellipsoid" @@ -1328,7 +1505,6 @@ def __init__(self, faces.append([i+j*n, i+1+j*n, i+1+j1n, i+j1n]) Mesh.__init__(self, [verts, faces], c, alpha) - self.orientation(normal) else: ps = vtk.vtkPlaneSource() @@ -1336,27 +1512,30 @@ def __init__(self, ps.Update() poly0 = ps.GetOutput() t0 = vtk.vtkTransform() + # t0.Translate(pos) t0.Scale(sx, sy, 1) tf0 = vtk.vtkTransformPolyDataFilter() tf0.SetInputData(poly0) tf0.SetTransform(t0) tf0.Update() poly = tf0.GetOutput() - axis = np.array(normal) / np.linalg.norm(normal) - theta = np.arccos(axis[2]) - phi = np.arctan2(axis[1], axis[0]) - t = vtk.vtkTransform() - t.PostMultiply() - t.RotateY(np.rad2deg(theta)) - t.RotateZ(np.rad2deg(phi)) - tf = vtk.vtkTransformPolyDataFilter() - tf.SetInputData(poly) - tf.SetTransform(t) - tf.Update() - Mesh.__init__(self, tf.GetOutput(), c, alpha) + # axis = np.array(normal) / np.linalg.norm(normal) + # theta = np.arccos(axis[2]) + # phi = np.arctan2(axis[1], axis[0]) + # t = vtk.vtkTransform() + # t.PostMultiply() + # t.RotateY(np.rad2deg(theta)) + # t.RotateZ(np.rad2deg(phi)) + # tf = vtk.vtkTransformPolyDataFilter() + # tf.SetInputData(poly) + # tf.SetTransform(t) + # tf.Update() + Mesh.__init__(self, poly, c, alpha) + self.SetPosition(pos) + + self.orientation(normal) self.wireframe().lw(lw) - self.SetPosition(pos) settings.collectable_actors.append(self) self.name = "Grid" @@ -1440,6 +1619,30 @@ def Cube(pos=(0, 0, 0), side=1, c="g", alpha=1): mesh.name = "Cube" return mesh +def CubicGrid(pos=(0, 0, 0), n=(10,10,10), spacing=(), c="lightgrey", alpha=0.1): + """Build a cubic Mesh made o `n` small cubes in the 3 axis directions. + + :param list pos: position of the left bottom corner + :param int n: number of subdivisions + :parameter list spacing: size of the side of the cube in the 3 directions + """ + from vtkplotter.volume import Volume + n = np.array(n).astype(int) + 2 + nx, ny, nz = n + mx, my, mz = n - 1 + data_matrix = np.zeros([nx, ny, nz], dtype=np.uint8) + data_matrix[0:mx, 0:my, 0:mz] = 1 + data_matrix[mx:nx, my:ny, mz:nz] = 2 + if len(spacing) == 0: + spacing = 1/(n-2) + vol = Volume(data_matrix, spacing=spacing) + mesh = vol.legosurface().clean().c(c).alpha(alpha) + mesh.SetPosition(pos) + mesh.base = np.array([0,0,0]) + mesh.top = np.array([0,0,1]) + mesh.name = "CubicGrid" + return mesh + class Spring(Mesh): """ diff --git a/vtkplotter/textures/earth.ppm b/vtkplotter/textures/earth.ppm deleted file mode 100644 index 0a41c2ec..00000000 Binary files a/vtkplotter/textures/earth.ppm and /dev/null differ diff --git a/vtkplotter/textures/earth1.jpg b/vtkplotter/textures/earth1.jpg new file mode 100644 index 00000000..e8292497 Binary files /dev/null and b/vtkplotter/textures/earth1.jpg differ diff --git a/vtkplotter/textures/earth2.jpg b/vtkplotter/textures/earth2.jpg new file mode 100644 index 00000000..2cbb7e85 Binary files /dev/null and b/vtkplotter/textures/earth2.jpg differ diff --git a/vtkplotter/textures/earth3.jpg b/vtkplotter/textures/earth3.jpg new file mode 100644 index 00000000..1765a647 Binary files /dev/null and b/vtkplotter/textures/earth3.jpg differ diff --git a/vtkplotter/textures/earth4.jpg b/vtkplotter/textures/earth4.jpg new file mode 100644 index 00000000..4885d8e9 Binary files /dev/null and b/vtkplotter/textures/earth4.jpg differ diff --git a/vtkplotter/textures/earth5.jpg b/vtkplotter/textures/earth5.jpg new file mode 100644 index 00000000..cbcdfc66 Binary files /dev/null and b/vtkplotter/textures/earth5.jpg differ diff --git a/vtkplotter/textures/earth6.jpg b/vtkplotter/textures/earth6.jpg new file mode 100644 index 00000000..368af757 Binary files /dev/null and b/vtkplotter/textures/earth6.jpg differ diff --git a/vtkplotter/textures/earth7.jpg b/vtkplotter/textures/earth7.jpg new file mode 100644 index 00000000..24a325f2 Binary files /dev/null and b/vtkplotter/textures/earth7.jpg differ diff --git a/vtkplotter/textures/sun.jpg b/vtkplotter/textures/sun.jpg new file mode 100644 index 00000000..9d787d85 Binary files /dev/null and b/vtkplotter/textures/sun.jpg differ diff --git a/vtkplotter/utils.py b/vtkplotter/utils.py index 8850f12f..9192dd28 100644 --- a/vtkplotter/utils.py +++ b/vtkplotter/utils.py @@ -216,6 +216,8 @@ def buildPolyData(vertices, faces=None, lines=None, indexOffset=0, fast=True): one by one. This is the fallback case when a mesh contains faces of different number of vertices. """ +# if not len(vertices): +# return None if len(vertices[0]) < 3: # make sure it is 3d vertices = np.c_[np.array(vertices), np.zeros(len(vertices))] @@ -232,7 +234,7 @@ def buildPolyData(vertices, faces=None, lines=None, indexOffset=0, fast=True): # Create a cell array to store the lines in and add the lines to it linesarr = vtk.vtkCellArray() - for i in range(1, len(lines)-1): + for i in range(0, len(lines)-1): vline = vtk.vtkLine() vline.GetPointIds().SetId(0,lines[i]) vline.GetPointIds().SetId(1,lines[i+1]) @@ -404,7 +406,7 @@ def distance(P0, P1, p): def linInterpolate(x, rangeX, rangeY): """ Interpolate linearly variable x in rangeX onto rangeY. - If x is a vector the linear weight is the distance to two the rangeX vectors. + If x is a 3D vector the linear weight is the distance to the two 3D rangeX vectors. E.g. if x runs in rangeX=[x0,x1] and I want it to run in rangeY=[y0,y1] then y = linInterpolate(x, rangeX, rangeY) will interpolate x onto rangeY. @@ -635,8 +637,8 @@ def cart2spher(x, y, z): """Cartesian to Spherical coordinate conversion.""" hxy = np.hypot(x, y) rho = np.hypot(hxy, z) - if not rho: - return np.array([0,0,0]) + #if not rho: + # return np.array([0,0,0]) theta = np.arctan2(hxy, z) phi = np.arctan2(y, x) return rho, theta, phi diff --git a/vtkplotter/version.py b/vtkplotter/version.py index e9fd90c5..a6cce7c6 100644 --- a/vtkplotter/version.py +++ b/vtkplotter/version.py @@ -1 +1 @@ -_version='2020.0.1' +_version='2020.0.2' diff --git a/vtkplotter/volume.py b/vtkplotter/volume.py index d6b37760..03e71465 100644 --- a/vtkplotter/volume.py +++ b/vtkplotter/volume.py @@ -189,11 +189,23 @@ def spacing(self, s=None): """Set/get the voxels size in the 3 dimensions.""" if s is not None: self._imagedata.SetSpacing(s) - self._mapper.Modified() + self._update(self._imagedata) return self else: return np.array(self._imagedata.GetSpacing()) + def center(self, center=None): + """Set/get the volume coordinates of its center. + Position is reset to (0,0,0).""" + if center is not None: + cn = self._imagedata.GetCenter() + self._imagedata.SetOrigin(-np.array(cn)/2) + self._update(self._imagedata) + self.pos(0,0,0) + return self + else: + return np.array(self._imagedata.GetCenter()) + def permuteAxes(self, x, y ,z): """Reorder the axes of the Volume by specifying the input axes which are supposed to become the new X, Y, and Z.""" diff --git a/vtkplotter/vtkio.py b/vtkplotter/vtkio.py index a0de5c33..c8899c4d 100644 --- a/vtkplotter/vtkio.py +++ b/vtkplotter/vtkio.py @@ -41,9 +41,10 @@ def load(inputobj, c=None, alpha=1, threshold=False, spacing=(), unpack=True): """ - Load ``Mesh`` and ``Volume`` objects from file. + Load ``Mesh``, ``Volume`` and ``Picture`` objects from file. The output will depend on the file extension. See examples below. + Unzip on the fly, if it ends with `.gz`. :param c: color in RGB format, hex, symbol or name :param alpha: transparency/opacity of the polygonal data. @@ -97,6 +98,9 @@ def load(inputobj, c=None, alpha=1, threshold=False, spacing=(), unpack=True): if os.path.isfile(fod): ### it's a file + if fod.endswith('.gz'): + fod = gunzip(fod) + a = _load_file(fod, c, alpha, threshold, spacing, unpack) acts.append(a) @@ -213,11 +217,12 @@ def _load_file(filename, c, alpha, threshold, spacing, unpack): for i in range(mb.GetNumberOfBlocks()): b = mb.GetBlock(i) if isinstance(b, (vtk.vtkPolyData, - vtk.vtkImageData, vtk.vtkUnstructuredGrid, vtk.vtkStructuredGrid, vtk.vtkRectilinearGrid)): - acts.append(b) + acts.append(Mesh(b, c=c, alpha=alpha)) + if isinstance(b, vtk.vtkImageData): + acts.append(Volume(b)) return acts else: return mb @@ -973,9 +978,9 @@ def write(objct, fileoutput, binary=True): obj = objct fr = fileoutput.lower() - if ".vtk" in fr: + if fr.endswith(".vtk"): writer = vtk.vtkPolyDataWriter() - elif ".ply" in fr: + elif fr.endswith(".ply"): writer = vtk.vtkPLYWriter() pscal = obj.GetPointData().GetScalars() if not pscal: @@ -986,15 +991,17 @@ def write(objct, fileoutput, binary=True): lut = objct.GetMapper().GetLookupTable() if lut: writer.SetLookupTable(lut) - elif ".stl" in fr: + elif fr.endswith(".stl"): writer = vtk.vtkSTLWriter() - elif ".obj" in fr: - writer = vtk.vtkOBJWriter() - elif ".vtp" in fr: + elif fr.endswith(".vtp"): writer = vtk.vtkXMLPolyDataWriter() - elif ".vtm" in fr: + elif fr.endswith(".vtm"): g = vtk.vtkMultiBlockDataGroupFilter() for ob in objct: + if isinstance(ob, Mesh): # picks transformation + ob = ob.polydata(True) + elif isinstance(ob, (vtk.vtkActor, vtk.vtkVolume)): + ob = ob.GetMapper().GetInput() g.AddInputData(ob) g.Update() mb = g.GetOutputDataObject(0) @@ -1003,26 +1010,26 @@ def write(objct, fileoutput, binary=True): wri.SetFileName(fileoutput) wri.Write() return mb - elif ".xyz" in fr: + elif fr.endswith(".xyz"): writer = vtk.vtkSimplePointsWriter() - elif ".facet" in fr: + elif fr.endswith(".facet"): writer = vtk.vtkFacetWriter() - elif ".tif" in fr: + elif fr.endswith(".tif"): writer = vtk.vtkTIFFWriter() writer.SetFileDimensionality(len(obj.GetDimensions())) - elif ".vti" in fr: + elif fr.endswith(".vti"): writer = vtk.vtkXMLImageDataWriter() - elif ".mhd" in fr: + elif fr.endswith(".mhd"): writer = vtk.vtkMetaImageWriter() - elif ".nii" in fr: + elif fr.endswith(".nii"): writer = vtk.vtkNIFTIImageWriter() - elif ".png" in fr: + elif fr.endswith(".png"): writer = vtk.vtkPNGWriter() - elif ".jpg" in fr: + elif fr.endswith(".jpg"): writer = vtk.vtkJPEGWriter() - elif ".bmp" in fr: + elif fr.endswith(".bmp"): writer = vtk.vtkBMPWriter() - elif ".npy" in fr: + elif fr.endswith(".npy"): if utils.isSequence(objct): objslist = objct else: @@ -1033,7 +1040,52 @@ def write(objct, fileoutput, binary=True): np.save(fileoutput, dicts2save) return dicts2save - elif ".xml" in fr: # write tetrahedral dolfin xml + elif fr.endswith(".obj"): + outF = open(fileoutput, "w") + outF.write('# OBJ file format with ext .obj\n') + outF.write('# File Created by vtkplotter\n') + cobjct = objct.clone().clean() + + for p in cobjct.points(): + outF.write('v '+ str(p[0]) +" "+ str(p[1])+" "+ str(p[2])+'\n') + + for vn in cobjct.normals(cells=False): + outF.write('vn '+str(vn[0])+" "+str(vn[1])+" "+str(vn[2])+'\n') + + #pdata = cobjct.polydata().GetPointData().GetScalars() + #if pdata: + # ndata = vtk_to_numpy(pdata) + # for vd in ndata: + # outF.write('vp '+ str(vd) +'\n') + + #ptxt = cobjct.polydata().GetPointData().GetTCoords() # not working + #if ptxt: + # ntxt = vtk_to_numpy(ptxt) + # print(len(cobjct.faces()), cobjct.points().shape, ntxt.shape) + # for vt in ntxt: + # outF.write('vt '+ str(vt[0]) +" "+ str(vt[1])+ ' 0\n') + + for f in cobjct.faces(): + fs = '' + for fi in f: + fs += " "+str(fi+1) + outF.write('f' + fs + '\n') + + #ldata = cobjct.polydata().GetLines().GetData() + #print(cobjct.polydata().GetLines()) + #if ldata: + # ndata = vtk_to_numpy(ldata) + # print(ndata) + # for l in ndata: + # ls = '' + # for li in l: + # ls += str(li+1)+" " + # outF.write('l '+ ls + '\n') + + outF.close() + return objct + + elif fr.endswith(".xml"): # write tetrahedral dolfin xml vertices = objct.points().astype(str) faces = np.array(objct.faces()).astype(str) ncoords = vertices.shape[0] @@ -1087,7 +1139,6 @@ def write(objct, fileoutput, binary=True): writer.SetInputData(obj) writer.SetFileName(fileoutput) writer.Write() - colors.printc("~save Saved file: " + fileoutput, c="g") except Exception as e: colors.printc("~noentry Error saving: " + fileoutput, "\n", e, c="r") return objct @@ -1123,13 +1174,19 @@ def exportWindow(fileoutput, binary=False, speed=None, html=True): ''' fr = fileoutput.lower() - if ".obj" in fr: + if fr.endswith(".obj"): w = vtk.vtkOBJExporter() w.SetInputData(settings.plotter_instance.window) w.Update() colors.printc("~save Saved file:", fileoutput, c="g") - elif ".x3d" in fr: + elif fr.endswith(".obj"): + writer = vtk.vtkOBJWriter() + writer.SetInputData(obj) + writer.SetFileName(fileoutput) + writer.Write() + + elif fr.endswith(".x3d"): exporter = vtk.vtkX3DExporter() exporter.SetBinary(binary) exporter.FastestOff() @@ -1156,7 +1213,7 @@ def exportWindow(fileoutput, binary=False, speed=None, html=True): outF.close() colors.printc("~save Saved files:", fileoutput, fileoutput.replace('.x3d', '.html'), c="g") - elif ".npy" in fr: + elif fr.endswith(".npy"): sdict = dict() vp = settings.plotter_instance sdict['shape'] = vp.shape #todo @@ -1274,13 +1331,17 @@ def importWindow(fileinput, mtlFile=None, texturePath=None): ########################################################## -def screenshot(filename="screenshot.png", scale=None): +def screenshot(filename="screenshot.png", scale=None, returnNumpy=False): """ Save a screenshot of the current rendering window. + + :param int scale: set image magnification + :param bool returnNumpy: return a numpy array of the image """ if not settings.plotter_instance or not settings.plotter_instance.window: colors.printc('~bomb screenshot(): Rendering window is not present, skip.', c=1) return + if scale is None: scale = settings.screeshotScale @@ -1297,6 +1358,15 @@ def screenshot(filename="screenshot.png", scale=None): w2if.ReadFrontBufferOff() # read from the back buffer w2if.Update() + if returnNumpy: + w2ifout = w2if.GetOutput() + npdata = vtk_to_numpy(w2ifout.GetPointData().GetArray("ImageScalars")) + npdata = npdata[:,[0,1,2]] + ydim, xdim, _ = w2ifout.GetDimensions() + npdata = npdata.reshape([xdim, ydim, -1]) + npdata = np.flip(npdata, axis=0) + return npdata + if filename.endswith('.png'): writer = vtk.vtkPNGWriter() writer.SetFileName(filename)