vedo.assembly

Submodule for managing groups of vedo objects

  1#!/usr/bin/env python3
  2# -*- coding: utf-8 -*-
  3from weakref import ref as weak_ref_to
  4from typing import List, Union, Any
  5from typing_extensions import Self
  6
  7import numpy as np
  8
  9import vedo.file_io
 10import vedo.vtkclasses as vtki  # a wrapper for lazy imports
 11
 12import vedo
 13from vedo.transformations import LinearTransform
 14from vedo.visual import CommonVisual, Actor3DHelper
 15
 16__docformat__ = "google"
 17
 18__doc__ = """
 19Submodule for managing groups of vedo objects
 20
 21![](https://vedo.embl.es/images/basic/align4.png)
 22"""
 23
 24__all__ = ["Group", "Assembly", "procrustes_alignment"]
 25
 26
 27#################################################
 28def procrustes_alignment(sources: List["vedo.Mesh"], rigid=False) -> "Assembly":
 29    """
 30    Return an `Assembly` of aligned source meshes with the `Procrustes` algorithm.
 31    The output `Assembly` is normalized in size.
 32
 33    The `Procrustes` algorithm takes N set of points and aligns them in a least-squares sense
 34    to their mutual mean. The algorithm is iterated until convergence,
 35    as the mean must be recomputed after each alignment.
 36
 37    The set of average points generated by the algorithm can be accessed with
 38    `algoutput.info['mean']` as a numpy array.
 39
 40    Arguments:
 41        rigid : bool
 42            if `True` scaling is disabled.
 43
 44    Examples:
 45        - [align4.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align4.py)
 46
 47        ![](https://vedo.embl.es/images/basic/align4.png)
 48    """
 49
 50    group = vtki.new("MultiBlockDataGroupFilter")
 51    for source in sources:
 52        if sources[0].npoints != source.npoints:
 53            vedo.logger.error("sources have different nr of points")
 54            raise RuntimeError()
 55        group.AddInputData(source.dataset)
 56    procrustes = vtki.new("ProcrustesAlignmentFilter")
 57    procrustes.StartFromCentroidOn()
 58    procrustes.SetInputConnection(group.GetOutputPort())
 59    if rigid:
 60        procrustes.GetLandmarkTransform().SetModeToRigidBody()
 61    procrustes.Update()
 62
 63    acts = []
 64    for i, s in enumerate(sources):
 65        poly = procrustes.GetOutput().GetBlock(i)
 66        mesh = vedo.mesh.Mesh(poly)
 67        mesh.actor.SetProperty(s.actor.GetProperty())
 68        mesh.properties = s.actor.GetProperty()
 69        if hasattr(s, "name"):
 70            mesh.name = s.name
 71        acts.append(mesh)
 72    assem = Assembly(acts)
 73    assem.transform = procrustes.GetLandmarkTransform()
 74    assem.info["mean"] = vedo.utils.vtk2numpy(procrustes.GetMeanPoints().GetData())
 75    return assem
 76
 77
 78#################################################
 79class Group(vtki.vtkPropAssembly):
 80    """Form groups of generic objects (not necessarily meshes)."""
 81
 82    def __init__(self, objects=()):
 83        """Form groups of generic objects (not necessarily meshes)."""
 84        super().__init__()
 85
 86        self.objects = []
 87
 88        if isinstance(objects, dict):
 89            for name in objects:
 90                objects[name].name = name
 91            objects = list(objects.values())
 92        elif vedo.utils.is_sequence(objects):
 93            self.objects = objects
 94
 95
 96        self.actor = self
 97
 98        self.name = "Group"
 99        self.filename = ""
