Skip to content

Commit 99abf86

Browse files
committed
Backport PR #13334: MAINT: Dont set private attributes for PyVista Actor
1 parent aacf0cf commit 99abf86

7 files changed

Lines changed: 73 additions & 104 deletions

File tree

.circleci/config.yml

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -93,11 +93,6 @@ jobs:
9393
name: Set BASH_ENV
9494
command: ./tools/circleci_bash_env.sh
9595

96-
- run:
97-
name: check neuromag2ft
98-
command: |
99-
neuromag2ft --version
100-
10196
- run:
10297
name: Install fonts needed for diagrams
10398
command: |

mne/gui/_coreg.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,7 @@ def _get_default(var, val):
292292
self._renderer.set_interaction(interaction)
293293

294294
# coregistration model setup
295+
self._picking_targets = list()
295296
self._immediate_redraw = self._renderer._kind != "qt"
296297
self._info = info
297298
self._fiducials = fiducials
@@ -899,10 +900,10 @@ def _on_button_release(self, vtk_picker, event):
899900
if self._mouse_no_mvt > 0:
900901
x, y = vtk_picker.GetEventPosition()
901902
# XXX: internal plotter/renderer should not be exposed
902-
plotter = self._renderer.figure.plotter
903+
picker = self._renderer._picker
903904
picked_renderer = self._renderer.figure.plotter.renderer
904905
# trigger the pick
905-
plotter.picker.Pick(x, y, 0, picked_renderer)
906+
picker.Pick(x, y, 0, picked_renderer)
906907
self._mouse_no_mvt = 0
907908

908909
def _on_pick(self, vtk_picker, event):
@@ -913,7 +914,7 @@ def _on_pick(self, vtk_picker, event):
913914
mesh = vtk_picker.GetDataSet()
914915
if mesh is None or cell_id == -1 or not self._mouse_no_mvt:
915916
return
916-
if not getattr(mesh, "_picking_target", False):
917+
if not any(mesh is target for target in self._picking_targets):
917918
return
918919
pos = np.array(vtk_picker.GetPickPosition())
919920
vtk_cell = mesh.GetCell(cell_id)
@@ -1341,7 +1342,7 @@ def _add_head_surface(self):
13411342
key = "low"
13421343
self._update_actor("head", head_actor)
13431344
# mark head surface mesh to restrict picking
1344-
head_surf._picking_target = True
1345+
self._picking_targets.append(head_surf)
13451346
# We need to use _get_processed_mri_points to incorporate grow_hair
13461347
rr = self.coreg._get_processed_mri_points(key) * self.coreg._scale.T
13471348
head_surf.points = rr

mne/viz/_brain/_brain.py

Lines changed: 46 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -535,10 +535,8 @@ def setup_time_viewer(self, time_viewer=True, show_traces=True):
535535
self.mpl_canvas = None
536536
self.help_canvas = None
537537
self.rms = None
538-
self.picked_patches = {key: list() for key in all_keys}
539-
self.picked_points = {key: list() for key in all_keys}
540-
self.pick_table = dict()
541-
self._spheres = list()
538+
self._picked_patches = {key: list() for key in all_keys}
539+
self._picked_points = dict()
542540
self._mouse_no_mvt = -1
543541

544542
# Derived parameters:
@@ -622,6 +620,8 @@ def _clean(self):
622620
self.plotter._Iren = _FakeIren()
623621
if getattr(self.plotter, "picker", None) is not None:
624622
self.plotter.picker = None
623+
if getattr(self._renderer, "_picker", None) is not None:
624+
self._renderer._picker = None
625625
# XXX end PyVista
626626
for key in (
627627
"plotter",
@@ -925,7 +925,7 @@ def _set_annot(annot):
925925
def _set_label_mode(mode):
926926
if self.traces_mode != "label":
927927
return
928-
glyphs = copy.deepcopy(self.picked_patches)
928+
glyphs = copy.deepcopy(self._picked_patches)
929929
self.label_extract_mode = mode
930930
self.clear_glyphs()
931931
for hemi in self._hemis:
@@ -1048,7 +1048,7 @@ def _configure_vertex_time_course(self):
10481048
# simulate a picked renderer
10491049
if self._hemi in ("both", "rh") or hemi == "vol":
10501050
idx = 0
1051-
self.picked_renderer = self._renderer._all_renderers[idx]
1051+
self._picked_renderer = self._renderer._all_renderers[idx]
10521052

10531053
# initialize the default point
10541054
if self._data["initial_time"] is not None:
@@ -1201,16 +1201,9 @@ def _on_button_release(self, vtk_picker, event):
12011201
if self._mouse_no_mvt > 0:
12021202
x, y = vtk_picker.GetEventPosition()
12031203
# programmatically detect the picked renderer
1204-
try:
1205-
# pyvista<0.30.0
1206-
self.picked_renderer = self.plotter.iren.FindPokedRenderer(x, y)
1207-
except AttributeError:
1208-
# pyvista>=0.30.0
1209-
self.picked_renderer = self.plotter.iren.interactor.FindPokedRenderer(
1210-
x, y
1211-
)
1204+
self._picked_renderer = self.plotter.iren.interactor.FindPokedRenderer(x, y)
12121205
# trigger the pick
1213-
self.plotter.picker.Pick(x, y, 0, self.picked_renderer)
1206+
self._renderer._picker.Pick(x, y, 0, self.picked_renderer)
12141207
self._mouse_no_mvt = 0
12151208

12161209
def _on_pick(self, vtk_picker, event):
@@ -1224,28 +1217,17 @@ def _on_pick(self, vtk_picker, event):
12241217
if mesh is None or cell_id == -1 or not self._mouse_no_mvt:
12251218
return # don't pick
12261219

1227-
# 1) Check to see if there are any spheres along the ray
1228-
if len(self._spheres):
1220+
# 1) Check to see if there are any spheres along the ray and remove if so
1221+
if len(self._picked_points):
12291222
collection = vtk_picker.GetProp3Ds()
1230-
found_sphere = None
12311223
for ii in range(collection.GetNumberOfItems()):
12321224
actor = collection.GetItemAsObject(ii)
1233-
for sphere in self._spheres:
1234-
if any(a is actor for a in sphere._actors):
1235-
found_sphere = sphere
1236-
break
1237-
if found_sphere is not None:
1238-
break
1239-
if found_sphere is not None:
1240-
assert found_sphere._is_glyph
1241-
mesh = found_sphere
1242-
1243-
# 2) Remove sphere if it's what we have
1244-
if hasattr(mesh, "_is_glyph"):
1245-
self._remove_vertex_glyph(mesh)
1246-
return
1225+
for (hemi, vertex_id), spheres in self._picked_points.items():
1226+
if any(sphere["actor"] is actor for sphere in spheres):
1227+
self._remove_vertex_glyph(hemi=hemi, vertex_id=vertex_id)
1228+
return
12471229

1248-
# 3) Otherwise, pick the objects in the scene
1230+
# 2) Otherwise, pick the objects in the scene
12491231
for hemi, this_mesh in self._layered_meshes.items():
12501232
assert hemi in ("lh", "rh"), f"Unexpected {hemi=}"
12511233
if this_mesh._polydata is mesh:
@@ -1359,24 +1341,25 @@ def _add_label_glyph(self, hemi, mesh, vertex_id):
13591341
label = self._annotation_labels[hemi][label_id]
13601342

13611343
# remove the patch if already picked
1362-
if label_id in self.picked_patches[hemi]:
1344+
if label_id in self._picked_patches[hemi]:
13631345
self._remove_label_glyph(hemi, label_id)
13641346
return
13651347

13661348
if hemi == label.hemi:
13671349
self.add_label(label, borders=True)
1368-
self.picked_patches[hemi].append(label_id)
1350+
self._picked_patches[hemi].append(label_id)
13691351

13701352
def _remove_label_glyph(self, hemi, label_id):
13711353
label = self._annotation_labels[hemi][label_id]
13721354
label._line.remove()
13731355
self.color_cycle.restore(label._color)
13741356
self.mpl_canvas.update_plot()
13751357
self._layered_meshes[hemi].remove_overlay(label.name)
1376-
self.picked_patches[hemi].remove(label_id)
1358+
self._picked_patches[hemi].remove(label_id)
13771359

13781360
def _add_vertex_glyph(self, hemi, mesh, vertex_id, update=True):
1379-
if vertex_id in self.picked_points[hemi]:
1361+
_ensure_int(vertex_id)
1362+
if (hemi, vertex_id) in self._picked_points:
13801363
return
13811364

13821365
# skip if the wrong hemi is selected
@@ -1400,10 +1383,9 @@ def _add_vertex_glyph(self, hemi, mesh, vertex_id, update=True):
14001383
lst = self._renderer._all_renderers._renderers
14011384
except AttributeError:
14021385
lst = self._renderer._all_renderers
1403-
rindex = lst.index(self.picked_renderer)
1386+
rindex = lst.index(self._picked_renderer)
14041387
row, col = self._renderer._index_to_loc(rindex)
14051388

1406-
actors = list()
14071389
spheres = list()
14081390
for _ in self._iter_views(hemi):
14091391
# Using _sphere() instead of renderer.sphere() for 2 reasons:
@@ -1412,39 +1394,28 @@ def _add_vertex_glyph(self, hemi, mesh, vertex_id, update=True):
14121394
# mitigated with synchronization/delay?)
14131395
# 2) the glyph filter is used in renderer.sphere() but only one
14141396
# sphere is required in this function.
1415-
actor, sphere = self._renderer._sphere(
1397+
actor, mesh = self._renderer._sphere(
14161398
center=np.array(center),
14171399
color=color,
14181400
radius=4.0,
14191401
)
1420-
actors.append(actor)
1421-
spheres.append(sphere)
1402+
spheres.append(dict(mesh=mesh, actor=actor))
14221403

14231404
# add metadata for picking
14241405
for sphere in spheres:
1425-
sphere._is_glyph = True
1426-
sphere._hemi = hemi
1427-
sphere._line = line
1428-
sphere._actors = actors
1429-
sphere._color = color
1430-
sphere._vertex_id = vertex_id
1431-
1432-
self.picked_points[hemi].append(vertex_id)
1433-
self._spheres.extend(spheres)
1434-
self.pick_table[vertex_id] = spheres
1435-
return sphere
1406+
sphere.update(hemi=hemi, line=line, color=color, vertex_id=vertex_id)
14361407

1437-
def _remove_vertex_glyph(self, mesh, render=True):
1438-
vertex_id = mesh._vertex_id
1439-
if vertex_id not in self.pick_table:
1440-
return
1408+
_ensure_int(vertex_id)
1409+
self._picked_points[(hemi, vertex_id)] = spheres
1410+
return sphere
14411411

1442-
hemi = mesh._hemi
1443-
color = mesh._color
1444-
spheres = self.pick_table[vertex_id]
1445-
spheres[0]._line.remove()
1412+
def _remove_vertex_glyph(self, *, hemi, vertex_id, render=True):
1413+
_ensure_int(vertex_id)
1414+
assert isinstance(hemi, str), f"got {type(hemi)} for {hemi=}"
1415+
spheres = self._picked_points.pop((hemi, vertex_id))
1416+
color, line = spheres[0]["color"], spheres[0]["line"]
1417+
line.remove()
14461418
self.mpl_canvas.update_plot()
1447-
self.picked_points[hemi].remove(vertex_id)
14481419

14491420
with warnings.catch_warnings(record=True):
14501421
# We intentionally ignore these in case we have traversed the
@@ -1453,26 +1424,21 @@ def _remove_vertex_glyph(self, mesh, render=True):
14531424
self.color_cycle.restore(color)
14541425
for sphere in spheres:
14551426
# remove all actors
1456-
self.plotter.remove_actor(sphere._actors, render=False)
1457-
sphere._actors = None
1458-
self._spheres.pop(self._spheres.index(sphere))
1427+
self.plotter.remove_actor(sphere.pop("actor"), render=False)
14591428
if render:
14601429
self._renderer._update()
1461-
self.pick_table.pop(vertex_id)
14621430

14631431
def clear_glyphs(self):
14641432
"""Clear the picking glyphs."""
14651433
if not self.time_viewer:
14661434
return
1467-
for sphere in list(self._spheres): # will remove itself, so copy
1468-
self._remove_vertex_glyph(sphere, render=False)
1469-
assert sum(len(v) for v in self.picked_points.values()) == 0
1470-
assert len(self.pick_table) == 0
1471-
assert len(self._spheres) == 0
1435+
for hemi, vertex_id in list(self._picked_points):
1436+
self._remove_vertex_glyph(hemi=hemi, vertex_id=vertex_id, render=False)
1437+
assert len(self._picked_points) == 0
14721438
for hemi in self._hemis:
1473-
for label_id in list(self.picked_patches[hemi]):
1439+
for label_id in list(self._picked_patches[hemi]):
14741440
self._remove_label_glyph(hemi, label_id)
1475-
assert sum(len(v) for v in self.picked_patches.values()) == 0
1441+
assert sum(len(v) for v in self._picked_patches.values()) == 0
14761442
if self.rms is not None:
14771443
self.rms.remove()
14781444
self.rms = None
@@ -4025,11 +3991,14 @@ def get_picked_points(self):
40253991
40263992
Returns
40273993
-------
4028-
points : list of int | None
4029-
The vertices picked by the time viewer.
3994+
points : dict | None
3995+
The vertices picked by the time viewer, one key per hemisphere with
3996+
a list of vertex indices.
40303997
"""
4031-
if hasattr(self, "time_viewer"):
4032-
return self.picked_points
3998+
out = dict(lh=[], rh=[], vol=[])
3999+
for hemi, vertex_id in self._picked_points:
4000+
out[hemi].append(vertex_id)
4001+
return out
40334002

40344003
def __hash__(self):
40354004
"""Hash the object."""

mne/viz/_brain/_linkviewer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ def _func_remove(*args, **kwargs):
4949
for hemi in ("lh", "rh"):
5050
initial_points[hemi] = set()
5151
for brain in self.brains:
52-
initial_points[hemi] |= set(brain.picked_points[hemi])
52+
initial_points[hemi] |= set(brain.get_picked_points()[hemi])
5353

5454
# link the viewers
5555
for brain in self.brains:

mne/viz/_brain/tests/test_brain.py

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1013,19 +1013,19 @@ def test_brain_traces(renderer_interactive_pyvistaqt, hemi, src, tmp_path, brain
10131013
current_mesh = brain._layered_meshes[current_hemi]._polydata
10141014
cell_id = rng.randint(0, current_mesh.n_cells)
10151015
test_picker = TstVTKPicker(current_mesh, cell_id, current_hemi, brain)
1016-
assert len(brain.picked_patches[current_hemi]) == 0
1016+
assert len(brain._picked_patches[current_hemi]) == 0
10171017
brain._on_pick(test_picker, None)
1018-
assert len(brain.picked_patches[current_hemi]) == 1
1019-
for label_id in list(brain.picked_patches[current_hemi]):
1018+
assert len(brain._picked_patches[current_hemi]) == 1
1019+
for label_id in list(brain._picked_patches[current_hemi]):
10201020
label = brain._annotation_labels[current_hemi][label_id]
10211021
assert isinstance(label._line, Line2D)
10221022
brain.widgets["extract_mode"].set_value("mean")
10231023
brain.clear_glyphs()
1024-
assert len(brain.picked_patches[current_hemi]) == 0
1024+
assert len(brain._picked_patches[current_hemi]) == 0
10251025
brain._on_pick(test_picker, None) # picked and added
1026-
assert len(brain.picked_patches[current_hemi]) == 1
1026+
assert len(brain._picked_patches[current_hemi]) == 1
10271027
brain._on_pick(test_picker, None) # picked again so removed
1028-
assert len(brain.picked_patches[current_hemi]) == 0
1028+
assert len(brain._picked_patches[current_hemi]) == 0
10291029
# test switching from 'label' to 'vertex'
10301030
brain.widgets["annotation"].set_value("None")
10311031
brain.widgets["extract_mode"].set_value("max")
@@ -1067,8 +1067,7 @@ def test_brain_traces(renderer_interactive_pyvistaqt, hemi, src, tmp_path, brain
10671067
)
10681068
assert brain.show_traces
10691069
assert brain.traces_mode == "vertex"
1070-
assert hasattr(brain, "picked_points")
1071-
assert hasattr(brain, "_spheres")
1070+
assert hasattr(brain, "_picked_points")
10721071
assert brain._scalar_bar.GetNumberOfLabels() == 3
10731072

10741073
# add foci should work for 'lh', 'rh' and 'vol'
@@ -1078,7 +1077,7 @@ def test_brain_traces(renderer_interactive_pyvistaqt, hemi, src, tmp_path, brain
10781077

10791078
# test points picked by default
10801079
picked_points = brain.get_picked_points()
1081-
spheres = brain._spheres
1080+
spheres = sum(brain._picked_points.values(), list())
10821081
for current_hemi in hemi_str:
10831082
assert len(picked_points[current_hemi]) == 1
10841083
n_spheres = len(hemi_str)
@@ -1096,7 +1095,9 @@ def test_brain_traces(renderer_interactive_pyvistaqt, hemi, src, tmp_path, brain
10961095
brain.widgets["annotation"].set_value("None")
10971096
# test removing points
10981097
brain.clear_glyphs()
1098+
spheres = sum(brain._picked_points.values(), list())
10991099
assert len(spheres) == 0
1100+
picked_points = brain.get_picked_points()
11001101
for key in ("lh", "rh", "vol"):
11011102
assert len(picked_points[key]) == 0
11021103

@@ -1120,13 +1121,16 @@ def test_brain_traces(renderer_interactive_pyvistaqt, hemi, src, tmp_path, brain
11201121
brain._on_pick(test_picker, None)
11211122
brain._on_pick(test_picker, None)
11221123
assert test_picker.point_id is not None
1124+
picked_points = brain.get_picked_points()
11231125
assert len(picked_points[current_hemi]) == 1
11241126
assert picked_points[current_hemi][0] == test_picker.point_id
1127+
spheres = sum(brain._picked_points.values(), list())
11251128
assert len(spheres) > 0
11261129
sphere = spheres[-1]
1127-
vertex_id = sphere._vertex_id
1130+
vertex_id = sphere["vertex_id"]
11281131
assert vertex_id == test_picker.point_id
1129-
line = sphere._line
1132+
line = sphere["line"]
1133+
del sphere
11301134

11311135
hemi_prefix = current_hemi[0].upper()
11321136
if current_hemi == "vol":
@@ -1148,8 +1152,9 @@ def test_brain_traces(renderer_interactive_pyvistaqt, hemi, src, tmp_path, brain
11481152

11491153
# remove the sphere by clicking in its vicinity
11501154
old_len = len(spheres)
1151-
test_picker._actors = sum((s._actors for s in spheres), [])
1155+
test_picker._actors = [s["actor"] for s in spheres]
11521156
brain._on_pick(test_picker, None)
1157+
spheres = sum(brain._picked_points.values(), list())
11531158
assert len(spheres) < old_len
11541159

11551160
screenshot = brain.screenshot()
@@ -1387,7 +1392,7 @@ def test_brain_ui_events(renderer_interactive_pyvistaqt, brain_gc):
13871392
assert brain._current_time == 1
13881393

13891394
ui_events.publish(brain, ui_events.VertexSelect(hemi="lh", vertex_id=1))
1390-
assert 1 in brain.picked_points["lh"]
1395+
assert 1 in brain.get_picked_points()["lh"]
13911396

13921397
ui_events.publish(
13931398
brain,

0 commit comments

Comments
 (0)