Skip to content

Commit f84b32d

Browse files
authored
Merge pull request #264 from NeuroML/feat/vispy-cap
Close cylindrical meshes for vispy viewer
2 parents aa5a104 + 813512d commit f84b32d

File tree

2 files changed

+97
-3
lines changed

2 files changed

+97
-3
lines changed

pyneuroml/plot/PlotMorphologyVispy.py

Lines changed: 89 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@
2929
)
3030
from scipy.spatial.transform import Rotation
3131
from vispy import app, scene, use
32-
from vispy.geometry.generation import create_cylinder, create_sphere
32+
from vispy.geometry.generation import create_sphere
33+
from vispy.geometry.meshdata import MeshData
3334
from vispy.scene.visuals import InstancedMesh
3435
from vispy.util.transforms import rotate
3536

@@ -825,8 +826,8 @@ def create_instanced_meshes(meshdata, plot_type, current_view, min_width):
825826
logger.debug(f"Created spherical mesh template with radius {r1}")
826827
else:
827828
rows = 2 + int(length / 2)
828-
seg_mesh = create_cylinder(
829-
rows=rows, cols=9, radius=[r1, r2], length=length
829+
seg_mesh = create_cylindrical_mesh(
830+
rows=rows, cols=9, radius=[r1, r2], length=length, closed=True
830831
)
831832
logger.debug(
832833
f"Created cylinderical mesh template with radii {r1}, {r2}, {length}"
@@ -1053,3 +1054,88 @@ def plot_3D_schematic(
10531054
if not nogui:
10541055
create_instanced_meshes(meshdata, "Detailed", current_view, width)
10551056
app.run()
1057+
1058+
1059+
def create_cylindrical_mesh(
1060+
rows: int,
1061+
cols: int,
1062+
radius: typing.Union[float, typing.List[float]] = [1.0, 1.0],
1063+
length: float = 1.0,
1064+
closed: bool = True,
1065+
):
1066+
"""Create a cylinderical mesh, adapted from vispy's generation method:
1067+
https://github.com/vispy/vispy/blob/main/vispy/geometry/generation.py#L451
1068+
1069+
:param rows: number of rows to use for mesh
1070+
:type rows: int
1071+
:param cols: number of columns
1072+
:type cols: int
1073+
:param radius: float or pair of floats for the two radii of the cylinder
1074+
:type radius: float or [float, float][]
1075+
:param length: length of cylinder
1076+
:type length: float
1077+
:param closed: whether the cylinder should be closed
1078+
:type closed: bool
1079+
:returns: Vertices and faces computed for a cylindrical surface.
1080+
:rtype: MeshData
1081+
1082+
"""
1083+
verts = numpy.empty((rows + 1, cols, 3), dtype=numpy.float32)
1084+
if isinstance(radius, int) or isinstance(radius, float):
1085+
radius = [radius, radius] # convert to list
1086+
1087+
# compute theta values
1088+
th = numpy.linspace(2 * numpy.pi, 0, cols).reshape(1, cols)
1089+
logger.debug(f"Thetas are: {th}")
1090+
1091+
# radius as a function of z
1092+
r = numpy.linspace(radius[0], radius[1], num=rows + 1, endpoint=True).reshape(
1093+
rows + 1, 1
1094+
)
1095+
1096+
verts[..., 0] = r * numpy.cos(th) # x = r cos(th)
1097+
verts[..., 1] = r * numpy.sin(th) # y = r sin(th)
1098+
verts[..., 2] = numpy.linspace(0, length, num=rows + 1, endpoint=True).reshape(
1099+
rows + 1, 1
1100+
) # z
1101+
# just reshape: no redundant vertices...
1102+
verts = verts.reshape((rows + 1) * cols, 3)
1103+
1104+
# add extra points for center of two circular planes that form the caps
1105+
if closed is True:
1106+
verts = numpy.append(verts, [[0.0, 0.0, 0.0], [0.0, 0.0, 1.0]], axis=0)
1107+
logger.debug(f"Verts are: {verts}")
1108+
1109+
# compute faces
1110+
faces = numpy.empty((rows * cols * 2, 3), dtype=numpy.uint32)
1111+
rowtemplate1 = (
1112+
(numpy.arange(cols).reshape(cols, 1) + numpy.array([[0, 1, 0]])) % cols
1113+
) + numpy.array([[0, 0, cols]])
1114+
logger.debug(f"Template1 is: {rowtemplate1}")
1115+
1116+
rowtemplate2 = (
1117+
(numpy.arange(cols).reshape(cols, 1) + numpy.array([[0, 1, 1]])) % cols
1118+
) + numpy.array([[cols, 0, cols]])
1119+
# logger.debug(f"Template2 is: {rowtemplate2}")
1120+
1121+
for row in range(rows):
1122+
start = row * cols * 2
1123+
faces[start : start + cols] = rowtemplate1 + row * cols
1124+
faces[start + cols : start + (cols * 2)] = rowtemplate2 + row * cols
1125+
1126+
# add extra faces to cover the caps
1127+
if closed is True:
1128+
cap1 = (numpy.arange(cols).reshape(cols, 1) + numpy.array([[0, 0, 1]])) % cols
1129+
cap1[..., 0] = len(verts) - 2
1130+
cap2 = (numpy.arange(cols).reshape(cols, 1) + numpy.array([[0, 0, 1]])) % cols
1131+
cap2[..., 0] = len(verts) - 1
1132+
1133+
logger.debug(f"cap1 is {cap1}")
1134+
logger.debug(f"cap2 is {cap2}")
1135+
1136+
faces = numpy.append(faces, cap1, axis=0)
1137+
faces = numpy.append(faces, cap2, axis=0)
1138+
1139+
logger.debug(f"Faces are: {faces}")
1140+
1141+
return MeshData(vertices=verts, faces=faces)

tests/plot/test_morphology_plot.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
plot_3D_schematic,
2828
plot_3D_cell_morphology,
2929
plot_interactive_3D,
30+
create_cylindrical_mesh,
3031
)
3132
from pyneuroml.pynml import read_neuroml2_file
3233
from .. import BaseTestCase
@@ -429,3 +430,10 @@ def test_plot_segment_groups_curtain_plots_with_data(self):
429430

430431
self.assertIsFile(filename)
431432
pl.Path(filename).unlink()
433+
434+
def test_cylindrical_mesh_generator(self):
435+
"""Test the create_cylindrical_mesh function"""
436+
mesh = create_cylindrical_mesh(5, 10, 1.0, 1, closed=False)
437+
mesh2 = create_cylindrical_mesh(5, 10, 1.0, 1, closed=True)
438+
439+
self.assertEqual(mesh.n_vertices + 2, mesh2.n_vertices)

0 commit comments

Comments
 (0)