100        self.trail = None
101        self.trail_points = []
102        self.trail_segment_size = 0
103        self.trail_offset = None
104        self.shadows = []
105        self.info = {}
106        self.rendered_at = set()
107        self.scalarbar = None
108
109        for a in vedo.utils.flatten(objects):
110            if a:
111                self.AddPart(a.actor)
112
113        self.PickableOff()
114
115
116    def __str__(self):
117        """Print info about Group object."""
118        module = self.__class__.__module__
119        name = self.__class__.__name__
120        out = vedo.printc(
121            f"{module}.{name} at ({hex(id(self))})".ljust(75),
122            bold=True, invert=True, return_string=True,
123        )
124        out += "\x1b[0m"
125        if self.name:
126            out += "name".ljust(14) + ": " + self.name
127            if "legend" in self.info.keys() and self.info["legend"]:
128                out+= f", legend='{self.info['legend']}'"
129            out += "\n"
130
131        n = len(self.unpack())
132        out += "n. of objects".ljust(14) + ": " + str(n) + " "
133        names = [a.name for a in self.unpack() if a.name]
134        if names:
135            out += str(names).replace("'","")[:56]
136        return out.rstrip() + "\x1b[0m"
137
138    def __iadd__(self, obj):
139        """Add an object to the group."""
140        if not vedo.utils.is_sequence(obj):
141            obj = [obj]
142        for a in obj:
143            if a:
144                try:
145                    self.AddPart(a)
146                except TypeError:
147                    self.AddPart(a.actor)
148                    self.objects.append(a)
149        return self
150
151    def __isub__(self, obj):
152        """Remove an object to the group."""
153        if not vedo.utils.is_sequence(obj):
154            obj = [obj]
155        for a in obj:
156            if a:
157                try:
158                    self.RemovePart(a)
159                except TypeError:
160                    self.RemovePart(a.actor)
161                    self.objects.append(a)
162        return self
163    
164    def rename(self, name: str) -> "Group":
165        """Set a new name for the Group object."""
166        self.name = name
167        return self
168
169    def add(self, obj):
170        """Add an object to the group."""
171        self.__iadd__(obj)
172        return self
173
174    def remove(self, obj):
175        """Remove an object to the group."""
176        self.__isub__(obj)
177        return self
178
179    def _unpack(self):
180        """Unpack the group into its elements"""
181        elements = []
182        self.InitPathTraversal()
183        parts = self.GetParts()
184        parts.InitTraversal()
185        for i in range(parts.GetNumberOfItems()):
186            ele = parts.GetItemAsObject(i)
187            elements.append(ele)
188
189        # gr.InitPathTraversal()
190        # for _ in range(gr.GetNumberOfPaths()):
191        #     path  = gr.GetNextPath()
192        #     print([path])
193        #     path.InitTraversal()
194        #     for i in range(path.GetNumberOfItems()):
195        #         a = path.GetItemAsObject(i).GetViewProp()
196        #         print([a])
197
198        return elements
199
200    def clear(self) -> "Group":
201        """Remove all parts"""
202        for a in self._unpack():
203            self.RemovePart(a)
204        self.objects = []
205        return self
206
207    def on(self) -> "Group":
208        """Switch on visibility"""
209        self.VisibilityOn()
210        return self
211
212    def off(self) -> "Group":
213        """Switch off visibility"""
214        self.VisibilityOff()
215        return self
216
217    def pickable(self, value=True) -> "Group":
218        """The pickability property of the Group."""
219        self.SetPickable(value)
220        return self
221
222    def use_bounds(self, value=True) -> "Group":
223        """Set the use bounds property of the Group."""
224        self.SetUseBounds(value)
225        return self
226
227    def print(self) -> "Group":
228        """Print info about the object."""
229        print(self)
230        return self
231
232
233#################################################
234class Assembly(CommonVisual, Actor3DHelper, vtki.vtkAssembly):
235    """
236    Group many objects and treat them as a single new object.
237    """
238
239    def __init__(self, *meshs):
240        """
241        Group many objects and treat them as a single new object,
242        keeping track of internal transformations.
243
244        A file can be loaded by passing its name as a string.
245        Format must be `.npy`.
246
247        Examples:
248            - [gyroscope1.py](https://github.com/marcomusy/vedo/tree/master/examples/simulations/gyroscope1.py)
249
250            ![](https://vedo.embl.es/images/simulations/39766016-85c1c1d6-52e3-11e8-8575-d167b7ce5217.gif)
251        """
252        super().__init__()
253
254        self.actor = self
255        self.actor.retrieve_object = weak_ref_to(self)
256
257        self.name = "Assembly"
258        self.filename = ""
259        self.rendered_at = set()
260        self.scalarbar = None
261        self.info = {}
262        self.time = 0
263
264        self.transform = LinearTransform()
265
266        # Init by filename
267        if len(meshs) == 1 and isinstance(meshs[0], str):
268            filename = vedo.file_io.download(meshs[0], verbose=False)
269            data = np.load(filename, allow_pickle=True)
270            try:
271                # old format with a single object
272                meshs = [vedo.file_io.from_numpy(dd) for dd in data]
273            except TypeError:
274                # new format with a dictionary
275                data = data.item()
276                meshs = []
277                for ad in data["objects"][0]["parts"]:
278                    obb = vedo.file_io.from_numpy(ad)
279                    meshs.append(obb)
280                self.transform = LinearTransform(data["objects"][0]["transform"])
281                self.actor.SetPosition(self.transform.T.GetPosition())
282                self.actor.SetOrientation(self.transform.T.GetOrientation())
283                self.actor.SetScale(self.transform.T.GetScale())
284
285        # Name and load from dictionary
286        if len(meshs) == 1 and isinstance(meshs[0], dict):
287            meshs = meshs[0]
288            for name in meshs:
289                meshs[name].name = name
290            meshs = list(meshs.values())
291        else:
292            if len(meshs) == 1:
293                meshs = meshs[0]
294            else:
295                meshs = vedo.utils.flatten(meshs)
296
297        self.objects = [m for m in meshs if m]
298        self.actors  = [m.actor for m in self.objects]
299
300        scalarbars = []
301        for a in self.actors:
302            if isinstance(a, vtki.get_class("Prop3D")): # and a.GetNumberOfPoints():
303                self.AddPart(a)
304            if hasattr(a, "scalarbar") and a.scalarbar is not None:
305                scalarbars.append(a.scalarbar)
306
307        if len(scalarbars) > 1:
308            self.scalarbar = Group(scalarbars)
309        elif len(scalarbars) == 1:
310            self.scalarbar = scalarbars[0]
311
312        self.pipeline = vedo.utils.OperationNode(
313            "Assembly",
314            parents=self.objects,
315            comment=f"#meshes {len(self.objects)}",
316            c="#f08080",
317        )
318        ##########################################
319
320    def __str__(self):
321        """Print info about Assembly object."""
322        module = self.__class__.__module__
323        cname = self.__class__.__name__
324        out = vedo.printc(
325            f"{module}.{cname} at ({hex(id(self))})".ljust(75),
326            bold=True, invert=True, return_string=True,
327        )
328        out += "\x1b[0m"
329
330        if self.name:
331            out += "name".ljust(14) + ": " + self.name
332            if "legend" in self.info.keys() and self.info["legend"]:
333                out+= f", legend='{self.info['legend']}'"
334            out += "\n"
335
336        n = len(self.unpack())
337        out += "n. of objects".ljust(14) + ": " + str(n) + " "
338        names = np.unique([a.name for a in self.unpack() if a.name])
339        if len(names)>0:
340            out += str(names).replace("'","")[:56]
341        out += "\n"
342
343        pos = self.GetPosition()
344        out += "position".ljust(14) + ": " + str(pos) + "\n"
345
346        bnds = self.GetBounds()
347        bx1, bx2 = vedo.utils.precision(bnds[0], 3), vedo.utils.precision(bnds[1], 3)
348        by1, by2 = vedo.utils.precision(bnds[2], 3), vedo.utils.precision(bnds[3], 3)
349        bz1, bz2 = vedo.utils.precision(bnds[4], 3), vedo.utils.precision(bnds[5], 3)
350        out += "bounds".ljust(14) + ":"
351        out += " x=(" + bx1 + ", " + bx2 + "),"
352        out += " y=(" + by1 + ", " + by2 + "),"
353        out += " z=(" + bz1 + ", " + bz2 + ")\n"
354
355        if "Histogram1D" in cname:
356            if self.title  != '': out += f"title".ljust(14) + ": " + f'{self.title}\n'
357            if self.xtitle and self.xtitle != ' ': out += f"xtitle".ljust(14) + ": " + f'{self.xtitle}\n'
358            if self.ytitle and self.ytitle != ' ': out += f"ytitle".ljust(14) + ": " + f'{self.ytitle}\n'
359            out += f"entries".ljust(14) + ": " + f"{self.entries}\n"
360            out += f"mean, mode".ljust(14) + ": " + f"{self.mean:.6f}, {self.mode:.6f}\n"
361            out += f"std".ljust(14) + ": " + f"{self.std:.6f}"
362        elif "Histogram2D" in cname:
363            if self.title  != '': out += f"title".ljust(14) + ": " + f'{self.title}\n'
364            if self.xtitle and self.xtitle != ' ': out += f"xtitle".ljust(14) + ": " + f'{self.xtitle}\n'
365            if self.ytitle and self.ytitle != ' ': out += f"ytitle".ljust(14) + ": " + f'{self.ytitle}\n'
366            out += f"entries".ljust(14) + ": " + f"{self.entries}\n"
367            out += f"mean".ljust(14) + ": " + f"{vedo.utils.precision(self.mean, 6)}\n"
368            out += f"std".ljust(14) + ": " + f"{vedo.utils.precision(self.std, 6)}"
369
370
371        return out.rstrip() + "\x1b[0m"
372
373    def _repr_html_(self):
374        """
375        HTML representation of the Assembly object for Jupyter Notebooks.
376
377        Returns:
378            HTML text with the image and some properties.
379        """
380        import io
381        import base64
382        from PIL import Image
383
384        library_name = "vedo.assembly.Assembly"
385        help_url = "https://vedo.embl.es/docs/vedo/assembly.html"
386
387        arr = self.thumbnail(zoom=1.1, elevation=-60)
388
389        im = Image.fromarray(arr)
390        buffered = io.BytesIO()
391        im.save(buffered, format="PNG", quality=100)
392        encoded = base64.b64encode(buffered.getvalue()).decode("utf-8")
393        url = "data:image/png;base64," + encoded
394        image = f"<img src='{url}'></img>"
395
396        # statisitics
397        bounds = "<br/>".join(
398            [
399                vedo.utils.precision(min_x, 4) + " ... " + vedo.utils.precision(max_x, 4)
400                for min_x, max_x in zip(self.bounds()[::2], self.bounds()[1::2])
401            ]
402        )
403
404        help_text = ""
405        if self.name:
406            help_text += f"<b> {self.name}: &nbsp&nbsp</b>"
407        help_text += '<b><a href="' + help_url + '" target="_blank">' + library_name + "</a></b>"
408        if self.filename:
409            dots = ""
410            if len(self.filename) > 30:
411                dots = "..."
412            help_text += f"<br/><code><i>({dots}{self.filename[-30:]})</i></code>"
413
414        allt = [
415            "<table>",
416            "<tr>",
417            "<td>",
418            image,
419            "</td>",
420            "<td style='text-align: center; vertical-align: center;'><br/>",
421            help_text,
422            "<table>",
423            "<tr><td><b> nr. of objects </b></td><td>"
424            + str(self.GetNumberOfPaths())
425            + "</td></tr>",
426            "<tr><td><b> position </b></td><td>" + str(self.GetPosition()) + "</td></tr>",
427            "<tr><td><b> diagonal size </b></td><td>"
428            + vedo.utils.precision(self.diagonal_size(), 5)
429            + "</td></tr>",
430            "<tr><td><b> bounds </b> <br/> (x/y/z) </td><td>" + str(bounds) + "</td></tr>",
431            "</table>",
432            "</table>",
433        ]
434        return "\n".join(allt)
435
436    def __add__(self, obj):
437        """
438        Add an object to the assembly
439        """
440        if isinstance(getattr(obj, "actor", None), vtki.get_class("Prop3D")):
441
442            self.objects.append(obj)
443            self.actors.append(obj.actor)
444            self.AddPart(obj.actor)
445
446            if hasattr(obj, "scalarbar") and obj.scalarbar is not None:
447                if self.scalarbar is None:
448                    self.scalarbar = obj.scalarbar
449                    return self
450
451                def unpack_group(scalarbar):
452                    if isinstance(scalarbar, Group):
453                        return scalarbar.unpack()
454                    else:
455                        return scalarbar
456
457                if isinstance(self.scalarbar, Group):
458                    self.scalarbar += unpack_group(obj.scalarbar)
459                else:
460                    self.scalarbar = Group([unpack_group(self.scalarbar), unpack_group(obj.scalarbar)])
461            self.pipeline = vedo.utils.OperationNode("add mesh", parents=[self, obj], c="#f08080")
462        return self
463
464    def __isub__(self, obj):
465        """
466        Remove an object to the assembly.
467        """
468        if not vedo.utils.is_sequence(obj):
469            obj = [obj]
470        for a in obj:
471            if a:
472                try:
473                    self.RemovePart(a)
474                    self.objects.remove(a)
475                except TypeError:
476                    self.RemovePart(a.actor)
477                    self.objects.remove(a)
478        return self
479
480    def rename(self, name: str) -> "Assembly":
481        """Set a new name for the Assembly object."""
482        self.name = name
483        return self
484
485    def add(self, obj):
486        """
487        Add an object to the assembly.
488        """
489        self.__add__(obj)
490        return self
491
492    def remove(self, obj):
493        """
494        Remove an object to the assembly.
495        """
496        self.__isub__(obj)
497        return self
498
499    def __contains__(self, obj):
500        """Allows to use `in` to check if an object is in the `Assembly`."""
501        return obj in self.objects
502
503    def __getitem__(self, i):
504        """Return i-th object."""
505        if isinstance(i, int):
506            return self.objects[i]
507        elif isinstance(i, str):
508            for m in self.objects:
509                if i == m.name:
510                    return m
511        return None
512
513    def __len__(self):
514        """Return nr. of objects in the assembly."""
515        return len(self.objects)
516
517    def write(self, filename="assembly.npy") -> Self:
518        """
519        Write the object to file in `numpy` format (npy).
520        """
521        vedo.file_io.write(self, filename)
522        return self
523
524    # TODO ####
525    # def propagate_transform(self):
526    #     """Propagate the transformation to all parts."""
527    #     # navigate the assembly and apply the transform to all parts
528    #     # and reset position, orientation and scale of the assembly
529    #     for i in range(self.GetNumberOfPaths()):
530    #         path = self.GetPath(i)
531    #         obj = path.GetLastNode().GetViewProp()
532    #         obj.SetUserTransform(self.transform.T)
533    #         obj.SetPosition(0, 0, 0)
534    #         obj.SetOrientation(0, 0, 0)
535    #         obj.SetScale(1, 1, 1)
536    #     raise NotImplementedError()
537
538    def unpack(self, i=None) -> Union[List["vedo.Mesh"], "vedo.Mesh"]:
539        """Unpack the list of objects from a `Assembly`.
540
541        If `i` is given, get `i-th` object from a `Assembly`.
542        Input can be a string, in this case returns the first object
543        whose name contains the given string.
544
545        Examples:
546            - [custom_axes4.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/custom_axes4.py)
547        """
548        if i is None:
549            return self.objects
550        elif isinstance(i, int):
551            return self.objects[i]
552        elif isinstance(i, str):
553            for m in self.objects:
554                if i == m.name:
555                    return m
556        return []
557
558    def recursive_unpack(self) -> List["vedo.Mesh"]:
559        """Flatten out an Assembly."""
560
561        def _genflatten(lst):
562            if lst:
563                ##
564                if isinstance(lst[0], Assembly):
565                    lst = lst[0].unpack()
566                ##
567                for elem in lst:
568                    if isinstance(elem, Assembly):
569                        apos = elem.GetPosition()
570                        asum = np.sum(apos)
571                        for x in elem.unpack():
572                            if asum:
573                                yield x.clone().shift(apos)
574                            else:
575                                yield x
576                    else:
577                        yield elem
578
579        return list(_genflatten([self]))
580
581    def pickable(self, value=True) -> "Assembly":
582        """Set/get the pickability property of an assembly and its elements"""
583        self.SetPickable(value)
584        # set property to each element
585        for elem in self.recursive_unpack():
586            elem.pickable(value)
587        return self
588
589    def clone(self) -> "Assembly":
590        """Make a clone copy of the object. Same as `copy()`."""
591        newlist = []
592        for a in self.objects:
593            newlist.append(a.clone())
594        return Assembly(newlist)
595
596    def clone2d(self, pos="bottom-left", size=1, rotation=0, ontop=False, scale=None) -> Group:
597        """
598        Convert the `Assembly` into a `Group` of 2D objects.
599
600        Arguments:
601            pos : (list, str)
602                Position in 2D, as a string or list (x,y).
603                The center of the renderer is [0,0] while top-right is [1,1].
604                Any combination of "center", "top", "bottom", "left" and "right" will work.
605            size : (float)
606                global scaling factor for the 2D object.
607                The scaling is normalized to the x-range of the original object.
608            rotation : (float)
609                rotation angle in degrees.
610            ontop : (bool)
611                if `True` the now 2D object is rendered on top of the 3D scene.
612            scale : (float)
613                deprecated, use `size` instead.
614
615        Returns:
616            `Group` object.
617        """
618        if scale is not None:
619            vedo.logger.warning("clone2d(scale=...) is deprecated, use clone2d(size=...) instead")
620            size = scale
621
622        padding = 0.05
623        x0, x1 = self.xbounds()
624        y0, y1 = self.ybounds()
625        pp = self.pos()
626        x0 -= pp[0]
627        x1 -= pp[0]
628        y0 -= pp[1]
629        y1 -= pp[1]
630
631        offset = [x0, y0]
632        if "cent" in pos:
633            offset = [(x0 + x1) / 2, (y0 + y1) / 2]
634            position = [0., 0.]
635            if "right" in pos:
636                offset[0] = x1
637                position = [1 - padding, 0]
638            if "left" in pos:
639                offset[0] = x0
640                position = [-1 + padding, 0]
641            if "top" in pos:
642                offset[1] = y1
643                position = [0, 1 - padding]
644            if "bottom" in pos:
645                offset[1] = y0
646                position = [0, -1 + padding]
647        elif "top" in pos:
648            if "right" in pos:
649                offset = [x1, y1]
650                position = [1 - padding, 1 - padding]
651            elif "left" in pos:
652                offset = [x0, y1]
653                position = [-1 + padding, 1 - padding]
654            else:
655                raise ValueError(f"incomplete position pos='{pos}'")
656        elif "bottom" in pos:
657            if "right" in pos:
658                offset = [x1, y0]
659                position = [1 - padding, -1 + padding]
660            elif "left" in pos:
661                offset = [x0, y0]
662                position = [-1 + padding, -1 + padding]
663            else:
664                raise ValueError(f"incomplete position pos='{pos}'")
665        else:
666            position = pos
667
668        scanned : List[Any] = []
669        group = Group()
670        for a in self.recursive_unpack():
671            if a in scanned:
672                continue
673            if not isinstance(a, vedo.Points):
674                continue
675            if a.npoints == 0:
676                continue
677
678            s = size * 500 / (x1 - x0)
679            if a.properties.GetRepresentation() == 1:
680                # wireframe is not rendered correctly in 2d
681                b = a.boundaries().lw(1).c(a.color(), a.alpha())
682                if rotation:
683                    b.rotate_z(rotation, around=self.origin())
684                a2d = b.clone2d(size=s, offset=offset)
685            else:
686                if rotation:
687                    # around=self.actor.GetCenter()
688                    a.rotate_z(rotation, around=self.origin())
689                a2d = a.clone2d(size=s, offset=offset)
690            a2d.pos(position).ontop(ontop)
691            group += a2d
692
693        try: # copy info from Histogram1D
694            group.entries = self.entries
695            group.frequencies = self.frequencies
696            group.errors = self.errors
697            group.edges = self.edges
698            group.centers = self.centers
699            group.mean = self.mean
700            group.mode = self.mode
701            group.std = self.std
702        except AttributeError:
703            pass
704
705        group.name = self.name
706        return group
707
708    def copy(self) -> "Assembly":
709        """Return a copy of the object. Alias of `clone()`."""
710        return self.clone()
class Group(vtkmodules.vtkRenderingCore.vtkPropAssembly):
 80class Group(vtki.vtkPropAssembly):
 81    """Form groups of generic objects (not necessarily meshes)."""
 82
 83    def __init__(self, objects=()):
 84        """Form groups of generic objects (not necessarily meshes)."""
 85        super().__init__()
 86
 87        self.objects = []
 88
 89        if isinstance(objects, dict):
 90            for name in objects:
 91                objects[name].name = name
 92            objects = list(objects.values())
 93        elif vedo.utils.is_sequence(objects):
 94            self.objects = objects
 95
 96
 97        self.actor = self
 98
 99        self.name = "Group"
100        self.filename = ""
101        self.trail = None
102        self.trail_points = []
103        self.trail_segment_size = 0
104        self.trail_offset = None
105        self.shadows = []
106        self.info = {}
107        self.rendered_at = set()
108        self.scalarbar = None
109
110        for a in vedo.utils.flatten(objects):
111            if a:
112                self.AddPart(a.actor)
113
114        self.PickableOff()
115
116
117    def __str__(self):
118        """Print info about Group object."""
119        module = self.__class__.__module__
120        name = self.__class__.__name__
121        out = vedo.printc(
122            f"{module}.{name} at ({hex(id(self))})".ljust(75),
123            bold=True, invert=True, return_string=True,
124        )
125        out += "\x1b[0m"
126        if self.name:
127            out += "name".ljust(14) + ": " + self.name
128            if "legend" in self.info.keys() and self.info["legend"]:
129                out+= f", legend='{self.info['legend']}'"
130            out += "\n"
131
132        n = len(self.unpack())
133        out += "n. of objects".ljust(14) + ": " + str(n) + " "
134        names = [a.name for a in self.unpack() if a.name]
135        if names:
136            out += str(names).replace("'","")[:56]
137        return out.rstrip() + "\x1b[0m"
138
139    def __iadd__(self, obj):
140        """Add an object to the group."""
141        if not vedo.utils.is_sequence(obj):
142            obj = [obj]
143        for a in obj:
144            if a:
145                try:
146                    self.AddPart(a)
147                except TypeError:
148                    self.AddPart(a.actor)
149                    self.objects.append(a)
150        return self
151
152    def __isub__(self, obj):
153        """Remove an object to the group."""
154        if not vedo.utils.is_sequence(obj):
155            obj = [obj]
156        for a in obj:
157            if a:
158                try:
159                    self.RemovePart(a)
160                except TypeError:
161                    self.RemovePart(a.actor)
162                    self.objects.append(a)
163        return self
164    
165    def rename(self, name: str) -> "Group":
166        """Set a new name for the Group object."""
167        self.name = name
168        return self
169
170    def add(self, obj):
171        """Add an object to the group."""
172        self.__iadd__(obj)
173        return self
174
175    def remove(self, obj):
176        """Remove an object to the group."""
177        self.__isub__(obj)
178        return self
179
180    def _unpack(self):
181        """Unpack the group into its elements"""
182        elements = []
183        self.InitPathTraversal()
184        parts = self.GetParts()
185        parts.InitTraversal()
186        for i in range(parts.GetNumberOfItems()):
187            ele = parts.GetItemAsObject(i)
188            elements.append(ele)
189
190        # gr.InitPathTraversal()
191        # for _ in range(gr.GetNumberOfPaths()):
192        #     path  = gr.GetNextPath()
193        #     print([path])
194        #     path.InitTraversal()
195        #     for i in range(path.GetNumberOfItems()):
196        #         a = path.GetItemAsObject(i).GetViewProp()
197        #         print([a])
198
199        return elements
200
201    def clear(self) -> "Group":
202        """Remove all parts"""
203        for a in self._unpack():
204            self.RemovePart(a)
205        self.objects = []
206        return self
207
208    def on(self) -> "Group":
209        """Switch on visibility"""
210        self.VisibilityOn()
211        return self
212
213    def off(self) -> "Group":
214        """Switch off visibility"""
215        self.VisibilityOff()
216        return self
217
218    def pickable(self, value=True) -> "Group":
219        """The pickability property of the Group."""
220        self.SetPickable(value)
221        return self
222
223    def use_bounds(self, value=True) -> "Group":
224        """Set the use bounds property of the Group."""
225        self.SetUseBounds(value)
226        return self
227
228    def print(self) -> "Group":
229        """Print info about the object."""
230        print(self)
231        return self

