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

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 objects(self) -> List[vedo.mesh.Mesh]:
228    def objects(self) -> List["vedo.Mesh"]:
229        """Return the list of objects in the group."""
230        return self.objects

Return the list of objects in the group.

def add(self, obj):
165    def add(self, obj):
166        """Add an object to the group."""
167        self.__iadd__(obj)
168        return self

Add an object to the group.

def remove(self, obj):
170    def remove(self, obj):
171        """Remove an object to the group."""
172        self.__isub__(obj)
173        return self

Remove an object to the group.

def clear(self) -> Group:
196    def clear(self) -> "Group":
197        """Remove all parts"""
198        for a in self._unpack():
199            self.RemovePart(a)
200        self.objects = []
201        return self

Remove all parts

def on(self) -> Group:
203    def on(self) -> "Group":
204        """Switch on visibility"""
205        self.VisibilityOn()
206        return self

Switch on visibility

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

Switch off visibility

def pickable(self, value=True) -> Group:
213    def pickable(self, value=True) -> "Group":
214        """The pickability property of the Group."""
215        self.SetPickable(value)
216        return self

The pickability property of the Group.

def use_bounds(self, value=True) -> Group:
218    def use_bounds(self, value=True) -> "Group":
219        """Set the use bounds property of the Group."""
220        self.SetUseBounds(value)
221        return self

Set the use bounds property of the Group.

def print(self) -> Group:
223    def print(self) -> "Group":
224        """Print info about the object."""
225        print(self)
226        return self

Print info about the object.

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

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

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

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

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

Examples:

def add(self, obj):
462    def add(self, obj):
463        """
464        Add an object to the assembly.
465        """
466        self.__add__(obj)
467        return self

Add an object to the assembly.

def remove(self, obj):
469    def remove(self, obj):
470        """
471        Remove an object to the assembly.
472        """
473        self.__isub__(obj)
474        return self

Remove an object to the assembly.

def write(self, filename='assembly.npy') -> Self:
494    def write(self, filename="assembly.npy") -> Self:
495        """
496        Write the object to file in `numpy` format (npy).
497        """
498        vedo.file_io.write(self, filename)
499        return self

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

def unpack(self, i=None) -> Union[List[vedo.mesh.Mesh], vedo.mesh.Mesh]:
515    def unpack(self, i=None) -> Union[List["vedo.Mesh"], "vedo.Mesh"]:
516        """Unpack the list of objects from a `Assembly`.
517
518        If `i` is given, get `i-th` object from a `Assembly`.
519        Input can be a string, in this case returns the first object
520        whose name contains the given string.
521
522        Examples:
523            - [custom_axes4.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/custom_axes4.py)
524        """
525        if i is None:
526            return self.objects
527        elif isinstance(i, int):
528            return self.objects[i]
529        elif isinstance(i, str):
530            for m in self.objects:
531                if i == m.name:
532                    return m
533        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]:
535    def recursive_unpack(self) -> List["vedo.Mesh"]:
536        """Flatten out an Assembly."""
537
538        def _genflatten(lst):
539            if not lst:
540                return []
541            ##
542            if isinstance(lst[0], Assembly):
543                lst = lst[0].unpack()
544            ##
545            for elem in lst:
546                if isinstance(elem, Assembly):
547                    apos = elem.GetPosition()
548                    asum = np.sum(apos)
549                    for x in elem.unpack():
550                        if asum:
551                            yield x.clone().shift(apos)
552                        else:
553                            yield x
554                else:
555                    yield elem
556
557        return list(_genflatten([self]))

Flatten out an Assembly.

def pickable(self, value=True) -> Assembly:
559    def pickable(self, value=True) -> "Assembly":
560        """Set/get the pickability property of an assembly and its elements"""
561        self.SetPickable(value)
562        # set property to each element
563        for elem in self.recursive_unpack():
564            elem.pickable(value)
565        return self

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

