From 953e2d9f9c0226bea88d93a973c85e8a210000a3 Mon Sep 17 00:00:00 2001 From: Marco Musy Date: Sun, 4 Aug 2019 14:09:00 +0200 Subject: [PATCH] 4.1 --- docs/source/conf.py | 2 +- examples/basic/ids.py | 12 - examples/basic/lights.py | 6 +- examples/basic/lorenz.py | 27 +- examples/basic/manypoints.py | 4 +- examples/basic/mesh_bands.py | 8 +- examples/basic/mesh_custom.py | 6 +- examples/basic/slider_browser.py | 33 ++ examples/notebooks/manipulate_camera.ipynb | 95 +++++ examples/other/dolfin/demo_cahn-hilliard.py | 1 + examples/other/dolfin/demo_submesh.ipynb | 28 +- examples/other/dolfin/navier-stokes_lshape.py | 1 + .../dolfin/noplot/ex05_non-matching-meshes.py | 4 +- examples/other/dolfin/stokes.py | 9 +- examples/other/dolfin/turing_2d.py | 92 +++++ examples/other/dolfin/turing_3d.py | 93 +++++ examples/other/menger_fractal.py | 2 +- examples/other/qt_window_split.py | 4 +- examples/other/sierpinski.ipynb | 4 +- examples/other/tf_learn_volume.py | 6 +- examples/other/trimesh/first_example.ipynb | 66 ++-- examples/other/trimesh/first_example.py | 20 +- examples/other/trimesh/nearest.py | 6 +- examples/other/trimesh/ray.ipynb | 42 +- examples/other/trimesh/section.ipynb | 23 +- examples/other/trimesh/shortest.py | 7 +- examples/run_all.sh | 3 + examples/simulations/doubleslit.py | 4 +- examples/simulations/multiple_pendulum.py | 2 +- examples/simulations/turing.py | 2 + examples/volumetric/isosurfaces1.py | 4 +- examples/volumetric/numpy2volume.py | 2 +- examples/volumetric/tensors.ipynb | 7 +- setup.py | 2 +- vtkplotter/__init__.py | 29 +- vtkplotter/actors.py | 362 ++++++++++-------- vtkplotter/addons.py | 41 +- vtkplotter/analysis.py | 45 +-- vtkplotter/colors.py | 4 +- vtkplotter/data/images/limbs_tc.jpg | Bin 0 -> 96547 bytes vtkplotter/data/timecourse1d.npy | Bin 672074 -> 702179 bytes .../data/timecourse1d/reference_243.vtk | Bin 0 -> 4915 bytes .../data/timecourse1d/reference_244.vtk | Bin 0 -> 4915 bytes .../data/timecourse1d/reference_245.vtk | Bin 0 -> 4915 bytes .../data/timecourse1d/reference_246.vtk | Bin 0 -> 4915 bytes .../data/timecourse1d/reference_247.vtk | Bin 0 -> 4915 bytes .../data/timecourse1d/reference_248.vtk | Bin 0 -> 4915 bytes vtkplotter/docs.py | 3 +- vtkplotter/dolfin.py | 105 +++-- vtkplotter/plotter.py | 124 +++--- vtkplotter/settings.py | 53 ++- vtkplotter/shapes.py | 154 ++------ vtkplotter/trimesh.py | 116 ++++++ vtkplotter/utils.py | 191 ++++++--- vtkplotter/version.py | 2 +- vtkplotter/vtkio.py | 244 +++++++----- 56 files changed, 1348 insertions(+), 752 deletions(-) delete mode 100644 examples/basic/ids.py create mode 100644 examples/basic/slider_browser.py create mode 100644 examples/notebooks/manipulate_camera.ipynb create mode 100644 examples/other/dolfin/turing_2d.py create mode 100644 examples/other/dolfin/turing_3d.py create mode 100644 vtkplotter/data/images/limbs_tc.jpg create mode 100644 vtkplotter/data/timecourse1d/reference_243.vtk create mode 100644 vtkplotter/data/timecourse1d/reference_244.vtk create mode 100644 vtkplotter/data/timecourse1d/reference_245.vtk create mode 100644 vtkplotter/data/timecourse1d/reference_246.vtk create mode 100644 vtkplotter/data/timecourse1d/reference_247.vtk create mode 100644 vtkplotter/data/timecourse1d/reference_248.vtk create mode 100644 vtkplotter/trimesh.py diff --git a/docs/source/conf.py b/docs/source/conf.py index 223220ad..0fc46092 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -89,7 +89,7 @@ # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path . -exclude_patterns = [] +exclude_patterns = ['../../vtkplotter/docs.py'] # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' diff --git a/examples/basic/ids.py b/examples/basic/ids.py deleted file mode 100644 index 37649371..00000000 --- a/examples/basic/ids.py +++ /dev/null @@ -1,12 +0,0 @@ -""" -Add an array of cell and point IDs. -""" -print(__doc__) - -from vtkplotter import * - -act = Spring().addIDs() - -print(act.scalars(), "press k to apply point scalars.") - -show(act, viewup="z", verbose=0) diff --git a/examples/basic/lights.py b/examples/basic/lights.py index f57ab544..a77f1cbe 100644 --- a/examples/basic/lights.py +++ b/examples/basic/lights.py @@ -3,8 +3,8 @@ vp = Plotter() cow = vp.load(datadir+"cow.byu", c="grey", alpha=0.7) -vp += Plane(pos=[0, -3.6, 0], normal=[0, 1, 0], sx=20, texture="grass") -vp.show(viewup='y') +vp += Plane(pos=[0, -3.6, 0], normal=[0, 1, 0], sx=20).texture("grass") +vp.show(viewup='y', interactive=0) # vp.light() returns a vtkLight object with focal Point, fp, to actor cow # fp can also be explicitly set as fp=[x,y,z] @@ -13,4 +13,4 @@ # can be switched on/off this way #l.SwitchOff() -vp.show() +vp.show(interactive=1) diff --git a/examples/basic/lorenz.py b/examples/basic/lorenz.py index 3afc4354..c746c20a 100644 --- a/examples/basic/lorenz.py +++ b/examples/basic/lorenz.py @@ -1,24 +1,29 @@ -from vtkplotter import Plotter, mag, Points, Point import numpy as np - dt = 0.002 -y = [25.0, -10.0, -7.0] # Starting point (initial condition) +y = (25.0, -10.0, -7.0) # Starting point (initial condition) pts, cols = [], [] - for t in np.linspace(0, 20, int(20 / dt)): # Integrate a funny differential equation dydt = np.array( - [-8 / 3.0 * y[0] + y[1] * y[2], -10.0 * (y[1] - y[2]), -y[1] * y[0] + 28.0 * y[1] - y[2]] + [-8 / 3.0 * y[0] + y[1] * y[2], + -10.0 * (y[1] - y[2]), + -y[1] * y[0] + 28.0 * y[1] - y[2]] ) y = y + dydt * dt - c = np.clip([mag(dydt) * 0.005], 0, 1)[0] # color by speed + c = np.clip([np.linalg.norm(dydt) * 0.005], 0, 1)[0] # color by speed + cols.append([c, 0, 1-c]) pts.append(y) - cols.append([c, 0, 1 - c]) -scene = Plotter(title="Lorenz attractor", axes=2, verbose=0, bg="w") -scene += Point(y, r=20, c="g") -scene += Points(pts, r=5, c=cols) -scene.show() + +from vtkplotter import Plotter, Line, Point, Points, settings +settings.renderPointsAsSpheres = False # render points as squares + +scene = Plotter(title="Lorenz attractor", axes=1, verbose=0, bg="w") +scene += Point(y, r=10, c="g") # end point +scene += Points(pts, r=3, c=cols) +scene += Line(pts).off().addShadow(x=3) # only show shadow, not line +scene += Line(pts).off().addShadow(z=-30) +scene.show(viewup='z') diff --git a/examples/basic/manypoints.py b/examples/basic/manypoints.py index df5b402a..8245e7c6 100644 --- a/examples/basic/manypoints.py +++ b/examples/basic/manypoints.py @@ -15,13 +15,13 @@ RGBA = np.c_[RGB, Alpha] # concatenate print("clock starts") -t0 = time.clock() +t0 = time.time() # passing c in format (R,G,B,A) is ~50x faster pts = Points(pts, r=2, c=RGBA) #fast #pts = Points(pts, r=2, c=pts, alpha=pts[:, 2]) #slow -t1 = time.clock() +t1 = time.time() print("----> elapsed time:", t1-t0, "seconds for N:", N) pts.show(bg="white", axes=True) diff --git a/examples/basic/mesh_bands.py b/examples/basic/mesh_bands.py index e3e6d25b..31136370 100644 --- a/examples/basic/mesh_bands.py +++ b/examples/basic/mesh_bands.py @@ -1,11 +1,13 @@ """ Use a scalar to paint colored bands on a mesh, this can be combined with opacities values for each vertex of the mesh. -Keyword depthpeeling improves the rendering of translucent objects. +useDepthPeeling improves the rendering of translucent objects. """ -from vtkplotter import show, Hyperboloid, Torus, Text +from vtkplotter import * from numpy import linspace +settings.useDepthPeeling = True + doc = Text(__doc__, c="k", bg="lg") @@ -18,4 +20,4 @@ transp = linspace(1, 0.5, len(scalars)) # set transparencies from 1 -> .5 tor.pointColors(scalars, alpha=transp, bands=3, cmap="winter") -show(hyp, tor, doc, viewup="z", depthpeeling=1, axes=2) +show(hyp, tor, doc, viewup="z", axes=2) diff --git a/examples/basic/mesh_custom.py b/examples/basic/mesh_custom.py index c37fed46..672dbeaf 100644 --- a/examples/basic/mesh_custom.py +++ b/examples/basic/mesh_custom.py @@ -1,10 +1,12 @@ """ Example on how to specify a color for each individual cell or point of an actor's mesh. -Keyword depthpeeling may improve the rendering of transparent objects. +useDepthPeeling may improve the rendering of transparent objects. """ from vtkplotter import * +settings.useDepthPeeling = True + doc = Text(__doc__, pos=1, c="w") @@ -28,4 +30,4 @@ man.pointColors(scals, cmap=mymap, alpha=alphas).addScalarBar() -show(man, doc, viewup="z", axes=1, depthpeeling=1) +show(man, doc, viewup="z", axes=1) diff --git a/examples/basic/slider_browser.py b/examples/basic/slider_browser.py new file mode 100644 index 00000000..980e9647 --- /dev/null +++ b/examples/basic/slider_browser.py @@ -0,0 +1,33 @@ +""" + Mouse hind limb growth from day 10 9h to day 15 21h +""" +from vtkplotter import * + +objs = load(datadir+'timecourse1d.npy') # list of vtkActors + +# show the biggest and continue (return a Plotter instance) +vp = show(objs[-1], axes=True, interactive=False) +vp.actors = objs # set Plotter internal list of objs to be shown + +# switch off all the others +[objs[i].c('gold').lw(2.0).off() for i in range(1, len(objs))] + +k = 0 # visible actor index +def sliderfunc(widget, event): + global k + vp.actors[k].off() #switch off + k = int(widget.GetRepresentation().GetValue()) + vp.actors[k].on() #switch on + days = int((k+249)/24) + hours = ' %2sh (' % (k+249 -days*24) + limbage = str(days)+ "d"+ hours + str(k+249)+"h)" + widget.GetRepresentation().SetTitleText(limbage) + +vp.addSlider2D(sliderfunc, 0, len(objs)-1, + pos=[(0.4,0.1), (0.9,0.1)], showValue=False) + +vp += Text(__doc__, font='SpecialElite', s=1.2) +vp += load(datadir+'images/limbs_tc.jpg').scale(0.0154).y(10.0) +vp += Line([(0,8), (0,10), (28.6,10), (4.5,8)], c='gray') + +vp.show(zoom=1.2, interactive=True) diff --git a/examples/notebooks/manipulate_camera.ipynb b/examples/notebooks/manipulate_camera.ipynb new file mode 100644 index 00000000..808f5103 --- /dev/null +++ b/examples/notebooks/manipulate_camera.ipynb @@ -0,0 +1,95 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "7811126ce93c494bb80ce84c8003236d", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Plot(antialias=3, axes=['x', 'y', 'z'], background_color=16777215, camera=[4.5, 4.5, 4.5, 0.0, 0.0, 0.0, 1.0, …" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from vtkplotter import *\n", + "\n", + "bu = load(datadir+'bunny.obj')\n", + "\n", + "bu.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "# Manually set the k3d camera. Syntax is:\n", + "# [posx,posy,posz, targetx,targety,targetz, upx,upy,upz]\n", + "settings.notebook_plotter.camera = [0., 0. ,1.,\n", + " 0., 0., 0.,\n", + " 0., 1., 0.]" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[1mk3dcam is [-0.0068 0.044 0.192 -0.017 0.11 0.08 0. 1. 0. ]\u001b[0m\n" + ] + } + ], + "source": [ + "# Convert a vtkCamera object into the appropiate K3D list\n", + "import vtk\n", + "vcam = vtk.vtkCamera()\n", + "vcam.SetPosition( [-0.017, 0.11, 0.48] )\n", + "vcam.SetFocalPoint( [-0.017, 0.11, -0.001] )\n", + "vcam.SetViewUp( [0.0, 1.0, 0.0] )\n", + "vcam.SetDistance( 0.4 )\n", + "k3dcam = vtkCameraToK3D(vcam)\n", + "printc('k3dcam is', k3dcam)\n", + "\n", + "settings.notebook_plotter.camera = k3dcam" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/other/dolfin/demo_cahn-hilliard.py b/examples/other/dolfin/demo_cahn-hilliard.py index 1d20da4d..eb4f3409 100644 --- a/examples/other/dolfin/demo_cahn-hilliard.py +++ b/examples/other/dolfin/demo_cahn-hilliard.py @@ -102,6 +102,7 @@ def J(self, A, x): assemble(self.a, tensor=A) elevation=-3, # move camera a bit azimuth=1, text='time: '+str(t*2e4), + lighting='plastic', interactive=0 ) plot() diff --git a/examples/other/dolfin/demo_submesh.ipynb b/examples/other/dolfin/demo_submesh.ipynb index 82981cb9..88723d02 100644 --- a/examples/other/dolfin/demo_submesh.ipynb +++ b/examples/other/dolfin/demo_submesh.ipynb @@ -2,9 +2,24 @@ "cells": [ { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "ba0959304b784ebab287f15f4d2dac67", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Plot(antialias=3, axes=['x', 'y', 'z'], background_color=16777215, camera=[4.5, 4.5, 4.5, 0.0, 0.0, 0.0, 1.0, …" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "\"\"\"\n", "How to extract matching sub meshes from a common mesh.\n", @@ -44,6 +59,13 @@ "plot(fluid_mesh)\n", "plot(structure_mesh, c='tomato', add=True)\n" ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { @@ -62,7 +84,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.7" + "version": "3.7.3" } }, "nbformat": 4, diff --git a/examples/other/dolfin/navier-stokes_lshape.py b/examples/other/dolfin/navier-stokes_lshape.py index 92886950..99f81f42 100644 --- a/examples/other/dolfin/navier-stokes_lshape.py +++ b/examples/other/dolfin/navier-stokes_lshape.py @@ -2,6 +2,7 @@ Solve the incompressible Navier-Stokes equations on an L-shaped domain using Chorin's splitting method. """ +from __future__ import print_function from dolfin import * from vtkplotter.dolfin import ProgressBar, plot, datadir diff --git a/examples/other/dolfin/noplot/ex05_non-matching-meshes.py b/examples/other/dolfin/noplot/ex05_non-matching-meshes.py index 6bfc1d7a..fb9b95af 100644 --- a/examples/other/dolfin/noplot/ex05_non-matching-meshes.py +++ b/examples/other/dolfin/noplot/ex05_non-matching-meshes.py @@ -26,7 +26,7 @@ ######################################### vtkplotter from vtkplotter.dolfin import * -s1 = MeshActor(v1).lineWidth(0.5).wire(False) -s3 = MeshActor(v3).lineWidth(0.5).wire(False) +s1 = MeshActor(v1).lineWidth(0.5).wireframe(False) +s3 = MeshActor(v3).lineWidth(0.5).wireframe(False) show(s1, s3, N=2) # distribute s1 and s3 on 2 synced renderers diff --git a/examples/other/dolfin/stokes.py b/examples/other/dolfin/stokes.py index 4c465e8d..8bd6f75d 100644 --- a/examples/other/dolfin/stokes.py +++ b/examples/other/dolfin/stokes.py @@ -36,7 +36,7 @@ L = inner(f, v)*dx w = Function(W) -solve(a == L, w, bcs, solver_parameters={'linear_solver' : 'mumps'}) +solve(a == L, w, bcs) # Split the mixed solution using a shallow copy (u, p) = w.split() @@ -45,6 +45,7 @@ f = r'-\nabla \cdot(\nabla u+p I)=f ~\mathrm{in}~\Omega' formula = Latex(f, pos=(0.55,0.45,-.05), s=0.1) -plot(u, formula, at=0, N=2, text="velocity", mode='mesh and arrows', - scale=.03, wire=1, scalarbar=False, style=1) -plot(p, at=1, text="pressure", cmap='jet') +plot(u, formula, at=0, N=2, + mode='mesh and arrows', scale=.03, + wireframe=True, scalarbar=False, style=1) +plot(p, at=1, text="pressure", cmap='rainbow') diff --git a/examples/other/dolfin/turing_2d.py b/examples/other/dolfin/turing_2d.py new file mode 100644 index 00000000..e2184cd0 --- /dev/null +++ b/examples/other/dolfin/turing_2d.py @@ -0,0 +1,92 @@ +# +# https://fenicsproject.org/qa/8612/difficulties-with-solving-the-gray-scott-model +from dolfin import * +import numpy as np + +# Set parameters +D_u = 8.0e-05 +D_v = 4.0e-05 +c = 0.022 +k = 0.055 + +dt = 12.0 +t_max = 100000 + +# Form compiler options +parameters["form_compiler"]["optimize"] = True +parameters["form_compiler"]["cpp_optimize"] = True +parameters["form_compiler"]["representation"] = "uflacs" +set_log_level(30) + + +# Class representing the intial conditions +class InitialConditions(UserExpression): + def eval(self, val, x): + if between(x[0], (1.0, 1.5)) and between(x[1], (1.0, 1.5)): + val[1] = 0.25*np.power(np.sin(4*np.pi*x[0]), 2)*np.power(np.sin(4*np.pi*x[1]), 2) + val[0] = 1 - 2*val[1] + else: + val[1] = 0 + val[0] = 1 + def value_shape(self): + return (2,) + +# Class for interfacing with the Newton solver +class GrayScottEquations(NonlinearProblem): + def __init__(self, a, L): + NonlinearProblem.__init__(self) + self.L = L + self.a = a + def F(self, b, x): + assemble(self.L, tensor=b) + def J(self, A, x): + assemble(self.a, tensor=A) + +# Define mesh and function space +p0 = Point(0.0, 0.0) +p1 = Point(2.5, 2.5) +mesh = RectangleMesh(p0, p1, 64, 64) +V = VectorFunctionSpace(mesh, 'CG', 2) + +# Define functions +W_init = InitialConditions(degree = 1) +phi = TestFunction(V) +dp = TrialFunction(V) +W0 = Function(V) +W = Function(V) +# Interpolate initial conditions and split functions +W0.interpolate(W_init) +q, p = split(phi) +u, v = split(W) +u0, v0 = split(W0) + +# Weak statement of the equations +F1 = u*q*dx -u0*q*dx +D_u*inner(grad(u), grad(q))*dt*dx +u*v*v*q*dt*dx -c*(1-u)*q*dt*dx +F2 = v*p*dx -v0*p*dx +D_v*inner(grad(v), grad(p))*dt*dx -u*v*v*p*dt*dx +(c+k)*v*p*dt*dx +F = F1 + F2 + +# Compute directional derivative about W in the direction of dp (Jacobian) +a = derivative(F, W, dp) + +# Create nonlinear problem and Newton solver +problem = GrayScottEquations(a, F) +solver = NewtonSolver() +#solver.parameters["linear_solver"] = "lu" +#solver.parameters["convergence_criterion"] = "incremental" +solver.parameters["relative_tolerance"] = 1e-3 + +from vtkplotter.dolfin import * + +t = 0.0 +W.assign(W0) +while (t < t_max): + t += dt + solver.solve(problem, W.vector()) + W0.assign(W) + u_out, v_out = W.split() + plot(u_out, Text("time = "+str(t)), + lw=0, warpZfactor=-0.1, + vmin=0, vmax=1, scalarbar=False, interactive=False) + +interactive() + diff --git a/examples/other/dolfin/turing_3d.py b/examples/other/dolfin/turing_3d.py new file mode 100644 index 00000000..254d6fbb --- /dev/null +++ b/examples/other/dolfin/turing_3d.py @@ -0,0 +1,93 @@ +# +# https://fenicsproject.org/qa/8612/difficulties-with-solving-the-gray-scott-model +from dolfin import * +import numpy as np +import mshr + +# Set parameters +D_u = 8.0e-05 +D_v = 4.0e-05 +c = 0.024 +k = 0.06 + +dt = 1.0 +t_max = 100 + +# Form compiler options +parameters["form_compiler"]["optimize"] = True +parameters["form_compiler"]["cpp_optimize"] = True +parameters["form_compiler"]["representation"] = "uflacs" +set_log_level(30) + +# Class representing the intial conditions +class InitialConditions(UserExpression): + def eval(self, val, x): + val[0] = np.random.rand() + val[1] = np.random.rand() + def value_shape(self): + return (2,) + + +# Class for interfacing with the Newton solver +class GrayScottEquations(NonlinearProblem): + def __init__(self, a, L): + NonlinearProblem.__init__(self) + self.L = L + self.a = a + def F(self, b, x): + assemble(self.L, tensor=b) + def J(self, A, x): + assemble(self.a, tensor=A) + + +# Define mesh and function space +p1 = Point(1.0, 1.0) +p0 = Point(0.0, 0.0, 0.0) +sphere = mshr.Sphere(p0, 1.0) +mesh = mshr.generate_mesh(sphere, 16) + +V_ele = FiniteElement("CG", mesh.ufl_cell(), 1) +V = FunctionSpace(mesh, MixedElement([V_ele, V_ele])) + +# Define functions +W_init = InitialConditions(degree=1) +phi = TestFunction(V) +dp = TrialFunction(V) +W0 = Function(V) +W = Function(V) + +# Interpolate initial conditions and split functions +W0.interpolate(W_init) +q, p = split(phi) +u, v = split(W) +u0, v0 = split(W0) + +# Weak statement of the equations +F1 = u*q*dx -u0*q*dx +D_u*inner(grad(u),grad(q)) *dt*dx +u*v*v*q*dt*dx -c*(1-u)*q*dt*dx +F2 = v*p*dx -v0*p*dx +D_v*inner(grad(v),grad(p)) *dt*dx -u*v*v*p*dt*dx +(c+k)*v*p*dt*dx +F = F1 + F2 + +# Compute directional derivative about W in the direction of dp (Jacobian) +a = derivative(F, W, dp) + +# Create nonlinear problem and Newton solver +problem = GrayScottEquations(a, F) +solver = NewtonSolver() +#solver.parameters["linear_solver"] = "lu" +#solver.parameters["convergence_criterion"] = "incremental" +solver.parameters["relative_tolerance"] = 1e-3 + + +from vtkplotter.dolfin import * + +t = 0.0 +W.assign(W0) +while t < t_max: + t += dt + solver.solve(problem, W.vector()) + W0.assign(W) + u_out, v_out = W.split() + plot(u_out, Text("time = "+str(t)), + vmin=0, vmax=1, scalarbar=False, interactive=False) + +interactive() diff --git a/examples/other/menger_fractal.py b/examples/other/menger_fractal.py index a41672de..9aa8480e 100644 --- a/examples/other/menger_fractal.py +++ b/examples/other/menger_fractal.py @@ -30,6 +30,6 @@ def iterate(length, x, y, z): print('voxels min, max =', np.min(voxels), np.max(voxels)) vol = Volume(voxels) -lego = legosurface(vol, -0.1, 1.1, cmap='afmhot_r') +lego = vol.legosurface(-0.1, 1.1, cmap='afmhot_r') show(vol, lego, N=2, bg='w') diff --git a/examples/other/qt_window_split.py b/examples/other/qt_window_split.py index ee6d0ff8..1fd1554a 100644 --- a/examples/other/qt_window_split.py +++ b/examples/other/qt_window_split.py @@ -41,12 +41,12 @@ def start(self, vp): self.iren.AddObserver("MiddleButtonPressEvent", vp._mousemiddle) def keypress(obj, e): - vtkio._keypress(obj, e) + vp._keypress(obj, e) if self.iren.GetKeySym() in ["q", "space"]: self.iren.ExitCallback() exit() - self.iren.AddObserver("KeyPressEvent", keypress) + self.frame.setLayout(self.vl) self.setCentralWidget(self.frame) self.show() # qt not Plotter method diff --git a/examples/other/sierpinski.ipynb b/examples/other/sierpinski.ipynb index 6070db43..f035bbc4 100644 --- a/examples/other/sierpinski.ipynb +++ b/examples/other/sierpinski.ipynb @@ -15,7 +15,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "2a8b7129ab714357918b4f095ceab46d", + "model_id": "56d04110c5e741d29fc39a99988d47c2", "version_major": 2, "version_minor": 0 }, @@ -115,7 +115,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.8" + "version": "3.7.3" } }, "nbformat": 4, diff --git a/examples/other/tf_learn_volume.py b/examples/other/tf_learn_volume.py index e7e4eee1..fbcda5c2 100644 --- a/examples/other/tf_learn_volume.py +++ b/examples/other/tf_learn_volume.py @@ -1,7 +1,9 @@ import numpy as np from keras.models import Sequential from keras.layers import Dense -from vtkplotter import Volume, show, arange +from vtkplotter import Volume, show, arange, settings + +settings.useDepthPeeling = True n = 10 neurons = 60 @@ -50,4 +52,4 @@ s2 = v2.isosurface(threshold=[t for t in arange(0, 1, 0.1)]) s2.alpha(0.5) -show([[v1, s1], s2], N=2, axes=8, bg="w", depthpeeling=1) +show([[v1, s1], s2], N=2, axes=8, bg="w") diff --git a/examples/other/trimesh/first_example.ipynb b/examples/other/trimesh/first_example.ipynb index 30bc8f99..4c5b75cf 100644 --- a/examples/other/trimesh/first_example.ipynb +++ b/examples/other/trimesh/first_example.ipynb @@ -2,69 +2,49 @@ "cells": [ { "cell_type": "code", - "execution_count": 3, + "execution_count": 1, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "..downloading:\n", - " https://raw.githubusercontent.com/mikedh/trimesh/master/models/machinist.XAML \n" - ] - } - ], + "outputs": [], "source": [ "\"\"\"\n", - "trimesh and vtkplotter interoperability\n", - "\"\"\"\n", + "Trimesh support and interoperability module.\n", "\n", - "# Install trimesh with:\n", - "# sudo apt install python3-rtree\n", - "# pip install rtree shapely\n", - "# conda install trimesh\n", + "Install trimesh with:\n", + "> sudo apt install python3-rtree\n", + "> pip install rtree shapely\n", + "> conda install trimesh\n", "\n", + "Check the example gallery at:\n", + " \n", + "https://github.com/marcomusy/vtkplotter/tree/master/examples/other/trimesh\n", + "\"\"\"\n", "import trimesh\n", - "from vtkplotter import download, trimesh2vtk, show, embedWindow, closePlotter\n", + "import vtkplotter\n", + "from vtkplotter.trimesh import trimesh2vtk, vtk2trimesh\n", "\n", "# uncomment this to pop a vtk rendering window instead of the K3D notebook backend\n", - "embedWindow(False)\n", + "vtkplotter.embedWindow(False)\n", "\n", "url = 'https://raw.githubusercontent.com/mikedh/trimesh/master/models/'\n", - "filename = download(url + 'machinist.XAML')\n", - "mesh = trimesh.load(filename)" + "filename = vtkplotter.download(url + 'machinist.XAML')\n", + "\n", + "mesh = trimesh.load(filename)\n" ] }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 3, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - " vtkplotter 2019.3.5 | vtk 8.1.2 | python 3.6 | press h for help. \n" - ] - } - ], + "outputs": [], "source": [ "actor = trimesh2vtk(mesh) # returns a Actor(vtkActor) object from Trimesh\n", + "vtkplotter.show(mesh) # vtkplotter visualizer (conversion is on the fly)\n", "\n", - "# Any of these will work:\n", - "show(mesh) # conversion is on the fly (don't need 'actor')\n", - "\n", - "# or\n", - "#actor.show()\n", - "\n", - "# or\n", - "#show(actor)\n", - "\n", - "# or\n", - "#mesh.show() # this is the trimesh built-in visualizer\n", + "trimsh_reconverted = vtk2trimesh(actor)\n", + "trimsh_reconverted.show() # this is the trimesh built-in visualizer\n", "\n", "# This closes the vtk rendering window. Use only if embedWindow(False)\n", - "closePlotter()" + "vtkplotter.closePlotter()" ] }, { diff --git a/examples/other/trimesh/first_example.py b/examples/other/trimesh/first_example.py index 6b0f1e8d..7c689016 100644 --- a/examples/other/trimesh/first_example.py +++ b/examples/other/trimesh/first_example.py @@ -1,29 +1,23 @@ """ trimesh to vtkplotter interoperability """ - # Install trimesh with: # sudo apt install python3-rtree # pip install rtree shapely # conda install trimesh import trimesh -from vtkplotter import download, trimesh2vtk, show +import vtkplotter +from vtkplotter.trimesh import trimesh2vtk, vtk2trimesh url = 'https://raw.githubusercontent.com/mikedh/trimesh/master/models/' -filename = download(url + 'machinist.XAML') +filename = vtkplotter.download(url + 'machinist.XAML') + mesh = trimesh.load(filename) actor = trimesh2vtk(mesh) # returns a Actor(vtkActor) object from Trimesh +vtkplotter.show(mesh) # vtkplotter visualizer (conversion is on the fly) -# Any of these will work: -show(mesh) # conversion is on the fly (don't need 'actor') - -# or -#actor.show() - -# or -#show(actor) +trimsh_reconverted = vtk2trimesh(actor) +trimsh_reconverted.show() # this is the trimesh built-in visualizer -# or -mesh.show() # this is the trimesh built-in visualizer diff --git a/examples/other/trimesh/nearest.py b/examples/other/trimesh/nearest.py index b296254a..23bc9cc6 100644 --- a/examples/other/trimesh/nearest.py +++ b/examples/other/trimesh/nearest.py @@ -4,7 +4,7 @@ """ import trimesh import numpy as np -from vtkplotter import Text, show +from vtkplotter import Text, show, Arrows mesh = trimesh.load_remote('https://github.com/mikedh/trimesh/raw/master/models/cycloidal.ply') points = mesh.bounding_box_oriented.sample_volume(count=30) @@ -24,5 +24,7 @@ cloud_original.vertices_color = cloud_colors cloud_close.vertices_color = cloud_colors +arrs = Arrows(cloud_original.vertices, cloud_close.vertices, c='w') + ## create a scene containing the mesh and two sets of points -show(mesh, cloud_original, cloud_close, Text(__doc__), bg='w') \ No newline at end of file +show(mesh, cloud_original, cloud_close, arrs, Text(__doc__)) diff --git a/examples/other/trimesh/ray.ipynb b/examples/other/trimesh/ray.ipynb index fc7cd76c..6016ba69 100644 --- a/examples/other/trimesh/ray.ipynb +++ b/examples/other/trimesh/ray.ipynb @@ -2,45 +2,9 @@ "cells": [ { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "SVG path loading unavailable!\n", - "Traceback (most recent call last):\n", - " File \"/home/musy/soft/anaconda3/lib/python3.6/site-packages/trimesh/path/exchange/svg_io.py\", line 18, in \n", - " from svg.path import parse_path\n", - "ModuleNotFoundError: No module named 'svg'\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "The rays hit the mesh at coordinates:\n", - " [[ 0. 0. -1.]\n", - " [ 0. 0. 1.]]\n", - "The rays with index: [0 0] hit triangles stored at mesh.faces[[1178 1018]]\n" - ] - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "913aa42929c6420784bc79eb10fe555c", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Plot(antialias=3, axes=['x', 'y', 'z'], background_color=16777215, camera=[4.5, 4.5, 4.5, 0.0, 0.0, 0.0, 1.0, …" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "import trimesh\n", "import numpy as np\n", @@ -103,7 +67,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.8" + "version": "3.7.3" } }, "nbformat": 4, diff --git a/examples/other/trimesh/section.ipynb b/examples/other/trimesh/section.ipynb index 00a3d260..8ce44a43 100644 --- a/examples/other/trimesh/section.ipynb +++ b/examples/other/trimesh/section.ipynb @@ -2,24 +2,13 @@ "cells": [ { "cell_type": "code", - "execution_count": 1, + "execution_count": 2, "metadata": {}, "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "SVG path loading unavailable!\n", - "Traceback (most recent call last):\n", - " File \"/home/musy/soft/anaconda3/lib/python3.6/site-packages/trimesh/path/exchange/svg_io.py\", line 18, in \n", - " from svg.path import parse_path\n", - "ModuleNotFoundError: No module named 'svg'\n" - ] - }, { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "4d1ddaee5f6d4d8bbfd6afef031832bb", + "model_id": "8385ec9d04794a369a268ee87f6a07d0", "version_major": 2, "version_minor": 0 }, @@ -53,20 +42,20 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 3, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "\u001b[1m\u001b[3m\u001b[32mnr. of sections: 11\u001b[0m\n" + "\u001b[1m\u001b[32mnr. of sections: 11\u001b[0m\n" ] }, { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "c59b2ba849e44e2583fd2c0f4dc64071", + "model_id": "5d3f2359b0f84bcf923bcc2044e1f7e8", "version_major": 2, "version_minor": 0 }, @@ -110,7 +99,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "37fcbf8a0e1c403ca5378e84cdc0046c", + "model_id": "45fdaf236923494092ecfedd01d49850", "version_major": 2, "version_minor": 0 }, diff --git a/examples/other/trimesh/shortest.py b/examples/other/trimesh/shortest.py index 44163216..076f73ae 100644 --- a/examples/other/trimesh/shortest.py +++ b/examples/other/trimesh/shortest.py @@ -1,6 +1,5 @@ import trimesh import networkx as nx -from vtkplotter import Text, show # test on a sphere mesh mesh = trimesh.primitives.Sphere() @@ -17,8 +16,6 @@ g.add_edge(*edge, length=L) # alternative method for weighted graph creation -# you can also create the graph with from_edgelist and -# a list comprehension, which is like 1.5x faster ga = nx.from_edgelist([(e[0], e[1], {"length": L}) for e, L in zip(edges, length)]) # arbitrary indices of mesh.vertices to test with @@ -28,7 +25,9 @@ # run the shortest path query using length for edge weight path = nx.shortest_path(g, source=start, target=end, weight="length") -# VISUALIZE RESULT +################################### VISUALIZE RESULT +from vtkplotter import Text, show + # make the sphere transparent-ish mesh.visual.face_colors = [100, 100, 100, 100] diff --git a/examples/run_all.sh b/examples/run_all.sh index 5b080db4..75fdc915 100755 --- a/examples/run_all.sh +++ b/examples/run_all.sh @@ -150,6 +150,9 @@ python basic/mirror.py echo Running basic/sliders.py python basic/sliders.py +echo Running basic/slider_browser.py +python advanced/slider_browse.py + echo Running basic/sliders3d.py python basic/sliders3d.py diff --git a/examples/simulations/doubleslit.py b/examples/simulations/doubleslit.py index 29c9cd29..b6ce14d3 100644 --- a/examples/simulations/doubleslit.py +++ b/examples/simulations/doubleslit.py @@ -24,7 +24,7 @@ vp = Plotter(title="The Double Slit Experiment", axes=0, verbose=0, bg="black") -screen = Grid(pos=[0, 0, -D], sx=0.1, sy=0.1, resx=200, resy=50) +screen = Grid(pos=[0, 0, -D], sx=0.1, sy=0.1, lw=0, resx=200, resy=50) screen.wireframe(False).phong() # show it as a solid plane (not as wireframe) k = 0.0 + 1j * 2 * pi / lambda1 # complex wave number @@ -44,7 +44,7 @@ vp += screen vp += Points(array(slits) * 200, c="w") # slits scale magnified by factor 200 -vp += Grid(sx=0.1, sy=0.1, resx=6, resy=6, c="w", alpha=0.1) # add some annotation +vp += Grid(sx=0.1, sy=0.1, resx=6, resy=6, c="w", alpha=0.1) vp += Line([0, 0, 0], [0, 0, -D], c="w", alpha=0.1) vp += Text("source plane", pos=[-0.05, -0.053, 0], s=0.002, c="gray") vp += Text("detector plane D = " + str(D) + " m", pos=[-0.05, -0.053, -D], s=0.002, c="gray") diff --git a/examples/simulations/multiple_pendulum.py b/examples/simulations/multiple_pendulum.py index f58d3cc9..3991830a 100644 --- a/examples/simulations/multiple_pendulum.py +++ b/examples/simulations/multiple_pendulum.py @@ -49,7 +49,7 @@ Dt2 = Dt / 2 # Midpoint time step DiaSq = (2 * R) ** 2 # Diameter of bob squared -printc("Press Esc to exit.", c="red") +printc("Press F1 to exit.", c="red", invert=1) while True: bob_x_m = list(map((lambda x, dx: x + Dt2 * dx), bob_x, x_dot)) # midpoint variables diff --git a/examples/simulations/turing.py b/examples/simulations/turing.py index fd2d9853..ae8bdec2 100644 --- a/examples/simulations/turing.py +++ b/examples/simulations/turing.py @@ -8,6 +8,8 @@ from vtkplotter import * import numpy as np +settings.renderPointsAsSpheres = False + doc = Text(__doc__, c="k") # Load (with numpy) an existing set of mesh points and a list diff --git a/examples/volumetric/isosurfaces1.py b/examples/volumetric/isosurfaces1.py index 1d71f8d9..230016b2 100644 --- a/examples/volumetric/isosurfaces1.py +++ b/examples/volumetric/isosurfaces1.py @@ -1,6 +1,8 @@ from vtkplotter import * import numpy as np +settings.useDepthPeeling = True + data_matrix = np.zeros([75, 75, 75]) # data_matrix[0:35, 0:35, 0:35] = 50 # data_matrix[25:55, 25:55, 25:55] = 100 @@ -16,4 +18,4 @@ s = v.isosurface(threshold=[t for t in arange(0, 200, 10)]) s.alpha(0.5).lw(0.1) -show(v, s, N=2, axes=8, bg="w", depthpeeling=1) +show(v, s, N=2, axes=8, bg="w") diff --git a/examples/volumetric/numpy2volume.py b/examples/volumetric/numpy2volume.py index d150a23a..483b55d9 100644 --- a/examples/volumetric/numpy2volume.py +++ b/examples/volumetric/numpy2volume.py @@ -11,7 +11,7 @@ vol = Volume(scalar_field) lego = vol.legosurface(vmin=1, vmax=2) text1 = Text('Make a Volume from a numpy object', c='blue') -text2 = Text('legosurface representation', c='darkred') +text2 = Text('lego isosurface representation\nvmin=1, vmax=2', c='darkred') print('numpy array from Volume:', vol.getPointArray().shape) diff --git a/examples/volumetric/tensors.ipynb b/examples/volumetric/tensors.ipynb index b2028d75..55e7e63d 100644 --- a/examples/volumetric/tensors.ipynb +++ b/examples/volumetric/tensors.ipynb @@ -2,13 +2,13 @@ "cells": [ { "cell_type": "code", - "execution_count": 4, + "execution_count": 1, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "9cb531de9af24cd091122bb96ae37e5f", + "model_id": "00f34fbc861943a3a158960361d42d08", "version_major": 2, "version_minor": 0 }, @@ -40,7 +40,6 @@ "# tens = Tensors(vol, source='ellipsoid', scale=10)\n", "tens = Tensors(zsl, source='ellipsoid', scale=20)\n", "\n", - "#show([vol, [tens, zsl]], N=2, axes=1, viewup='z')\n", "show(vol, tens, zsl)" ] }, @@ -68,7 +67,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.8" + "version": "3.7.3" } }, "nbformat": 4, diff --git a/setup.py b/setup.py index 531162ef..b876e14d 100644 --- a/setup.py +++ b/setup.py @@ -61,7 +61,7 @@ # ( sudo -H pip install . ) # cd examples && ./run_all.sh -# python test_filetypes.py +# python tests/test_filetypes.py # check vtkconvert: # vtkconvert vtkplotter/data/290.vtk -to ply diff --git a/vtkplotter/__init__.py b/vtkplotter/__init__.py index 6fe8db7e..69681aba 100644 --- a/vtkplotter/__init__.py +++ b/vtkplotter/__init__.py @@ -15,10 +15,6 @@ - `examples/others `_. - -References -^^^^^^^^^^^ - Publications where ``vtkplotter`` has been used so far: 1. Diego, X. *et al,*: *"Key features of Turing systems are determined purely by network topology"*, @@ -31,15 +27,20 @@ 3. G. Dalmasso *et al.*, "Evolution in space and time of 3D volumetric images", in preparation. +4. M. Musy, G. Dalmasso, J. Sharpe and N. Sime, "`vtkplotter`: plotting in FEniCS with python", +`link `_. +Poster at the FEniCS'2019 Conference, +Carnegie Institution for Science Department of Terrestrial Magnetism, Washington DC, June 2019. + **Have you found this software useful for your research? Please cite it as:** M. Musy, et al., "`vtkplotter`, a python module for scientific visualization, -analysis and animation of 3D objects and point clouds based on VTK." (version v8.9.0). Zenodo, -`doi: 10.5281/zenodo.2561402 `_, 10 February 2019. +analysis and animation of 3D objects and point clouds based on VTK.". Zenodo, +`doi: 10.5281/zenodo.2561402 `_, +10 February 2019. """ from __future__ import print_function -from vtkplotter.version import _version __author__ = "Marco Musy" __license__ = "MIT" @@ -47,8 +48,8 @@ __email__ = "marco.musy@embl.es" __status__ = "dev" __website__ = "https://github.com/marcomusy/vtkplotter" -__version__ = _version +from vtkplotter.version import _version as __version__ from vtkplotter.plotter import * from vtkplotter.analysis import * from vtkplotter.shapes import * @@ -58,9 +59,13 @@ from vtkplotter.colors import * import vtkplotter.settings as settings from vtkplotter.settings import datadir, embedWindow -import vtkplotter.dolfin as dolfin -from numpy import sin, cos, sqrt, exp, log, dot, cross, array, arange +# hack to make docs work +# need to uncomment this to generate documentation html +#from vtkplotter.trimesh import * +#from vtkplotter.dolfin import _inputsort + +from numpy import sin, cos, sqrt, exp, log, dot, cross, array, arange # imports hierarchy # plotter : utils, colors, actors, vtkio, shapes @@ -77,11 +82,11 @@ ############### -## deprecations +## deprecations ############################################################ def isolines(*args, **kargs): printc("Obsolete. Use mesh.isolines() instead of isolines(mesh).", c=1) raise RuntimeError() - + def isosurface(*args, **kargs): printc("Obsolete. Use volume.isosurface() instead of isosurface(volume).", c=1) raise RuntimeError() diff --git a/vtkplotter/actors.py b/vtkplotter/actors.py index de5b6a98..5102ecd3 100644 --- a/vtkplotter/actors.py +++ b/vtkplotter/actors.py @@ -21,7 +21,7 @@ 'Prop', 'Actor', 'Assembly', - 'Image', + 'Picture', 'Volume', 'mergeActors', 'collection', @@ -611,29 +611,6 @@ def c(self, color=False): """ return self.color(color) - def getArrayNames(self): - from vtk.numpy_interface import dataset_adapter - wrapped = dataset_adapter.WrapDataObject(self.GetMapper().GetInput()) - return {"PointData":wrapped.PointData.keys(), "CellData":wrapped.CellData.keys()} - - def getPointArray(self, name=0): - """Return point array content as a ``numpy.array``. - This can be identified either as a string or by an integer number.""" - data = None - if hasattr(self, 'poly') and self.poly: - data = self.poly - elif hasattr(self, '_image') and self._image: - data = self._image - return vtk_to_numpy(data.GetPointData().GetArray(name)) - - def getCellArray(self, name=0): - """Return cell array content as a ``numpy.array``.""" - data = None - if hasattr(self, 'poly') and self.poly: - data = self.poly - elif hasattr(self, '_image') and self._image: - data = self._image - return vtk_to_numpy(data.GetCellData().GetArray(name)) def getTransform(self): """ @@ -666,6 +643,105 @@ def setTransform(self, T): return self + def getArrayNames(self): + from vtk.numpy_interface import dataset_adapter + wrapped = dataset_adapter.WrapDataObject(self.GetMapper().GetInput()) + return {"PointData":wrapped.PointData.keys(), "CellData":wrapped.CellData.keys()} + + def getPointArray(self, name=0): + """Return point array content as a ``numpy.array``. + This can be identified either as a string or by an integer number.""" + data = None + if hasattr(self, 'poly') and self.poly: + data = self.poly + elif hasattr(self, '_image') and self._image: + data = self._image + return vtk_to_numpy(data.GetPointData().GetArray(name)) + + def getCellArray(self, name=0): + """Return cell array content as a ``numpy.array``.""" + data = None + if hasattr(self, 'poly') and self.poly: + data = self.poly + elif hasattr(self, '_image') and self._image: + data = self._image + return vtk_to_numpy(data.GetCellData().GetArray(name)) + + def addPointScalars(self, scalars, name): + """ + Add point scalars and assigning it a name. + + |mesh_coloring| |mesh_coloring.py|_ + """ + data = self.inputdata() + if len(scalars) != data.GetNumberOfPoints(): + colors.printc('~times addPointScalars(): Number of scalars != nr. of points', + len(scalars), data.GetNumberOfPoints(), c=1) + raise RuntimeError() + + arr = numpy_to_vtk(np.ascontiguousarray(scalars), deep=True) + arr.SetName(name) + data.GetPointData().AddArray(arr) + data.GetPointData().SetActiveScalars(name) + self.mapper.SetArrayName(name) + self.mapper.SetScalarRange(np.min(scalars), np.max(scalars)) + self.mapper.SetScalarModeToUsePointData() + self.mapper.ScalarVisibilityOn() + return self + + def addCellScalars(self, scalars, name): + """ + Add cell scalars and assigning it a name. + """ + data = self.inputdata() + if isinstance(scalars, str): + scalars = vtk_to_numpy(data.GetPointData().GetArray(scalars)) + + if len(scalars) != data.GetNumberOfCells(): + colors.printc("~times addCellScalars() Number of scalars != nr. of cells", + len(scalars), data.GetNumberOfCells(), c=1) + raise RuntimeError() + + arr = numpy_to_vtk(np.ascontiguousarray(scalars), deep=True) + arr.SetName(name) + data.GetCellData().AddArray(arr) + data.GetCellData().SetActiveScalars(name) + self.mapper.SetArrayName(name) + self.mapper.SetScalarRange(np.min(scalars), np.max(scalars)) + self.mapper.SetScalarModeToUseCellData() + self.mapper.ScalarVisibilityOn() + return self + + def mapCellsToPoints(self): + """ + Transform cell data (i.e., data specified per cell) + into point data (i.e., data specified at cell points). + The method of transformation is based on averaging the data values + of all cells using a particular point. + """ + c2p = vtk.vtkCellDataToPointData() + c2p.SetInputData(self.inputdata()) + c2p.Update() + self.mapper.SetScalarModeToUsePointData() + return self.updateMesh(c2p.GetOutput()) + + def mapPointsToCells(self): + """ + Transform point data (i.e., data specified per point) + into cell data (i.e., data specified per cell). + The method of transformation is based on averaging the data values + of all points defining a particular cell. + + |mesh_map2cell| |mesh_map2cell.py|_ + + """ + p2c = vtk.vtkPointDataToCellData() + p2c.SetInputData(self.polydata(False)) + p2c.Update() + self.mapper.SetScalarModeToUseCellData() + return self.updateMesh(p2c.GetOutput()) + + #################################################### # Actor inherits from vtkActor and Prop class Actor(vtk.vtkActor, Prop): @@ -743,13 +819,6 @@ def __init__( gf.Update() self.poly = gf.GetOutput() self.mapper = vtk.vtkPolyDataMapper() -# elif "structured" in inputtype.lower() or "RectilinearGrid" in inputtype: -# # picks vtkUnstructuredGrid, vtkStructuredGrid, vtkStructuredPoints -# gf = vtk.vtkGeometryFilter() -# gf.SetInputData(inputobj) -# gf.Update() -# self.poly = gf.GetOutput() -# self.mapper = vtk.vtkPolyDataMapper() elif "trimesh" in inputtype: tact = utils.trimesh2vtk(inputobj, alphaPerCell=False) self.poly = tact.polydata() @@ -817,33 +886,35 @@ def __init__( if cldata.GetNumberOfArrays(): for i in range(cldata.GetNumberOfArrays()): iarr = cldata.GetArray(i) - icname = iarr.GetName() -# if icname is None: -# icname = 'cellarray' -# iarr.SetName(icname) - if icname and all(s not in icname.lower() for s in exclude): - cldata.SetActiveScalars(icname) - self.mapper.ScalarVisibilityOn() - self.mapper.SetScalarModeToUseCellData() - self.mapper.SetScalarRange(iarr.GetRange()) - arrexists = True - break # stop at first good one + if iarr: + icname = iarr.GetName() + # if icname is None: + # icname = 'cellarray' + # iarr.SetName(icname) + if icname and all(s not in icname.lower() for s in exclude): + cldata.SetActiveScalars(icname) + self.mapper.ScalarVisibilityOn() + self.mapper.SetScalarModeToUseCellData() + self.mapper.SetScalarRange(iarr.GetRange()) + arrexists = True + break # stop at first good one # point come after so it has priority if ptdata.GetNumberOfArrays(): for i in range(ptdata.GetNumberOfArrays()): iarr = ptdata.GetArray(i) - ipname = iarr.GetName() -# if ipname is None: -# ipname = 'pointarray' -# iarr.SetName(ipname) - if ipname and all(s not in ipname.lower() for s in exclude): - ptdata.SetActiveScalars(ipname) - self.mapper.ScalarVisibilityOn() - self.mapper.SetScalarModeToUsePointData() - self.mapper.SetScalarRange(iarr.GetRange()) - arrexists = True - break + if iarr: + ipname = iarr.GetName() + # if ipname is None: + # ipname = 'pointarray' + # iarr.SetName(ipname) + if ipname and all(s not in ipname.lower() for s in exclude): + ptdata.SetActiveScalars(ipname) + self.mapper.ScalarVisibilityOn() + self.mapper.SetScalarModeToUsePointData() + self.mapper.SetScalarRange(iarr.GetRange()) + arrexists = True + break if arrexists == False: if c is None: @@ -974,6 +1045,10 @@ def getConnectivity(self): if len(arr1d) == 0: arr1d = vtk_to_numpy(self.polydata().GetStrips().GetData()) + #conn = arr1d.reshape(ncells, int(len(arr1d)/len(arr1d))) + #return conn[:, 1:] + # instead of: + i = 0 conn = [] n = len(arr1d) @@ -1689,7 +1764,7 @@ def stretch(self, q1, q2): return self def crop(self, top=None, bottom=None, right=None, left=None, front=None, back=None): - """Crop an ``Actor`` object. + """Crop an ``Actor`` object. Input object is modified. :param float top: fraction to crop from the top plane (positive z) :param float bottom: fraction to crop from the bottom plane (negative z) @@ -1734,6 +1809,7 @@ def crop(self, top=None, bottom=None, right=None, left=None, front=None, back=No def cutWithPlane(self, origin=(0, 0, 0), normal=(1, 0, 0), showcut=False): """ Takes a ``vtkActor`` and cuts it with the plane defined by a point and a normal. + Input object is modified. :param origin: the cutting plane goes through this point :param normal: normal of the cutting plane @@ -1790,6 +1866,7 @@ def cutWithPlane(self, origin=(0, 0, 0), normal=(1, 0, 0), showcut=False): def cutWithMesh(self, mesh, invert=False): """ Cut an ``Actor`` mesh with another ``vtkPolyData`` or ``Actor``. + Input object is modified. :param bool invert: if True return cut off part of actor. @@ -1799,7 +1876,7 @@ def cutWithMesh(self, mesh, invert=False): """ if isinstance(mesh, vtk.vtkPolyData): polymesh = mesh - if isinstance(mesh, Actor): + elif isinstance(mesh, Actor): polymesh = mesh.polydata() else: polymesh = mesh.GetMapper().GetInput() @@ -1837,6 +1914,7 @@ def cutWithMesh(self, mesh, invert=False): def cap(self, returnCap=False): """ Generate a "cap" on a clipped actor, or caps sharp edges. + Input object is modified. |cutAndCap| |cutAndCap.py|_ """ @@ -1923,35 +2001,6 @@ def triangle(self, verts=True, lines=True): tf.Update() return self.updateMesh(tf.GetOutput()) - def mapCellsToPoints(self): - """ - Transform cell data (i.e., data specified per cell) - into point data (i.e., data specified at cell points). - The method of transformation is based on averaging the data values - of all cells using a particular point. - """ - c2p = vtk.vtkCellDataToPointData() - c2p.SetInputData(self.inputdata()) - c2p.Update() - self.mapper.SetScalarModeToUsePointData() - return self.updateMesh(c2p.GetOutput()) - - def mapPointsToCells(self): - """ - Transform point data (i.e., data specified per point) - into cell data (i.e., data specified per cell). - The method of transformation is based on averaging the data values - of all points defining a particular cell. - - |mesh_map2cell| |mesh_map2cell.py|_ - - """ - p2c = vtk.vtkPointDataToCellData() - p2c.SetInputData(self.polydata(False)) - p2c.Update() - self.mapper.SetScalarModeToUseCellData() - return self.updateMesh(p2c.GetOutput()) - def pointColors(self, scalars, cmap="jet", alpha=1, bands=None, vmin=None, vmax=None): """ Set individual point colors by providing a list of scalar values and a color map. @@ -1979,9 +2028,6 @@ def pointColors(self, scalars, cmap="jet", alpha=1, bands=None, vmin=None, vmax= except TypeError: # invalid type return self -# if hasattr(scalars, 'astype'): -# scalars = scalars.astype(np.float) - useAlpha = False if n != poly.GetNumberOfPoints(): colors.printc('~times pointColors Error: nr. of scalars != nr. of points', @@ -2063,9 +2109,6 @@ def cellColors(self, scalars, cmap="jet", alpha=1, bands=None, vmin=None, vmax=N if isinstance(scalars, str): # if a name is passed scalars = vtk_to_numpy(poly.GetCellData().GetArray(scalars)) -# if hasattr(scalars, 'astype'): -# scalars = scalars.astype(np.float) - n = len(scalars) useAlpha = False if n != poly.GetNumberOfCells(): @@ -2129,56 +2172,6 @@ def cellColors(self, scalars, cmap="jet", alpha=1, bands=None, vmin=None, vmax=N poly.GetCellData().SetActiveScalars(sname) return self - def addPointScalars(self, scalars, name): - """ - Add point scalars to the actor's polydata assigning it a name. - - |mesh_coloring| |mesh_coloring.py|_ - """ - poly = self.polydata(False) - if len(scalars) != poly.GetNumberOfPoints(): - colors.printc('~times addPointScalars(): Number of scalars != nr. of points', - len(scalars), poly.GetNumberOfPoints(), c=1) - raise RuntimeError() -# if hasattr(scalars, 'astype'): -# scalars = scalars.astype(np.float) - - arr = numpy_to_vtk(np.ascontiguousarray(scalars), deep=True) - arr.SetName(name) - poly.GetPointData().AddArray(arr) - poly.GetPointData().SetActiveScalars(name) - self.mapper.SetArrayName(name) - self.mapper.SetScalarRange(np.min(scalars), np.max(scalars)) - self.mapper.SetScalarModeToUsePointData() - self.mapper.ScalarVisibilityOn() - return self - - def addCellScalars(self, scalars, name): - """ - Add cell scalars to the actor's polydata assigning it a name. - """ - poly = self.polydata(False) - if isinstance(scalars, str): - scalars = vtk_to_numpy(poly.GetPointData().GetArray(scalars)) - - if len(scalars) != poly.GetNumberOfCells(): - colors.printc("~times addCellScalars() Number of scalars != nr. of cells", - len(scalars), poly.GetNumberOfCells(), c=1) - raise RuntimeError() - -# if hasattr(scalars, 'astype'): -# scalars = scalars.astype(np.float) - - arr = numpy_to_vtk(np.ascontiguousarray(scalars), deep=True) - arr.SetName(name) - poly.GetCellData().AddArray(arr) - poly.GetCellData().SetActiveScalars(name) - self.mapper.SetArrayName(name) - self.mapper.SetScalarRange(np.min(scalars), np.max(scalars)) - self.mapper.SetScalarModeToUseCellData() - self.mapper.ScalarVisibilityOn() - return self - def colorCellsByArray(self, acolors, alphas=1, alphaPerCell=False): """ Colorize faces of a mesh one by one, passing a 1-to-1 list of colors and @@ -2289,13 +2282,12 @@ def addPointVectors(self, vectors, name): """ Add a point vector field to the actor's polydata assigning it a name. """ - #print('addPointVectors is DEPRECATED') poly = self.polydata(False) if len(vectors) != poly.GetNumberOfPoints(): colors.printc('~times addPointVectors Error: Number of vectors != nr. of points', len(vectors), poly.GetNumberOfPoints(), c=1) raise RuntimeError() - arr = vtk.vtkDoubleArray() + arr = vtk.vtkFloatArray() arr.SetNumberOfComponents(3) arr.SetName(name) for v in vectors: @@ -2360,6 +2352,8 @@ def scalars(self, name_or_idx=None, datatype="point"): Retrieve point or cell scalars using array name or index number. If no ``name`` is given return the list of names of existing arrays. + :param str datatype: search given name in point-data or cell-data + .. hint:: |mesh_coloring.py|_ """ #print('scalars is DEPRECATED') @@ -2826,18 +2820,17 @@ def intersectWithLine(self, p0, p1): |intline| """ if not self.line_locator: - line_locator = vtk.vtkOBBTree() - line_locator.SetDataSet(self.polydata(True)) - line_locator.BuildLocator() - self.line_locator = line_locator + self.line_locator = vtk.vtkOBBTree() + self.line_locator.SetDataSet(self.polydata()) + self.line_locator.BuildLocator() intersectPoints = vtk.vtkPoints() - intersection = [0, 0, 0] self.line_locator.IntersectWithLine(p0, p1, intersectPoints, None) pts = [] for i in range(intersectPoints.GetNumberOfPoints()): + intersection = [0, 0, 0] intersectPoints.GetPoint(i, intersection) - pts.append(list(intersection)) + pts.append(intersection) return pts def projectOnPlane(self, direction='z'): @@ -2975,40 +2968,64 @@ def diagonalSize(self): szs = [a.diagonalSize() for a in self.actors] return np.max(szs) - def lighting(self, *args, **lgt): - """Set the lighting type to all ``Actor`` in the ``Assembly``.""" + def lighting(self, style='', ambient=None, diffuse=None, + specular=None, specularPower=None, specularColor=None, enabled=True): + """Set the lighting type to all ``Actor`` in the ``Assembly`` object. + + :param str style: preset style, can be `[metallic, plastic, shiny, reflective]` + :param float ambient: ambient fraction of emission [0-1] + :param float diffuse: emission of diffused light in fraction [0-1] + :param float specular: fraction of reflected light [0-1] + :param float specularPower: precision of reflection [1-100] + :param color specularColor: color that is being reflected by the surface + :param bool enabled: enable/disable all surface light emission + """ for a in self.actors: - a.lighting(**lgt) + a.lighting(style, ambient, diffuse, + specular, specularPower, specularColor, enabled) return self - ################################################# -class Image(vtk.vtkImageActor, Prop): +class Picture(vtk.vtkImageActor, Prop): """ Derived class of ``vtkImageActor``. Used to represent 2D pictures. + Can be instantiated with a path file name or with a numpy array. |rotateImage| |rotateImage.py|_ """ - def __init__(self, array=()): + def __init__(self, obj=None): vtk.vtkImageActor.__init__(self) Prop.__init__(self) - if len(array): + if utils.isSequence(obj) and len(obj): iac = vtk.vtkImageAppendComponents() for i in range(3): -# arr = np.flip(np.flip(array[:,:,i], 0), 0).ravel() - arr = np.flip(array[:,:,i], 0).ravel() + #arr = np.flip(np.flip(array[:,:,i], 0), 0).ravel() + arr = np.flip(obj[:,:,i], 0).ravel() varb = numpy_to_vtk(arr, deep=True, array_type=vtk.VTK_UNSIGNED_CHAR) imgb = vtk.vtkImageData() - imgb.SetDimensions(array.shape[1], array.shape[0], 1) + imgb.SetDimensions(obj.shape[1], obj.shape[0], 1) imgb.GetPointData().SetScalars(varb) iac.AddInputData(0, imgb) iac.Update() self.SetInputData(iac.GetOutput()) + #self.mirror() + + elif isinstance(obj, str): + if ".png" in obj: + picr = vtk.vtkPNGReader() + elif ".jpg" in obj or ".jpeg" in obj: + picr = vtk.vtkJPEGReader() + elif ".bmp" in obj: + picr = vtk.vtkBMPReader() + picr.SetFileName(obj) + picr.Update() + self.SetInputData(picr.GetOutput()) + def alpha(self, a=None): - """Set/get actor's transparency.""" + """Set/get picture's transparency.""" if a is not None: self.GetProperty().SetOpacity(a) return self @@ -3016,14 +3033,12 @@ def alpha(self, a=None): return self.GetProperty().GetOpacity() def crop(self, top=None, bottom=None, right=None, left=None): - """Crop image. + """Crop picture. :param float top: fraction to crop from the top margin :param float bottom: fraction to crop from the bottom margin :param float left: fraction to crop from the left margin :param float right: fraction to crop from the right margin - - |legosurface| |legosurface.py|_ """ extractVOI = vtk.vtkExtractVOI() extractVOI.SetInputData(self.GetInput()) @@ -3038,11 +3053,26 @@ def crop(self, top=None, bottom=None, right=None, left=None): extractVOI.SetVOI(bx0, bx1, by0, by1, 0, 0) extractVOI.Update() img = extractVOI.GetOutput() - #img.SetOrigin(-bx0, -by0, 0) self.GetMapper().SetInputData(img) self.GetMapper().Modified() return self + def mirror(self, axis="x"): + """Mirror picture along x or y axis.""" + ff = vtk.vtkImageFlip() + ff.SetInputData(self.inputdata()) + if axis.lower() == "x": + ff.SetFilteredAxis(0) + elif axis.lower() == "y": + ff.SetFilteredAxis(1) + else: + colors.printc("~times Error in mirror(): mirror must be set to x or y.", c=1) + raise RuntimeError() + ff.Update() + self.GetMapper().SetInputData(ff.GetOutput()) + self.GetMapper().Modified() + return self + ########################################################################## class Volume(vtk.vtkVolume, Prop): @@ -3091,6 +3121,7 @@ def __init__(self, inputobj, img = vtk.vtkImageData() elif utils.isSequence(inputobj): varr = numpy_to_vtk(inputobj.ravel(), deep=True, array_type=vtk.VTK_FLOAT) + varr.SetName('input_scalars') img = vtk.vtkImageData() img.SetDimensions(inputobj.shape) img.GetPointData().SetScalars(varr) @@ -3208,7 +3239,7 @@ def color(self, col): r, g, b = colors.getColor(ci) xalpha = smin + (smax - smin) * i / (len(col) - 1) ctf.AddRGBPoint(xalpha, r, g, b) - # colors.printc('\tcolor at', round(xalpha, 1), + #colors.printc('\tcolor at', round(xalpha, 1), # '\tset to', colors.getColorName((r, g, b)), c='b', bold=0) elif isinstance(col, str): if col in colors.colors.keys() or col in colors.color_nicks.keys(): @@ -3228,6 +3259,7 @@ def color(self, col): volumeProperty.SetColor(ctf) volumeProperty.SetInterpolationTypeToLinear() + #volumeProperty.SetInterpolationTypeToNearest() return self def alpha(self, alpha): @@ -3411,7 +3443,7 @@ def scaleVoxels(self, scale=1): def mirror(self, axis="x"): """ - Mirror the actor polydata along one of the cartesian axes. + Mirror flip along one of the cartesian axes. .. note:: ``axis='n'``, will flip only mesh normals. diff --git a/vtkplotter/addons.py b/vtkplotter/addons.py index a7b4425d..7536e3f5 100644 --- a/vtkplotter/addons.py +++ b/vtkplotter/addons.py @@ -19,6 +19,7 @@ ) __all__ = [ + "addLight", "addScalarBar", "addScalarBar3D", "addSlider2D", @@ -31,6 +32,44 @@ "addLegend", ] +def addLight( + pos=(1, 1, 1), + focalPoint=(0, 0, 0), + deg=90, + ambient=None, + diffuse=None, + specular=None, + showsource=False, +): + """ + Generate a source of light placed at pos, directed to focal point. + + :param focalPoint: focal point, if this is a ``vtkActor`` use its position. + :type fp: vtkActor, list + :param deg: aperture angle of the light source + :param showsource: if `True`, will show a vtk representation + of the source of light as an extra actor + + .. hint:: |lights.py|_ + """ + if isinstance(focalPoint, vtk.vtkActor): + focalPoint = focalPoint.GetPosition() + light = vtk.vtkLight() + light.SetLightTypeToSceneLight() + light.SetPosition(pos) + light.SetPositional(1) + light.SetConeAngle(deg) + light.SetFocalPoint(focalPoint) + if diffuse is not None: light.SetDiffuseColor(colors.getColor(diffuse)) + if ambient is not None: light.SetAmbientColor(colors.getColor(ambient)) + if specular is not None: light.SetSpecularColor(colors.getColor(specular)) + if showsource: + lightActor = vtk.vtkLightActor() + lightActor.SetLight(light) + settings.plotter_instance.renderer.AddViewProp(lightActor) + settings.plotter_instance.renderer.AddLight(light) + return light + def addScalarBar(actor=None, c=None, title="", horizontal=False, @@ -193,7 +232,7 @@ def addScalarBar3D( # build the color scale part scale = shapes.Grid([-sx * gap, 0, 0], c=c, alpha=alpha, sx=sx, sy=sy, resx=1, resy=ncols) - scale.GetProperty().SetRepresentationToSurface() + scale.lw(0).GetProperty().SetRepresentationToSurface() cscals = scale.cellCenters()[:, 1] def _cellColors(scale, scalars, cmap, alpha): diff --git a/vtkplotter/analysis.py b/vtkplotter/analysis.py index a823f64b..bd851456 100644 --- a/vtkplotter/analysis.py +++ b/vtkplotter/analysis.py @@ -55,7 +55,6 @@ "splitByConnectivity", "projectSphereFilter", "extractSurface", - "geometry", "voronoi3D", "connectedPoints", "interpolateToVolume", @@ -76,26 +75,6 @@ ] -def geometry(obj, extent=None): - """ - Apply the ``vtkGeometryFilter``. - This is a general-purpose filter to extract geometry (and associated data) from any type of dataset. - This filter also may be used to convert any type of data to polygonal type. - The conversion process may be less than satisfactory for some 3D datasets. - For example, this filter will extract the outer surface of a volume or structured grid dataset. - - Returns an ``Actor`` object. - - :param list extent: set a `[xmin,xmax, ymin,ymax, zmin,zmax]` bounding box to clip data. - """ - gf = vtk.vtkGeometryFilter() - gf.SetInputData(obj) - if extent is not None: - gf.SetExtent(extent) - gf.Update() - return Actor(gf.GetOutput()) - - def spline(points, smooth=0.5, degree=2, s=2, nodes=False, res=20): """ Return an ``Actor`` for a spline so that it does not necessarly pass exactly throught all points. @@ -1443,7 +1422,7 @@ def volumeOperation(volume1, operation, volume2=None): return Volume(mat.GetOutput()) -def thinPlateSpline(actor, sourcePts, targetPts, userFunctions=(None, None)): +def thinPlateSpline(actor, sourcePts, targetPts, userFunctions=(None, None), sigma=1): """ `Thin Plate Spline` transformations describe a nonlinear warp transform defined by a set of source and target landmarks. Any point on the mesh close to a source landmark will @@ -1452,10 +1431,10 @@ def thinPlateSpline(actor, sourcePts, targetPts, userFunctions=(None, None)): Transformation object can be retrieved with ``actor.getTransform()``. - :param userFunctions: You must supply both the function - and its derivative with respect to r. + :param userFunctions: You may supply both the function and its derivative with respect to r. - .. hint:: Examples: |thinplate.py|_ |thinplate_grid.py|_ |thinplate_morphing.py|_ |interpolateField.py|_ |thinplate_morphing_2d.py|_ + .. hint:: Examples: |thinplate.py|_ |thinplate_grid.py|_ |thinplate_morphing.py|_ + |interpolateField.py|_ |thinplate_morphing_2d.py|_ |thinplate| |thinplate_grid| |thinplate_morphing| |interpolateField| |thinplate_morphing_2d| """ @@ -1465,7 +1444,7 @@ def thinPlateSpline(actor, sourcePts, targetPts, userFunctions=(None, None)): for i in range(ns): ptsou.SetPoint(i, sourcePts[i]) - nt = len(sourcePts) + nt = len(targetPts) if ns != nt: colors.printc("~times thinPlateSpline Error: #source != #target points", ns, nt, c=1) raise RuntimeError() @@ -1480,7 +1459,7 @@ def thinPlateSpline(actor, sourcePts, targetPts, userFunctions=(None, None)): if userFunctions[0]: transform.SetBasisFunction(userFunctions[0]) transform.SetBasisDerivative(userFunctions[1]) - transform.SetSigma(1) + transform.SetSigma(sigma) transform.SetSourceLandmarks(ptsou) transform.SetTargetLandmarks(pttar) @@ -1755,7 +1734,7 @@ def geodesic(actor, start, end): def convexHull(actor_or_list, alphaConstant=0): """ - Create a 3D Delaunay triangulation of input points. + Create a 2D/3D Delaunay triangulation of input points. :param actor_or_list: can be either an ``Actor`` or a list of 3D points. :param float alphaConstant: For a non-zero alpha value, only verts, edges, faces, @@ -1775,7 +1754,11 @@ def convexHull(actor_or_list, alphaConstant=0): triangleFilter.Update() poly = triangleFilter.GetOutput() - delaunay = vtk.vtkDelaunay3D() # Create the convex hull of the pointcloud + if np.count_nonzero(actor.coordinates()[:,2]): + delaunay = vtk.vtkDelaunay3D() # Create the convex hull of the pointcloud + else: + delaunay = vtk.vtkDelaunay2D() + if alphaConstant: delaunay.SetAlpha(alphaConstant) delaunay.SetInputData(poly) @@ -1784,9 +1767,7 @@ def convexHull(actor_or_list, alphaConstant=0): surfaceFilter = vtk.vtkDataSetSurfaceFilter() surfaceFilter.SetInputConnection(delaunay.GetOutputPort()) surfaceFilter.Update() - - chuact = Actor(surfaceFilter.GetOutput()) - return chuact + return Actor(surfaceFilter.GetOutput()) def actor2Volume(actor, spacing=(1, 1, 1)): diff --git a/vtkplotter/colors.py b/vtkplotter/colors.py index 4985ef5e..f566e235 100644 --- a/vtkplotter/colors.py +++ b/vtkplotter/colors.py @@ -245,11 +245,11 @@ "gist_ncar", "gist_ncar_r", "gist_rainbow", "gist_rainbow_r", "gist_stern","gist_stern_r","gist_yarg", "gist_yarg_r", "gnuplot", "gnuplot2", "gnuplot2_r", "gnuplot_r", - "gray", "gray_r", "hot", "hot_r", + "gray_r", "hot", "hot_r", "hsv", "hsv_r", "inferno", "inferno_r", "jet", "jet_r", "magma", "magma_r", "nipy_spectral", "nipy_spectral_r", "ocean", "ocean_r", - "pink", "pink_r", "plasma", "plasma_r", + "pink_r", "plasma", "plasma_r", "prism", "prism_r", "rainbow", "rainbow_r", "seismic", "seismic_r", "spring", "spring_r", "summer", "summer_r", "tab10", "tab10_r", diff --git a/vtkplotter/data/images/limbs_tc.jpg b/vtkplotter/data/images/limbs_tc.jpg new file mode 100644 index 0000000000000000000000000000000000000000..47e1264c67f6c85f6620f248d60536ed117bc7c4 GIT binary patch literal 96547 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