Form groups of generic objects (not necessarily meshes).

Group(objects=())
 83    def __init__(self, objects=()):
 84        """Form groups of generic objects (not necessarily meshes)."""
 85        super().__init__()
 86
 87        self.objects = []
 88
 89        if isinstance(objects, dict):
 90            for name in objects:
 91                objects[name].name = name
 92            objects = list(objects.values())
 93        elif vedo.utils.is_sequence(objects):
 94            self.objects = objects
 95
 96
 97        self.actor = self
 98
 99        self.name = "Group"
100        self.filename = ""
101        self.trail = None
102        self.trail_points = []
103        self.trail_segment_size = 0
104        self.trail_offset = None
105        self.shadows = []
106        self.info = {}
107        self.rendered_at = set()
108        self.scalarbar = None
109
110        for a in vedo.utils.flatten(objects):
111            if a:
112                self.AddPart(a.actor)
113
114        self.PickableOff()

Form groups of generic objects (not necessarily meshes).

def rename(self, name: str) -> Group:
165    def rename(self, name: str) -> "Group":
166        """Set a new name for the Group object."""
167        self.name = name
168        return self

Set a new name for the Group object.

def add(self, obj):
170    def add(self, obj):
171        """Add an object to the group."""
172        self.__iadd__(obj)
173        return self

Add an object to the group.

def remove(self, obj):
175    def remove(self, obj):
176        """Remove an object to the group."""
177        self.__isub__(obj)
178        return self

Remove an object to the group.

def clear(self) -> Group:
201    def clear(self) -> "Group":
202        """Remove all parts"""
203        for a in self._unpack():
204            self.RemovePart(a)
205        self.objects = []
206        return self

Remove all parts

def on(self) -> Group:
208    def on(self) -> "Group":
209        """Switch on visibility"""
210        self.VisibilityOn()
211        return self

Switch on visibility

def off(self) -> Group:
213    def off(self) -> "Group":
214        """Switch off visibility"""
215        self.VisibilityOff()
216        return self

Switch off visibility

def pickable(self, value=True) -> Group:
218    def pickable(self, value=True) -> "Group":
219        """The pickability property of the Group."""
220        self.SetPickable(value)
221        return self

The pickability property of the Group.

def use_bounds(self, value=True) -> Group:
223    def use_bounds(self, value=True) -> "Group":
224        """Set the use bounds property of the Group."""
225        self.SetUseBounds(value)
226        return self

Set the use bounds property of the Group.

def print(self) -> Group:
228    def print(self) -> "Group":
229        """Print info about the object."""
230        print(self)
231        return self

Print info about the object.

class Assembly(vedo.visual.CommonVisual, vedo.visual.Actor3DHelper, vtkmodules.vtkRenderingCore.vtkAssembly):
235class Assembly(CommonVisual, Actor3DHelper, vtki.vtkAssembly):
236    """
237    Group many objects and treat them as a single new object.
238    """
239
240    def __init__(self, *meshs):
241        """
242        Group many objects and treat them as a single new object,
243        keeping track of internal transformations.
244
245        A file can be loaded by passing its name as a string.
246        Format must be `.npy`.
247
248        Examples:
249            - [gyroscope1.py](https://github.com/marcomusy/vedo/tree/master/examples/simulations/gyroscope1.py)
250
251            ![](https://vedo.embl.es/images/simulations/39766016-85c1c1d6-52e3-11e8-8575-d167b7ce5217.gif)
252        """
253        super().__init__()
254
255        self.actor = self
256        self.actor.retrieve_object = weak_ref_to(self)
257
258        self.name = "Assembly"
259        self.filename = ""
260        self.rendered_at = set()
261        self.scalarbar = None
262        self.info = {}
263        self.time = 0
264
265        self.transform = LinearTransform()
266
267        # Init by filename
268        if len(meshs) == 1 and isinstance(meshs[0], str):
269            filename = vedo.file_io.download(meshs[0], verbose=False)
270            data = np.load(filename, allow_pickle=True)
271            try:
272                # old format with a single object
273                meshs = [vedo.file_io.from_numpy(dd) for dd in data]
274            except TypeError:
275                # new format with a dictionary
276                data = data.item()
277                meshs = []
278                for ad in data["objects"][0]["parts"]:
279                    obb = vedo.file_io.from_numpy(ad)
280                    meshs.append(obb)
281                self.transform = LinearTransform(data["objects"][0]["transform"])
282                self.actor.SetPosition(self.transform.T.GetPosition())
283                self.actor.SetOrientation(self.transform.T.GetOrientation())
284                self.actor.SetScale(self.transform.T.GetScale())
285
286        # Name and load from dictionary
287        if len(meshs) == 1 and isinstance(meshs[0], dict):
288            meshs = meshs[0]
289            for name in meshs:
290                meshs[name].name = name
291            meshs = list(meshs.values())
292        else:
293            if len(meshs) == 1:
294                meshs = meshs[0]
295            else:
296                meshs = vedo.utils.flatten(meshs)
297
298        self.objects = [m for m in meshs if m]
299        self.actors  = [m.actor for m in self.objects]
300
301        scalarbars = []
302        for a in self.actors:
303            if isinstance(a, vtki.get_class("Prop3D")): # and a.GetNumberOfPoints():
304                self.AddPart(a)
305            if hasattr(a, "scalarbar") and a.scalarbar is not None:
306                scalarbars.append(a.scalarbar)
307
308        if len(scalarbars) > 1:
309            self.scalarbar = Group(scalarbars)
310        elif len(scalarbars) == 1:
311            self.scalarbar = scalarbars[0]
312
313        self.pipeline = vedo.utils.OperationNode(
314            "Assembly",
315            parents=self.objects,
316            comment=f"#meshes {len(self.objects)}",
317            c="#f08080",
318        )
319        ##########################################
320
321    def __str__(self):
322        """Print info about Assembly object."""
323        module = self.__class__.__module__
324        cname = self.__class__.__name__
325        out = vedo.printc(
326            f"{module}.{cname} at ({hex(id(self))})".ljust(75),
327            bold=True, invert=True, return_string=True,
328        )
329        out += "\x1b[0m"
330
331        if self.name:
332            out += "name".ljust(14) + ": " + self.name
333            if "legend" in self.info.keys() and self.info["legend"]:
334                out+= f", legend='{self.info['legend']}'"
335            out += "\n"
336
337        n = len(self.unpack())
338        out += "n. of objects".ljust(14) + ": " + str(n) + " "
339        names = np.unique([a.name for a in self.unpack() if a.name])
340        if len(names)>0:
341            out += str(names).replace("'","")[:56]
342        out += "\n"
343
344        pos = self.GetPosition()
345        out += "position".ljust(14) + ": " + str(pos) + "\n"
346
347        bnds = self.GetBounds()
348        bx1, bx2 = vedo.utils.precision(bnds[0], 3), vedo.utils.precision(bnds[1], 3)
349        by1, by2 = vedo.utils.precision(bnds[2], 3), vedo.utils.precision(bnds[3], 3)
350        bz1, bz2 = vedo.utils.precision(bnds[4], 3), vedo.utils.precision(bnds[5], 3)
351        out += "bounds".ljust(14) + ":"
352        out += " x=(" + bx1 + ", " + bx2 + "),"
353        out += " y=(" + by1 + ", " + by2 + "),"
354        out += " z=(" + bz1 + ", " + bz2 + ")\n"
355
356        if "Histogram1D" in cname:
357            if self.title  != '': out += f"title".ljust(14) + ": " + f'{self.title}\n'
358            if self.xtitle and self.xtitle != ' ': out += f"xtitle".ljust(14) + ": " + f'{self.xtitle}\n'
359            if self.ytitle and self.ytitle != ' ': out += f"ytitle".ljust(14) + ": " + f'{self.ytitle}\n'
360            out += f"entries".ljust(14) + ": " + f"{self.entries}\n"
361            out += f"mean, mode".ljust(14) + ": " + f"{self.mean:.6f}, {self.mode:.6f}\n"
362            out += f"std".ljust(14) + ": " + f"{self.std:.6f}"
363        elif "Histogram2D" in cname:
364            if self.title  != '': out += f"title".ljust(14) + ": " + f'{self.title}\n'
365            if self.xtitle and self.xtitle != ' ': out += f"xtitle".ljust(14) + ": " + f'{self.xtitle}\n'
366            if self.ytitle and self.ytitle != ' ': out += f"ytitle".ljust(14) + ": " + f'{self.ytitle}\n'
367            out += f"entries".ljust(14) + ": " + f"{self.entries}\n"
368            out += f"mean".ljust(14) + ": " + f"{vedo.utils.precision(self.mean, 6)}\n"
369            out += f"std".ljust(14) + ": " + f"{vedo.utils.precision(self.std, 6)}"
370
371
372        return out.rstrip() + "\x1b[0m"
373
374    def _repr_html_(self):
375        """
376        HTML representation of the Assembly object for Jupyter Notebooks.
377
378        Returns:
379            HTML text with the image and some properties.
380        """
381        import io
382        import base64
383        from PIL import Image
384
385        library_name = "vedo.assembly.Assembly"
386        help_url = "https://vedo.embl.es/docs/vedo/assembly.html"
387
388        arr = self.thumbnail(zoom=1.1, elevation=-60)
389
390        im = Image.fromarray(arr)
391        buffered = io.BytesIO()
392        im.save(buffered, format="PNG", quality=100)
393        encoded = base64.b64encode(buffered.getvalue()).decode("utf-8")
394        url = "data:image/png;base64," + encoded
395        image = f"<img src='{url}'></img>"
396
397        # statisitics
398        bounds = "<br/>".join(
399            [
400                vedo.utils.precision(min_x, 4) + " ... " + vedo.utils.precision(max_x, 4)
401                for min_x, max_x in zip(self.bounds()[::2], self.bounds()[1::2])
402            ]
403        )
404
405        help_text = ""
406        if self.name:
407            help_text += f"<b> {self.name}: &nbsp&nbsp</b>"
408        help_text += '<b><a href="' + help_url + '" target="_blank">' + library_name + "</a></b>"
409        if self.filename:
410            dots = ""
411            if len(self.filename) > 30:
412                dots = "..."
413            help_text += f"<br/><code><i>({dots}{self.filename[-30:]})</i></code>"
414
415        allt = [
416            "<table>",
417            "<tr>",
418            "<td>",
419            image,
420            "</td>",
421            "<td style='text-align: center; vertical-align: center;'><br/>",
422            help_text,
423            "<table>",
424            "<tr><td><b> nr. of objects </b></td><td>"
425            + str(self.GetNumberOfPaths())
426            + "</td></tr>",
427            "<tr><td><b> position </b></td><td>" + str(self.GetPosition()) + "</td></tr>",
428            "<tr><td><b> diagonal size </b></td><td>"
429            + vedo.utils.precision(self.diagonal_size(), 5)
430            + "</td></tr>",
431            "<tr><td><b> bounds </b> <br/> (x/y/z) </td><td>" + str(bounds) + "</td></tr>",
432            "</table>",
433            "</table>",
434        ]
435        return "\n".join(allt)
436
437    def __add__(self, obj):
438        """
439        Add an object to the assembly
440        """
441        if isinstance(getattr(obj, "actor", None), vtki.get_class("Prop3D")):
442
443            self.objects.append(obj)
444            self.actors.append(obj.actor)
445            self.AddPart(obj.actor)
446
447            if hasattr(obj, "scalarbar") and obj.scalarbar is not None:
448                if self.scalarbar is None:
449                    self.scalarbar = obj.scalarbar
450                    return self
451
452                def unpack_group(scalarbar):
453                    if isinstance(scalarbar, Group):
454                        return scalarbar.unpack()
455                    else:
456                        return scalarbar
457
458                if isinstance(self.scalarbar, Group):
459                    self.scalarbar += unpack_group(obj.scalarbar)
460                else:
461                    self.scalarbar = Group([unpack_group(self.scalarbar), unpack_group(obj.scalarbar)])
462            self.pipeline = vedo.utils.OperationNode("add mesh", parents=[self, obj], c="#f08080")
463        return self
464
465    def __isub__(self, obj):
466        """
467        Remove an object to the assembly.
468        """
469        if not vedo.utils.is_sequence(obj):
470            obj = [obj]
471        for a in obj:
472            if a:
473                try:
474                    self.RemovePart(a)
475                    self.objects.remove(a)
476                except TypeError:
477                    self.RemovePart(a.actor)
478                    self.objects.remove(a)
479        return self
480
481    def rename(self, name: str) -> "Assembly":
482        """Set a new name for the Assembly object."""
483        self.name = name
484        return self
485
486    def add(self, obj):
487        """
488        Add an object to the assembly.
489        """
490        self.__add__(obj)
491        return self
492
493    def remove(self, obj):
494        """
495        Remove an object to the assembly.
496        """
497        self.__isub__(obj)
498        return self
499
500    def __contains__(self, obj):
501        """Allows to use `in` to check if an object is in the `Assembly`."""
502        return obj in self.objects
503
504    def __getitem__(self, i):
505        """Return i-th object."""
506        if isinstance(i, int):
507            return self.objects[i]
508        elif isinstance(i, str):
509            for m in self.objects:
510                if i == m.name:
511                    return m
512        return None
513
514    def __len__(self):
515        """Return nr. of objects in the assembly."""
516        return len(self.objects)
517
518    def write(self, filename="assembly.npy") -> Self:
519        """
520        Write the object to file in `numpy` format (npy).
521        """
522        vedo.file_io.write(self, filename)
523        return self
524
525    # TODO ####
526    # def propagate_transform(self):
527    #     """Propagate the transformation to all parts."""
528    #     # navigate the assembly and apply the transform to all parts
529    #     # and reset position, orientation and scale of the assembly
530    #     for i in range(self.GetNumberOfPaths()):
531    #         path = self.GetPath(i)
532    #         obj = path.GetLastNode().GetViewProp()
533    #         obj.SetUserTransform(self.transform.T)
534    #         obj.SetPosition(0, 0, 0)
535    #         obj.SetOrientation(0, 0, 0)
536    #         obj.SetScale(1, 1, 1)
537    #     raise NotImplementedError()
538
539    def unpack(self, i=None) -> Union[List["vedo.Mesh"], "vedo.Mesh"]:
540        """Unpack the list of objects from a `Assembly`.
541
542        If `i` is given, get `i-th` object from a `Assembly`.
543        Input can be a string, in this case returns the first object
544        whose name contains the given string.
545
546        Examples:
547            - [custom_axes4.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/custom_axes4.py)
548        """
549        if i is None:
550            return self.objects
551        elif isinstance(i, int):
552            return self.objects[i]
553        elif isinstance(i, str):
554            for m in self.objects:
555                if i == m.name:
556                    return m
557        return []
558
559    def recursive_unpack(self) -> List["vedo.Mesh"]:
560        """Flatten out an Assembly."""
561
562        def _genflatten(lst):
563            if lst:
564                ##
565                if isinstance(lst[0], Assembly):
566                    lst = lst[0].unpack()
567                ##
568                for elem in lst:
569                    if isinstance(elem, Assembly):
570                        apos = elem.GetPosition()
571                        asum = np.sum(apos)
572                        for x in elem.unpack():
573                            if asum:
574                                yield x.clone().shift(apos)
575                            else:
576                                yield x
577                    else:
578                        yield elem
579
580        return list(_genflatten([self]))
581
582    def pickable(self, value=True) -> "Assembly":
583        """Set/get the pickability property of an assembly and its elements"""
584        self.SetPickable(value)
585        # set property to each element
586        for elem in self.recursive_unpack():
587            elem.pickable(value)
588        return self
589
590    def clone(self) -> "Assembly":
591        """Make a clone copy of the object. Same as `copy()`."""
592        newlist = []
593        for a in self.objects:
594            newlist.append(a.clone())
595        return Assembly(newlist)
596
597    def clone2d(self, pos="bottom-left", size=1, rotation=0, ontop=False, scale=None) -> Group:
598        """
599        Convert the `Assembly` into a `Group` of 2D objects.
600
601        Arguments:
602            pos : (list, str)
603                Position in 2D, as a string or list (x,y).
604                The center of the renderer is [0,0] while top-right is [1,1].
605                Any combination of "center", "top", "bottom", "left" and "right" will work.
606            size : (float)
607                global scaling factor for the 2D object.
608                The scaling is normalized to the x-range of the original object.
609            rotation : (float)
610                rotation angle in degrees.
611            ontop : (bool)
612                if `True` the now 2D object is rendered on top of the 3D scene.
613            scale : (float)
614                deprecated, use `size` instead.
615
616        Returns:
617            `Group` object.
618        """
619        if scale is not None:
620            vedo.logger.warning("clone2d(scale=...) is deprecated, use clone2d(size=...) instead")
621            size = scale
622
623        padding = 0.05
624        x0, x1 = self.xbounds()
625        y0, y1 = self.ybounds()
626        pp = self.pos()
627        x0 -= pp[0]
628        x1 -= pp[0]
629        y0 -= pp[1]
630        y1 -= pp[1]
631
632        offset = [x0, y0]
633        if "cent" in pos:
634            offset = [(x0 + x1) / 2, (y0 + y1) / 2]
635            position = [0., 0.]
636            if "right" in pos:
637                offset[0] = x1
638                position = [1 - padding, 0]
639            if "left" in pos:
640                offset[0] = x0
641                position = [-1 + padding, 0]
642            if "top" in pos:
643                offset[1] = y1
644                position = [0, 1 - padding]
645            if "bottom" in pos:
646                offset[1] = y0
647                position = [0, -1 + padding]
648        elif "top" in pos:
649            if "right" in pos:
650                offset = [x1, y1]
651                position = [1 - padding, 1 - padding]
652            elif "left" in pos:
653                offset = [x0, y1]
654                position = [-1 + padding, 1 - padding]
655            else:
656                raise ValueError(f"incomplete position pos='{pos}'")
657        elif "bottom" in pos:
658            if "right" in pos:
659                offset = [x1, y0]
660                position = [1 - padding, -1 + padding]
661            elif "left" in pos:
662                offset = [x0, y0]
663                position = [-1 + padding, -1 + padding]
664            else:
665                raise ValueError(f"incomplete position pos='{pos}'")
666        else:
667            position = pos
668
669        scanned : List[Any] = []
670        group = Group()
671        for a in self.recursive_unpack():
672            if a in scanned:
673                continue
674            if not isinstance(a, vedo.Points):
675                continue
676            if a.npoints == 0:
677                continue
678
679            s = size * 500 / (x1 - x0)
680            if a.properties.GetRepresentation() == 1:
681                # wireframe is not rendered correctly in 2d
682                b = a.boundaries().lw(1).c(a.color(), a.alpha())
683                if rotation:
684                    b.rotate_z(rotation, around=self.origin())
685                a2d = b.clone2d(size=s, offset=offset)
686            else:
687                if rotation:
688                    # around=self.actor.GetCenter()
689                    a.rotate_z(rotation, around=self.origin())
690                a2d = a.clone2d(size=s, offset=offset)
691            a2d.pos(position).ontop(ontop)
692            group += a2d
693
694        try: # copy info from Histogram1D
695            group.entries = self.entries
696            group.frequencies = self.frequencies
697            group.errors = self.errors
698            group.edges = self.edges
699            group.centers = self.centers
700            group.mean = self.mean
701            group.mode = self.mode
702            group.std = self.std
703        except AttributeError:
704            pass
705
706        group.name = self.name
707        return group
708
709    def copy(self) -> "Assembly":
710        """Return a copy of the object. Alias of `clone()`."""
711        return self.clone()

Group many objects and treat them as a single new object.

Assembly(*meshs)
240    def __init__(self, *meshs):
241        """
242        Group many objects and treat them as a single new object,
243        keeping track of internal transformations.
244
245        A file can be loaded by passing its name as a string.
246        Format must be `.npy`.
247
248        Examples:
249            - [gyroscope1.py](https://github.com/marcomusy/vedo/tree/master/examples/simulations/gyroscope1.py)
250
251            ![](https://vedo.embl.es/images/simulations/39766016-85c1c1d6-52e3-11e8-8575-d167b7ce5217.gif)
252        """
253        super().__init__()
254
255        self.actor = self
256        self.actor.retrieve_object = weak_ref_to(self)
257
258        self.name = "Assembly"
259        self.filename = ""
260        self.rendered_at = set()
261        self.scalarbar = None
262        self.info = {}
263        self.time = 0
264
265        self.transform = LinearTransform()
266
267        # Init by filename
268        if len(meshs) == 1 and isinstance(meshs[0], str):
269            filename = vedo.file_io.download(meshs[0], verbose=False)
270            data = np.load(filename, allow_pickle=True)
271            try:
272                # old format with a single object
273                meshs = [vedo.file_io.from_numpy(dd) for dd in data]
274            except TypeError:
275                # new format with a dictionary
276                data = data.item()
277                meshs = []
278                for ad in data["objects"][0]["parts"]:
279                    obb = vedo.file_io.from_numpy(ad)
280                    meshs.append(obb)
281                self.transform = LinearTransform(data["objects"][0]["transform"])
282                self.actor.SetPosition(self.transform.T.GetPosition())
283                self.actor.SetOrientation(self.transform.T.GetOrientation())
284                self.actor.SetScale(self.transform.T.GetScale())
285
286        # Name and load from dictionary
287        if len(meshs) == 1 and isinstance(meshs[0], dict):
288            meshs = meshs[0]
289            for name in meshs:
290                meshs[name].name = name
291            meshs = list(meshs.values())
292        else:
293            if len(meshs) == 1:
294                meshs = meshs[0]
295            else:
296                meshs = vedo.utils.flatten(meshs)
297
298        self.objects = [m for m in meshs if m]
299        self.actors  = [m.actor for m in self.objects]
300
301        scalarbars = []
302        for a in self.actors:
303            if isinstance(a, vtki.get_class("Prop3D")): # and a.GetNumberOfPoints():
304                self.AddPart(a)
305            if hasattr(a, "scalarbar") and a.scalarbar is not None:
306                scalarbars.append(a.scalarbar)
307
308        if len(scalarbars) > 1:
309            self.scalarbar = Group(scalarbars)
310        elif len(scalarbars) == 1:
311            self.scalarbar = scalarbars[0]
312
313        self.pipeline = vedo.utils.OperationNode(
314            "Assembly",
315            parents=self.objects,
316            comment=f"#meshes {len(self.objects)}",
317            c="#f08080",
318        )
319        ##########################################

