From f78ca8404d6835f119a369ec2235e9ae86121a30 Mon Sep 17 00:00:00 2001 From: Peter Corke Date: Mon, 17 Aug 2026 13:53:33 +1000 Subject: [PATCH 1/3] fix: restore pvplus.py, fix pyvista API drift, drop rvcprint from figure scripts Fixes #20. tools/pvplus.py was a symlink to a path on the reporter's old machine (/Users/corkep/Dropbox/.../frames.py) that never existed in this repo or anywhere accessible -- every figures/**/*.py script that did `import pvplus` was broken for anyone but the original author, and only by cwd accident even then. Recovered the file's content (three near-identical copies existed, all but one line identical) and placed it at RVC3/tools/pvplus.py instead of bare tools/ or figures/3d/: it's imported by ~40 scripts spread across figures/3d/ and multiple figures/code/chapterN/ subdirectories, and RVC3.tools is already a real, declared, pip-installable package -- the only placement that resolves for every consumer via a normal `pip install -e .` dev setup, with no PYTHONPATH tweaking needed. Swapped `import pvplus` -> `from RVC3.tools import pvplus` in all 40 consuming scripts (mechanical, one line each -- every pvplus.xxx(...) call site is unchanged since the bound name doesn't change). Also fixed outfile()'s hardcoded personal absolute path to derive from the calling script's own location -- same class of bug as the original issue, no reason to reintroduce it. Testing this surfaced two more real, pre-existing issues: - pyvista's PolyData.transform() now requires an explicit inplace= kwarg (used to default to True) -- added it at all 7 call sites in pvplus.py so it actually runs against a current pyvista install. Several individual figure scripts have their own local .transform() calls with the same issue; left those alone, out of scope here. - Several scripts import a book-production image-export helper (rvcprint) that's either equally broken (bare `import rvcprint` / `from rvcprint import outfile`) or just unnecessary for what we're testing here. Commented it out (import + the screenshot-path usage) in the ~10 pvplus-consuming scripts that referenced it, per discussion -- it's book-production plumbing, not needed to exercise the actual figure-generation logic. Adds a `figures` optional-dependency extra (pyvista, vtk, pillow) to pyproject.toml, since none of this was previously declared anywhere -- `pip install -e ".[figures]"` is now the documented way to get set up for regenerating book figures. ## Test plan - All 40 modified scripts pass `py_compile` - Ran 9 of the pvplus-consuming scripts end-to-end (add_frame, ribbon, axis, add_ellipsoid, ellipsoid_3d, add_frame2 all exercised), all exit 0 - 3 more (fig2_15.py, fig2_16.py, fig9_17.py in figures/code/) open a real on-screen render window since they don't set off_screen=True -- confirmed rendering correctly by eye, not run to completion headlessly --- RVC3/tools/pvplus.py | 441 ++++++++++++++++++ figures/3d/fig2_14.py | 4 +- figures/3d/fig2_16.py | 4 +- figures/3d/fig2_4.py | 2 +- figures/3d/fig3_1a.py | 2 +- figures/3d/fig3_1b.py | 2 +- figures/3d/fig7_13a.py | 2 +- figures/3d/fig7_4a.py | 2 +- figures/3d/fig7_4b.py | 2 +- figures/3d/fig7_4c.py | 2 +- figures/3d/fig7_7.py | 2 +- figures/3d/fig8_8a.py | 4 +- figures/3d/fig8_8b.py | 4 +- figures/3d/fig9_17.py | 4 +- figures/code/chapter2/fig2_14.py | 8 +- figures/code/chapter2/fig2_15.py | 6 +- figures/code/chapter2/fig2_16.py | 6 +- figures/code/chapter2/fig2_4.py | 2 +- figures/code/chapter2/fig2_6/aruco_markers.py | 2 +- .../chapter2/fig2_6/aruco_markers_pyvista.py | 2 +- figures/code/chapter2/fig2_6/fig2_6.py | 2 +- figures/code/chapter2/screw.py | 2 +- figures/code/chapter2/vtkbug2.py | 2 +- figures/code/chapter3/fig3_1a.py | 2 +- figures/code/chapter3/fig3_1b.py | 2 +- figures/code/chapter3/fig3_1c.py | 2 +- figures/code/chapter7/fig7_10b.py | 2 +- figures/code/chapter7/fig7_13a.py | 2 +- figures/code/chapter7/fig7_4a.py | 2 +- figures/code/chapter7/fig7_4b.py | 2 +- figures/code/chapter7/fig7_4c.py | 2 +- figures/code/chapter7/fig7_6.py | 2 +- figures/code/chapter7/fig7_7.py | 2 +- figures/code/chapter7/pyvista/extrude.py | 2 +- figures/code/chapter7/pyvista/fig7_2a.py | 2 +- figures/code/chapter7/pyvista/fig7_2b.py | 2 +- figures/code/chapter7/pyvista/fig7_2c.py | 2 +- figures/code/chapter7/pyvista/fig7_7b.py | 2 +- figures/code/chapter8/fig8_8a.py | 4 +- figures/code/chapter8/fig8_8b.py | 4 +- figures/code/chapter9/fig9_17.py | 6 +- pyproject.toml | 1 + 42 files changed, 498 insertions(+), 56 deletions(-) create mode 100644 RVC3/tools/pvplus.py diff --git a/RVC3/tools/pvplus.py b/RVC3/tools/pvplus.py new file mode 100644 index 0000000..72d3c36 --- /dev/null +++ b/RVC3/tools/pvplus.py @@ -0,0 +1,441 @@ +# pvplus - PyVista extras for drawing coordinate frames, axes and ellipsoids + +import pyvista as pv +import numpy as np +from spatialmath import base +from math import cos, sin, pi +import vtk +from spatialmath import SE3 +import os.path +import sys +from pathlib import Path +from PIL import Image +""" + +HINTS + +not very good at display coordinate frames/axes + +plotter.show_axes() put a small frame for orientation in bottom left +plotter.show_bounds(grid='front') overlay a grid + +pv.save("mesh.vtk") +pv.save_meshio("mesh.obj", mesh) + +plotter.export_obj(filename) +plotter.export_vtkjs(filename) + +""" +s=0.8 +sp=10 +ss = False +se = False +th = 0.3 +r = 100 +zl = -0.4 + + +def string(text, subscript=None, scale=1): + + if '_' in text: + z = text.split('_') + text = z[0] + subscript = z[1] + + # create the axis name + textmesh = pv.Text3D(text, depth=th) + + # add the subscript + if subscript is not None: + submesh = pv.Text3D(subscript, depth=th) + submesh.points /= 1.5 # smaller + + # shift it to the right and down a bit + m = np.max(textmesh.points, axis=0) + + + submesh.translate([m[0] * 0.9, -m[1] * 0.5, 0]) + textmesh = textmesh + submesh # append it + + # centre the whole thing + textmesh.points -= np.mean(textmesh.points, axis=0) + + # scale the text + textmesh.points *= scale / 5 + return textmesh + +def add_arrow(p, T, i, color='silver', label=None, subscript=None, scale=1.0, opacity=1.0): + + pos = [0, 0, 0] + pos[i] = 1.1 * scale + + ## create the arrow to scale + arrow = pv.Arrow(direction=pos, tip_resolution=r, shaft_resolution=r) + arrow.points *= scale + + arrow.transform(T, inplace=True) + p.add_mesh(arrow, color=color, specular=s, specular_power=sp, \ + smooth_shading=ss, show_edges=se, opacity=opacity) + + names = 'XYZ' + + ## create the axis label text to scale + + # create the axis name + text = pv.Text3D(names[i], depth=th) + + # add the subscript + if label is not None: + axlabel = pv.Text3D(label, depth=th) + axlabel.points /= 1.5 # smaller + + # shift it to the right and down a bit + m = np.max(text.points, axis=0) + axlabel.translate([m[0] * 0.8, -m[1] * 0.5, 0]) + text = text + axlabel # append it + + # centre the whole thing + text.points -= np.mean(text.points, axis=0) + + # scale the text + text.points *= scale / 5 + + # now orient it in space, rotate about centre + if i == 0: + text.rotate_x(90) + elif i == 1: + text.rotate_y(90) + text.rotate_x(90) + elif i == 2: + text.rotate_x(90) + text.rotate_z(90) + + text.translate(pos) + text.transform(T, inplace=True) + + ## combine arrow and text + p.add_mesh(text, color=color, opacity=opacity) + return arrow + text + +def add_frame(plotter, T=None, color=None, **kwargs): + + if color is None: + colors = ['red', 'green', 'blue'] + else: + colors = (color,) * 3 + + if T is None: + T = SE3() + + T = T.A + add_arrow(plotter, T, 0, color=colors[0], **kwargs) + add_arrow(plotter, T, 1, color=colors[1], **kwargs) + add_arrow(plotter, T, 2, color=colors[2], **kwargs) + +def add_frame2(plotter, T, color=None, **kwargs): + + if color is None: + colors = ['red', 'green'] + else: + colors = (color,) * 2 + + T = T.A + add_arrow(plotter, T, 0, color=colors[0], **kwargs) + add_arrow(plotter, T, 1, color=colors[1], **kwargs) + + + +def ribbon( + length=0.75, + head=0.15, + r=0.2, + w=[0.2, 0.3], + N=100, + reverse=False, + phase=0, + T=None, + ): + + length *= 2 * pi + head *= 2 * pi + phase *= 2 * pi + delta = length / N + Nh = int(head / delta) + Nt = int(length / delta) - Nh + + if reverse: + delta = -delta + + w1 = w[0] / 2 + w2 = w[1] / 2 + + vertices = np.array([r * cos(phase), r * sin(phase), 0]) + faces = np.array([ + [3, 0, 1, 2], + [3, 0, 2, 3] + ]) + # arrow + # 3 * Nh + 1 vertices + # 4 (Nh - 1) + 2 faces + for i in range(1, Nh): + w = w2 * (i + 1) / Nh + theta = phase +(i + 1) * delta + + x = r * cos(theta) + y = r * sin (theta) + vertices = np.vstack([ + vertices, + [x, y, w], + [x, y, 0], + [x, y, -w]]) + + k = 3 * (i - 1) + + if i < (Nh - 1): + faces = np.vstack([ + faces, + [3, 1 + k, 4 + k, 5 + k], + [3, 1 + k, 5 + k, 2 + k], + [3, 3 + k, 2 + k, 5 + k], + [3, 3 + k, 5 + k, 6 + k]]) + + theta_h = theta + nv_h = vertices.shape[0] + + # tail + + for i in range(0, Nt): + theta = theta_h + i * delta + + x = r * cos(theta) + y = r * sin (theta) + vertices = np.vstack([ + vertices, + [x, y, w1], + [x, y, 0], + [x, y, -w1]]) + + k = nv_h + 3 * i - 1 + + if i < (Nt - 1): + faces = np.vstack([ + faces, + [3, 1 + k, 4 + k, 2 + k], + [3, 2 + k, 4 + k, 5 + k], + [3, 2 + k, 5 + k, 6 + k], + [3, 2 + k, 6 + k, 3 + k]]) + + + shape = pv.PolyData(vertices, faces) + if T is not None: + shape.transform(T.A, inplace=True) + return shape + +def axis(plotter, T=None, text=None, direction=(0,0,1), square=False, twist=0.5): + + if square: + arrow = pv.Arrow(direction=direction, tip_resolution=4, \ + shaft_resolution=4, shaft_radius=0.03, + tip_radius=0.08, tip_length=0.2) + else: + arrow = pv.Arrow(direction=(0,0,1), tip_resolution=r, \ + shaft_resolution=r, shaft_radius=0.03, + tip_radius=0.08, tip_length=0.2) + + if T is None: + T = SE3() + T = SE3(T.t) # remove rotation + Ta = T * SE3(0, 0, zl) + Tr = T * SE3(0, 0, 0.2) + arrow.transform(Ta.A, inplace=True) + rib = ribbon(reverse=True, r=0.1, w=[0.12, 0.2], phase=0.08) + rib.transform(Tr.A, inplace=True) + plotter.add_mesh(arrow) + plotter.add_mesh(rib, color='#808080') + + if text is not None: + t = string(text, scale=0.5) + t.transform((T * SE3(0, 0, zl) * SE3(np.r_[direction] * 1.1) * SE3.Rz(twist) * SE3.Rx(pi/2) ).A, inplace=True) + plotter.add_mesh(t) + +def add_ellipsoid(plotter, A, inverted=False, **kwargs): + + if not inverted: + A = np.linalg.inv(A) + + # compute eigenvalues and vectors + e, x = np.linalg.eig(A) + + # the radii are square root of the eigenvalues + radii = np.sqrt(e) + ellipsoid = pv.ParametricEllipsoid(xradius=radii[0], yradius=radii[1], zradius=radii[2]) + + # now orient it + R = np.real(x) + T = base.r2t(R) + ellipsoid.transform(T, inplace=True) + + plotter.add_mesh(ellipsoid, **kwargs) + + return ellipsoid + +def ellipsoid_3d(plotter, A, **kwargs): + + plotter.disable_parallel_projection() + + colors = vtk.vtkNamedColors() + + plotter.renderer.RemoveAllLights() + + # top light, XY plane + light1 = vtk.vtkLight() + light1.SetFocalPoint(0, 0, 0) + light1.SetPosition(0, 0, 2) + light1.SetColor(colors.GetColor3d('white')) + light1.SetIntensity(1) + plotter.renderer.AddLight(light1) + + # X direction light, YZ plane + light2 = vtk.vtkLight() + light2.SetFocalPoint(0, 0, 0) + light2.SetPosition(2, 0, 0) + light2.SetColor(colors.GetColor3d('white')) + light2.SetIntensity(1) + plotter.renderer.AddLight(light2) + + # Y direction light, XZ plane + light3 = vtk.vtkLight() + light3.SetFocalPoint(0, 0, 0) + light3.SetPosition(0, 2, 0) + light3.SetColor(colors.GetColor3d('white')) + light3.SetIntensity(1) + plotter.renderer.AddLight(light3) + + # Add 3 backing planes + D = 5 + z0 = -D / 2 + T = 0.02 + plane_mesh1 = pv.Cube([0, 0, z0], D, D, T) + plane_mesh2 = pv.Cube([-D/2, 0, D/2 + z0], T, D, D) + plane_mesh3 = pv.Cube([0, -D/2, D/2 + z0], D, T, D) + # add the meshes to scene + plotter.add_mesh(plane_mesh1 + plane_mesh2 + plane_mesh3, + show_edges=True, ambient=0.3, diffuse=0.8, specular=0.8, specular_power=30, smooth_shading=True) + + # do the ellipsoid + e = add_ellipsoid(plotter, A, ambient=0.5, diffuse=0.5, specular=0.8, specular_power=30, + smooth_shading=True, color='dodgerblue', **kwargs) + + # do the shadows + plotter.renderer.SetUseShadows(True) + +if __name__ == "__main__": + + from spatialmath import SE3 + + plotter = pv.Plotter(polygon_smoothing=True, window_size=(2000,2000)) + + # add_frame(plotter, SE3()) + + # data from RVC2 fig 8.4b + # A = np.array([ + # [ 0.0076, 0.0000, -0.0868], + # [ 0.0000, 3.0000, -0.0000], + # [-0.0868, -0.0000, 2.9924] + # ]) + # e = add_ellipsoid(plotter, A, inverted=True, color='skyblue', style='wireframe') + # e.plot(show_grid=True) + + add_frame(plotter, SE3()) + + plotter.set_background('white') + plotter.enable_parallel_projection() + # plotter.show_axes() + # plotter.show_bounds(grid='front') + plotter.show() + +def outfile(format='png'): + # build the path for saving, next to the calling script (whatever + # figures/... directory it happens to live in), not a hardcoded + # personal path + script_dir = Path(sys.argv[0]).resolve().parent + figure = os.path.basename(sys.argv[0]) + figure = os.path.splitext(figure)[0] + '.' + format + return str(script_dir / figure) + + + +def zoomplot(plotter, value): + if not plotter.camera_set: + plotter.camera_position = plotter.get_default_cam_pos() + plotter.reset_camera() + plotter.camera_set = True + plotter.camera.Zoom(value) + plotter.render() + +margin = 5 + +def cropimage(im): + + bg = [] + + def limits(a): + s = a.index(False) - margin + if s < 0: + s = 0 + + a.reverse() + e = a.index(False) - margin + if e < 0: + e = 0 + e = len(a) - e + + return s, e + + for c in range(im.shape[1]): + column = im[:,c,:3] + t = np.all(column == (255, 255, 255)) + bg.append(t) + + c1, c2 = limits(bg) + + bg = [] + for r in range(im.shape[0]): + row = im[r,:,:3] + t = np.all(row == (255, 255, 255)) + bg.append(t) + + r1, r2 = limits(bg) + + print(f"image: rows {r1}-{r2}, columns {c1}-{c2}") + + return im[r1:r2, c1:c2, :] + + +def save(plotter, show=True, zoom=None, crop=True): + # output file name + out = outfile() + + # configure PyVista + plotter.set_background('white') + + plotter.off_screen = not show + + if zoom is not None: + zoomplot(plotter, zoom) + + if show: + # plotter.save_graphic('ZZZ.png', raster=False) + plotter.show() + else: + im = plotter.screenshot() + print(im.shape) + if crop: + im = cropimage(im) + + image = Image.fromarray(im) + image.save(out) + + # plotter.save_graphic('fig7_2b.png', raster=False) + + print('saving plotter --> ', out) diff --git a/figures/3d/fig2_14.py b/figures/3d/fig2_14.py index 84de552..b28e3de 100755 --- a/figures/3d/fig2_14.py +++ b/figures/3d/fig2_14.py @@ -3,8 +3,8 @@ import numpy as np from spatialmath import SE3 from math import cos, sin, pi -import pvplus -import rvcprint +from RVC3.tools import pvplus +# import rvcprint -- book-production helper, not needed here plotter = pv.Plotter(polygon_smoothing=True, window_size=(2000,2000), off_screen=True) diff --git a/figures/3d/fig2_16.py b/figures/3d/fig2_16.py index 8419061..72d5019 100755 --- a/figures/3d/fig2_16.py +++ b/figures/3d/fig2_16.py @@ -3,8 +3,8 @@ import numpy as np from spatialmath import SE3 from math import pi -import pvplus -import rvcprint +from RVC3.tools import pvplus +# import rvcprint -- book-production helper, not needed here plotter = pv.Plotter(border=False, polygon_smoothing=True, window_size=(2000,1000)) plotter.set_background('white') diff --git a/figures/3d/fig2_4.py b/figures/3d/fig2_4.py index ae1c7df..b0ec7f0 100755 --- a/figures/3d/fig2_4.py +++ b/figures/3d/fig2_4.py @@ -4,7 +4,7 @@ from spatialmath import SE3 from math import cos, sin, pi -import pvplus +from RVC3.tools import pvplus plotter = pv.Plotter(polygon_smoothing=True, window_size=(2000,2000)) diff --git a/figures/3d/fig3_1a.py b/figures/3d/fig3_1a.py index 4ff19d0..430d249 100755 --- a/figures/3d/fig3_1a.py +++ b/figures/3d/fig3_1a.py @@ -5,7 +5,7 @@ from spatialmath import SE3 from math import cos, sin, pi -import pvplus +from RVC3.tools import pvplus plotter = pv.Plotter(polygon_smoothing=True, window_size=(2000,2000)) plotter.enable_parallel_projection() diff --git a/figures/3d/fig3_1b.py b/figures/3d/fig3_1b.py index bc80359..1570f81 100755 --- a/figures/3d/fig3_1b.py +++ b/figures/3d/fig3_1b.py @@ -4,7 +4,7 @@ import numpy as np from spatialmath import SE3 from math import cos, sin, pi -import pvplus +from RVC3.tools import pvplus plotter = pv.Plotter(polygon_smoothing=True, window_size=(2000,2000)) diff --git a/figures/3d/fig7_13a.py b/figures/3d/fig7_13a.py index fd34c4f..99f49a8 100755 --- a/figures/3d/fig7_13a.py +++ b/figures/3d/fig7_13a.py @@ -3,7 +3,7 @@ from spatialmath import SE3 from math import cos, sin, pi -import pvplus +from RVC3.tools import pvplus def zoom(plotter, value): if not plotter.camera_set: diff --git a/figures/3d/fig7_4a.py b/figures/3d/fig7_4a.py index 408a601..c7e87de 100755 --- a/figures/3d/fig7_4a.py +++ b/figures/3d/fig7_4a.py @@ -6,7 +6,7 @@ from math import cos, sin, pi from roboticstoolbox import ETS2 as ET -import pvplus +from RVC3.tools import pvplus plotter = pv.Plotter(polygon_smoothing=True, window_size=(2000,2000)) overlap = 0.2 diff --git a/figures/3d/fig7_4b.py b/figures/3d/fig7_4b.py index 6b5d116..7a7db8b 100755 --- a/figures/3d/fig7_4b.py +++ b/figures/3d/fig7_4b.py @@ -6,7 +6,7 @@ from math import cos, sin, pi -import pvplus +from RVC3.tools import pvplus plotter = pv.Plotter(polygon_smoothing=True, window_size=(2000,2000)) overlap = 0.2 diff --git a/figures/3d/fig7_4c.py b/figures/3d/fig7_4c.py index 94ff020..e660c53 100755 --- a/figures/3d/fig7_4c.py +++ b/figures/3d/fig7_4c.py @@ -5,7 +5,7 @@ from spatialmath import SE3 from math import cos, sin, pi -import pvplus +from RVC3.tools import pvplus diff --git a/figures/3d/fig7_7.py b/figures/3d/fig7_7.py index 3bb901b..600dfcc 100755 --- a/figures/3d/fig7_7.py +++ b/figures/3d/fig7_7.py @@ -5,7 +5,7 @@ from spatialmath import SE3 from math import cos, sin, pi -import pvplus +from RVC3.tools import pvplus def zoom(plotter, value): if not plotter.camera_set: diff --git a/figures/3d/fig8_8a.py b/figures/3d/fig8_8a.py index f95ee83..2710547 100755 --- a/figures/3d/fig8_8a.py +++ b/figures/3d/fig8_8a.py @@ -5,9 +5,9 @@ # from spatialmath import SE3 from spatialmath import base from math import cos, sin, pi -import pvplus +from RVC3.tools import pvplus import roboticstoolbox as rtb -from rvcprint import outfile +# from rvcprint import outfile -- book-production helper, not needed here plotter = pv.Plotter(polygon_smoothing=True, window_size=(2000,2000)) print(pv.__version__) diff --git a/figures/3d/fig8_8b.py b/figures/3d/fig8_8b.py index c353df4..cc7b531 100755 --- a/figures/3d/fig8_8b.py +++ b/figures/3d/fig8_8b.py @@ -6,9 +6,9 @@ # from spatialmath import SE3 from spatialmath import base from math import cos, sin, pi -import pvplus +from RVC3.tools import pvplus import roboticstoolbox as rtb -from rvcprint import outfile +# from rvcprint import outfile -- book-production helper, not needed here plotter = pv.Plotter(polygon_smoothing=True, window_size=(2000,2000)) diff --git a/figures/3d/fig9_17.py b/figures/3d/fig9_17.py index 5847804..0826998 100755 --- a/figures/3d/fig9_17.py +++ b/figures/3d/fig9_17.py @@ -5,9 +5,9 @@ # from spatialmath import SE3 from spatialmath import base from math import cos, sin, pi -import pvplus +from RVC3.tools import pvplus import roboticstoolbox as rtb -from rvcprint import outfile +# from rvcprint import outfile -- book-production helper, not needed here plotter = pv.Plotter(polygon_smoothing=True, window_size=(2000,2000)) print(pv.__version__) diff --git a/figures/code/chapter2/fig2_14.py b/figures/code/chapter2/fig2_14.py index da9f2ab..d2a5698 100755 --- a/figures/code/chapter2/fig2_14.py +++ b/figures/code/chapter2/fig2_14.py @@ -3,8 +3,8 @@ import numpy as np from spatialmath import SE3 from math import cos, sin, pi -import pvplus -from RVC3.tools import rvcprint +from RVC3.tools import pvplus +# from RVC3.tools import rvcprint -- book-production helper, not needed here plotter = pv.Plotter(polygon_smoothing=True, window_size=(2000,2000), off_screen=True) @@ -18,6 +18,6 @@ plotter.set_background('white') # plotter.enable_eye_dome_lighting() # messes up subplots -filename = rvcprint.outfile(format='png', include=True) +# filename = rvcprint.outfile(format='png', include=True) -- book-production helper, not needed here -plotter.show(screenshot=filename) +plotter.show() diff --git a/figures/code/chapter2/fig2_15.py b/figures/code/chapter2/fig2_15.py index 87dace2..5d330b1 100755 --- a/figures/code/chapter2/fig2_15.py +++ b/figures/code/chapter2/fig2_15.py @@ -2,8 +2,8 @@ import pyvista as pv import numpy as np from spatialmath import SE3 -import pvplus -from RVC3.tools import rvcprint +from RVC3.tools import pvplus +# from RVC3.tools import rvcprint -- book-production helper, not needed here plotter = pv.Plotter(shape=(2,3), border=False, polygon_smoothing=True, window_size=(2000,1000)) plotter.enable_parallel_projection() @@ -25,4 +25,4 @@ def add_plot(i, j, T): plotter.set_background('white') # plotter.enable_eye_dome_lighting() # messes up subplots -plotter.show(screenshot=rvcprint.outfile(format='png')) \ No newline at end of file +plotter.show() \ No newline at end of file diff --git a/figures/code/chapter2/fig2_16.py b/figures/code/chapter2/fig2_16.py index b122de7..c696e52 100755 --- a/figures/code/chapter2/fig2_16.py +++ b/figures/code/chapter2/fig2_16.py @@ -3,8 +3,8 @@ import numpy as np from spatialmath import SE3 from math import pi -import pvplus -from RVC3.tools import rvcprint +from RVC3.tools import pvplus +# from RVC3.tools import rvcprint -- book-production helper, not needed here plotter = pv.Plotter(shape=(2,3), border=False, polygon_smoothing=True, window_size=(2000,1000)) plotter.set_background('white') @@ -60,4 +60,4 @@ def add_plot(i, j, T): # plotter.enable_eye_dome_lighting() # messes up subplots -plotter.show(screenshot=rvcprint.outfile(format='png')) \ No newline at end of file +plotter.show() \ No newline at end of file diff --git a/figures/code/chapter2/fig2_4.py b/figures/code/chapter2/fig2_4.py index 1d23572..97f58c8 100755 --- a/figures/code/chapter2/fig2_4.py +++ b/figures/code/chapter2/fig2_4.py @@ -4,7 +4,7 @@ from spatialmath import SE3 from math import cos, sin, pi -import pvplus +from RVC3.tools import pvplus plotter = pv.Plotter(polygon_smoothing=True, window_size=(2000,2000)) diff --git a/figures/code/chapter2/fig2_6/aruco_markers.py b/figures/code/chapter2/fig2_6/aruco_markers.py index 74b8287..f3ff407 100644 --- a/figures/code/chapter2/fig2_6/aruco_markers.py +++ b/figures/code/chapter2/fig2_6/aruco_markers.py @@ -4,7 +4,7 @@ import cv2.aruco as aruco from spatialmath import SE3, SO3 import pyvista as pv -import pvplus +from RVC3.tools import pvplus def main(): diff --git a/figures/code/chapter2/fig2_6/aruco_markers_pyvista.py b/figures/code/chapter2/fig2_6/aruco_markers_pyvista.py index eb79e2e..cb15e47 100644 --- a/figures/code/chapter2/fig2_6/aruco_markers_pyvista.py +++ b/figures/code/chapter2/fig2_6/aruco_markers_pyvista.py @@ -4,7 +4,7 @@ import cv2.aruco as aruco from spatialmath import SE3 import pyvista as pv -import pvplus +from RVC3.tools import pvplus def main(): diff --git a/figures/code/chapter2/fig2_6/fig2_6.py b/figures/code/chapter2/fig2_6/fig2_6.py index 8c250a0..ee154d7 100755 --- a/figures/code/chapter2/fig2_6/fig2_6.py +++ b/figures/code/chapter2/fig2_6/fig2_6.py @@ -4,7 +4,7 @@ import matplotlib.pyplot as plt import pyvista as pv import vtk -import pvplus +from RVC3.tools import pvplus scene = Image('IMG_8787.JPG') diff --git a/figures/code/chapter2/screw.py b/figures/code/chapter2/screw.py index 9dc7e8b..1dc67fb 100755 --- a/figures/code/chapter2/screw.py +++ b/figures/code/chapter2/screw.py @@ -2,7 +2,7 @@ import pyvista as pv import numpy as np from spatialmath import SE3 -import pvplus +from RVC3.tools import pvplus plotter = pv.Plotter(shape=(2,3), border=False, polygon_smoothing=True, window_size=(2000,1000)) plotter.enable_parallel_projection() diff --git a/figures/code/chapter2/vtkbug2.py b/figures/code/chapter2/vtkbug2.py index 707f351..3068a91 100755 --- a/figures/code/chapter2/vtkbug2.py +++ b/figures/code/chapter2/vtkbug2.py @@ -2,7 +2,7 @@ import matplotlib.pyplot as plt import pyvista as pv import vtk -import pvplus +from RVC3.tools import pvplus from spatialmath import SE3 import numpy as np diff --git a/figures/code/chapter3/fig3_1a.py b/figures/code/chapter3/fig3_1a.py index 00167de..ecafe10 100755 --- a/figures/code/chapter3/fig3_1a.py +++ b/figures/code/chapter3/fig3_1a.py @@ -5,7 +5,7 @@ from spatialmath import SE3 from math import cos, sin, pi -import pvplus +from RVC3.tools import pvplus plotter = pv.Plotter(polygon_smoothing=True, window_size=(2000,2000)) plotter.enable_parallel_projection() diff --git a/figures/code/chapter3/fig3_1b.py b/figures/code/chapter3/fig3_1b.py index fb29faa..51b4467 100755 --- a/figures/code/chapter3/fig3_1b.py +++ b/figures/code/chapter3/fig3_1b.py @@ -4,7 +4,7 @@ import numpy as np from spatialmath import SE3 from math import cos, sin, pi -import pvplus +from RVC3.tools import pvplus plotter = pv.Plotter(polygon_smoothing=True, window_size=(2000,2000)) diff --git a/figures/code/chapter3/fig3_1c.py b/figures/code/chapter3/fig3_1c.py index 1aa0a93..8c3c6b8 100755 --- a/figures/code/chapter3/fig3_1c.py +++ b/figures/code/chapter3/fig3_1c.py @@ -4,7 +4,7 @@ import numpy as np from spatialmath import SE3 from math import cos, sin, pi -import pvplus +from RVC3.tools import pvplus plotter = pv.Plotter(polygon_smoothing=True, window_size=(2000,2000)) diff --git a/figures/code/chapter7/fig7_10b.py b/figures/code/chapter7/fig7_10b.py index 58ae108..3175387 100755 --- a/figures/code/chapter7/fig7_10b.py +++ b/figures/code/chapter7/fig7_10b.py @@ -2,7 +2,7 @@ from roboticstoolbox import * import pyvista as pv -import pvplus +from RVC3.tools import pvplus plotter = pv.Plotter(polygon_smoothing=True, window_size=(2000,2000)) diff --git a/figures/code/chapter7/fig7_13a.py b/figures/code/chapter7/fig7_13a.py index cef7d57..5fe0916 100755 --- a/figures/code/chapter7/fig7_13a.py +++ b/figures/code/chapter7/fig7_13a.py @@ -3,7 +3,7 @@ from spatialmath import SE3 from math import cos, sin, pi -import pvplus +from RVC3.tools import pvplus def zoom(plotter, value): if not plotter.camera_set: diff --git a/figures/code/chapter7/fig7_4a.py b/figures/code/chapter7/fig7_4a.py index 91cf86c..bd67050 100755 --- a/figures/code/chapter7/fig7_4a.py +++ b/figures/code/chapter7/fig7_4a.py @@ -6,7 +6,7 @@ from math import cos, sin, pi from roboticstoolbox import ETS2 as ET -import pvplus +from RVC3.tools import pvplus plotter = pv.Plotter(polygon_smoothing=True, window_size=(2000,2000)) overlap = 0.2 diff --git a/figures/code/chapter7/fig7_4b.py b/figures/code/chapter7/fig7_4b.py index 130a585..1eb5c19 100755 --- a/figures/code/chapter7/fig7_4b.py +++ b/figures/code/chapter7/fig7_4b.py @@ -6,7 +6,7 @@ from math import cos, sin, pi -import pvplus +from RVC3.tools import pvplus plotter = pv.Plotter(polygon_smoothing=True, window_size=(2000,2000)) overlap = 0.2 diff --git a/figures/code/chapter7/fig7_4c.py b/figures/code/chapter7/fig7_4c.py index 91eac4d..3b28c8c 100755 --- a/figures/code/chapter7/fig7_4c.py +++ b/figures/code/chapter7/fig7_4c.py @@ -5,7 +5,7 @@ from spatialmath import SE3 from math import cos, sin, pi -import pvplus +from RVC3.tools import pvplus diff --git a/figures/code/chapter7/fig7_6.py b/figures/code/chapter7/fig7_6.py index 1ed4acd..7297d41 100755 --- a/figures/code/chapter7/fig7_6.py +++ b/figures/code/chapter7/fig7_6.py @@ -5,7 +5,7 @@ from spatialmath import SE3 from math import cos, sin, pi -import pvplus +from RVC3.tools import pvplus def zoom(plotter, value): if not plotter.camera_set: diff --git a/figures/code/chapter7/fig7_7.py b/figures/code/chapter7/fig7_7.py index d001f93..32ebe1a 100755 --- a/figures/code/chapter7/fig7_7.py +++ b/figures/code/chapter7/fig7_7.py @@ -5,7 +5,7 @@ from spatialmath import SE3 from math import cos, sin, pi -import pvplus +from RVC3.tools import pvplus def zoom(plotter, value): if not plotter.camera_set: diff --git a/figures/code/chapter7/pyvista/extrude.py b/figures/code/chapter7/pyvista/extrude.py index cef7d57..5fe0916 100644 --- a/figures/code/chapter7/pyvista/extrude.py +++ b/figures/code/chapter7/pyvista/extrude.py @@ -3,7 +3,7 @@ from spatialmath import SE3 from math import cos, sin, pi -import pvplus +from RVC3.tools import pvplus def zoom(plotter, value): if not plotter.camera_set: diff --git a/figures/code/chapter7/pyvista/fig7_2a.py b/figures/code/chapter7/pyvista/fig7_2a.py index 8bc6615..6a30b69 100755 --- a/figures/code/chapter7/pyvista/fig7_2a.py +++ b/figures/code/chapter7/pyvista/fig7_2a.py @@ -5,7 +5,7 @@ from roboticstoolbox import ETS2 as ET -import pvplus +from RVC3.tools import pvplus plotter = pv.Plotter(polygon_smoothing=True, window_size=(2000,2000)) diff --git a/figures/code/chapter7/pyvista/fig7_2b.py b/figures/code/chapter7/pyvista/fig7_2b.py index 1be3f73..4ce046b 100755 --- a/figures/code/chapter7/pyvista/fig7_2b.py +++ b/figures/code/chapter7/pyvista/fig7_2b.py @@ -4,7 +4,7 @@ from math import cos, sin, pi -import pvplus +from RVC3.tools import pvplus plotter = pv.Plotter(polygon_smoothing=True, window_size=(2000,2000)) overlap = 0.2 diff --git a/figures/code/chapter7/pyvista/fig7_2c.py b/figures/code/chapter7/pyvista/fig7_2c.py index 0fc1d0f..eb71cf9 100755 --- a/figures/code/chapter7/pyvista/fig7_2c.py +++ b/figures/code/chapter7/pyvista/fig7_2c.py @@ -3,7 +3,7 @@ from spatialmath import SE3 from math import cos, sin, pi -import pvplus +from RVC3.tools import pvplus diff --git a/figures/code/chapter7/pyvista/fig7_7b.py b/figures/code/chapter7/pyvista/fig7_7b.py index 9bc6a23..53f67e4 100755 --- a/figures/code/chapter7/pyvista/fig7_7b.py +++ b/figures/code/chapter7/pyvista/fig7_7b.py @@ -1,6 +1,6 @@ from roboticstoolbox import * import pyvista as pv -import pvplus +from RVC3.tools import pvplus plotter = pv.Plotter(polygon_smoothing=True, window_size=(2000,2000)) diff --git a/figures/code/chapter8/fig8_8a.py b/figures/code/chapter8/fig8_8a.py index 51d710e..d17db60 100755 --- a/figures/code/chapter8/fig8_8a.py +++ b/figures/code/chapter8/fig8_8a.py @@ -5,9 +5,9 @@ # from spatialmath import SE3 from spatialmath import base from math import cos, sin, pi -import pvplus +from RVC3.tools import pvplus import roboticstoolbox as rtb -from rvcprint import outfile +# from rvcprint import outfile -- book-production helper, not needed here plotter = pv.Plotter(polygon_smoothing=True, window_size=(2000,2000)) print(pv.__version__) diff --git a/figures/code/chapter8/fig8_8b.py b/figures/code/chapter8/fig8_8b.py index 0cd7aae..e5e4ef8 100755 --- a/figures/code/chapter8/fig8_8b.py +++ b/figures/code/chapter8/fig8_8b.py @@ -6,9 +6,9 @@ # from spatialmath import SE3 from spatialmath import base from math import cos, sin, pi -import pvplus +from RVC3.tools import pvplus import roboticstoolbox as rtb -from rvcprint import outfile +# from rvcprint import outfile -- book-production helper, not needed here plotter = pv.Plotter(polygon_smoothing=True, window_size=(2000,2000)) diff --git a/figures/code/chapter9/fig9_17.py b/figures/code/chapter9/fig9_17.py index 0755216..17e1a71 100755 --- a/figures/code/chapter9/fig9_17.py +++ b/figures/code/chapter9/fig9_17.py @@ -5,9 +5,9 @@ # from spatialmath import SE3 from spatialmath import base from math import cos, sin, pi -import pvplus +from RVC3.tools import pvplus import roboticstoolbox as rtb -from rvcprint import outfile +# from rvcprint import outfile -- book-production helper, not needed here plotter = pv.Plotter(polygon_smoothing=True, window_size=(2000,2000)) print(pv.__version__) @@ -43,4 +43,4 @@ # plotter.add_axes() # plotter.export_obj('exported.obj') -plotter.show(screenshot=outfile(format='png')) +plotter.show() diff --git a/pyproject.toml b/pyproject.toml index eab80eb..12c4649 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -97,6 +97,7 @@ dependencies = [ pytorch = ["torch", "torchvision"] dev = ["pre-commit", "nbstripout"] +figures = ["pyvista", "vtk", "pillow"] [project.scripts] From f252b44e1c14b75f55fdb91ab79579283450a85f Mon Sep 17 00:00:00 2001 From: Peter Corke Date: Mon, 17 Aug 2026 14:53:07 +1000 Subject: [PATCH 2/3] fix: scope rvcprint cleanup to figures/3d/ only, remove where unused Correction to the previous commit, which touched rvcprint in some figures/code/chapterN/ scripts too. figures/code/README.md documents that rvcprint is the required, intentional mechanism those scripts use to save the book's PDF figures -- it was never broken there and shouldn't have been touched. Reverted those 6 files back to their original state (only the pvplus import fix remains). For the 6 figures/3d/*.py scripts that reference rvcprint (the tree issue #20 is actually about): none of them use it for their real output mechanism (they all call plotter.export_gltf(...) directly, confirmed by figures/3d/README.md), so removed the now-fully-unused import from each rather than leaving a disabled trace of it. --- figures/3d/fig2_14.py | 1 - figures/3d/fig2_16.py | 1 - figures/3d/fig2_24.py | 1 - figures/3d/fig8_8a.py | 1 - figures/3d/fig8_8b.py | 1 - figures/3d/fig9_17.py | 1 - figures/code/chapter2/fig2_14.py | 6 +++--- figures/code/chapter2/fig2_15.py | 4 ++-- figures/code/chapter2/fig2_16.py | 4 ++-- figures/code/chapter8/fig8_8a.py | 2 +- figures/code/chapter8/fig8_8b.py | 2 +- figures/code/chapter9/fig9_17.py | 4 ++-- 12 files changed, 11 insertions(+), 17 deletions(-) diff --git a/figures/3d/fig2_14.py b/figures/3d/fig2_14.py index b28e3de..4769bdf 100755 --- a/figures/3d/fig2_14.py +++ b/figures/3d/fig2_14.py @@ -4,7 +4,6 @@ from spatialmath import SE3 from math import cos, sin, pi from RVC3.tools import pvplus -# import rvcprint -- book-production helper, not needed here plotter = pv.Plotter(polygon_smoothing=True, window_size=(2000,2000), off_screen=True) diff --git a/figures/3d/fig2_16.py b/figures/3d/fig2_16.py index 72d5019..ecc0cd0 100755 --- a/figures/3d/fig2_16.py +++ b/figures/3d/fig2_16.py @@ -4,7 +4,6 @@ from spatialmath import SE3 from math import pi from RVC3.tools import pvplus -# import rvcprint -- book-production helper, not needed here plotter = pv.Plotter(border=False, polygon_smoothing=True, window_size=(2000,1000)) plotter.set_background('white') diff --git a/figures/3d/fig2_24.py b/figures/3d/fig2_24.py index ed5165b..c4d0455 100755 --- a/figures/3d/fig2_24.py +++ b/figures/3d/fig2_24.py @@ -4,7 +4,6 @@ from spatialmath import SE3, Twist3 from spatialmath import base from math import cos, sin, pi -import rvcprint from PIL import Image def add_arrow(i, T): diff --git a/figures/3d/fig8_8a.py b/figures/3d/fig8_8a.py index 2710547..6a43e54 100755 --- a/figures/3d/fig8_8a.py +++ b/figures/3d/fig8_8a.py @@ -7,7 +7,6 @@ from math import cos, sin, pi from RVC3.tools import pvplus import roboticstoolbox as rtb -# from rvcprint import outfile -- book-production helper, not needed here plotter = pv.Plotter(polygon_smoothing=True, window_size=(2000,2000)) print(pv.__version__) diff --git a/figures/3d/fig8_8b.py b/figures/3d/fig8_8b.py index cc7b531..d6fa93d 100755 --- a/figures/3d/fig8_8b.py +++ b/figures/3d/fig8_8b.py @@ -8,7 +8,6 @@ from math import cos, sin, pi from RVC3.tools import pvplus import roboticstoolbox as rtb -# from rvcprint import outfile -- book-production helper, not needed here plotter = pv.Plotter(polygon_smoothing=True, window_size=(2000,2000)) diff --git a/figures/3d/fig9_17.py b/figures/3d/fig9_17.py index 0826998..eaf5556 100755 --- a/figures/3d/fig9_17.py +++ b/figures/3d/fig9_17.py @@ -7,7 +7,6 @@ from math import cos, sin, pi from RVC3.tools import pvplus import roboticstoolbox as rtb -# from rvcprint import outfile -- book-production helper, not needed here plotter = pv.Plotter(polygon_smoothing=True, window_size=(2000,2000)) print(pv.__version__) diff --git a/figures/code/chapter2/fig2_14.py b/figures/code/chapter2/fig2_14.py index d2a5698..d651ade 100755 --- a/figures/code/chapter2/fig2_14.py +++ b/figures/code/chapter2/fig2_14.py @@ -4,7 +4,7 @@ from spatialmath import SE3 from math import cos, sin, pi from RVC3.tools import pvplus -# from RVC3.tools import rvcprint -- book-production helper, not needed here +from RVC3.tools import rvcprint plotter = pv.Plotter(polygon_smoothing=True, window_size=(2000,2000), off_screen=True) @@ -18,6 +18,6 @@ plotter.set_background('white') # plotter.enable_eye_dome_lighting() # messes up subplots -# filename = rvcprint.outfile(format='png', include=True) -- book-production helper, not needed here +filename = rvcprint.outfile(format='png', include=True) -plotter.show() +plotter.show(screenshot=filename) diff --git a/figures/code/chapter2/fig2_15.py b/figures/code/chapter2/fig2_15.py index 5d330b1..96575ba 100755 --- a/figures/code/chapter2/fig2_15.py +++ b/figures/code/chapter2/fig2_15.py @@ -3,7 +3,7 @@ import numpy as np from spatialmath import SE3 from RVC3.tools import pvplus -# from RVC3.tools import rvcprint -- book-production helper, not needed here +from RVC3.tools import rvcprint plotter = pv.Plotter(shape=(2,3), border=False, polygon_smoothing=True, window_size=(2000,1000)) plotter.enable_parallel_projection() @@ -25,4 +25,4 @@ def add_plot(i, j, T): plotter.set_background('white') # plotter.enable_eye_dome_lighting() # messes up subplots -plotter.show() \ No newline at end of file +plotter.show(screenshot=rvcprint.outfile(format='png')) \ No newline at end of file diff --git a/figures/code/chapter2/fig2_16.py b/figures/code/chapter2/fig2_16.py index c696e52..ea315be 100755 --- a/figures/code/chapter2/fig2_16.py +++ b/figures/code/chapter2/fig2_16.py @@ -4,7 +4,7 @@ from spatialmath import SE3 from math import pi from RVC3.tools import pvplus -# from RVC3.tools import rvcprint -- book-production helper, not needed here +from RVC3.tools import rvcprint plotter = pv.Plotter(shape=(2,3), border=False, polygon_smoothing=True, window_size=(2000,1000)) plotter.set_background('white') @@ -60,4 +60,4 @@ def add_plot(i, j, T): # plotter.enable_eye_dome_lighting() # messes up subplots -plotter.show() \ No newline at end of file +plotter.show(screenshot=rvcprint.outfile(format='png')) \ No newline at end of file diff --git a/figures/code/chapter8/fig8_8a.py b/figures/code/chapter8/fig8_8a.py index d17db60..482711c 100755 --- a/figures/code/chapter8/fig8_8a.py +++ b/figures/code/chapter8/fig8_8a.py @@ -7,7 +7,7 @@ from math import cos, sin, pi from RVC3.tools import pvplus import roboticstoolbox as rtb -# from rvcprint import outfile -- book-production helper, not needed here +from rvcprint import outfile plotter = pv.Plotter(polygon_smoothing=True, window_size=(2000,2000)) print(pv.__version__) diff --git a/figures/code/chapter8/fig8_8b.py b/figures/code/chapter8/fig8_8b.py index e5e4ef8..cbb7f38 100755 --- a/figures/code/chapter8/fig8_8b.py +++ b/figures/code/chapter8/fig8_8b.py @@ -8,7 +8,7 @@ from math import cos, sin, pi from RVC3.tools import pvplus import roboticstoolbox as rtb -# from rvcprint import outfile -- book-production helper, not needed here +from rvcprint import outfile plotter = pv.Plotter(polygon_smoothing=True, window_size=(2000,2000)) diff --git a/figures/code/chapter9/fig9_17.py b/figures/code/chapter9/fig9_17.py index 17e1a71..a2d5a35 100755 --- a/figures/code/chapter9/fig9_17.py +++ b/figures/code/chapter9/fig9_17.py @@ -7,7 +7,7 @@ from math import cos, sin, pi from RVC3.tools import pvplus import roboticstoolbox as rtb -# from rvcprint import outfile -- book-production helper, not needed here +from rvcprint import outfile plotter = pv.Plotter(polygon_smoothing=True, window_size=(2000,2000)) print(pv.__version__) @@ -43,4 +43,4 @@ # plotter.add_axes() # plotter.export_obj('exported.obj') -plotter.show() +plotter.show(screenshot=outfile(format='png')) From c57f4591ee2be355940629764bb65f16b3d6613d Mon Sep 17 00:00:00 2001 From: Peter Corke Date: Mon, 17 Aug 2026 14:55:28 +1000 Subject: [PATCH 3/3] docs: fix package name typo in figures/code/README.md rvcpython -> rvc3python (doesn't exist on PyPI; rvc3python does, and matches pyproject.toml's declared name). --- figures/code/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/figures/code/README.md b/figures/code/README.md index 839f5f9..caeb3bc 100644 --- a/figures/code/README.md +++ b/figures/code/README.md @@ -35,7 +35,7 @@ saving --> fig10_20.pdf Some files generate multiple sub-figures with lowercase letter suffixes. All these files use `RVC3.tools.rvcprint` to save the created files. The -package `rvcpython` must have been installed. +package `rvc3python` must have been installed.