jOe&BX=iP|dpB41RXpRZ-eawfkY9fd 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 literal 0 HcmV?d00001 diff --git a/vtkplotter/data/timecourse1d.npy b/vtkplotter/data/timecourse1d.npy index 6c52558e40ce30109211b9f24b71e8d52d7b2faf..0e110169b8b988790f6fb22a3d27beaa17fa227d 100644 GIT binary patch delta 25314 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 N3lsXh^itZe{{tf_h5!Hn diff --git a/vtkplotter/data/timecourse1d/reference_243.vtk b/vtkplotter/data/timecourse1d/reference_243.vtk new file mode 100644 index 0000000000000000000000000000000000000000..21b77a29ce45cc234c5309d020fdcfa6c0aac7d0 GIT binary patch literal 4915 zcmXw+30zI-8-P=iuCyo-(v56sOvun(&Y=aRY0+-a*usdEVT6=IN)3{Vi4-zMmeS^) zFOp1#nT%z|GO}eFOe1O&Y| zfxTD?7dp7kO;(TxS)54vS?%3H=EQOnPeX#d$ z*f0FNu@pP+yB6Wc8}j=68$E;{`qksSu%%U~4Xwm3GISCyY#)z(!s4T#E7oBbTZ=-) z)t=aAEM^L&_vH6mYLFzHZj|3+*~@0(`@8aMuKe+XP*gAfy>y4?3WdeEPt?^>`-MZ> z0`Yljoa=-G!MGpPwM~9PwsjuPucR*!_SE0PzB+P_kg=)_`&vbSklG^w`{xM(!qzM| z>>Ecbg+!}+*td+Fgbl{N*mpv13#+e}VE>loEG%6gf_?AG8zI6X75l-fi9+b^b=ZxD zeFR?vHTGkRJ%Y#lR_v#izX+-l`S&*UG!mTO$;bJ5r;jjf+$dbr{JcT1T_pF5A2J2Y zG9uRK_C>N2{%mX8SnZv4Uf6FB|EjI=&%kc?_)1%B zT!sD5twq|L{95dPgQK;{{qnKj)eO?kZ~Ptm{eX8`*U8P;A0o1~W{WprcV^^k>Qfci zAB+CgEIaUqw?;42M4hk$_kc$4`i`n!&fy-=bW#6!KIIM`e@&M>#ra1TxF0lKI*N7u z>+ays>Q8OgO>uZ8cSeXVIyYJFpL*-kE#F}q7>4Q!o@|hNtc$L)3a>L7gRn5&&)E~@ zc}%#jDQ=J4$-n8|FE}W7dZ}bQY>V7G*GN_a-pZX&FWFhVlzaJ7$%Y%NwnX$}N#%GA(de0{Wq+*pF_S>9Lsl;90d#(2)qzX^@Z~fZBUXsLP zxTZxlLaG^)jr}U@o^-{|8~eqI1nF8geEsU?^}D5p=ki{99zR^VUE_{xp2dYp_p-ml zeiC(8YK)YxKaXboBRw1Y6V4xMFH6lG@}7Cn*ID}O+#sCa{d1c1W(6KM^{*v1Qit6& zoZpV!C%vzKj(u}fAk!;L!ftqQiy0U;VE>f3h8g%uW&W!KK$K=Ym zdS+sI3Foz^w9M4|HFnLw4rU&gh+Vxok@Y$zV(aeYFso}7*j36lX8lq=KIOA7F`KUP zb+~lrIM&~KEv`9pb{w;{--}&*Er8h#4Z=S0z?s=Qe8et#GKUSet;Igl7|Dj1Kf*qI z^AQ``xf%Q51!Fe6=`8kvLPzHKLl$<4mHq!BwScqJRP@hbKn`&Q;0;E0{kw1tha zm7g;$zk`i^8iVtc&~9v8f&5;!>f5vNvpaB}SkRBTnzducYl@hvawPV;tgB2jD-t`p zU=Gu^Kg3>^r(ojtIoJ!feq@s@bFpX7t!HkTN3cVNzGEI{aoD~Ob~4ZSx7bq>pRmcz zcz&rT_A6&oz1QG;LVhmu$}h$qJ)n?zw?4)mv3)%AQOo<*{*O)U^Yz1V-cN19e9z+N zzPeXJDf4@3igQJ^F$*x2_q_hwQ*8PeKGsvUyF0N!|9#j^8D=bKDPEIRw;SSFa6IlA zRh@Ae3)zM5OI4;yWub={u0OhP9Sb{&`$UzMX2rrQ@ffPMmKd@bm*?Y}sIp=<^F|o9 zzv*i>>)~Z==T%BJyTudR{AnYbtFPqk`uM&%o7eYm?1CYyS%k|C>}d<8v-u&}ycHpy z(}TR+J>~CQF3!&UZ0W)ufgAUK=u;bzyOIsb{4C&W`8NV?ex=~%M~VBt=j}oFP00My zm_N>sEVmiioNPhvMYbgKs{>!(o6N6j{60Ucx%-lB$o%N@d4KW%GQXPh`9Lz?1^m8} zY)|G_4n7}Db|4QS4<++&aeN)$LEIzA{H)>k`L5^YpPJmCkol)Bzt6vga66Mnlld0o z^RZ+X@;LHmn7TM?OeCM9wE4CKr$k$w$aX$;Ze=2f2jB@z2BrX00WZKCmp331k!;F;2U5Uup8I|WCDAEEMOmy4dej1z<%HW@GX!B90U#l`M_bI04M~G z07rpiKoM{p_zpM$d=H!iih)zWY2XZS7AOHqfpb6^P!3c8l|U769?$_2z<_Gt0&o$i N0WJZxz-6FL@qd|6p8x;= literal 0 HcmV?d00001 diff --git a/vtkplotter/data/timecourse1d/reference_244.vtk b/vtkplotter/data/timecourse1d/reference_244.vtk new file mode 100644 index 0000000000000000000000000000000000000000..54145d5de95d0f8367afa3a5d439fd02c82f846e GIT binary patch literal 4915 zcmXw+3sg;M8-P)yQ%N-;iX%!Bl2l6P?CsK3R4OGzNi`};Dw131Mn&ZkQX%A$Qt6`3 z{?diHn2Q+^!~bKLW@v_)HS^DyShHMy4pNZrM*0KP=H71b=fH_5Lf`xQyrR=BPhLw0>-foujKQTJ3hkZiM;d zDX!R04Gl%h+rGS2I=aT<#Es$D7W)cC<)}z(M}1v!d|L|k!lB)wg%>`LD#AQe9M?A# z=V{@u#j#0k*x74zMYEB+u#2kn#nDY8u*-fG#ZeK7*wsD{Mbi;w*bUX=L=$!po9Ts! z#>tu3pC-A9M%HfFy?0AQ!v{^+Pn{FQ5&8IfRRhHu(Lm*e^FMy574?3^b5IXH{+*~O z>ELs=!ubLk)&wTkNnCwN6gJwtTA;hgjqD zt8LbOBMz!I#rgCvx`g)&{=}YTeOLI~`g?4bE?S?Que{@+<7b%zoj{>I&=>HzVQm;)mE<`d8uj2-8I9``=wU6+WZ(h-%Ju}4#r^@&dC)ngrCAb z7<5fIJHZ{hcWso5gz(ui!KjCwga))wd)@V$``n9 z)phsng;940a9%&hSR~s{?pHrrFdDmg>ofJO^R?J5 z4Pol~pK#5qTb~%H%Z<0xwxl~tZG6;U?tygG zm-=!umZ3^|xE$vlyNlhjk~d-BaJtjh7cZ}$8xQK+3x2A{dFO@$?cW<6llyla%>=;^ zyK8;2#<%Emx%)CToBz3r-R)edDV`^vLwBy0rn(?Yp5OjLBR%~HyGMJkrq}IPx$Uwv zuTFd__d-u;@F2Xd)jgpHB>h=E@_gx4X>_do8hhMlNS4Rtb=zZEAx*!H_bYYx8-K~= zFZr|XzU(ErPrir8c86S&{C(`OyI#jh%VY4KrtV7nM2gOkue;8HM^aLDI_}fC)REX{zn(1pracL}YDSCn=e^h1r*llCcW30a zRN3di20O}YwZc+jS|zt|pOY~WZ0Kuy>=UO;*>F#JpJVsEnBv$9oR@0-#Ps{+`4O9C z%+UTE&JVfUF{6|X*u|l9m`QyG_Q6O?X8KaT-U}le*ckJ&-m-oK0?(zaw_^8Zc{3EAF#3pq$yXy~0jq zOPForUhIt%3)r;t2e1=1cQCud@*0c1(a-F+8{<4`kPUNKEnl;%99}U;m3(gr3%kjj zO>A+W;FJ_L`#bp_>%V&+bG;%TyKs*KbK5up_wh)Z!qn5`&v)+fcT9MagmbrhM*qV+!sWKs>0_Qhl;d1^QpLRE~#f_LNlsrpK3z?L=y;k-L>I171<>qOO3AIX*t(ct{@fF298e1?6> zLdlj3SFjIz`LGqCc+XVj#sA1wCZymzV~jHk--*|wDk`X)ttwiHbKgQ8w)*s2>{*|5 zvxr9dInDK)SmeixajqTQ$JRW?`?=eL&;S!I*W07kMlA6 z)hu?EGjBzRM_{ne0uT8&m!rMC0)~V67Wm`-7yhluTZ^ns9!lm{CEubyR%C1P6tWHZLo&Zo_;FLo)5z1w{L16oXOQj5GszC*S!90K z=leU6`KQSr^Q)8Fh3rb6Lv|yp$ZE1c7RhtT^T_kb3&`$d53(oOi|kGIAulBRlKC$q zex7^iGV_dA`L6{2{OibZ z$w$aX$))6D4vA zI04SUY`_I@1?B*5fC^9p0w4l&fqB4uU;*F`cmSS&7vK%}01E+MU=gqw@B{pT0AL9a z2m}Gaz)~Ot2nCh_VZd@=1+Wqb2UY>Afe0WHSOY`>Yk_DW28abd0@eX>Ks=BDtOqs# zi9iyt5l99$0VzN#kOpi9wgBnCRv-h|25bj*06T$QKqjyo*aPeZvVd$L2gn8TfPKJz zARj0I3V{Q_L7)gI21_m2G9v~0o_0k Ja1*$t_&-0sl)V4| literal 0 HcmV?d00001 diff --git a/vtkplotter/data/timecourse1d/reference_245.vtk b/vtkplotter/data/timecourse1d/reference_245.vtk new file mode 100644 index 0000000000000000000000000000000000000000..5635c0e7fa73e193c6c9c0fc04f9a3ade999f902 GIT binary patch literal 4915 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 HT+#dwt&5Md literal 0 HcmV?d00001 diff --git a/vtkplotter/data/timecourse1d/reference_247.vtk b/vtkplotter/data/timecourse1d/reference_247.vtk new file mode 100644 index 0000000000000000000000000000000000000000..1c3f605b2fb98f3a5b40081c23e347a67e48b5ba GIT binary patch literal 4915 zcmXw+30RF;AHYMVX;I0fvURD+-a@j}J+Cb^X;Gm~v`ATslq?MqEh4)tWl0)DMVW~E z9$7Lo5@s07$7E@&lWEM*V0`oa&;R^C&+~hJzxUkp-0nT^dEfW4(=1A!qZu2S962t2 z?o7>;nMn)d6B9J9j)RQE3yCSo^HY+IMtk{=3J5V8J1S^Ypl6WA-)~}wXd{0=FW;a* z%^+uIP3+vn$YdjjmhxQ1wa8Tu{<+`H3swd@mx%TV$tzS`xFPm|eoczA&ob=0Fz)!D zUqovS-rFbxXZvB>^?RrE|NA^P3$IoBW!Yng78jx z*MD=c|0;N+bScEY%i1_6C^qiVVw`<${#dcP-HL76{7LDYZO{{P{K!2rTFn2w6@RDsx`-|;+ZX z?9;9c?9>OV*!$Lf*k8BqX79#@W3Os+n7zre#$KZr$X?e>!Om)Z$X-5NioN!EJbU`# zbL{muZm}l@8tjdgH&~;Ag1sd#l->J?dw^|^T*!WY9D(CI2UM^hbspHeTl`qVy1v+Z zj`n3g_&&nkKm81=YafeUZ0yWx%kN;9lvOgF??2dwy)Lm+j~%f~@0+r!_=(tMGaXoY zQ)ldoCRbKE=^1uq@F`Yu!X5i$?Vqejza0CNtq&`l@C3Ui@ej6RIqpezw$g}gDXzkC z^|>C)J0s6mt!X;Txo{B2FF2fFt94VcFS)d1D*|n>FFST;DV@4ve{b8C&DI%UU(qjQ zGXmxJt#5qICf~k^;|<4KnYWMp%-5GSu#qR_`uxaJn45(q&iUzsCF>t8Kj*Eoa;7Qm zgX6cujxx(ZV;S*w1^6RP!U{cX?)3tQOkZ;rOq=y;F;RI)~jC z*X_1-rh4~x&C_tEcifgxt;}w7ILT5*EnbAyAIr?a0_<6`*c27XN?2jKXGJ=b~QBYECOcM9Z_)BeP9_r>4wsb-_GU9V;G;2o2& z9j)DXsI^?H-htsfY(qO7*Q|WY!#~MqkWFzYkBE}{&7!7~PpA8j@udtNX>kqb>(?do zsE8WT+K*}GJo*6cH*Is+avt*tuMutIfQLNR2KTV``kOpHYhrhtfBx8g9=CKZcGZjq z9)GYSc1Z^_KD&Msc7EkMKIf0)*f~@0@p+bbZE92AdGUl{XK*}(rSimZ{G3{k^=tY3 zl}~Wo_URhF;P7hfc7Ahs(#;0Z9#``dc(Q&h_SV`Ko-!Qgcnk}8$rq*Iea2{tXF!nG z7*F||%hlOgT*>$gf-G7f&aSXnJz;&a_+=oD#ZC}5Ad4rrI2KQ5;s3v{*oETQge*RV zI2Kp9uo>B$ELKX4i(gj4;tCf2f-Ig~;#fS{gvF&SEY7U3xFZU8A&V!bI2LD2Sezl@ z9%Qj|#j%FmliZ7JN46(_Np>LjCX1ae-bXx%h5M2Flf`Zp;{(ZJ*N9^$vNPF*EOwiC zeK6UTJcR5<9!efY9!?%X7Qar!`*@JGWJXrVBgv!4qse2)W67T6ab&S;#rsbnizl!+ z{)+5Po=6sVN%6WbS=?pCu|IhdIe;8U4kAw`Pa#hw2a`j{q2w^KxQmIuGmRWUo=%=Y zjwDBsqscMknPhR5iT9gDjw8pDXOriU#oa*6pGQt0Cz8bzOuW8;oJ3woP9~?27m-uR zi^)sKOUcW~Y2>fT%gHOq>ExB-<$=k@=$@%0R7u%HTe|zG`WU+hJ2Q+BdcUiK1Z%4pC?}+UnE~5*O4z9 zP4x1G$B7I4C*H*f2EPNuKe3tH0_^~Opgr&zU;um$7y@FxF<=68089Zhz#QlZSOA@X zF91uRGhhW+12#YxpexV~=nnJ%Yyl0>6X*rl0rtR`fCJDQ=mYcx`T_lc0l+}O5pV*W z0T*BpFc@$Jh5&BBP+%A^92fz(10H}DV1NRQ1V#a)fib{Xz!Mk;j0YwFUcgs?H!u6_Yz4Lf+kt#w2e1>^1?&b2fI?souou_|><5Z~ z13)ow5GVn@0S*C&fg?aEa1=NOlmX>H1#lcV0aOB2z)9d+pc*&@oCa!uGr(Cu2dDrC U&H=T+dEf$Y5x4}@0hf*b2hIVibpQYW literal 0 HcmV?d00001 diff --git a/vtkplotter/data/timecourse1d/reference_248.vtk b/vtkplotter/data/timecourse1d/reference_248.vtk new file mode 100644 index 0000000000000000000000000000000000000000..7d330a367e60176ffa49e43af62583adbc74a93a GIT binary patch literal 4915 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 literal 0 HcmV?d00001 diff --git a/vtkplotter/docs.py b/vtkplotter/docs.py index 8b9eeaf7..bd17555e 100644 --- a/vtkplotter/docs.py +++ b/vtkplotter/docs.py @@ -59,12 +59,13 @@ def tips(): msg += "| 4 to use scalars as colors (if present) |\n" msg += "| 5 to change background color |\n" msg += "| 0-9 to change axes style (use keypad) |\n" - msg += "| k/K to show point/cell scalars as color map |\n" +# msg += "| k/K to show point/cell scalars as color map |\n" msg += "| n to show surface mesh normals |\n" msg += "| a to toggle interaction to Actor Mode |\n" msg += "| j to toggle interaction to Joystick Mode |\n" msg += "| C to print current camera info |\n" msg += "| S to save a screenshot |\n" + msg += "| E to export rendering window to numpy file |\n" msg += "| q to return control to python script |\n" msg += "| Esc to close the rendering window |\n" msg += "| F1 to abort execution and exit python |\n" diff --git a/vtkplotter/dolfin.py b/vtkplotter/dolfin.py index bfc5565e..e854e85b 100644 --- a/vtkplotter/dolfin.py +++ b/vtkplotter/dolfin.py @@ -190,8 +190,8 @@ def plot(*inputobj, **options): """ Plot the object(s) provided. - Input can be: ``vtkActor``, ``vtkVolume``, ``dolfin.Mesh``, ``dolfin.MeshFunction*``, - ``dolfin.Expression`` or ``dolfin.Function``. + Input can be any combination of: ``Actor``, ``Volume``, ``dolfin.Mesh``, + ``dolfin.MeshFunction``, ``dolfin.Expression`` or ``dolfin.Function``. :return: the current ``Plotter`` class instance. @@ -268,10 +268,50 @@ def plot(*inputobj, **options): - 9, show the bounding box outLine, - 10, show three circles representing the maximum bounding box. + Axis 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 + :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 :param bool interactive: if True will stop after show() to allow interaction w/ window - :param bool depthpeeling: depth-peel volumes along with the translucent geometry :param bool offscreen: if True will not show the rendering window :param float zoom: camera zooming factor @@ -284,24 +324,32 @@ def plot(*inputobj, **options): assigned to the ``camera`` keyword: (E.g. `show(camera={'pos':(1,2,3), 'thickness':1000,})`) - - pos, `(list)`, the position of the camera in world coordinates - - focalPoint `(list)`, the focal point of the camera in world coordinates - - viewup `(list)`, the view up direction for the camera - - distance `(float)`, set the focal point to the specified distance from the camera position. - - clippingRange `(float)`, distance of the near and far clipping planes along - the direction of projection. + - `pos`, `(list)`, + the position of the camera in world coordinates + + - `focalPoint`, `(list)`, + the focal point of the camera in world coordinates + + - `viewup`, `(list)`, + the view up direction for the camera - - parallelScale `(float)`, + - `distance`, `(float)`, + set the focal point to the specified distance from the camera position. + + - `clippingRange`, `(float)`, + distance of the near and far clipping planes along the direction of projection. + + - `parallelScale`, `(float)`, scaling used for a parallel projection, i.e. the height of the viewport in world-coordinate distances. The default is 1. Note that the "scale" parameter works as an "inverse scale", larger numbers produce smaller images. This method has no effect in perspective projection mode. - - thickness `(float)`, + - `thickness`, `(float)`, set the distance between clipping planes. This method adjusts the far clipping plane to be set a distance 'thickness' beyond the near clipping plane. - - viewAngle `(float)`, + - `viewAngle`, `(float)`, the camera view angle, which is the angular height of the camera view measured in degrees. The default angle is 30 degrees. This method has no effect in parallel projection mode. @@ -504,30 +552,26 @@ def plot(*inputobj, **options): actor.flat() elif shading[0] == 'g': actor.gouraud() + delta = None if cmap and u and c is None: delta = [u(p) for p in mesh.coordinates()] - #delta = u.compute_vertex_values(mesh) # needs reshape + #delta = u.compute_vertex_values(mesh) # needs reshape, not faster.. if u.value_rank() > 0: # wiil show the size of the vector actor.pointColors(utils.mag(delta), cmap=cmap, bands=bands, vmin=vmin, vmax=vmax) else: actor.pointColors(delta, cmap=cmap, bands=bands, vmin=vmin, vmax=vmax) + if 'warp' in mode or 'displac' in mode: + actor.move(u, delta) + if scbar and c is None: if 'h' in scbar: actor.addScalarBar(horizontal=True, vmin=vmin, vmax=vmax) else: actor.addScalarBar(horizontal=False, vmin=vmin, vmax=vmax) - if 'warp' in mode or 'displac' in mode: - if delta is None: - delta = [u(p) for p in mesh.coordinates()] - movedpts = mesh.coordinates() + delta - actor.polydata(False).GetPoints().SetData(numpy_to_vtk(movedpts)) - actor.poly.GetPoints().Modified() - actor.u_values = delta - if warpZfactor: scals = actor.scalars(0) if len(scals): @@ -643,7 +687,7 @@ def __init__(self, *inputobj, **options): if u: u_values = np.array([u(p) for p in self.mesh.coordinates()]) - #print(u_values) + #print('u_values are', u_values) if u_values is not None: # colorize if a dolfin function is passed if len(u_values.shape) == 2: @@ -653,16 +697,23 @@ def __init__(self, *inputobj, **options): else: # u_values is 1D dispsizes = u_values - self.addPointScalars(dispsizes, "u_values")#.mapPointsToCells() + self.addPointScalars(dispsizes, "u_values") - def move(self, u=None): + def move(self, u=None, deltas=None): + """Move mesh according to solution `u` + or from calculated vertex displacements `deltas`. + """ if u is None: u = self.u - delta = [u(p) for p in self.mesh.coordinates()] - movedpts = self.mesh.coordinates() + delta + if deltas is None: + deltas = [u(p) for p in self.mesh.coordinates()] + + movedpts = self.mesh.coordinates() + 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.poly.GetPoints().Modified() - self.u_values = delta + self.u_values = deltas def MeshPoints(*inputobj, **options): """ diff --git a/vtkplotter/plotter.py b/vtkplotter/plotter.py index d81a7131..bcfe8370 100644 --- a/vtkplotter/plotter.py +++ b/vtkplotter/plotter.py @@ -9,7 +9,7 @@ import vtkplotter.vtkio as vtkio import vtkplotter.utils as utils import vtkplotter.colors as colors -from vtkplotter.actors import Actor, Assembly, Volume +from vtkplotter.actors import Actor, Assembly, Volume, Picture import vtkplotter.docs as docs import vtkplotter.settings as settings import vtkplotter.addons as addons @@ -53,6 +53,47 @@ def show(*actors, **options): - 9, show the bounding box outLine, - 10, show three circles representing the maximum bounding box + Axis 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 + :param c: surface color, in rgb, hex or name formats :param bc: set a color for the internal surface face :param bool wire: show actor in wireframe representation @@ -153,7 +194,6 @@ def show(*actors, **options): camera = options.pop("camera", None) interactorStyle = options.pop("interactorStyle", 0) newPlotter = options.pop("newPlotter", False) - depthpeeling = options.pop("depthpeeling", False) q = options.pop("q", False) if len(actors) == 0: @@ -194,7 +234,6 @@ def show(*actors, **options): axes=axes, sharecam=sharecam, infinity=infinity, - depthpeeling=depthpeeling, verbose=verbose, interactive=interactive, offscreen=offscreen, @@ -353,7 +392,6 @@ class Plotter: :param bool sharecam: if False each renderer will have an independent vtkCamera :param bool interactive: if True will stop after show() to allow interaction w/ window :param bool offscreen: if True will not show the rendering window - :param bool depthpeeling: depth-peel volumes along with the translucent geometry :param QVTKRenderWindowInteractor qtWidget: render in a Qt-Widget using an QVTKRenderWindowInteractor. @@ -381,7 +419,6 @@ def __init__( verbose=True, interactive=None, offscreen=False, - depthpeeling=False, qtWidget = None ): @@ -450,17 +487,6 @@ def __init__( self.ztitle = settings.ztitle # z axis label and units - if settings.notebookBackend: - self.interactive = False - self.interactor = None - self.window = None - if size == "auto": - self.size = (1000, 1000) - ############################ - return ##################### - ############################ - - # build the renderering window: if settings.useOpenVR: self.camera = vtk.vtkOpenVRCamera() @@ -471,6 +497,16 @@ def __init__( self.window.PointSmoothingOn() + if settings.notebookBackend: + self.interactive = False + self.interactor = None + self.window = None + if size == "auto": + self.size = (1000, 1000) + ############################ + return ##################### + ############################ + # sort out screen size if screensize == "auto": aus = self.window.GetScreenSize() @@ -536,20 +572,14 @@ def __init__( arenderer = vtk.vtkOpenVRRenderer() else: arenderer = vtk.vtkRenderer() - arenderer.SetUseHiddenLineRemoval(settings.hiddenLineRemoval) - arenderer.SetUseDepthPeeling(depthpeeling) - if "jpg" in str(self.backgrcol).lower() or "jpeg" in str(self.backgrcol).lower(): + arenderer.SetUseHiddenLineRemoval(settings.hiddenLineRemoval) + arenderer.SetLightFollowCamera(settings.lightFollowsCamera) + arenderer.SetUseFXAA(settings.useFXAA) + arenderer.SetUseDepthPeeling(settings.useDepthPeeling) + + if ".jpg" in str(self.backgrcol).lower() or ".jpeg" in str(self.backgrcol).lower(): if i == 0: - jpeg_reader = vtk.vtkJPEGReader() - if not jpeg_reader.CanReadFile(self.backgrcol): - colors.printc("~times Error reading background image file", self.backgrcol, c=1) - raise RuntimeError() - jpeg_reader.SetFileName(self.backgrcol) - jpeg_reader.Update() - image_data = jpeg_reader.GetOutput() - image_actor = vtk.vtkImageActor() - image_actor.InterpolateOn() - image_actor.SetInputData(image_data) + image_actor = Picture(self.backgrcol) self.backgroundRenderer = vtk.vtkRenderer() self.backgroundRenderer.SetLayer(0) self.backgroundRenderer.InteractiveOff() @@ -595,7 +625,9 @@ def __init__( if self.qtWidget is not None: self.interactor = self.qtWidget.GetRenderWindow().GetInteractor() self.window.SetOffScreenRendering(True) + ######################## return + ######################## if offscreen: self.window.SetOffScreenRendering(True) @@ -884,8 +916,7 @@ def moveCamera(self, camstart, camstop, fraction): self.interactive = save_int ################################################################## AddOns - def addLight( - self, + def addLight(self, pos=(1, 1, 1), focalPoint=(0, 0, 0), deg=90, @@ -895,9 +926,9 @@ def addLight( showsource=False, ): """ - Generate a source of light placed at pos, directed to focal point fp. + Generate a source of light placed at pos, directed to focal point. - :param fp: focal Point, if this is a ``vtkActor`` use its position. + :param focalPoint: focal point, if this is a ``vtkActor`` use its position. :type fp: vtkActor, list :param deg: aperture angle of the light source :param showsource: if `True`, will show a vtk representation @@ -905,23 +936,8 @@ def addLight( .. hint:: |lights.py|_ """ - if isinstance(focalPoint, vtk.vtkActor): - focalPoint = focalPoint.GetPosition() - light = vtk.vtkLight() - light.SetLightTypeToSceneLight() - light.SetPosition(pos) - light.SetPositional(1) - light.SetConeAngle(deg) - light.SetFocalPoint(focalPoint) - if diffuse is not None: light.SetDiffuseColor(colors.getColor(diffuse)) - if ambient is not None: light.SetAmbientColor(colors.getColor(ambient)) - if specular is not None: light.SetSpecularColor(colors.getColor(specular)) - if showsource: - lightActor = vtk.vtkLightActor() - lightActor.SetLight(light) - self.renderer.AddViewProp(lightActor) - self.renderer.AddLight(light) - return light + return addons.addLight(pos, focalPoint, deg, + ambient, diffuse, specular, showsource) def addScalarBar(self, actor=None, c=None, title="", horizontal=False, vmin=None, vmax=None): """Add a 2D scalar bar for the specified actor. @@ -1321,7 +1337,8 @@ def scan(wannabeacts): scannedacts.append(out) elif "trimesh" in str(type(a)): - scannedacts.append(utils.trimesh2vtk(a)) + from vtkplotter.trimesh import trimesh2vtk + scannedacts.append(trimesh2vtk(a)) else: colors.printc("~!? Cannot understand input in show():", type(a), c=1) @@ -2303,6 +2320,11 @@ def _keypress(self, iren, event): colors.printc("Click an actor and press X to open the cutter box widget.", c=4) + elif key == "E": + colors.printc("~camera Exporting rendering window to scene.npy..", c="blue", end="") + vtkio.exportWindow('scene.npy') + colors.printc(" ..done. Try:\n> vtkplotter scene.npy #(still experimental)", c="blue") + elif key == "i": # print info if self.clickedActor: utils.printInfo(self.clickedActor) diff --git a/vtkplotter/settings.py b/vtkplotter/settings.py index 3fa10840..433a8a78 100644 --- a/vtkplotter/settings.py +++ b/vtkplotter/settings.py @@ -26,53 +26,66 @@ __all__ = ['datadir', 'embedWindow'] #################################################################################### -# recompute vertex and cell normals +# Axes titles +xtitle = 'x' +ytitle = 'y' +ztitle = 'z' + +# Scale magnification of the screenshot (must be an integer) +screeshotScale = 1 + +screenshotTransparentBackground = False + +# Recompute vertex and cell normals computeNormals = None -# default style is TrackBallCamera +# Default style is TrackBallCamera interactorStyle = None -# allow to interact with scene during interactor.Start() execution +# Allow to interact with scene during interactor.Start() execution allowInteraction = True -# usetex, matplotlib latex compiler +# Use tex, matplotlib latex compiler usetex = False # Qt embedding usingQt = False -# OpenVR +# OpenVR rendering useOpenVR = False -# on some vtk versions/platforms points are redered as ugly squares +# On some vtk versions/platforms points are redered as ugly squares renderPointsAsSpheres = True +# Wrap lines in tubes renderLinesAsTubes = False -# remove hidden lines when in wireframe mode +# Remove hidden lines when in wireframe mode hiddenLineRemoval = False -# +# For (Un)Structured and RectilinearGrid: show internal edges not only outline visibleGridEdges = False -# notebook support with K3D -notebookBackend = None -notebook_plotter = None +# Turn on/off the automatic repositioning of lights as the camera moves. +lightFollowsCamera = False + +# Turn on/off nvidia FXAA anti-aliasing, if supported +useFXAA = False -# path to Voro++ library -# http://math.lbl.gov/voro++ +# Turn on/off rendering of translucent material with depth peeling technique. +useDepthPeeling = False + +# Path to Voro++ library, http://math.lbl.gov/voro++ voro_path = '/usr/local/bin' -# axes titles -xtitle = 'x' -ytitle = 'y' -ztitle = 'z' -# scale magnification of the screenshot -screeshotScale = 1 -screenshotTransparentBackground = False +#################################################################################### +# notebook support with K3D +notebookBackend = None +notebook_plotter = None + #################################################################################### import os _cdir = os.path.dirname(__file__) diff --git a/vtkplotter/shapes.py b/vtkplotter/shapes.py index 75e43ea4..682cbe27 100644 --- a/vtkplotter/shapes.py +++ b/vtkplotter/shapes.py @@ -55,11 +55,10 @@ def Point(pos=(0, 0, 0), r=12, c="red", alpha=1): """Create a simple point actor.""" if len(pos) == 2: pos = (pos[0], pos[1], 0) - actor = Points([pos], r, c, alpha) - return actor + return Points([pos], r, c, alpha) -def Points(plist, r=5, c="gray", alpha=1): +def Points(plist, r=5, c="gold", alpha=1): """ Build a point ``Actor`` for a list of 2D/3D points. Both shapes (N, 3) or (3, N) are accepted as input - if N>3. @@ -91,13 +90,15 @@ def Points(plist, r=5, c="gray", alpha=1): plist = np.c_[np.array(plist), np.zeros(len(plist))] ################ - if ( (utils.isSequence(c) and (len(c) > 3 or len(c[0]) == 4)) + if ( ( + utils.isSequence(c) + and (len(c)>3 or (utils.isSequence(c[0]) and len(c[0])==4)) + ) or utils.isSequence(alpha) ): actor = _PointsColors(plist, r, c, alpha) else: - n = len(plist) # refresh sourcePoints = vtk.vtkPoints() sourceVertices = vtk.vtkCellArray() @@ -169,10 +170,8 @@ def _PointsColors(plist, r, cols, alpha): c = cols pd.GetPointData().SetScalars(ucols) - actor = Actor(pd, c, alpha) + actor = Actor(pd, c, alpha).flat().pointSize(r) actor.mapper.ScalarVisibilityOn() - actor.GetProperty().SetInterpolationToFlat() - actor.GetProperty().SetPointSize(r) return actor @@ -255,9 +254,8 @@ def Glyph(actor, glyphObj, orientationArray=None, gly.SetColorModeToColorByScalar() gly.Update() - pd = gly.GetOutput() - gactor = Actor(pd, c, alpha) + gactor = Actor(gly.GetOutput(), c, alpha).flat() if cmap: lut = vtk.vtkLookupTable() @@ -269,10 +267,9 @@ def Glyph(actor, glyphObj, orientationArray=None, gactor.mapper.SetLookupTable(lut) gactor.mapper.ScalarVisibilityOn() gactor.mapper.SetScalarModeToUsePointData() - rng = pd.GetPointData().GetScalars().GetRange() + rng = gly.GetOutput().GetPointData().GetScalars().GetRange() gactor.mapper.SetScalarRange(rng[0], rng[1]) - gactor.GetProperty().SetInterpolationToFlat() settings.collectable_actors.append(gactor) return gactor @@ -404,8 +401,7 @@ def Line(p0, p1=None, c="r", alpha=1, lw=1, dotted=False, res=None): lineSource.Update() poly = lineSource.GetOutput() - actor = Actor(poly, c, alpha) - actor.GetProperty().SetLineWidth(lw) + actor = Actor(poly, c, alpha).lw(lw) if dotted: actor.GetProperty().SetLineStipplePattern(0xF0F0) actor.GetProperty().SetLineStippleRepeatFactor(1) @@ -444,8 +440,7 @@ def Lines(startPoints, endPoints=None, c=None, alpha=1, lw=1, dotted=False, scal polylns.AddInputConnection(lineSource.GetOutputPort()) polylns.Update() - actor = Actor(polylns.GetOutput(), c, alpha) - actor.GetProperty().SetLineWidth(lw) + actor = Actor(polylns.GetOutput(), c, alpha).lw(lw) if dotted: actor.GetProperty().SetLineStipplePattern(0xF0F0) actor.GetProperty().SetLineStippleRepeatFactor(1) @@ -501,12 +496,9 @@ def Tube(points, r=1, c="r", alpha=1, res=12): cc.InsertTuple3(i, int(255 * r), int(255 * g), int(255 * b)) polyln.GetPointData().AddArray(cc) c = None - tuf.Update() - polytu = tuf.GetOutput() - actor = Actor(polytu, c, alpha, computeNormals=0) - actor.phong() + actor = Actor(tuf.GetOutput(), c, alpha, computeNormals=0).phong() if usingColScals: actor.mapper.SetScalarModeToUsePointFieldData() actor.mapper.ScalarVisibilityOn() @@ -579,7 +571,7 @@ def Ribbon(line1, line2, c="m", alpha=1, res=(200, 5)): rsf.SetResolution(res[0], res[1]) rsf.SetInputData(mergedPolyData.GetOutput()) rsf.Update() - actor = Actor(rsf.GetOutput(), c=c, alpha=alpha) + actor = Actor(rsf.GetOutput(), c, alpha) settings.collectable_actors.append(actor) return actor @@ -655,8 +647,7 @@ def Arrow(startPoint, endPoint, s=None, c="r", alpha=1, res=12): tf.SetTransform(t) tf.Update() - actor = Actor(tf.GetOutput(), c, alpha) - actor.GetProperty().SetInterpolationToPhong() + actor = Actor(tf.GetOutput(), c, alpha).phong() actor.SetPosition(startPoint) actor.DragableOff() actor.base = np.array(startPoint) @@ -769,9 +760,7 @@ def Sphere(pos=(0, 0, 0), r=1, c="r", alpha=1, res=24): ss.SetThetaResolution(2 * res) ss.SetPhiResolution(res) ss.Update() - pd = ss.GetOutput() - actor = Actor(pd, c, alpha) - actor.GetProperty().SetInterpolationToPhong() + actor = Actor(ss.GetOutput(), c, alpha).phong() actor.SetPosition(pos) settings.collectable_actors.append(actor) return actor @@ -853,17 +842,11 @@ def Spheres(centers, r=1, c="r", alpha=1, res=8): glyph.SetInputData(pd) glyph.Update() - mapper = vtk.vtkPolyDataMapper() - mapper.SetInputData(glyph.GetOutput()) - - actor = Actor() - actor.SetMapper(mapper) - actor.GetProperty().SetInterpolationToPhong() - actor.GetProperty().SetOpacity(alpha) + actor = Actor(glyph.GetOutput(), alpha=alpha).phong() if cisseq: - mapper.ScalarVisibilityOn() + actor.mapper.ScalarVisibilityOn() else: - mapper.ScalarVisibilityOff() + actor.mapper.ScalarVisibilityOff() actor.GetProperty().SetColor(colors.getColor(c)) settings.collectable_actors.append(actor) return actor @@ -946,9 +929,8 @@ def Ellipsoid(pos=(0, 0, 0), axis1=(1, 0, 0), axis2=(0, 2, 0), axis3=(0, 0, 3), tf.Update() pd = tf.GetOutput() - actor = Actor(pd, c=c, alpha=alpha) + actor = Actor(pd, c, alpha).phong() actor.GetProperty().BackfaceCullingOn() - actor.GetProperty().SetInterpolationToPhong() actor.SetPosition(pos) actor.base = -np.array(axis1) / 2 + pos actor.top = np.array(axis1) / 2 + pos @@ -993,10 +975,7 @@ def Grid( tf.SetInputData(poly) tf.SetTransform(t) tf.Update() - pd = tf.GetOutput() - actor = Actor(pd, c, alpha) - actor.GetProperty().SetRepresentationToWireframe() - actor.GetProperty().SetLineWidth(lw) + actor = Actor(tf.GetOutput(), c, alpha).wireframe().lw(lw) actor.SetPosition(pos) settings.collectable_actors.append(actor) return actor @@ -1029,8 +1008,7 @@ def Plane(pos=(0, 0, 0), normal=(0, 0, 1), sx=1, sy=None, c="g", alpha=1): tf.SetInputData(poly) tf.SetTransform(t) tf.Update() - pd = tf.GetOutput() - actor = Actor(pd, c, alpha) + actor = Actor(tf.GetOutput(), c, alpha) actor.SetPosition(pos) settings.collectable_actors.append(actor) return actor @@ -1118,9 +1096,7 @@ def Spring( thickness = r / 10 tuf.SetRadius(thickness) tuf.Update() - poly = tuf.GetOutput() - actor = Actor(poly, c, alpha) - actor.GetProperty().SetInterpolationToPhong() + actor = Actor(tuf.GetOutput(), c, alpha).phong() actor.SetPosition(startPoint) actor.base = np.array(startPoint) actor.top = np.array(endPoint) @@ -1128,7 +1104,7 @@ def Spring( return actor -def Cylinder(pos=(0, 0, 0), r=1, height=1, axis=(0, 0, 1), c="teal", alpha=1, res=24): +def Cylinder(pos=(0,0,0), r=1, height=1, axis=(0,0,1), c="teal", alpha=1, res=24): """ Build a cylinder of specified height and radius `r`, centered at `pos`. @@ -1169,8 +1145,7 @@ def Cylinder(pos=(0, 0, 0), r=1, height=1, axis=(0, 0, 1), c="teal", alpha=1, re tf.Update() pd = tf.GetOutput() - actor = Actor(pd, c, alpha) - actor.GetProperty().SetInterpolationToPhong() + actor = Actor(pd, c, alpha).phong() actor.SetPosition(pos) actor.base = base + pos actor.top = top + pos @@ -1178,7 +1153,7 @@ def Cylinder(pos=(0, 0, 0), r=1, height=1, axis=(0, 0, 1), c="teal", alpha=1, re return actor -def Cone(pos=(0, 0, 0), r=1, height=3, axis=(0, 0, 1), c="dg", alpha=1, res=48): +def Cone(pos=(0,0,0), r=1, height=3, axis=(0,0,1), c="dg", alpha=1, res=48): """ Build a cone of specified radius `r` and `height`, centered at `pos`. @@ -1190,8 +1165,7 @@ def Cone(pos=(0, 0, 0), r=1, height=3, axis=(0, 0, 1), c="dg", alpha=1, res=48): con.SetHeight(height) con.SetDirection(axis) con.Update() - actor = Actor(con.GetOutput(), c, alpha) - actor.GetProperty().SetInterpolationToPhong() + actor = Actor(con.GetOutput(), c, alpha).phong() actor.SetPosition(pos) v = utils.versor(axis) * height / 2 actor.base = pos - v @@ -1200,14 +1174,14 @@ def Cone(pos=(0, 0, 0), r=1, height=3, axis=(0, 0, 1), c="dg", alpha=1, res=48): return actor -def Pyramid(pos=(0, 0, 0), s=1, height=1, axis=(0, 0, 1), c="dg", alpha=1): +def Pyramid(pos=(0,0,0), s=1, height=1, axis=(0,0,1), c="dg", alpha=1): """ Build a pyramid of specified base size `s` and `height`, centered at `pos`. """ return Cone(pos, s, height, axis, c, alpha, 4) -def Torus(pos=(0, 0, 0), r=1, thickness=0.2, axis=(0, 0, 1), c="khaki", alpha=1, res=30): +def Torus(pos=(0, 0, 0), r=1, thickness=0.2, c="khaki", alpha=1, res=30): """ Build a torus of specified outer radius `r` internal radius `thickness`, centered at `pos`. @@ -1222,29 +1196,13 @@ def Torus(pos=(0, 0, 0), r=1, thickness=0.2, axis=(0, 0, 1), c="khaki", alpha=1, pfs.SetVResolution(res) pfs.Update() - nax = np.linalg.norm(axis) - if nax: - axis = np.array(axis) / nax - 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(pfs.GetOutput()) - tf.SetTransform(t) - tf.Update() - pd = tf.GetOutput() - - actor = Actor(pd, c, alpha) - actor.GetProperty().SetInterpolationToPhong() + actor = Actor(pfs.GetOutput(), c, alpha).phong() actor.SetPosition(pos) settings.collectable_actors.append(actor) return actor -def Paraboloid(pos=(0, 0, 0), r=1, height=1, axis=(0, 0, 1), c="cyan", alpha=1, res=50): +def Paraboloid(pos=(0,0,0), r=1, height=1, c="cyan", alpha=1, res=50): """ Build a paraboloid of specified height and radius `r`, centered at `pos`. @@ -1268,30 +1226,14 @@ def Paraboloid(pos=(0, 0, 0), r=1, height=1, axis=(0, 0, 1), c="cyan", alpha=1, contours.GenerateValues(1, 0.01, 0.01) contours.Update() - axis = np.array(axis) / np.linalg.norm(axis) - 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)) - t.Scale(r, r, r) - tf = vtk.vtkTransformPolyDataFilter() - tf.SetInputData(contours.GetOutput()) - tf.SetTransform(t) - tf.Update() - pd = tf.GetOutput() - - actor = Actor(pd, c, alpha).flipNormals() - actor.GetProperty().SetInterpolationToPhong() + actor = Actor(contours.GetOutput(), c, alpha).flipNormals().phong() actor.mapper.ScalarVisibilityOff() actor.SetPosition(pos) settings.collectable_actors.append(actor) return actor -def Hyperboloid(pos=(0, 0, 0), a2=1, value=0.5, height=1, axis=(0, 0, 1), - c="magenta", alpha=1, res=100): +def Hyperboloid(pos=(0,0,0), a2=1, value=0.5, height=1, c="m", alpha=1, res=100): """ Build a hyperboloid of specified aperture `a2` and `height`, centered at `pos`. @@ -1314,22 +1256,7 @@ def Hyperboloid(pos=(0, 0, 0), a2=1, value=0.5, height=1, axis=(0, 0, 1), contours.GenerateValues(1, value, value) contours.Update() - axis = np.array(axis) / np.linalg.norm(axis) - 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)) - t.Scale(1, 1, height) - tf = vtk.vtkTransformPolyDataFilter() - tf.SetInputData(contours.GetOutput()) - tf.SetTransform(t) - tf.Update() - pd = tf.GetOutput() - - actor = Actor(pd, c, alpha).flipNormals() - actor.GetProperty().SetInterpolationToPhong() + actor = Actor(contours.GetOutput(), c, alpha).flipNormals().phong() actor.mapper.ScalarVisibilityOff() actor.SetPosition(pos) settings.collectable_actors.append(actor) @@ -1620,26 +1547,15 @@ def build_img_plt(formula, tfile): else: build_img_plt(formula, '_lateximg.png') - from vtkplotter.actors import Image + from vtkplotter.actors import Picture - picr = vtk.vtkPNGReader() - picr.SetFileName('_lateximg.png') - picr.Update() - vactor = Image() - vactor.SetInputData(picr.GetOutput()) + vactor = Picture('_lateximg.png') vactor.info['formula'] = formula vactor.alpha(alpha) b = vactor.GetBounds() xm, ym = (b[1]+b[0])/200*s, (b[3]+b[2])/200*s vactor.SetOrigin(-xm, -ym, 0) -# nax = np.linalg.norm(normal) -# if nax: -# normal = np.array(normal) / nax -# theta = np.arccos(normal[2]) -# phi = np.arctan2(normal[1], normal[0]) vactor.SetScale(0.25/res*s, 0.25/res*s, 0.25/res*s) -# vactor.RotateZ(np.rad2deg(phi)) -# vactor.RotateY(np.rad2deg(theta)) vactor.SetPosition(pos) try: import os diff --git a/vtkplotter/trimesh.py b/vtkplotter/trimesh.py new file mode 100644 index 00000000..36c02fb2 --- /dev/null +++ b/vtkplotter/trimesh.py @@ -0,0 +1,116 @@ +""" +Trimesh support and interoperability module. + +Install trimesh with: + +.. code-block:: bash + + sudo apt install python3-rtree + pip install rtree shapely + conda install trimesh + +Check out `trimesh `_ github page for more info. + +Check the example gallery at: +`examples/other/trimesh `_ +""" + +from __future__ import division, print_function +import vtkplotter.utils as utils +import numpy as np + + +__all__ = ["vtk2trimesh", "trimesh2vtk"] + + +########################################################################### +def vtk2trimesh(actor): + """ + Convert vtk ``Actor`` to ``Trimesh`` object. + """ + from trimesh import Trimesh + + lut = actor.mapper.GetLookupTable() + + tris = actor.faces() + carr = actor.scalars('CellColors', datatype='cell') + ccols = None + if carr is not None and len(carr)==len(tris): + ccols = [] + for i in range(len(tris)): + r,g,b,a = lut.GetTableValue(carr[i]) + ccols.append((r*255, g*255, b*255, a*255)) + ccols = np.array(ccols, dtype=np.int16) + + points = actor.coordinates() + varr = actor.scalars('VertexColors', datatype='point') + vcols = None + if varr is not None and len(varr)==len(points): + vcols = [] + for i in range(len(points)): + r,g,b,a = lut.GetTableValue(varr[i]) + vcols.append((r*255, g*255, b*255, a*255)) + vcols = np.array(vcols, dtype=np.int16) + + if len(tris)==0: + tris = None + + return Trimesh(vertices=points, faces=tris, + face_colors=ccols, vertex_colors=vcols) + + +def trimesh2vtk(inputobj, alphaPerCell=False): + """ + Convert ``Trimesh`` object to ``Actor(vtkActor)`` or ``Assembly`` object. + """ + # print('trimesh2vtk inputobj', type(inputobj)) + + inputobj_type = str(type(inputobj)) + + if "Trimesh" in inputobj_type or "primitives" in inputobj_type: + from vtkplotter import Actor + + faces = inputobj.faces + poly = utils.buildPolyData(inputobj.vertices, faces) + tact = Actor(poly) + if inputobj.visual.kind == "face": + trim_c = inputobj.visual.face_colors + else: + trim_c = inputobj.visual.vertex_colors + + if utils.isSequence(trim_c): + if utils.isSequence(trim_c[0]): + trim_cc = trim_c[:, [0, 1, 2]] / 255 + trim_al = trim_c[:, 3] / 255 + if inputobj.visual.kind == "face": + tact.colorCellsByArray(trim_cc, trim_al, alphaPerCell) + else: + tact.colorVerticesByArray(trim_cc, trim_al) + else: + print("trim_c not sequence?", trim_c) + return tact + + elif "PointCloud" in inputobj_type: + from vtkplotter.shapes import Points + + trim_cc, trim_al = "black", 1 + if hasattr(inputobj, "vertices_color"): + trim_c = inputobj.vertices_color + if len(trim_c): + trim_cc = trim_c[:, [0, 1, 2]] / 255 + trim_al = trim_c[:, 3] / 255 + trim_al = np.sum(trim_al) / len(trim_al) # just the average + return Points(inputobj.vertices, r=8, c=trim_cc, alpha=trim_al) + + elif "path" in inputobj_type: + from vtkplotter.shapes import Line + from vtkplotter.actors import Assembly + + lines = [] + for e in inputobj.entities: + # print('trimesh entity', e.to_dict()) + l = Line(inputobj.vertices[e.points], c="k", lw=2) + lines.append(l) + return Assembly(lines) + + return None diff --git a/vtkplotter/utils.py b/vtkplotter/utils.py index 8bce21c5..fa98c7dc 100644 --- a/vtkplotter/utils.py +++ b/vtkplotter/utils.py @@ -15,6 +15,7 @@ __all__ = [ "ProgressBar", + "geometry", "isSequence", "vector", "mag", @@ -33,8 +34,11 @@ "humansort", "resampleArrays", "printHistogram", - "trimesh2vtk", "plotMatrix", + "cameraFromQuaternion", + "cameraFromNeuroglancer", + "orientedCamera", + "vtkCameraToK3D", ] ########################################################################### @@ -161,61 +165,30 @@ def _update(self, counts): self.bar += ps -########################################################################### -def trimesh2vtk(inputobj, alphaPerCell=False): - """Convert trimesh object to ``Actor(vtkActor)`` object.""" - from vtkplotter import Actor - - #colors.printc('trimesh2vtk inputobj', type(inputobj), c=3) +########################################################### +def geometry(obj, extent=None): + """ + Apply the ``vtkGeometryFilter``. + This is a general-purpose filter to extract geometry (and associated data) + from any type of dataset. + This filter also may be used to convert any type of data to polygonal type. + The conversion process may be less than satisfactory for some 3D datasets. + For example, this filter will extract the outer surface of a volume + or structured grid dataset. - inputobj_type = str(type(inputobj)) + Returns an ``Actor`` object. - if "Trimesh" in inputobj_type or "primitives" in inputobj_type: - faces = inputobj.faces - poly = buildPolyData(inputobj.vertices, faces) - tact = Actor(poly) - if inputobj.visual.kind == 'face': - trim_c = inputobj.visual.face_colors - else: - trim_c = inputobj.visual.vertex_colors - - if isSequence(trim_c): - if isSequence(trim_c[0]): - trim_cc = trim_c[:,[0,1,2]]/255 - trim_al = trim_c[:,3]/255 - if inputobj.visual.kind == 'face': - tact.colorCellsByArray(trim_cc, trim_al, alphaPerCell) - else: - tact.colorVerticesByArray(trim_cc, trim_al) - else: - print('trim_c not sequence?', trim_c) - return tact - - elif "PointCloud" in inputobj_type: - from vtkplotter.shapes import Points - trim_cc, trim_al = 'black', 1 - if hasattr(inputobj, 'vertices_color'): - trim_c = inputobj.vertices_color - if len(trim_c): - trim_cc = trim_c[:,[0,1,2]]/255 - trim_al = trim_c[:,3]/255 - trim_al = np.sum(trim_al)/len(trim_al) # just the average - return Points(inputobj.vertices, r=8, c=trim_cc, alpha=trim_al) - - elif "path" in inputobj_type: - from vtkplotter.shapes import Line - from vtkplotter.actors import Assembly - lines = [] - for e in inputobj.entities: - #print('trimesh entity', e.to_dict()) - l = Line(inputobj.vertices[e.points], c='k', lw=2) - lines.append(l) - return Assembly(lines) - - return None + :param list extent: set a `[xmin,xmax, ymin,ymax, zmin,zmax]` bounding box to clip data. + """ + from vtkplotter.actors import Actor + gf = vtk.vtkGeometryFilter() + gf.SetInputData(obj) + if extent is not None: + gf.SetExtent(extent) + gf.Update() + return Actor(gf.GetOutput()) -########################################################### def buildPolyData(vertices, faces=None, lines=None, indexOffset=0, fast=True): """ Build a ``vtkPolyData`` object from a list of vertices @@ -375,6 +348,8 @@ def genflatten(lst): def humansort(l): """Sort in place a given list the way humans expect. + NB: input list is modified + E.g. ['file11', 'file1'] -> ['file1', 'file11'] """ import re @@ -390,7 +365,7 @@ def tryint(s): return [tryint(c) for c in re.split("([0-9]+)", s)] l.sort(key=alphanum_key) - return None # NB: input list is modified + return l # NB: input list is modified def lin_interp(x, rangeX, rangeY): @@ -1113,3 +1088,113 @@ def plotMatrix(M, title='matrix', continuous=True, cmap='Greys'): cb.set_ticklabels(unq) plt.show() + +################################################################# +# Functions adapted from: +# https://github.com/sdorkenw/MeshParty/blob/master/meshparty/trimesh_vtk.py +def cameraFromQuaternion(pos, quaternion, distance=10000, ngl_correct=True): + """Define a ``vtkCamera`` with a particular orientation. + + Parameters + ---------- + pos: np.array, list, tuple + an iterator of length 3 containing the focus point of the camera + quaternion: np.array, list, tuple + a len(4) quaternion (x,y,z,w) describing the rotation of the camera + such as returned by neuroglancer x,y,z,w all in [0,1] range + distance: float + the desired distance from pos to the camera (default = 10000 nm) + + Returns + ------- + vtk.vtkCamera + a vtk camera setup according to these rules. + """ + camera = vtk.vtkCamera() + # define the quaternion in vtk, note the swapped order + # w,x,y,z instead of x,y,z,w + quat_vtk = vtk.vtkQuaterniond( + quaternion[3], quaternion[0], quaternion[1], quaternion[2] + ) + # use this to define a rotation matrix in x,y,z + # right handed units + M = np.zeros((3, 3), dtype=np.float32) + quat_vtk.ToMatrix3x3(M) + # the default camera orientation is y up + up = [0, 1, 0] + # calculate default camera position is backed off in positive z + pos = [0, 0, distance] + + # set the camera rototation by applying the rotation matrix + camera.SetViewUp(*np.dot(M, up)) + # set the camera position by applying the rotation matrix + camera.SetPosition(*np.dot(M, pos)) + if ngl_correct: + # neuroglancer has positive y going down + # so apply these azimuth and roll corrections + # to fix orientatins + camera.Azimuth(-180) + camera.Roll(180) + + # shift the camera posiiton and focal position + # to be centered on the desired location + p = camera.GetPosition() + p_new = np.array(p) + pos + camera.SetPosition(*p_new) + camera.SetFocalPoint(*pos) + return camera + + +def cameraFromNeuroglancer(state, zoom=300): + """Define a ``vtkCamera`` from a neuroglancer state dictionary. + + Parameters + ---------- + state: dict + an neuroglancer state dictionary. + zoom: float + how much to multiply zoom by to get camera backoff distance + default = 300 > ngl_zoom = 1 > 300 nm backoff distance. + + Returns + ------- + vtk.vtkCamera + a vtk camera setup that matches this state. + """ + orient = state.get("perspectiveOrientation", [0.0, 0.0, 0.0, 1.0]) + pzoom = state.get("perspectiveZoom", 10.0) + position = state["navigation"]["pose"]["position"] + pos_nm = np.array(position["voxelCoordinates"]) * position["voxelSize"] + return cameraFromQuaternion(pos_nm, orient, pzoom * zoom, ngl_correct=True) + + +def orientedCamera(center, upVector=(0,-1,0), backoffVector=(0,0,1), backoff=500): + """ + Generate a ``vtkCamera`` pointed at a specific location, + oriented with a given up direction, set to a backoff. + """ + vup = np.array(upVector) + vup = vup / np.linalg.norm(vup) + + pt_backoff = center - backoff * 1000 * np.array(backoffVector) + + camera = vtk.vtkCamera() + camera.SetFocalPoint(*center) + camera.SetViewUp(*vup) + camera.SetPosition(*pt_backoff) + return camera + + +def vtkCameraToK3D(vtkcam): + """ + Convert a ``vtkCamera`` object into a 9-element list to be used by K3D backend. + + Output format is: [posx,posy,posz, targetx,targety,targetz, upx,upy,upz] + """ + cdis = vtkcam.GetDistance() + cpos = np.array(vtkcam.GetPosition())*cdis + kam = [cpos.tolist()] + kam.append(vtkcam.GetFocalPoint()) + kam.append(vtkcam.GetViewUp()) + return np.array(kam).ravel() + diff --git a/vtkplotter/version.py b/vtkplotter/version.py index 98f77fb9..f00289ca 100644 --- a/vtkplotter/version.py +++ b/vtkplotter/version.py @@ -1 +1 @@ -_version='2019.4.0' +_version='2019.4.1' diff --git a/vtkplotter/vtkio.py b/vtkplotter/vtkio.py index 074e7ac9..7dde6ad2 100644 --- a/vtkplotter/vtkio.py +++ b/vtkplotter/vtkio.py @@ -6,7 +6,7 @@ import vtkplotter.utils as utils import vtkplotter.colors as colors -from vtkplotter.actors import Actor, Volume, Assembly, Image +from vtkplotter.actors import Actor, Volume, Assembly, Picture import vtkplotter.docs as docs import vtkplotter.settings as settings @@ -188,7 +188,7 @@ def _load_file(filename, c, alpha, threshold, spacing, unpack): picr = vtk.vtkBMPReader() picr.SetFileName(filename) picr.Update() - actor = Image() # object derived from vtk.vtkImageActor() + actor = Picture() # object derived from vtk.vtkImageActor() actor.SetInputData(picr.GetOutput()) if alpha is None: alpha = 1 @@ -445,6 +445,10 @@ def loadGeoJSON(filename): def loadDolfin(filename, exterior=False): """Reads a `Fenics/Dolfin` file format (.xml or .xdmf). Return an ``Actor(vtkActor)`` object.""" + import sys + if sys.version_info[0] < 3: + return _loadDolfin_old(filename) + import dolfin if filename.lower().endswith('.xdmf'): @@ -469,52 +473,52 @@ def loadDolfin(filename, exterior=False): return Actor(poly).lw(0.1) -#def loadDolfin_old(filename, exterior='dummy'): -# import xml.etree.ElementTree as et -# -# if filename.endswith(".gz"): -# import gzip -# -# inF = gzip.open(filename, "rb") -# outF = open("/tmp/filename.xml", "wb") -# outF.write(inF.read()) -# outF.close() -# inF.close() -# tree = et.parse("/tmp/filename.xml") -# else: -# tree = et.parse(filename) -# -# coords, connectivity = [], [] -# for mesh in tree.getroot(): -# for elem in mesh: -# for e in elem.findall("vertex"): -# x = float(e.get("x")) -# y = float(e.get("y")) -# ez = e.get("z") -# if ez is None: -# coords.append([x, y]) -# else: -# z = float(ez) -# coords.append([x, y, z]) -# -# tets = elem.findall("tetrahedron") -# if not len(tets): -# tris = elem.findall("triangle") -# for e in tris: -# v0 = int(e.get("v0")) -# v1 = int(e.get("v1")) -# v2 = int(e.get("v2")) -# connectivity.append([v0, v1, v2]) -# else: -# for e in tets: -# v0 = int(e.get("v0")) -# v1 = int(e.get("v1")) -# v2 = int(e.get("v2")) -# v3 = int(e.get("v3")) -# connectivity.append([v0, v1, v2, v3]) -# -# poly = utils.buildPolyData(coords, connectivity) -# return Actor(poly) +def _loadDolfin_old(filename, exterior='dummy'): + import xml.etree.ElementTree as et + + if filename.endswith(".gz"): + import gzip + + inF = gzip.open(filename, "rb") + outF = open("/tmp/filename.xml", "wb") + outF.write(inF.read()) + outF.close() + inF.close() + tree = et.parse("/tmp/filename.xml") + else: + tree = et.parse(filename) + + coords, connectivity = [], [] + for mesh in tree.getroot(): + for elem in mesh: + for e in elem.findall("vertex"): + x = float(e.get("x")) + y = float(e.get("y")) + ez = e.get("z") + if ez is None: + coords.append([x, y]) + else: + z = float(ez) + coords.append([x, y, z]) + + tets = elem.findall("tetrahedron") + if not len(tets): + tris = elem.findall("triangle") + for e in tris: + v0 = int(e.get("v0")) + v1 = int(e.get("v1")) + v2 = int(e.get("v2")) + connectivity.append([v0, v1, v2]) + else: + for e in tets: + v0 = int(e.get("v0")) + v1 = int(e.get("v1")) + v2 = int(e.get("v2")) + v3 = int(e.get("v3")) + connectivity.append([v0, v1, v2, v3]) + + poly = utils.buildPolyData(coords, connectivity) + return Actor(poly) def loadNeutral(filename): @@ -702,7 +706,7 @@ def _buildactor(d): bcv = arr0[:,2].reshape(shp) arr = np.array([rcv, gcv, bcv]) arr = np.swapaxes(arr, 0, 2) - vimg = Image(arr) + vimg = Picture(arr) loadcommon(vimg, d) objs.append(vimg) @@ -823,7 +827,7 @@ def _doactor(obj, adict): adict['actors'].append(assdict) fillcommon(obj, adict) - elif isinstance(obj, Image): + elif isinstance(obj, Picture): adict['type'] = 'image' arr = vtk_to_numpy(obj.inputdata().GetPointData().GetScalars()) adict['array'] = arr @@ -958,31 +962,48 @@ def write(objct, fileoutput, binary=True): dicts2save.append( _np_dump(obj) ) np.save(fileoutput, dicts2save) return dicts2save + elif ".xml" in fr: # write tetrahedral dolfin xml - vertices = obj.coordinates() - faces = obj.cells() + vertices = objct.coordinates().astype(str) + faces = np.array(objct.faces()).astype(str) ncoords = vertices.shape[0] - ntets = faces.shape[0] outF = open(fileoutput, "w") outF.write('\n') outF.write('\n') - outF.write(' \n') - outF.write(' \n') - for i in range(ncoords): - x, y, z = vertices[i] - outF.write(' \n') - outF.write(' \n') - outF.write(' \n') - for i in range(ntets): - v0, v1, v2, v3 = faces[i] - outF.write(' \n') + + if len(faces[0]) == 4:# write tetrahedral mesh + ntets = faces.shape[0] + outF.write(' \n') + outF.write(' \n') + for i in range(ncoords): + x, y, z = vertices[i] + outF.write(' \n') + outF.write(' \n') + outF.write(' \n') + for i in range(ntets): + v0, v1, v2, v3 = faces[i] + outF.write(' \n') + + elif len(faces[0]) == 3:# write triangle mesh + ntri = faces.shape[0] + outF.write(' \n') + outF.write(' \n') + for i in range(ncoords): + x, y, dummy_z = vertices[i] + outF.write(' \n') + outF.write(' \n') + outF.write(' \n') + for i in range(ntri): + v0, v1, v2 = faces[i] + outF.write(' \n') + outF.write(' \n') outF.write(" \n") outF.write("\n") outF.close() return objct + else: colors.printc("~noentry Unknown format", fileoutput, "file not saved.", c="r") return objct @@ -1026,6 +1047,9 @@ def exportWindow(fileoutput, binary=False, speed=None, html=True): `generated webpage `_ See also: FEniCS test `webpage `_. + + .. note:: the rendering window can also be exported to `numpy` file `scene.npy` + by pressing ``E`` keyboard at any moment during visualization. ''' fr = fileoutput.lower() @@ -1066,6 +1090,8 @@ def exportWindow(fileoutput, binary=False, speed=None, html=True): sdict = dict() vp = settings.plotter_instance sdict['shape'] = vp.shape #todo + sdict['sharecam'] = vp.sharecam #todo + sdict['camera'] = None #todo sdict['position'] = vp.pos sdict['size'] = vp.size sdict['axes'] = vp.axes @@ -1074,15 +1100,13 @@ def exportWindow(fileoutput, binary=False, speed=None, html=True): sdict['ytitle'] = vp.ytitle sdict['ztitle'] = vp.ztitle sdict['backgrcol'] = colors.getColor(vp.backgrcol) - sdict['sharecam'] = vp.sharecam #todo sdict['infinity'] = vp.infinity - sdict['depthpeeling'] = vp.renderer.GetUseDepthPeeling() + sdict['useDepthPeeling'] = settings.useDepthPeeling sdict['renderPointsAsSpheres'] = settings.renderPointsAsSpheres sdict['renderLinesAsTubes'] = settings.renderLinesAsTubes sdict['hiddenLineRemoval'] = settings.hiddenLineRemoval sdict['visibleGridEdges'] = settings.visibleGridEdges sdict['interactorStyle'] = settings.interactorStyle - sdict['camera'] = None #todo sdict['objects'] = [] for a in vp.getActors() + vp.getVolumes(): sdict['objects'].append(_np_dump(a)) @@ -1091,30 +1115,55 @@ def exportWindow(fileoutput, binary=False, speed=None, html=True): return def importWindow(fileinput): - """Import a whole scene from a Numpy file.""" + """Import a whole scene from a Numpy file. + Return ``Plotter`` instance.""" import numpy as np from vtkplotter import Plotter data = np.load(fileinput, allow_pickle=True)[0] - settings.renderPointsAsSpheres = data['renderPointsAsSpheres'] - settings.renderLinesAsTubes = data['renderLinesAsTubes'] - settings.hiddenLineRemoval = data['hiddenLineRemoval'] - settings.visibleGridEdges = data['visibleGridEdges'] - settings.interactorStyle = data['interactorStyle'] - - vp = Plotter(pos=data['position'], - size=data['size'], - axes=data['axes'], - title=data['title'], - bg=data['backgrcol'], - infinity=data['infinity'], - depthpeeling=data['depthpeeling'], + if 'renderPointsAsSpheres' in data.keys(): + settings.renderPointsAsSpheres = data['renderPointsAsSpheres'] + if 'renderLinesAsTubes' in data.keys(): + settings.renderLinesAsTubes = data['renderLinesAsTubes'] + if 'hiddenLineRemoval' in data.keys(): + settings.hiddenLineRemoval = data['hiddenLineRemoval'] + if 'visibleGridEdges' in data.keys(): + settings.visibleGridEdges = data['visibleGridEdges'] + if 'interactorStyle' in data.keys(): + settings.interactorStyle = data['interactorStyle'] + + pos = data.pop('position', (0, 0)) + axes = data.pop('axes', 4) + title = data.pop('title', '') + backgrcol = data.pop('backgrcol', "blackboard") + infinity = data.pop('infinity', False) + + vp = Plotter(pos=pos, + #size=data['size'], # not necessarily a good idea to set it + #shape=data['shape'], + axes=axes, + title=title, + bg=backgrcol, + infinity=infinity, ) - vp.xtitle = data['xtitle'] - vp.ytitle = data['ytitle'] - vp.ztitle = data['ztitle'] - vp.actors = loadNumpy(data['objects']) + vp.xtitle = data.pop('xtitle', 'x') + vp.ytitle = data.pop('ytitle', 'y') + vp.ztitle = data.pop('ztitle', 'z') + + objs = loadNumpy(data['objects']) + if not utils.isSequence(objs): + objs = [objs] + vp.actors = objs + +# if vp.shape==(1,1): +# vp.actors = loadNumpy(data['objects']) +# else: +# print(objs, ) +# for a in objs: +# for ar in a.renderedAt: +# print(vp.shape, [a], ar ) +# vp.show(a, at=ar) return vp @@ -1134,10 +1183,25 @@ def screenshot(filename="screenshot.png"): w2if.SetInputBufferTypeToRGBA() w2if.ReadFrontBufferOff() # read from the back buffer w2if.Update() - pngwriter = vtk.vtkPNGWriter() - pngwriter.SetFileName(filename) - pngwriter.SetInputConnection(w2if.GetOutputPort()) - pngwriter.Write() + if filename.endswith('.png'): + writer = vtk.vtkPNGWriter() + writer.SetFileName(filename) + writer.SetInputConnection(w2if.GetOutputPort()) + writer.Write() + elif filename.endswith('.jpg'): + writer = vtk.vtkJPEGWriter() + writer.SetFileName(filename) + writer.SetInputConnection(w2if.GetOutputPort()) + writer.Write() + elif filename.endswith('.svg'): + writer = vtk.vtkGL2PSExporter() + #writer.SetFileFormatToPDF() + #writer.SetFileFormatToTeX() + writer.SetFileFormatToSVG() + writer.CompressOff() + writer.SetInput(settings.plotter_instance.window) + writer.SetFilePrefix(filename.split('.')[0]) + writer.Write() class Video: