Coverage for /usr/local/lib/python3.11/dist-packages/pyrocko/gui/sparrow/snapshots.py: 56%
532 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-10-04 09:52 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2023-10-04 09:52 +0000
1from subprocess import check_call, CalledProcessError
2import logging
3import tempfile
5from pyrocko import util
6from pyrocko.guts import Object, String, Float, Bytes, clone, \
7 dump_all, load_all
9from pyrocko.gui.qt_compat import qw, qc, qg, get_em
10from .state import ViewerState, Interpolator, interpolateables
11from vtk.util.numpy_support import vtk_to_numpy
12import vtk
13from . import common
15guts_prefix = 'sparrow'
17logger = logging.getLogger('pyrocko.gui.sparrow.snapshots')
19thumb_size = 128, 72
22def to_rect(r):
23 return [float(x) for x in (r.left(), r.top(), r.width(), r.height())]
26def fit_to_rect(frame, size, halign='center', valign='center'):
27 fl, ft, fw, fh = to_rect(frame)
28 rw, rh = size.width(), size.height()
30 ft += 1
31 fh -= 1
33 fl += 1
34 fw -= 1
36 fa = fh / fw
37 ra = rh / rw
39 if fa <= ra:
40 rh = fh
41 rw = rh / ra
42 if halign == 'left':
43 rl = fl
44 elif halign == 'center':
45 rl = fl + 0.5 * (fw - rw)
46 elif halign == 'right':
47 rl = fl + fw - rw
49 rt = ft
50 else:
51 rw = fw
52 rh = rw * ra
53 rl = fl
54 if valign == 'top':
55 rt = ft
56 elif valign == 'center':
57 rt = ft + 0.5 * (fh - rh)
58 elif valign == 'bottom':
59 rt = ft + fh - rh
61 return qc.QRectF(rl, rt, rw, rh)
64def getitem_or_none(items, i):
65 try:
66 return items[i]
67 except IndexError:
68 return None
71def iround(f):
72 return int(round(f))
75class SnapshotItemDelegate(qw.QStyledItemDelegate):
76 def __init__(self, model, parent):
77 qw.QStyledItemDelegate.__init__(self, parent=parent)
78 self.model = model
80 def sizeHint(self, option, index):
81 item = self.model.get_item_or_none(index)
82 if isinstance(item, Snapshot):
83 return qc.QSize(*thumb_size)
84 else:
85 return qw.QStyledItemDelegate.sizeHint(self, option, index)
87 def paint(self, painter, option, index):
88 app = common.get_app()
89 item = self.model.get_item_or_none(index)
90 em = get_em(painter)
91 frect = option.rect.adjusted(0, 0, 0, 0)
92 nb = iround(em*0.5)
93 trect = option.rect.adjusted(nb, nb, -nb, -nb)
95 if isinstance(item, Snapshot):
97 old_pen = painter.pen()
98 if option.state & qw.QStyle.State_Selected:
99 bg_brush = app.palette().brush(
100 qg.QPalette.Active, qg.QPalette.Highlight)
102 fg_pen = qg.QPen(app.palette().color(
103 qg.QPalette.Active, qg.QPalette.HighlightedText))
105 painter.fillRect(frect, bg_brush)
106 painter.setPen(fg_pen)
108 else:
109 bg_brush = app.palette().brush(
110 qg.QPalette.Active, qg.QPalette.AlternateBase)
112 painter.fillRect(frect, bg_brush)
114 # painter.drawRect(frect)
115 img = item.get_image()
116 if img is not None:
117 prect = fit_to_rect(frect, img.size(), halign='right')
118 painter.drawImage(prect, img)
120 painter.drawText(
121 trect,
122 qc.Qt.AlignLeft | qc.Qt.AlignTop,
123 item.name)
125 painter.setPen(
126 app.palette().brush(
127 qg.QPalette.Disabled
128 if item.duration is None
129 else qg.QPalette.Active,
130 qg.QPalette.Text).color())
132 ed = item.effective_duration
133 painter.drawText(
134 trect,
135 qc.Qt.AlignLeft | qc.Qt.AlignBottom,
136 '%.2f s' % ed if ed != 0.0 else '')
138 painter.setPen(old_pen)
140 else:
141 qw.QStyledItemDelegate.paint(self, painter, option, index)
143 # painter.drawText(
144 # trect,
145 # qc.Qt.AlignRight | qc.Qt.AlignTop,
146 # '%.2f' % item.effective_duration)
148 def editorEvent(self, event, model, option, index):
150 item = self.model.get_item_or_none(index)
152 if isinstance(event, qg.QMouseEvent) \
153 and event.button() == qc.Qt.RightButton:
155 menu = qw.QMenu()
157 for name, duration in [
158 ('Auto', None),
159 ('0 s', 0.0),
160 ('1/2 s', 0.5),
161 ('1 s', 1.0),
162 ('2 s', 2.0),
163 ('3 s', 3.0),
164 ('5 s', 5.0),
165 ('10 s', 10.0),
166 ('60 s', 60.0)]:
168 def make_triggered(duration):
169 def triggered():
170 item.duration = duration
172 return triggered
174 action = qw.QAction(name, menu)
175 action.triggered.connect(make_triggered(duration))
176 menu.addAction(action)
178 action = qw.QAction('Custom...', menu)
180 def triggered():
181 self.parent().edit(index)
183 action.triggered.connect(triggered)
185 menu.addAction(action)
186 menu.exec_(event.globalPos())
188 return True
190 else:
191 return qw.QStyledItemDelegate.editorEvent(
192 self, event, model, option, index)
194 def createEditor(self, parent, option, index):
195 return qw.QLineEdit(parent=parent)
197 def setModelData(self, editor, model, index):
198 item = self.model.get_item_or_none(index)
199 if item:
200 try:
201 item.duration = max(float(editor.text()), 0.0)
202 except ValueError:
203 item.duration = None
205 def setEditorData(self, editor, index):
206 item = self.model.get_item_or_none(index)
207 if item:
208 editor.setText(
209 'Auto' if item.duration is None else '%g' % item.duration)
212class SnapshotListView(qw.QListView):
214 def startDrag(self, supported):
215 if supported & (qc.Qt.CopyAction | qc.Qt.MoveAction):
216 drag = qg.QDrag(self)
217 selected_indexes = self.selectedIndexes()
218 mime_data = self.model().mimeData(selected_indexes)
219 drag.setMimeData(mime_data)
220 drag.exec(qc.Qt.MoveAction)
222 def dropEvent(self, *args):
223 mod = self.model()
224 selected_items = [
225 mod.get_item_or_none(index) for index in self.selectedIndexes()]
227 selected_items = [item for item in selected_items if item is not None]
229 result = qw.QListView.dropEvent(self, *args)
231 indexes = [mod.get_index_for_item(item) for item in selected_items]
233 smod = self.selectionModel()
234 smod.clear()
235 scroll_index = None
236 for index in indexes:
237 if index is not None:
238 smod.select(index, qc.QItemSelectionModel.Select)
239 if scroll_index is None:
240 scroll_index = index
242 if scroll_index is not None:
243 self.scrollTo(scroll_index)
245 return result
248class SnapshotsPanel(qw.QFrame):
250 def __init__(self, viewer):
251 qw.QFrame.__init__(self)
252 layout = qw.QGridLayout()
253 self.setLayout(layout)
255 self.model = SnapshotsModel()
257 self.viewer = viewer
259 lv = SnapshotListView()
260 lv.sizePolicy().setVerticalPolicy(qw.QSizePolicy.Expanding)
261 lv.setModel(self.model)
262 lv.doubleClicked.connect(self.goto_snapshot)
263 lv.setSelectionMode(qw.QAbstractItemView.ExtendedSelection)
264 lv.setDragDropMode(qw.QAbstractItemView.InternalMove)
265 lv.setEditTriggers(qw.QAbstractItemView.NoEditTriggers)
266 lv.viewport().setAcceptDrops(True)
267 self.item_delegate = SnapshotItemDelegate(self.model, lv)
268 lv.setItemDelegate(self.item_delegate)
269 self.list_view = lv
270 layout.addWidget(lv, 0, 0, 1, 3)
272 pb = qw.QPushButton('New')
273 pb.clicked.connect(self.take_snapshot)
274 layout.addWidget(pb, 1, 0, 1, 1)
276 pb = qw.QPushButton('Replace')
277 pb.clicked.connect(self.replace_snapshot)
278 layout.addWidget(pb, 1, 1, 1, 1)
280 pb = qw.QPushButton('Delete')
281 pb.clicked.connect(self.delete_snapshots)
282 layout.addWidget(pb, 1, 2, 1, 1)
284 self.window_to_image_filter = None
286 def setup_menu(self, menu):
287 menu.addAction(
288 'New',
289 self.take_snapshot,
290 qg.QKeySequence(qc.Qt.CTRL | qc.Qt.Key_N)).setShortcutContext(
291 qc.Qt.ApplicationShortcut)
293 menu.addSeparator()
295 menu.addAction(
296 'Next',
297 self.transition_to_next_snapshot,
298 qg.QKeySequence(qc.Qt.Key_PageDown)).setShortcutContext(
299 qc.Qt.ApplicationShortcut)
301 menu.addAction(
302 'Previous',
303 self.transition_to_previous_snapshot,
304 qg.QKeySequence(qc.Qt.Key_PageUp)).setShortcutContext(
305 qc.Qt.ApplicationShortcut)
307 menu.addSeparator()
309 menu.addAction(
310 'Import...',
311 self.import_snapshots)
313 menu.addAction(
314 'Export...',
315 self.export_snapshots)
317 menu.addAction(
318 'Animate',
319 self.animate_snapshots)
321 menu.addAction(
322 'Export Movie...',
323 self.render_movie)
325 menu.addSeparator()
327 menu.addAction(
328 'Show Panel',
329 self.show_and_raise)
331 def show_and_raise(self):
332 self.viewer.raise_panel(self)
334 def get_snapshot_image(self):
335 if not self.window_to_image_filter:
336 wif = vtk.vtkWindowToImageFilter()
337 wif.SetInput(self.viewer.renwin)
338 wif.SetInputBufferTypeToRGBA()
339 wif.ReadFrontBufferOff()
340 self.window_to_image_filter = wif
342 writer = vtk.vtkPNGWriter()
343 writer.SetInputConnection(wif.GetOutputPort())
344 writer.SetWriteToMemory(True)
345 self.png_writer = writer
347 self.viewer.renwin.Render()
348 self.window_to_image_filter.Modified()
349 self.png_writer.Write()
350 data = vtk_to_numpy(self.png_writer.GetResult()).tobytes()
351 img = qg.QImage()
352 img.loadFromData(data)
353 return img
355 def get_snapshot_thumbnail(self):
356 return self.get_snapshot_image().scaled(
357 thumb_size[0], thumb_size[1],
358 qc.Qt.KeepAspectRatio, qc.Qt.SmoothTransformation)
360 def get_snapshot_thumbnail_png(self):
361 img = self.get_snapshot_thumbnail()
363 ba = qc.QByteArray()
364 buf = qc.QBuffer(ba)
365 buf.open(qc.QIODevice.WriteOnly)
366 img.save(buf, format='PNG')
367 return ba.data()
369 def take_snapshot(self):
370 self.model.add_snapshot(
371 Snapshot(
372 state=clone(self.viewer.state),
373 thumb=self.get_snapshot_thumbnail_png()))
374 self.viewer.raise_panel(self)
376 def replace_snapshot(self):
377 state = clone(self.viewer.state)
378 selected_indexes = self.list_view.selectedIndexes()
380 if len(selected_indexes) == 1:
381 self.model.replace_snapshot(
382 selected_indexes[0],
383 Snapshot(
384 state,
385 thumb=self.get_snapshot_thumbnail_png()))
387 self.list_view.update()
389 def goto_snapshot(self, index):
390 item = self.model.get_item_or_none(index)
391 if isinstance(item, Snapshot):
392 self.viewer.set_state(item.state)
393 elif isinstance(item, Transition):
394 snap1 = self.model.get_item_or_none(index.row()-1)
395 snap2 = self.model.get_item_or_none(index.row()+1)
396 if isinstance(snap1, Snapshot) and isinstance(snap2, Snapshot):
397 ip = Interpolator(
398 [0.0, item.effective_duration],
399 [snap1.state, snap2.state])
401 self.viewer.start_animation(ip)
403 def transition_to_next_snapshot(self, direction=1):
404 index = self.list_view.currentIndex()
405 if index.row() == -1:
406 if direction == 1:
407 index = self.model.createIndex(0, 0)
409 item = self.model.get_item_or_none(index)
410 if item is None:
411 return
413 if isinstance(item, Snapshot):
414 snap1 = item
415 transition = self.model.get_item_or_none(index.row()+1*direction)
416 snap2 = self.model.get_item_or_none(index.row()+2*direction)
417 elif isinstance(item, Transition):
418 snap1 = self.model.get_item_or_none(index.row()-1*direction)
419 transition = item
420 snap2 = self.model.get_item_or_none(index.row()+1*direction)
422 if None not in (snap1, transition, snap2):
423 ip = Interpolator(
424 [0.0, transition.effective_duration],
425 [snap1.state, snap2.state])
427 index = self.model.get_index_for_item(snap2)
428 self.list_view.setCurrentIndex(index)
430 self.viewer.start_animation(ip)
432 elif snap2 is not None:
433 index = self.model.get_index_for_item(snap2)
434 self.list_view.setCurrentIndex(index)
435 self.viewer.set_state(snap2.state)
437 def transition_to_previous_snapshot(self):
438 self.transition_to_next_snapshot(-1)
440 def delete_snapshots(self):
441 selected_indexes = self.list_view.selectedIndexes()
442 self.model.remove_snapshots(selected_indexes)
444 def animate_snapshots(self, **kwargs):
445 selected_indexes = self.list_view.selectedIndexes()
446 items = self.model.get_series(selected_indexes)
448 time_state = []
449 item_previous = None
450 t = 0.0
451 for i, item in enumerate(items):
452 item_next = getitem_or_none(items, i+1)
453 item_previous = getitem_or_none(items, i-1)
455 if isinstance(item, Snapshot):
456 time_state.append((t, item.state))
457 if item.effective_duration > 0:
458 time_state.append((t+item.effective_duration, item.state))
460 t += item.effective_duration
462 elif isinstance(item, Transition):
463 if None not in (item_previous, item_next) \
464 and item.effective_duration != 0.0:
466 t += item.effective_duration
468 item_previous = item
470 if len(time_state) < 2:
471 return
473 ip = Interpolator(*zip(*time_state))
475 self.viewer.start_animation(
476 ip, output_path=kwargs.get('output_path', None))
478 def render_movie(self):
479 try:
480 check_call(['ffmpeg', '-loglevel', 'panic'])
481 except CalledProcessError:
482 pass
483 except (TypeError, FileNotFoundError):
484 logger.warn(
485 'Package ffmpeg needed for movie rendering. Please install it '
486 '(e.g. on linux distr. via sudo apt-get ffmpeg.) and retry.')
487 return
489 caption = 'Export Movie'
490 fn_out, _ = qw.QFileDialog.getSaveFileName(
491 self, caption, 'movie.mp4',
492 options=common.qfiledialog_options)
494 if fn_out:
495 self.animate_snapshots(output_path=fn_out)
497 def export_snapshots(self):
498 caption = 'Export Snapshots'
499 fn, _ = qw.QFileDialog.getSaveFileName(
500 self, caption, options=common.qfiledialog_options)
502 selected_indexes = self.list_view.selectedIndexes()
503 items = self.model.get_series(selected_indexes)
505 if fn:
506 dump_all(items, filename=fn)
508 def add_snapshots(self, snapshots):
509 self.model.append_series(snapshots)
511 def load_snapshots(self, path):
512 items = load_snapshots(path)
513 self.add_snapshots(items)
515 def import_snapshots(self):
516 caption = 'Import Snapshots'
517 path, _ = qw.QFileDialog.getOpenFileName(
518 self, caption, options=common.qfiledialog_options)
520 if path:
521 self.load_snapshots(path)
524class Item(Object):
525 duration = Float.T(optional=True)
527 def __init__(self, **kwargs):
528 Object.__init__(self, **kwargs)
529 self.auto_duration = 0.0
531 @property
532 def effective_duration(self):
533 if self.duration is not None:
534 return self.duration
535 else:
536 return self.auto_duration
539class Snapshot(Item):
540 name = String.T()
541 state = ViewerState.T()
542 thumb = Bytes.T(optional=True)
544 isnapshot = 0
546 def __init__(self, state, name=None, thumb=None, **kwargs):
548 if name is None:
549 Snapshot.isnapshot += 1
550 name = '%i' % Snapshot.isnapshot
552 Item.__init__(self, state=state, name=name, thumb=thumb, **kwargs)
553 self._img = None
555 def get_name(self):
556 return self.name
558 def get_image(self):
559 if self.thumb is not None and not self._img:
560 img = qg.QImage()
561 img.loadFromData(self.thumb)
562 self._img = img
564 return self._img
567class Transition(Item):
569 def __init__(self, **kwargs):
570 Item.__init__(self, **kwargs)
571 self.animate = []
573 def get_name(self):
574 ed = self.effective_duration
575 return '%s %s' % (
576 'T' if self.animate and self.effective_duration > 0.0 else '',
577 '%.2f s' % ed if ed != 0.0 else '')
579 @property
580 def name(self):
581 return self.get_name()
584class SnapshotsModel(qc.QAbstractListModel):
586 def __init__(self):
587 qc.QAbstractListModel.__init__(self)
588 self._items = []
590 def supportedDropActions(self):
591 return qc.Qt.MoveAction
593 def rowCount(self, parent=None):
594 return len(self._items)
596 def insertRows(self, index):
597 pass
599 def mimeTypes(self):
600 return ['text/plain']
602 def mimeData(self, indices):
603 objects = [self._items[i.row()] for i in indices]
604 serialized = dump_all(objects)
605 md = qc.QMimeData()
606 md.setText(serialized)
607 md._item_objects = objects
608 return md
610 def dropMimeData(self, md, action, row, col, index):
611 i = index.row()
612 items = getattr(md, '_item_objects', [])
613 self.beginInsertRows(qc.QModelIndex(), i, i)
614 self._items[i:i] = items
615 self.endInsertRows()
616 n = len(items)
617 joff = 0
618 for j in range(len(self._items)):
619 if (j < i or j >= i+n) and self._items[j+joff] in items:
620 self.beginRemoveRows(qc.QModelIndex(), j+joff, j+joff)
621 self._items[j+joff:j+joff+1] = []
622 self.endRemoveRows()
623 joff -= 1
625 self.repair_transitions()
626 return True
628 def removeRows(self, i, n, parent):
629 return True
631 def flags(self, index):
632 if index.isValid():
633 i = index.row()
634 if isinstance(self._items[i], Snapshot):
635 return qc.Qt.ItemFlags(
636 qc.Qt.ItemIsSelectable
637 | qc.Qt.ItemIsEnabled
638 | qc.Qt.ItemIsDragEnabled
639 | qc.Qt.ItemIsEditable)
641 else:
642 return qc.Qt.ItemFlags(
643 qc.Qt.ItemIsEnabled
644 | qc.Qt.ItemIsEnabled
645 | qc.Qt.ItemIsDropEnabled
646 | qc.Qt.ItemIsEditable)
647 else:
648 return qc.QAbstractListModel.flags(self, index)
650 def data(self, index, role):
651 app = common.get_app()
652 i = index.row()
653 item = self._items[i]
654 is_snap = isinstance(item, Snapshot)
655 if role == qc.Qt.DisplayRole:
656 if is_snap:
657 return qc.QVariant(str(item.get_name()))
658 else:
659 return qc.QVariant(str(item.get_name()))
661 elif role == qc.Qt.ToolTipRole:
662 if is_snap:
663 # return qc.QVariant(str(item.state))
664 return qc.QVariant()
665 else:
666 if item.animate:
667 label = 'Interpolation: %s' % \
668 ', '.join(x[0] for x in item.animate)
669 else:
670 label = 'Not interpolable.'
672 return qc.QVariant(label)
674 elif role == qc.Qt.TextAlignmentRole and not is_snap:
675 return qc.QVariant(qc.Qt.AlignRight)
677 elif role == qc.Qt.ForegroundRole and not is_snap:
678 if item.duration is None:
679 return qc.QVariant(app.palette().brush(
680 qg.QPalette.Disabled, qg.QPalette.Text))
681 else:
682 return qc.QVariant(app.palette().brush(
683 qg.QPalette.Active, qg.QPalette.Text))
685 else:
686 qc.QVariant()
688 def headerData(self):
689 pass
691 def add_snapshot(self, snapshot):
692 self.beginInsertRows(
693 qc.QModelIndex(), self.rowCount(), self.rowCount())
694 self._items.append(snapshot)
695 self.endInsertRows()
696 self.repair_transitions()
698 def replace_snapshot(self, index, snapshot):
699 self._items[index.row()] = snapshot
700 self.dataChanged.emit(index, index)
701 self.repair_transitions()
703 def remove_snapshots(self, indexes):
704 indexes = sorted(indexes, key=lambda index: index.row())
705 ioff = 0
706 for index in indexes:
707 i = index.row()
708 self.beginRemoveRows(qc.QModelIndex(), i+ioff, i+ioff)
709 self._items[i+ioff:i+ioff+1] = []
710 self.endRemoveRows()
711 ioff -= 1
713 self.repair_transitions()
715 def repair_transitions(self):
716 items = self._items
717 i = 0
718 need = 0
719 while i < len(items):
720 if need == 0:
721 if not isinstance(items[i], Transition):
722 self.beginInsertRows(qc.QModelIndex(), i, i)
723 items[i:i] = [Transition()]
724 self.endInsertRows()
725 else:
726 i += 1
727 need = 1
728 elif need == 1:
729 if not isinstance(items[i], Snapshot):
730 self.beginRemoveRows(qc.QModelIndex(), i, i)
731 items[i:i+1] = []
732 self.endRemoveRows()
733 else:
734 i += 1
735 need = 0
737 if len(items) == 1:
738 self.beginRemoveRows(qc.QModelIndex(), 0, 0)
739 items[:] = []
740 self.endRemoveRows()
742 elif len(items) > 1:
743 if not isinstance(items[-1], Transition):
744 self.beginInsertRows(
745 qc.QModelIndex(), self.rowCount(), self.rowCount())
746 items.append(Transition())
747 self.endInsertRows()
749 self.update_auto_durations()
751 def update_auto_durations(self):
752 items = self._items
753 for i, item in enumerate(items):
754 if isinstance(item, Transition):
755 if 0 < i < len(items)-1:
756 item.animate = interpolateables(
757 items[i-1].state, items[i+1].state)
759 if item.animate:
760 item.auto_duration = 1.
761 else:
762 item.auto_duration = 0.
764 for i, item in enumerate(items):
765 if isinstance(item, Snapshot):
766 if 0 < i < len(items)-1:
767 if items[i-1].effective_duration == 0 \
768 and items[i+1].effective_duration == 0:
769 item.auto_duration = 1.
770 else:
771 item.auto_duration = 0.
773 def get_index_for_item(self, item):
774 for i, candidate in enumerate(self._items):
775 if candidate is item:
776 return self.createIndex(i, 0)
778 return None
780 def get_item_or_none(self, index):
781 if not isinstance(index, int):
782 i = index.row()
783 else:
784 i = index
786 if i < 0 or len(self._items) <= i:
787 return None
789 try:
790 return self._items[i]
791 except IndexError:
792 return None
794 def get_series(self, indexes):
795 items = self._items
797 ilist = sorted([index.row() for index in indexes])
798 if len(ilist) <= 1:
799 ilist = list(range(0, len(self._items)))
801 ilist = [i for i in ilist if isinstance(items[i], Snapshot)]
802 if len(ilist) == 0:
803 return []
805 i = ilist[0]
807 series = []
808 while ilist:
809 i = ilist.pop(0)
810 series.append(items[i])
811 if ilist and ilist[0] == i+2:
812 series.append(items[i+1])
814 return series
816 def append_series(self, items):
817 self.beginInsertRows(
818 qc.QModelIndex(),
819 self.rowCount(), self.rowCount() + len(items) - 1)
821 self._items.extend(items)
822 self.endInsertRows()
824 self.repair_transitions()
827def load_snapshots(path):
829 if path.startswith('http://') or path.startswith('https://'):
830 with tempfile.NamedTemporaryFile() as fp:
831 util.download_file(path, fp.name)
832 return load_snapshots(fp.name)
834 items = load_all(filename=path)
835 for i in range(len(items)):
836 if not isinstance(
837 items[i], (ViewerState, Snapshot, Transition)):
839 logger.warn(
840 'Only Snapshot, Transition and ViewerState objects '
841 'are accepted. Object #%i from file %s ignored.'
842 % (i, path))
844 if isinstance(items[i], ViewerState):
845 items[i] = Snapshot(items[i])
847 for item in items:
848 if isinstance(item, Snapshot):
849 item.state.sort_elements()
851 return items