Group many objects and treat them as a single new object, keeping track of internal transformations.

A file can be loaded by passing its name as a string. Format must be .npy.

Examples:

def rename(self, name: str) -> Assembly:
481    def rename(self, name: str) -> "Assembly":
482        """Set a new name for the Assembly object."""
483        self.name = name
484        return self

Set a new name for the Assembly object.

def add(self, obj):
486    def add(self, obj):
487        """
488        Add an object to the assembly.
489        """
490        self.__add__(obj)
491        return self

Add an object to the assembly.

def remove(self, obj):
493    def remove(self, obj):
494        """
495        Remove an object to the assembly.
496        """
497        self.__isub__(obj)
498        return self

Remove an object to the assembly.

def write(self, filename='assembly.npy') -> Self:
518    def write(self, filename="assembly.npy") -> Self:
519        """
520        Write the object to file in `numpy` format (npy).
521        """
522        vedo.file_io.write(self, filename)
523        return self

Write the object to file in numpy format (npy).

def unpack(self, i=None) -> Union[List[vedo.mesh.Mesh], vedo.mesh.Mesh]:
539    def unpack(self, i=None) -> Union[List["vedo.Mesh"], "vedo.Mesh"]:
540        """Unpack the list of objects from a `Assembly`.
541
542        If `i` is given, get `i-th` object from a `Assembly`.
543        Input can be a string, in this case returns the first object
544        whose name contains the given string.
545
546        Examples:
547            - [custom_axes4.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/custom_axes4.py)
548        """
549        if i is None:
550            return self.objects
551        elif isinstance(i, int):
552            return self.objects[i]
553        elif isinstance(i, str):
554            for m in self.objects:
555                if i == m.name:
556                    return m
557        return []

Unpack the list of objects from a Assembly.

If i is given, get i-th object from a Assembly. Input can be a string, in this case returns the first object whose name contains the given string.

Examples:
def recursive_unpack(self) -> List[vedo.mesh.Mesh]:
559    def recursive_unpack(self) -> List["vedo.Mesh"]:
560        """Flatten out an Assembly."""
561
562        def _genflatten(lst):
563            if lst:
564                ##
565                if isinstance(lst[0], Assembly):
566                    lst = lst[0].unpack()
567                ##
568                for elem in lst:
569                    if isinstance(elem, Assembly):
570                        apos = elem.GetPosition()
571                        asum = np.sum(apos)
572                        for x in elem.unpack():
573                            if asum:
574                                yield x.clone().shift(apos)
575                            else:
576                                yield x
577                    else:
578                        yield elem
579
580        return list(_genflatten([self]))