def clone(self) -> Assembly:
567    def clone(self) -> "Assembly":
568        """Make a clone copy of the object. Same as `copy()`."""
569        newlist = []
570        for a in self.objects:
571            newlist.append(a.clone())
572        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:
574    def clone2d(self, pos="bottom-left", size=1, rotation=0, ontop=False, scale=None) -> Group:
575        """
576        Convert the `Assembly` into a `Group` of 2D objects.
577
578        Arguments:
579            pos : (list, str)
580                Position in 2D, as a string or list (x,y).
581                The center of the renderer is [0,0] while top-right is [1,1].
582                Any combination of "center", "top", "bottom", "left" and "right" will work.
583            size : (float)
584                global scaling factor for the 2D object.
585                The scaling is normalized to the x-range of the original object.
586            rotation : (float)
587                rotation angle in degrees.
588            ontop : (bool)
589                if `True` the now 2D object is rendered on top of the 3D scene.
590            scale : (float)
591                deprecated, use `size` instead.
592
593        Returns:
594            `Group` object.
595        """
596        if scale is not None:
597            vedo.logger.warning("clone2d(scale=...) is deprecated, use clone2d(size=...) instead")
598            size = scale
599
600        padding = 0.05
601        x0, x1 = self.xbounds()
602        y0, y1 = self.ybounds()
603        pp = self.pos()
604        x0 -= pp[0]
605        x1 -= pp[0]
606        y0 -= pp[1]
607        y1 -= pp[1]
608
609        offset = [x0, y0]
610        if "cent" in pos:
611            offset = [(x0 + x1) / 2, (y0 + y1) / 2]
612            position = [0., 0.]
613            if "right" in pos:
614                offset[0] = x1
615                position = [1 - padding, 0]
616            if "left" in pos:
617                offset[0] = x0
618                position = [-1 + padding, 0]
619            if "top" in pos:
620                offset[1] = y1
621                position = [0, 1 - padding]
622            if "bottom" in pos:
623                offset[1] = y0
624                position = [0, -1 + padding]
625        elif "top" in pos:
626            if "right" in pos:
627                offset = [x1, y1]
628                position = [1 - padding, 1 - padding]
629            elif "left" in pos:
630                offset = [x0, y1]
631                position = [-1 + padding, 1 - padding]
632            else:
633                raise ValueError(f"incomplete position pos='{pos}'")
634        elif "bottom" in pos:
635            if "right" in pos:
636                offset = [x1, y0]
637                position = [1 - padding, -1 + padding]
638            elif "left" in pos:
639                offset = [x0, y0]
640                position = [-1 + padding, -1 + padding]
641            else:
642                raise ValueError(f"incomplete position pos='{pos}'")
643        else:
644            position = pos
645
646        scanned : List[Any] = []
647        group = Group()
648        for a in self.recursive_unpack():
649            if a in scanned:
650                continue
651            if not isinstance(a, vedo.Points):
652                continue
653            if a.npoints == 0:
654                continue
655
656            s = size * 500 / (x1 - x0)
657            if a.properties.GetRepresentation() == 1:
658                # wireframe is not rendered correctly in 2d
659                b = a.boundaries().lw(1).c(a.color(), a.alpha())
660                if rotation:
661                    b.rotate_z(rotation, around=self.origin())
662                a2d = b.clone2d(size=s, offset=offset)
663            else:
664                if rotation:
665                    # around=self.actor.GetCenter()
666                    a.rotate_z(rotation, around=self.origin())
667                a2d = a.clone2d(size=s, offset=offset)
668            a2d.pos(position).ontop(ontop)
669            group += a2d
670
671        try: # copy info from Histogram1D
672            group.entries = self.entries
673            group.frequencies = self.frequencies
674            group.errors = self.errors
675            group.edges = self.edges
676            group.centers = self.centers
677            group.mean = self.mean
678            group.mode = self.mode
679            group.std = self.std
680        except AttributeError:
681            pass
682
683        group.name = self.name
684        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:
686    def copy(self) -> "Assembly":
687        """Return a copy of the object. Alias of `clone()`."""
688        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: