2222_TRIS = np .array ([[0 , 1 , 2 ], [0 , 2 , 3 ]])
2323
2424
25+ def _drawn (renderer ):
26+ """Return the geometry of the mesh the renderer drew last.
27+
28+ sphere() and the other instanced_mesh callers hand back the instance cloud
29+ rather than the drawn geometry, matching _PyVistaRenderer, so read what
30+ actually reached the plotter instead of the return value.
31+ """
32+ return renderer .plotter .actors [- 1 ]["mesh" ]
33+
34+
2535def test_is_a_registered_backend (renderer_lite ):
2636 """``set_3d_backend("jupyterlite_notebook")`` must hand out this renderer."""
2737 assert renderer_lite .get_3d_backend () == "jupyterlite_notebook"
@@ -159,8 +169,8 @@ def test_draws_every_primitive(renderer_lite):
159169 assert_allclose (np .asarray (mesh .points ), _RR , atol = 1e-6 )
160170
161171 # scale 0.1 means radius 0.05, centered where it was asked for
162- _ , mesh = r .sphere (np .array ([[1.0 , 0 , 0 ]]), "green" , 0.1 )
163- points = np .asarray (mesh .points )
172+ r .sphere (np .array ([[1.0 , 0 , 0 ]]), "green" , 0.1 )
173+ points = np .asarray (_drawn ( r ) .points )
164174 assert_allclose (points .mean (axis = 0 ), [1 , 0 , 0 ], atol = 1e-6 )
165175 assert np .linalg .norm (points - [1 , 0 , 0 ], axis = 1 ).max () == pytest .approx (0.05 )
166176
@@ -264,11 +274,13 @@ def test_sphere_radius_matches_pyvista(renderer_lite):
264274 and no caller in ``_3d.py`` passes ``radius`` to say otherwise.
265275 """
266276 r = renderer_lite ._get_renderer (size = (200 , 200 ))
267- _ , mesh = r .sphere (np .zeros ((1 , 3 )), "red" , 0.01 )
268- assert np .linalg .norm (np .asarray (mesh .points ), axis = 1 ).max () == pytest .approx (0.005 )
277+ r .sphere (np .zeros ((1 , 3 )), "red" , 0.01 )
278+ drawn = np .asarray (_drawn (r ).points )
279+ assert np .linalg .norm (drawn , axis = 1 ).max () == pytest .approx (0.005 )
269280 # an explicit radius is used as-is, again matching _pyvista.py
270- _ , mesh = r .sphere (np .zeros ((1 , 3 )), "red" , 1.0 , radius = 0.02 )
271- assert np .linalg .norm (np .asarray (mesh .points ), axis = 1 ).max () == pytest .approx (0.02 )
281+ r .sphere (np .zeros ((1 , 3 )), "red" , 1.0 , radius = 0.02 )
282+ drawn = np .asarray (_drawn (r ).points )
283+ assert np .linalg .norm (drawn , axis = 1 ).max () == pytest .approx (0.02 )
272284
273285
274286def test_cylinder_center_is_turned_with_the_axis (renderer_lite ):
@@ -290,31 +302,56 @@ def test_cylinder_center_is_turned_with_the_axis(renderer_lite):
290302def test_instances_are_merged_per_color (renderer_lite ):
291303 """``instanced_mesh`` draws one actor per distinct color, not one per instance.
292304
293- One color hands back ``( actor, mesh)``, the way ``_PyVistaRenderer`` always
294- does; several hand back both lists, since vtk.js cannot color per instance
295- inside a single actor .
305+ vtk.js cannot color per instance inside a single actor, so one color gives
306+ one actor and several give the list of them. The second return value is the
307+ instance cloud either way, matching ``_PyVistaRenderer`` .
296308 """
297309 quats = np .tile ([1.0 , 0 , 0 , 0 ], (3 , 1 ))
298- positions = np .zeros (( 3 , 3 ) )
310+ positions = np .array ([[ 0.0 , 0 , 0 ], [ 1.0 , 0 , 0 ], [ 2.0 , 0 , 0 ]] )
299311
300- # one color: a single actor, and the pair _PyVistaRenderer also hands back
312+ # one color: a single actor
301313 r = renderer_lite ._get_renderer (size = (200 , 200 ))
302314 colors = np .tile ([1.0 , 0 , 0 ], (3 , 1 ))
303- actor , mesh = r .instanced_mesh (_RR , _TRIS , positions , quats , colors = colors )
315+ actor , cloud = r .instanced_mesh (_RR , _TRIS , positions , quats , colors = colors )
304316 assert len (r .plotter .actors ) == 1
305- assert not isinstance (actor , list ) and not isinstance (mesh , list )
317+ assert not isinstance (actor , list )
318+ assert_allclose (np .asarray (cloud .points ), positions , atol = 1e-6 )
306319
307- # two colors: one actor each, and both lists come back
320+ # two colors: one actor each, and the cloud is still a single object
308321 r = renderer_lite ._get_renderer (size = (200 , 200 ))
309322 colors = np .array ([[1.0 , 0 , 0 ], [0 , 1.0 , 0 ], [1.0 , 0 , 0 ]])
310- actors , meshes = r .instanced_mesh (_RR , _TRIS , positions , quats , colors = colors )
323+ actors , cloud = r .instanced_mesh (_RR , _TRIS , positions , quats , colors = colors )
311324 assert len (r .plotter .actors ) == 2
312- assert len (actors ) == len (meshes ) == 2
325+ assert len (actors ) == 2
326+ assert not isinstance (cloud , list )
327+ assert_allclose (np .asarray (cloud .points ), positions , atol = 1e-6 )
313328
314329 # sphere routes through instanced_mesh with a single color, so it has to
315330 # keep handing back the pair its own callers unpack
316- actor , mesh = r .sphere (np .zeros ((1 , 3 )), "red" , 0.01 )
317- assert not isinstance (actor , list ) and not isinstance (mesh , list )
331+ actor , cloud = r .sphere (np .zeros ((1 , 3 )), "red" , 0.01 )
332+ assert not isinstance (actor , list ) and not isinstance (cloud , list )
333+
334+
335+ def test_instance_cloud_takes_channel_names (renderer_lite ):
336+ """mne/viz/_3d.py writes channel names onto the cloud (gh-13074).
337+
338+ _PyVistaRenderer hands back a PolyData whose ``field_data`` takes them; a
339+ pyvista-js PolyData has no such attribute, so the renderer supplies one.
340+ An empty ``positions`` must still give a cloud, since the caller assigns
341+ without checking.
342+ """
343+ r = renderer_lite ._get_renderer (size = (200 , 200 ))
344+ positions = np .array ([[0.0 , 0 , 0 ], [1.0 , 0 , 0 ]])
345+ _ , cloud = r .instanced_mesh (_RR , _TRIS , positions , colors = (1.0 , 0 , 0 ))
346+ # one cloud point per instance, in order: _3d.py indexes the names against
347+ # them, so a cloud that did not carry the positions would mislabel sensors
348+ assert_allclose (np .asarray (cloud .points ), positions , atol = 1e-6 )
349+ cloud .field_data ["ch_names" ] = np .array (["MEG 0113" , "MEG 0112" ], dtype = "U" )
350+ assert list (cloud .field_data ["ch_names" ]) == ["MEG 0113" , "MEG 0112" ]
351+
352+ actor , cloud = r .instanced_mesh (_RR , _TRIS , np .zeros ((0 , 3 )))
353+ assert actor is None
354+ cloud .field_data ["ch_names" ] = np .array ([], dtype = "U" ) # must not raise
318355
319356
320357def test_draws_into_an_existing_figure (renderer_lite ):
0 commit comments