Flatten out an Assembly.

def pickable(self, value=True) -> Assembly:
582    def pickable(self, value=True) -> "Assembly":
583        """Set/get the pickability property of an assembly and its elements"""
584        self.SetPickable(value)
585        # set property to each element
586        for elem in self.recursive_unpack():
587            elem.pickable(value)
588        return self

Set/get the pickability property of an assembly and its elements

def clone(self) -> Assembly:
590    def clone(self) -> "Assembly":
591        """Make a clone copy of the object. Same as `copy()`."""
592        newlist = []
593        for a in self.objects:
594            newlist.append(a.clone())
595        return Assembly(newlist)

Make a clone copy of the object. Same as copy().

def clone2d( self, pos='bottom-left', size=1, rotation=0, ontop=False, scale=None) -> Group:
597    def clone2d(self, pos="bottom-left", size=1, rotation=0, ontop=False, scale=None) -> Group:
598        """
599        Convert the `Assembly` into a `Group` of 2D objects.
600
601        Arguments:
602            pos : (list, str)
603                Position in 2D, as a string or list (x,y).
604                The center of the renderer is [0,0] while top-right is [1,1].
605                Any combination of "center", "top", "bottom", "left" and "right" will work.
606            size : (float)
607                global scaling factor for the 2D object.
608                The scaling is normalized to the x-range of the original object.
609            rotation : (float)
610                rotation angle in degrees.
611            ontop : (bool)
612                if `True` the now 2D object is rendered on top of the 3D scene.
613            scale : (float)
614                deprecated, use `size` instead.
615
616        Returns:
617            `Group` object.
618        """
619        if scale is not None:
620            vedo.logger.warning("clone2d(scale=...) is deprecated, use clone2d(size=...) instead")
621            size = scale
622
623        padding = 0.05
624        x0, x1 = self.xbounds()
625        y0, y1 = self.ybounds()
626        pp = self.pos()
627        x0 -= pp[0]
628        x1 -= pp[0]
629        y0 -= pp[1]
630        y1 -= pp[1]
631
632        offset = [x0, y0]
633        if "cent" in pos:
634            offset = [(x0 + x1) / 2, (y0 + y1) / 2]
635            position = [0., 0.]
636            if "right" in pos:
637                offset[0] = x1
638                position = [1 - padding, 0]
639            if "left" in pos:
640                offset[0] = x0
641                position = [-1 + padding, 0]
642            if "top" in pos:
643                offset[1] = y1
644                position = [0, 1 - padding]
645            if "bottom" in pos:
646                offset[1] = y0
647                position = [0, -1 + padding]
648        elif "top" in pos:
649            if "right" in pos:
650                offset = [x1, y1]
651                position = [1 - padding, 1 - padding]
652            elif "left" in pos:
653                offset = [x0, y1]
654                position = [-1 + padding, 1 - padding]
655            else:
656                raise ValueError(f"incomplete position pos='{pos}'")
657        elif "bottom" in pos:
658            if "right" in pos:
659                offset = [x1, y0]
660                position = [1 - padding, -1 + padding]
661            elif "left" in pos:
662                offset = [x0, y0]
663                position = [-1 + padding, -1 + padding]
664            else:
665                raise ValueError(f"incomplete position pos='{pos}'")
666        else:
667            position = pos
668
669        scanned : List[Any] = []
670        group = Group()
671        for a in self.recursive_unpack():
672            if a in scanned:
673                continue
674            if not isinstance(a, vedo.Points):
675                continue
676            if a.npoints == 0:
677                continue
678
679            s = size * 500 / (x1 - x0)
680            if a.properties.GetRepresentation() == 1:
681                # wireframe is not rendered correctly in 2d
682                b = a.boundaries().lw(1).c(a.color(), a.alpha())
683                if rotation:
684                    b.rotate_z(rotation, around=self.origin())
685                a2d = b.clone2d(size=s, offset=offset)
686            else:
687                if rotation:
688                    # around=self.actor.GetCenter()
689                    a.rotate_z(rotation, around=self.origin())
690                a2d = a.clone2d(size=s, offset=offset)
691            a2d.pos(position).ontop(ontop)
692            group += a2d
693
694        try: # copy info from Histogram1D
695            group.entries = self.entries
696            group.frequencies = self.frequencies
697            group.errors = self.errors
698            group.edges = self.edges
699            group.centers = self.centers
700            group.mean = self.mean
701            group.mode = self.mode
702            group.std = self.std
703        except AttributeError:
704            pass
705
706        group.name = self.name
707        return group

Convert the Assembly into a Group of 2D objects.

Arguments:
  • pos : (list, str) Position in 2D, as a string or list (x,y). The center of the renderer is [0,0] while top-right is [1,1]. Any combination of "center", "top", "bottom", "left" and "right" will work.
  • size : (float) global scaling factor for the 2D object. The scaling is normalized to the x-range of the original object.
  • rotation : (float) rotation angle in degrees.
  • ontop : (bool) if True the now 2D object is rendered on top of the 3D scene.
  • scale : (float) deprecated, use size instead.
Returns:

Group object.

def copy(self) -> Assembly:
709    def copy(self) -> "Assembly":
710        """Return a copy of the object. Alias of `clone()`."""
711        return self.clone()

Return a copy of the object. Alias of clone().

def procrustes_alignment(sources: List[vedo.mesh.Mesh], rigid=False) -> Assembly:
29def procrustes_alignment(sources: List["vedo.Mesh"], rigid=False) -> "Assembly":
30    """
31    Return an `Assembly` of aligned source meshes with the `Procrustes` algorithm.
32    The output `Assembly` is normalized in size.
33
34    The `Procrustes` algorithm takes N set of points and aligns them in a least-squares sense
35    to their mutual mean. The algorithm is iterated until convergence,
36    as the mean must be recomputed after each alignment.
37
38    The set of average points generated by the algorithm can be accessed with
39    `algoutput.info['mean']` as a numpy array.
40
41    Arguments:
42        rigid : bool
43            if `True` scaling is disabled.
44
45    Examples:
46        - [align4.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align4.py)
47
48        ![](https://vedo.embl.es/images/basic/align4.png)
49    """
50
51    group = vtki.new("MultiBlockDataGroupFilter")
52    for source in sources:
53        if sources[0].npoints != source.npoints:
54            vedo.logger.error("sources have different nr of points")
55            raise RuntimeError()
56        group.AddInputData(source.dataset)
57    procrustes = vtki.new("ProcrustesAlignmentFilter")
58    procrustes.StartFromCentroidOn()
59    procrustes.SetInputConnection(group.GetOutputPort())
60    if rigid:
61        procrustes.GetLandmarkTransform().SetModeToRigidBody()
62    procrustes.Update()
63
64    acts = []
65    for i, s in enumerate(sources):
66        poly = procrustes.GetOutput().GetBlock(i)
67        mesh = vedo.mesh.Mesh(poly)
68        mesh.actor.SetProperty(s.actor.GetProperty())
69        mesh.properties = s.actor.GetProperty()
70        if hasattr(s, "name"):
71            mesh.name = s.name
72        acts.append(mesh)
73    assem = Assembly(acts)
74    assem.transform = procrustes.GetLandmarkTransform()
75    assem.info["mean"] = vedo.utils.vtk2numpy(procrustes.GetMeanPoints().GetData())
76    return assem

Return an Assembly of aligned source meshes with the Procrustes algorithm. The output Assembly is normalized in size.

The Procrustes algorithm takes N set of points and aligns them in a least-squares sense to their mutual mean. The algorithm is iterated until convergence, as the mean must be recomputed after each alignment.

The set of average points generated by the algorithm can be accessed with algoutput.info['mean'] as a numpy array.

Arguments:
  • rigid : bool if True scaling is disabled.
Examples: